1use std::future::Future;
6use std::pin::Pin;
7use std::sync::Arc;
8use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
9use tokio::task::JoinSet;
10
11use camel_api::{
12 AggregationStrategy, Body, CamelError, Exchange, OutcomeSegment, PipelineOutcome,
13 SplitExpression, Value,
14};
15
16use crate::splitter::{CAMEL_SPLIT_COMPLETE, CAMEL_SPLIT_INDEX, CAMEL_SPLIT_SIZE};
17
18pub(crate) fn aggregate_completed(
25 completed: Vec<Exchange>,
26 original: Exchange,
27 strategy: AggregationStrategy,
28) -> Exchange {
29 match strategy {
30 AggregationStrategy::LastWins => completed.into_iter().last().unwrap_or(original),
31 AggregationStrategy::CollectAll => {
32 let mut bodies = Vec::new();
33 for ex in &completed {
34 let value = match &ex.input.body {
35 Body::Text(s) => Value::String(s.clone()),
36 Body::Json(v) => v.clone(),
37 Body::Xml(s) => Value::String(s.clone()),
38 Body::Bytes(b) => Value::String(String::from_utf8_lossy(b).into_owned()),
39 Body::Stream(s) => serde_json::json!({
40 "_stream": {
41 "origin": s.metadata.origin,
42 "placeholder": true,
43 "hint": "Materialize exchange body with .into_bytes() before aggregation if content needed"
44 }
45 }),
46 _ => Value::Null,
48 };
49 bodies.push(value);
50 }
51 let mut out = original;
52 out.input.body = Body::Json(Value::Array(bodies));
53 out
54 }
55 AggregationStrategy::Custom(fold_fn) => {
56 let mut iter = completed.into_iter();
57 let first = iter.next().unwrap_or(original);
58 iter.fold(first, |acc, next| fold_fn(acc, next))
59 }
60 _ => original,
62 }
63}
64
65pub struct SplitSegment {
86 pub splitter: SplitExpression,
88 pub body: OutcomeSegment,
90 pub parallel: bool,
92 pub parallel_limit: Option<usize>,
94 pub stop_on_exception: bool,
104 pub aggregation: AggregationStrategy,
106}
107
108impl Clone for SplitSegment {
109 fn clone(&self) -> Self {
110 Self {
111 splitter: Arc::clone(&self.splitter),
112 body: self.body.clone(),
113 parallel: self.parallel,
114 parallel_limit: self.parallel_limit,
115 stop_on_exception: self.stop_on_exception,
116 aggregation: self.aggregation.clone(),
117 }
118 }
119}
120
121impl camel_api::OutcomePipeline for SplitSegment {
122 fn clone_box(&self) -> Box<dyn camel_api::OutcomePipeline> {
123 Box::new(self.clone())
124 }
125
126 fn run<'a>(
127 &'a mut self,
128 exchange: camel_api::Exchange,
129 ) -> Pin<Box<dyn Future<Output = camel_api::PipelineOutcome> + Send + 'a>> {
130 let splitter = Arc::clone(&self.splitter);
131 let aggregation = self.aggregation.clone();
132 let parallel = self.parallel;
133 let parallel_limit = self.parallel_limit;
134 let stop_on_exception = self.stop_on_exception;
135 let body = &mut self.body;
136
137 Box::pin(async move {
138 let original = exchange;
139 let mut fragments = match splitter(&original) {
142 Ok(fragments) => fragments,
143 Err(err) => return PipelineOutcome::Failed(err),
144 };
145
146 if fragments.is_empty() {
147 return PipelineOutcome::Completed(original);
148 }
149
150 let total = fragments.len();
154 for (i, frag) in fragments.iter_mut().enumerate() {
155 frag.set_property(CAMEL_SPLIT_INDEX, Value::from(i as u64));
156 frag.set_property(CAMEL_SPLIT_SIZE, Value::from(total as u64));
157 frag.set_property(CAMEL_SPLIT_COMPLETE, Value::Bool(i == total - 1));
158 }
159
160 if parallel {
161 parallel_split(
162 fragments,
163 original,
164 body,
165 &aggregation,
166 parallel_limit,
167 stop_on_exception,
168 )
169 .await
170 } else {
171 sequential_split(fragments, original, body, &aggregation, stop_on_exception).await
172 }
173 })
174 }
175}
176
177async fn sequential_split(
180 fragments: Vec<Exchange>,
181 original: Exchange,
182 body: &mut OutcomeSegment,
183 aggregation: &AggregationStrategy,
184 stop_on_exception: bool,
185) -> PipelineOutcome {
186 let mut outputs = Vec::new();
187 let mut last_error: Option<CamelError> = None;
188 for frag in fragments {
189 match body.run(frag).await {
190 PipelineOutcome::Completed(ex) => outputs.push(ex),
191 PipelineOutcome::Stopped(ex) => return PipelineOutcome::Stopped(ex),
192 PipelineOutcome::Failed(err) => {
193 if stop_on_exception {
194 return PipelineOutcome::Failed(err);
195 }
196 last_error = Some(err);
198 }
199 }
200 }
201 if let Some(err) = last_error {
202 return PipelineOutcome::Failed(err);
203 }
204 PipelineOutcome::Completed(aggregate_completed(outputs, original, aggregation.clone()))
205}
206
207async fn parallel_split(
214 fragments: Vec<Exchange>,
215 original: Exchange,
216 body: &mut OutcomeSegment,
217 aggregation: &AggregationStrategy,
218 parallel_limit: Option<usize>,
219 stop_on_exception: bool,
220) -> PipelineOutcome {
221 use tokio::sync::Semaphore;
222
223 let stopped_seen = Arc::new(AtomicBool::new(false));
224 let stopped_idx = Arc::new(AtomicUsize::new(usize::MAX));
225 let aggregation = aggregation.clone();
226 let semaphore = parallel_limit
227 .filter(|&limit| limit > 0)
228 .map(|limit| Arc::new(Semaphore::new(limit)));
229
230 let mut set: JoinSet<(usize, Option<PipelineOutcome>)> = JoinSet::new();
231
232 for (idx, frag) in fragments.into_iter().enumerate() {
233 let mut body = body.clone();
234 let stopped_seen = Arc::clone(&stopped_seen);
235 let stopped_idx = Arc::clone(&stopped_idx);
236 let sem = semaphore.clone();
237 set.spawn(async move {
238 if stopped_seen.load(Ordering::SeqCst) {
243 return (idx, None);
244 }
245 let _permit: Option<tokio::sync::OwnedSemaphorePermit> = match &sem {
247 Some(s) => match std::sync::Arc::clone(s).acquire_owned().await {
248 Ok(p) => Some(p),
249 Err(_) => {
250 return (
251 idx,
252 Some(PipelineOutcome::Failed(CamelError::ProcessorError(
253 "semaphore closed".into(),
254 ))),
255 );
256 }
257 },
258 None => None,
259 };
260 if stopped_seen.load(Ordering::SeqCst) {
263 return (idx, None);
264 }
265 let outcome = body.run(frag).await;
266 if let PipelineOutcome::Stopped(_) = &outcome {
267 loop {
272 let cur = stopped_idx.load(Ordering::SeqCst);
273 if idx >= cur {
274 break; }
276 match stopped_idx.compare_exchange_weak(
277 cur,
278 idx,
279 Ordering::SeqCst,
280 Ordering::SeqCst,
281 ) {
282 Ok(_) => break,
283 Err(actual) => {
284 if actual <= idx {
287 break;
288 }
289 }
290 }
291 }
292 stopped_seen.store(true, Ordering::SeqCst);
293 }
294 (idx, Some(outcome))
295 });
296 }
297
298 let mut results: Vec<(usize, PipelineOutcome)> = Vec::new();
302 while let Some(res) = set.join_next().await {
303 if let Ok((idx, Some(o))) = res {
304 results.push((idx, o));
305 }
306 }
307
308 if stopped_seen.load(Ordering::SeqCst) {
310 let winning_idx = stopped_idx.load(Ordering::SeqCst);
311 if winning_idx == usize::MAX {
312 tracing::warn!(
313 target: "camel.phase4.split",
314 "stopped_seen=true but stopped_idx=usize::MAX — race condition; falling back to pre-split exchange"
315 );
316 return PipelineOutcome::Stopped(original);
317 }
318 let stopped_ex = results
319 .iter()
320 .find(|(idx, _)| *idx == winning_idx)
321 .and_then(|(_, o)| match o {
322 PipelineOutcome::Stopped(ex) => Some(ex.clone()),
323 _ => None,
324 });
325 if let Some(ex) = stopped_ex {
326 return PipelineOutcome::Stopped(ex);
327 }
328 tracing::warn!(
329 target: "camel.phase4.split",
330 winning_idx = winning_idx,
331 "winning_idx not found in results — falling back to pre-split exchange"
332 );
333 return PipelineOutcome::Stopped(original);
334 }
335
336 results.sort_by_key(|(idx, _)| *idx);
340 if stop_on_exception {
341 let mut first_failed: Option<(usize, CamelError)> = None;
342 for (idx, o) in &results {
343 if let PipelineOutcome::Failed(err) = o
344 && first_failed
345 .as_ref()
346 .map(|(i, _)| *i > *idx)
347 .unwrap_or(true)
348 {
349 first_failed = Some((*idx, err.clone()));
350 }
351 }
352 if let Some((_, err)) = first_failed {
353 return PipelineOutcome::Failed(err);
354 }
355 } else {
356 let mut last_error: Option<CamelError> = None;
358 for (_, o) in &results {
359 if let PipelineOutcome::Failed(err) = o {
360 last_error = Some(err.clone());
361 }
362 }
363 if let Some(err) = last_error {
364 return PipelineOutcome::Failed(err);
365 }
366 }
367
368 let completed: Vec<Exchange> = results
370 .into_iter()
371 .filter_map(|(_, o)| match o {
372 PipelineOutcome::Completed(ex) => Some(ex),
373 _ => None,
374 })
375 .collect();
376 PipelineOutcome::Completed(aggregate_completed(completed, original, aggregation))
377}
378
379#[cfg(test)]
382mod tests {
383 use super::*;
384 use camel_api::Message;
385
386 #[derive(Clone)]
390 #[allow(dead_code)]
391 struct CompletedBody;
392 impl camel_api::OutcomePipeline for CompletedBody {
393 fn clone_box(&self) -> Box<dyn camel_api::OutcomePipeline> {
394 Box::new(CompletedBody)
395 }
396 fn run<'a>(
397 &'a mut self,
398 exchange: Exchange,
399 ) -> Pin<Box<dyn Future<Output = PipelineOutcome> + Send + 'a>> {
400 Box::pin(async move { PipelineOutcome::Completed(exchange) })
401 }
402 }
403
404 #[derive(Clone)]
406 #[allow(dead_code)]
407 struct StopBody;
408 impl camel_api::OutcomePipeline for StopBody {
409 fn clone_box(&self) -> Box<dyn camel_api::OutcomePipeline> {
410 Box::new(StopBody)
411 }
412 fn run<'a>(
413 &'a mut self,
414 exchange: Exchange,
415 ) -> Pin<Box<dyn Future<Output = PipelineOutcome> + Send + 'a>> {
416 Box::pin(async move { PipelineOutcome::Stopped(exchange) })
417 }
418 }
419
420 #[derive(Clone)]
422 struct StopOnNthBody {
423 counter: Arc<AtomicUsize>,
424 stop_at: usize,
425 }
426 impl camel_api::OutcomePipeline for StopOnNthBody {
427 fn clone_box(&self) -> Box<dyn camel_api::OutcomePipeline> {
428 Box::new(self.clone())
429 }
430 fn run<'a>(
431 &'a mut self,
432 exchange: Exchange,
433 ) -> Pin<Box<dyn Future<Output = PipelineOutcome> + Send + 'a>> {
434 let count = self.counter.fetch_add(1, Ordering::SeqCst);
435 let stop_at = self.stop_at;
436 Box::pin(async move {
437 if count >= stop_at {
438 PipelineOutcome::Stopped(exchange)
439 } else {
440 PipelineOutcome::Completed(exchange)
441 }
442 })
443 }
444 }
445
446 #[derive(Clone)]
448 struct MutateAndStopBody;
449 impl camel_api::OutcomePipeline for MutateAndStopBody {
450 fn clone_box(&self) -> Box<dyn camel_api::OutcomePipeline> {
451 Box::new(MutateAndStopBody)
452 }
453 fn run<'a>(
454 &'a mut self,
455 mut exchange: Exchange,
456 ) -> Pin<Box<dyn Future<Output = PipelineOutcome> + Send + 'a>> {
457 Box::pin(async move {
458 exchange.input.body = Body::Text("mutated-by-body".to_string());
459 PipelineOutcome::Stopped(exchange)
460 })
461 }
462 }
463
464 #[tokio::test]
467 async fn stop_inside_split_sequential_halts_remaining_fragments() {
468 let invocations = Arc::new(AtomicUsize::new(0));
469 let body = StopOnNthBody {
470 counter: Arc::clone(&invocations),
471 stop_at: 1, };
473
474 let mut seg = SplitSegment {
475 splitter: camel_api::split_body_lines(),
476 body: OutcomeSegment::new(Box::new(body)),
477 parallel: false,
478 parallel_limit: None,
479 stop_on_exception: true,
480 aggregation: AggregationStrategy::LastWins,
481 };
482
483 let ex = Exchange::new(Message::new("a\nb\nc"));
484 let result = camel_api::OutcomePipeline::run(&mut seg, ex).await;
485
486 assert!(matches!(result, PipelineOutcome::Stopped(_)));
487 assert_eq!(invocations.load(Ordering::SeqCst), 2);
489 }
490
491 #[tokio::test]
494 async fn stop_inside_split_sequential_preserves_exchange_mutations() {
495 let mut seg = SplitSegment {
496 splitter: camel_api::split_body_lines(),
497 body: OutcomeSegment::new(Box::new(MutateAndStopBody)),
498 parallel: false,
499 parallel_limit: None,
500 stop_on_exception: true,
501 aggregation: AggregationStrategy::LastWins,
502 };
503
504 let ex = Exchange::new(Message::new("hello"));
505 let result = camel_api::OutcomePipeline::run(&mut seg, ex).await;
506
507 match result {
508 PipelineOutcome::Stopped(ex) => {
509 assert_eq!(
510 ex.input.body.as_text(),
511 Some("mutated-by-body"),
512 "Stopped exchange should carry body mutation"
513 );
514 }
515 other => panic!("Expected Stopped, got {other:?}"),
516 }
517 }
518
519 #[tokio::test(flavor = "multi_thread")]
530 async fn stop_inside_split_parallel_cancels_pending_and_waits_inflight() {
531 use tokio::sync::Barrier;
532
533 let barrier = Arc::new(Barrier::new(3));
534 let fragment1_completed = Arc::new(AtomicBool::new(false));
535 let fragment2_completed = Arc::new(AtomicBool::new(false));
536 let frag1_ok = Arc::clone(&fragment1_completed);
537 let frag2_ok = Arc::clone(&fragment2_completed);
538 let bar = Arc::clone(&barrier);
539
540 let splitter: SplitExpression = Arc::new(|ex: &Exchange| {
542 Ok((0..3)
543 .map(|i| {
544 let mut frag = ex.clone();
545 frag.input.body = Body::Text(format!("frag-{i}"));
546 frag
547 })
548 .collect())
549 });
550
551 struct BarrierDispatchBody {
557 barrier: Arc<Barrier>,
558 f1_completed: Arc<AtomicBool>,
559 f2_completed: Arc<AtomicBool>,
560 }
561 impl Clone for BarrierDispatchBody {
562 fn clone(&self) -> Self {
563 Self {
564 barrier: Arc::clone(&self.barrier),
565 f1_completed: Arc::clone(&self.f1_completed),
566 f2_completed: Arc::clone(&self.f2_completed),
567 }
568 }
569 }
570 impl camel_api::OutcomePipeline for BarrierDispatchBody {
571 fn clone_box(&self) -> Box<dyn camel_api::OutcomePipeline> {
572 Box::new(self.clone())
573 }
574 fn run<'a>(
575 &'a mut self,
576 exchange: Exchange,
577 ) -> Pin<Box<dyn Future<Output = PipelineOutcome> + Send + 'a>> {
578 let bar = Arc::clone(&self.barrier);
579 let f1c = Arc::clone(&self.f1_completed);
580 let f2c = Arc::clone(&self.f2_completed);
581 Box::pin(async move {
582 let body_text = exchange.input.body.as_text().unwrap_or("").to_string();
583
584 bar.wait().await;
588
589 match body_text.as_str() {
590 "frag-0" => PipelineOutcome::Stopped(exchange),
591 "frag-1" => {
592 tokio::time::sleep(std::time::Duration::from_millis(100)).await;
595 f1c.store(true, Ordering::SeqCst);
596 PipelineOutcome::Completed(exchange)
597 }
598 "frag-2" => {
599 f2c.store(true, Ordering::SeqCst);
600 PipelineOutcome::Completed(exchange)
601 }
602 _ => PipelineOutcome::Completed(exchange),
603 }
604 })
605 }
606 }
607
608 let body = BarrierDispatchBody {
609 barrier: bar,
610 f1_completed: frag1_ok,
611 f2_completed: frag2_ok,
612 };
613
614 let mut seg = SplitSegment {
615 splitter,
616 body: OutcomeSegment::new(Box::new(body)),
617 parallel: true,
618 parallel_limit: None,
619 stop_on_exception: true,
620 aggregation: AggregationStrategy::LastWins,
621 };
622
623 let ex = Exchange::new(Message::new("test"));
624 let result = camel_api::OutcomePipeline::run(&mut seg, ex).await;
625
626 assert!(
627 matches!(result, PipelineOutcome::Stopped(_)),
628 "Expected Stopped, got {result:?}"
629 );
630 assert!(
632 fragment1_completed.load(Ordering::SeqCst),
633 "fragment 1 should have completed despite Stop"
634 );
635 assert!(
637 fragment2_completed.load(Ordering::SeqCst),
638 "fragment 2 should have completed despite Stop"
639 );
640 }
641
642 #[tokio::test(flavor = "multi_thread")]
645 async fn stop_inside_split_parallel_lowest_stopped_index_wins() {
646 let splitter: SplitExpression = Arc::new(|ex: &Exchange| {
648 Ok((0..3)
649 .map(|i| {
650 let mut frag = ex.clone();
651 frag.input.body = Body::Text(format!("from-fragment-{i}"));
652 frag
653 })
654 .collect())
655 });
656
657 struct DualStopBody;
659 impl Clone for DualStopBody {
660 fn clone(&self) -> Self {
661 DualStopBody
662 }
663 }
664 impl camel_api::OutcomePipeline for DualStopBody {
665 fn clone_box(&self) -> Box<dyn camel_api::OutcomePipeline> {
666 Box::new(DualStopBody)
667 }
668 fn run<'a>(
669 &'a mut self,
670 exchange: Exchange,
671 ) -> Pin<Box<dyn Future<Output = PipelineOutcome> + Send + 'a>> {
672 let is_frag0 = exchange
673 .input
674 .body
675 .as_text()
676 .map(|s| s == "from-fragment-0")
677 .unwrap_or(false);
678 let is_frag2 = exchange
679 .input
680 .body
681 .as_text()
682 .map(|s| s == "from-fragment-2")
683 .unwrap_or(false);
684 Box::pin(async move {
685 if is_frag0 {
686 return PipelineOutcome::Stopped(exchange);
687 }
688 if is_frag2 {
689 tokio::time::sleep(std::time::Duration::from_millis(10)).await;
692 return PipelineOutcome::Stopped(exchange);
693 }
694 PipelineOutcome::Completed(exchange)
696 })
697 }
698 }
699
700 let mut seg = SplitSegment {
701 splitter,
702 body: OutcomeSegment::new(Box::new(DualStopBody)),
703 parallel: true,
704 parallel_limit: None,
705 stop_on_exception: true,
706 aggregation: AggregationStrategy::LastWins,
707 };
708
709 let ex = Exchange::new(Message::new("test"));
710 let result = camel_api::OutcomePipeline::run(&mut seg, ex).await;
711
712 match result {
713 PipelineOutcome::Stopped(ex) => {
714 assert_eq!(
715 ex.input.body.as_text(),
716 Some("from-fragment-0"),
717 "Lowest stopped index (0) should win, got body {:?}",
718 ex.input.body.as_text()
719 );
720 }
721 other => panic!("Expected Stopped with fragment-0 body, got {other:?}"),
722 }
723 }
724
725 #[tokio::test(flavor = "multi_thread")]
728 async fn split_parallel_limit_enforces_concurrency_cap() {
729 let concurrent = Arc::new(AtomicUsize::new(0));
730 let max_concurrent = Arc::new(AtomicUsize::new(0));
731
732 let splitter: SplitExpression = Arc::new(|ex: &Exchange| {
734 Ok((0..6)
735 .map(|i| {
736 let mut frag = ex.clone();
737 frag.input.body = Body::Text(format!("frag-{i}"));
738 frag
739 })
740 .collect())
741 });
742
743 let c = Arc::clone(&concurrent);
744 let mc = Arc::clone(&max_concurrent);
745 struct LimitedBody {
746 concurrent: Arc<AtomicUsize>,
747 max_concurrent: Arc<AtomicUsize>,
748 }
749 impl Clone for LimitedBody {
750 fn clone(&self) -> Self {
751 Self {
752 concurrent: Arc::clone(&self.concurrent),
753 max_concurrent: Arc::clone(&self.max_concurrent),
754 }
755 }
756 }
757 impl camel_api::OutcomePipeline for LimitedBody {
758 fn clone_box(&self) -> Box<dyn camel_api::OutcomePipeline> {
759 Box::new(self.clone())
760 }
761 fn run<'a>(
762 &'a mut self,
763 exchange: Exchange,
764 ) -> Pin<Box<dyn Future<Output = PipelineOutcome> + Send + 'a>> {
765 let c = Arc::clone(&self.concurrent);
766 let mc = Arc::clone(&self.max_concurrent);
767 Box::pin(async move {
768 let current = c.fetch_add(1, Ordering::SeqCst) + 1;
769 mc.fetch_max(current, Ordering::SeqCst);
770 tokio::task::yield_now().await;
771 c.fetch_sub(1, Ordering::SeqCst);
772 PipelineOutcome::Completed(exchange)
773 })
774 }
775 }
776
777 let mut seg = SplitSegment {
778 splitter,
779 body: OutcomeSegment::new(Box::new(LimitedBody {
780 concurrent: c,
781 max_concurrent: mc,
782 })),
783 parallel: true,
784 parallel_limit: Some(2),
785 stop_on_exception: true,
786 aggregation: AggregationStrategy::LastWins,
787 };
788
789 let ex = Exchange::new(Message::new("test"));
790 let result = camel_api::OutcomePipeline::run(&mut seg, ex).await;
791 assert!(
792 matches!(result, PipelineOutcome::Completed(_)),
793 "Expected Completed, got {result:?}"
794 );
795
796 let observed_max = max_concurrent.load(Ordering::SeqCst);
797 assert!(
798 observed_max <= 2,
799 "parallel_limit=2 but max concurrency was {observed_max}"
800 );
801 }
802
803 #[tokio::test]
806 async fn split_sequential_stop_on_exception_true() {
807 fn make_fail_body(
809 fail_at: usize,
810 counter: Arc<AtomicUsize>,
811 ) -> impl camel_api::OutcomePipeline + Clone {
812 #[derive(Clone)]
813 struct FailAtBody {
814 fail_at: usize,
815 counter: Arc<AtomicUsize>,
816 }
817 impl camel_api::OutcomePipeline for FailAtBody {
818 fn clone_box(&self) -> Box<dyn camel_api::OutcomePipeline> {
819 Box::new(self.clone())
820 }
821 fn run<'a>(
822 &'a mut self,
823 exchange: Exchange,
824 ) -> Pin<Box<dyn Future<Output = PipelineOutcome> + Send + 'a>> {
825 let count = self.counter.fetch_add(1, Ordering::SeqCst);
826 let fail_at = self.fail_at;
827 Box::pin(async move {
828 if count == fail_at {
829 PipelineOutcome::Failed(CamelError::ProcessorError(format!(
830 "fail at {count}"
831 )))
832 } else {
833 PipelineOutcome::Completed(exchange)
834 }
835 })
836 }
837 }
838 FailAtBody { fail_at, counter }
839 }
840
841 let invocations = Arc::new(AtomicUsize::new(0));
842 let body = make_fail_body(1, Arc::clone(&invocations));
843 let mut seg = SplitSegment {
844 splitter: camel_api::split_body_lines(),
845 body: OutcomeSegment::new(Box::new(body)),
846 parallel: false,
847 parallel_limit: None,
848 stop_on_exception: true,
849 aggregation: AggregationStrategy::LastWins,
850 };
851
852 let ex = Exchange::new(Message::new("a\nb\nc\nd\ne"));
853 let result = camel_api::OutcomePipeline::run(&mut seg, ex).await;
854
855 assert!(
856 matches!(result, PipelineOutcome::Failed(_)),
857 "stop_on_exception=true should propagate first failure"
858 );
859 assert_eq!(
862 invocations.load(Ordering::SeqCst),
863 2,
864 "should stop after 2 fragments (0 pass, 1 fail)"
865 );
866 }
867
868 #[tokio::test]
871 async fn split_sequential_stop_on_exception_false() {
872 fn make_fail_body(
874 fail_at: usize,
875 counter: Arc<AtomicUsize>,
876 ) -> impl camel_api::OutcomePipeline + Clone {
877 #[derive(Clone)]
878 struct FailAtBody {
879 fail_at: usize,
880 counter: Arc<AtomicUsize>,
881 }
882 impl camel_api::OutcomePipeline for FailAtBody {
883 fn clone_box(&self) -> Box<dyn camel_api::OutcomePipeline> {
884 Box::new(self.clone())
885 }
886 fn run<'a>(
887 &'a mut self,
888 exchange: Exchange,
889 ) -> Pin<Box<dyn Future<Output = PipelineOutcome> + Send + 'a>> {
890 let count = self.counter.fetch_add(1, Ordering::SeqCst);
891 let fail_at = self.fail_at;
892 Box::pin(async move {
893 if count == fail_at {
894 PipelineOutcome::Failed(CamelError::ProcessorError(format!(
895 "fail at {count}"
896 )))
897 } else {
898 PipelineOutcome::Completed(exchange)
899 }
900 })
901 }
902 }
903 FailAtBody { fail_at, counter }
904 }
905
906 let invocations = Arc::new(AtomicUsize::new(0));
907 let body = make_fail_body(1, Arc::clone(&invocations));
908 let mut seg = SplitSegment {
909 splitter: camel_api::split_body_lines(),
910 body: OutcomeSegment::new(Box::new(body)),
911 parallel: false,
912 parallel_limit: None,
913 stop_on_exception: false,
914 aggregation: AggregationStrategy::LastWins,
915 };
916
917 let ex = Exchange::new(Message::new("a\nb\nc\nd\ne"));
918 let result = camel_api::OutcomePipeline::run(&mut seg, ex).await;
919
920 assert!(
923 matches!(result, PipelineOutcome::Failed(_)),
924 "stop_on_exception=false should still propagate error at end"
925 );
926 assert_eq!(
928 invocations.load(Ordering::SeqCst),
929 5,
930 "all fragments should be processed when stop_on_exception=false"
931 );
932 }
933
934 #[tokio::test(flavor = "multi_thread")]
937 async fn split_parallel_stop_on_exception_true() {
938 let splitter: SplitExpression = Arc::new(|ex: &Exchange| {
939 Ok((0..5)
940 .map(|i| {
941 let mut frag = ex.clone();
942 frag.input.body = Body::Text(format!("frag-{i}"));
943 frag
944 })
945 .collect())
946 });
947
948 let invocations = Arc::new(AtomicUsize::new(0));
950 struct FailBody {
951 counter: Arc<AtomicUsize>,
952 }
953 impl Clone for FailBody {
954 fn clone(&self) -> Self {
955 Self {
956 counter: Arc::clone(&self.counter),
957 }
958 }
959 }
960 impl camel_api::OutcomePipeline for FailBody {
961 fn clone_box(&self) -> Box<dyn camel_api::OutcomePipeline> {
962 Box::new(self.clone())
963 }
964 fn run<'a>(
965 &'a mut self,
966 _exchange: Exchange,
967 ) -> Pin<Box<dyn Future<Output = PipelineOutcome> + Send + 'a>> {
968 let count = self.counter.fetch_add(1, Ordering::SeqCst);
969 Box::pin(async move {
970 PipelineOutcome::Failed(CamelError::ProcessorError(format!("fail {count}")))
971 })
972 }
973 }
974
975 let mut seg = SplitSegment {
976 splitter,
977 body: OutcomeSegment::new(Box::new(FailBody {
978 counter: Arc::clone(&invocations),
979 })),
980 parallel: true,
981 parallel_limit: None,
982 stop_on_exception: true,
983 aggregation: AggregationStrategy::LastWins,
984 };
985
986 let ex = Exchange::new(Message::new("test"));
987 let result = camel_api::OutcomePipeline::run(&mut seg, ex).await;
988
989 assert!(
990 matches!(result, PipelineOutcome::Failed(_)),
991 "stop_on_exception=true should propagate first failure"
992 );
993 assert_eq!(
995 invocations.load(Ordering::SeqCst),
996 5,
997 "all fragments should be spawned"
998 );
999 }
1000
1001 #[tokio::test(flavor = "multi_thread")]
1004 async fn split_parallel_stop_on_exception_false() {
1005 let splitter: SplitExpression = Arc::new(|ex: &Exchange| {
1006 Ok((0..5)
1007 .map(|i| {
1008 let mut frag = ex.clone();
1009 frag.input.body = Body::Text(format!("frag-{i}"));
1010 frag
1011 })
1012 .collect())
1013 });
1014
1015 let invocations = Arc::new(AtomicUsize::new(0));
1017 struct MixedBody {
1018 counter: Arc<AtomicUsize>,
1019 }
1020 impl Clone for MixedBody {
1021 fn clone(&self) -> Self {
1022 Self {
1023 counter: Arc::clone(&self.counter),
1024 }
1025 }
1026 }
1027 impl camel_api::OutcomePipeline for MixedBody {
1028 fn clone_box(&self) -> Box<dyn camel_api::OutcomePipeline> {
1029 Box::new(self.clone())
1030 }
1031 fn run<'a>(
1032 &'a mut self,
1033 exchange: Exchange,
1034 ) -> Pin<Box<dyn Future<Output = PipelineOutcome> + Send + 'a>> {
1035 let count = self.counter.fetch_add(1, Ordering::SeqCst);
1036 Box::pin(async move {
1037 if count == 1 {
1038 PipelineOutcome::Failed(CamelError::ProcessorError("fail 1".into()))
1039 } else {
1040 PipelineOutcome::Completed(exchange)
1041 }
1042 })
1043 }
1044 }
1045
1046 let mut seg = SplitSegment {
1047 splitter,
1048 body: OutcomeSegment::new(Box::new(MixedBody {
1049 counter: Arc::clone(&invocations),
1050 })),
1051 parallel: true,
1052 parallel_limit: None,
1053 stop_on_exception: false,
1054 aggregation: AggregationStrategy::LastWins,
1055 };
1056
1057 let ex = Exchange::new(Message::new("test"));
1058 let result = camel_api::OutcomePipeline::run(&mut seg, ex).await;
1059
1060 assert!(
1062 matches!(result, PipelineOutcome::Failed(_)),
1063 "stop_on_exception=false should propagate failure at end; got {result:?}"
1064 );
1065 assert_eq!(
1066 invocations.load(Ordering::SeqCst),
1067 5,
1068 "all fragments should be spawned"
1069 );
1070 }
1071
1072 #[tokio::test]
1075 async fn test_split_segment_expression_error_is_failed() {
1076 #[derive(Clone)]
1078 struct RecordingBody {
1079 counter: Arc<AtomicUsize>,
1080 }
1081 impl camel_api::OutcomePipeline for RecordingBody {
1082 fn clone_box(&self) -> Box<dyn camel_api::OutcomePipeline> {
1083 Box::new(self.clone())
1084 }
1085 fn run<'a>(
1086 &'a mut self,
1087 exchange: Exchange,
1088 ) -> Pin<Box<dyn Future<Output = PipelineOutcome> + Send + 'a>> {
1089 self.counter.fetch_add(1, Ordering::SeqCst);
1090 Box::pin(async move { PipelineOutcome::Completed(exchange) })
1091 }
1092 }
1093
1094 let invocations = Arc::new(AtomicUsize::new(0));
1095 let splitter: SplitExpression = Arc::new(|_| {
1096 Err(CamelError::TypeConversionFailed(
1097 "declarative split requires a text or array value, got number; add an unmarshal step before split"
1098 .to_string(),
1099 ))
1100 });
1101
1102 let mut seg = SplitSegment {
1103 splitter,
1104 body: OutcomeSegment::new(Box::new(RecordingBody {
1105 counter: Arc::clone(&invocations),
1106 })),
1107 parallel: false,
1108 parallel_limit: None,
1109 stop_on_exception: true,
1110 aggregation: AggregationStrategy::LastWins,
1111 };
1112
1113 let ex = Exchange::new(Message::new("anything"));
1114 let result = camel_api::OutcomePipeline::run(&mut seg, ex).await;
1115
1116 match result {
1117 PipelineOutcome::Failed(err) => {
1118 let msg = err.to_string();
1119 assert!(
1120 msg.contains("declarative split"),
1121 "carried error should mention 'declarative split': {msg}"
1122 );
1123 }
1124 other => panic!("Expected Failed, got {other:?}"),
1125 }
1126 assert_eq!(
1127 invocations.load(Ordering::SeqCst),
1128 0,
1129 "body segment must record zero invocations when the split expression errors"
1130 );
1131 }
1132 #[derive(Clone)]
1138 struct MetadataCaptureBody {
1139 triples: Arc<std::sync::Mutex<Vec<(u64, u64, bool)>>>,
1140 }
1141 impl camel_api::OutcomePipeline for MetadataCaptureBody {
1142 fn clone_box(&self) -> Box<dyn camel_api::OutcomePipeline> {
1143 Box::new(self.clone())
1144 }
1145 fn run<'a>(
1146 &'a mut self,
1147 exchange: Exchange,
1148 ) -> Pin<Box<dyn Future<Output = PipelineOutcome> + Send + 'a>> {
1149 let triples = Arc::clone(&self.triples);
1150 Box::pin(async move {
1151 let index = exchange
1152 .property(crate::splitter::CAMEL_SPLIT_INDEX)
1153 .and_then(|v| v.as_u64())
1154 .unwrap_or(u64::MAX);
1155 let size = exchange
1156 .property(crate::splitter::CAMEL_SPLIT_SIZE)
1157 .and_then(|v| v.as_u64())
1158 .unwrap_or(u64::MAX);
1159 let complete = exchange
1160 .property(crate::splitter::CAMEL_SPLIT_COMPLETE)
1161 .and_then(|v| v.as_bool())
1162 .unwrap_or(false);
1163 triples.lock().unwrap().push((index, size, complete));
1164 PipelineOutcome::Completed(exchange)
1165 })
1166 }
1167 }
1168
1169 fn n_fragment_splitter(n: u64) -> SplitExpression {
1171 Arc::new(move |ex: &Exchange| {
1172 Ok((0..n)
1173 .map(|i| {
1174 let mut frag = ex.clone();
1175 frag.input.body = Body::Text(format!("frag-{i}"));
1176 frag
1177 })
1178 .collect())
1179 })
1180 }
1181
1182 #[tokio::test]
1183 async fn sequential_split_stamps_fragment_metadata() {
1184 let triples = Arc::new(std::sync::Mutex::new(Vec::new()));
1185
1186 let mut seg = SplitSegment {
1187 splitter: n_fragment_splitter(3),
1188 body: OutcomeSegment::new(Box::new(MetadataCaptureBody {
1189 triples: Arc::clone(&triples),
1190 })),
1191 parallel: false,
1192 parallel_limit: None,
1193 stop_on_exception: true,
1194 aggregation: AggregationStrategy::LastWins,
1195 };
1196
1197 let ex = Exchange::new(Message::new("test"));
1198 let result = camel_api::OutcomePipeline::run(&mut seg, ex).await;
1199 assert!(
1200 matches!(result, PipelineOutcome::Completed(_)),
1201 "Expected Completed, got {result:?}"
1202 );
1203
1204 assert_eq!(
1205 *triples.lock().unwrap(),
1206 vec![(0, 3, false), (1, 3, false), (2, 3, true)]
1207 );
1208 }
1209
1210 #[tokio::test(flavor = "multi_thread")]
1211 async fn parallel_split_stamps_fragment_metadata() {
1212 let triples = Arc::new(std::sync::Mutex::new(Vec::new()));
1213
1214 let mut seg = SplitSegment {
1215 splitter: n_fragment_splitter(3),
1216 body: OutcomeSegment::new(Box::new(MetadataCaptureBody {
1217 triples: Arc::clone(&triples),
1218 })),
1219 parallel: true,
1220 parallel_limit: None,
1221 stop_on_exception: true,
1222 aggregation: AggregationStrategy::LastWins,
1223 };
1224
1225 let ex = Exchange::new(Message::new("test"));
1226 let result = camel_api::OutcomePipeline::run(&mut seg, ex).await;
1227 assert!(
1228 matches!(result, PipelineOutcome::Completed(_)),
1229 "Expected Completed, got {result:?}"
1230 );
1231
1232 let mut got = triples.lock().unwrap().clone();
1233 got.sort_by_key(|(idx, _, _)| *idx);
1234 assert_eq!(got, vec![(0, 3, false), (1, 3, false), (2, 3, true)]);
1235 }
1236
1237 #[tokio::test]
1238 async fn single_fragment_is_complete() {
1239 let triples = Arc::new(std::sync::Mutex::new(Vec::new()));
1240
1241 let mut seg = SplitSegment {
1242 splitter: n_fragment_splitter(1),
1243 body: OutcomeSegment::new(Box::new(MetadataCaptureBody {
1244 triples: Arc::clone(&triples),
1245 })),
1246 parallel: false,
1247 parallel_limit: None,
1248 stop_on_exception: true,
1249 aggregation: AggregationStrategy::LastWins,
1250 };
1251
1252 let ex = Exchange::new(Message::new("test"));
1253 let result = camel_api::OutcomePipeline::run(&mut seg, ex).await;
1254 assert!(
1255 matches!(result, PipelineOutcome::Completed(_)),
1256 "Expected Completed, got {result:?}"
1257 );
1258
1259 assert_eq!(*triples.lock().unwrap(), vec![(0, 1, true)]);
1260 }
1261}