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
16pub(crate) fn aggregate_completed(
23 completed: Vec<Exchange>,
24 original: Exchange,
25 strategy: AggregationStrategy,
26) -> Exchange {
27 match strategy {
28 AggregationStrategy::LastWins => completed.into_iter().last().unwrap_or(original),
29 AggregationStrategy::CollectAll => {
30 let mut bodies = Vec::new();
31 for ex in &completed {
32 let value = match &ex.input.body {
33 Body::Text(s) => Value::String(s.clone()),
34 Body::Json(v) => v.clone(),
35 Body::Xml(s) => Value::String(s.clone()),
36 Body::Bytes(b) => Value::String(String::from_utf8_lossy(b).into_owned()),
37 Body::Stream(s) => serde_json::json!({
38 "_stream": {
39 "origin": s.metadata.origin,
40 "placeholder": true,
41 "hint": "Materialize exchange body with .into_bytes() before aggregation if content needed"
42 }
43 }),
44 _ => Value::Null,
46 };
47 bodies.push(value);
48 }
49 let mut out = original;
50 out.input.body = Body::Json(Value::Array(bodies));
51 out
52 }
53 AggregationStrategy::Custom(fold_fn) => {
54 let mut iter = completed.into_iter();
55 let first = iter.next().unwrap_or(original);
56 iter.fold(first, |acc, next| fold_fn(acc, next))
57 }
58 _ => original,
60 }
61}
62
63pub struct SplitSegment {
84 pub splitter: SplitExpression,
86 pub body: OutcomeSegment,
88 pub parallel: bool,
90 pub parallel_limit: Option<usize>,
92 pub stop_on_exception: bool,
102 pub aggregation: AggregationStrategy,
104}
105
106impl Clone for SplitSegment {
107 fn clone(&self) -> Self {
108 Self {
109 splitter: Arc::clone(&self.splitter),
110 body: self.body.clone(),
111 parallel: self.parallel,
112 parallel_limit: self.parallel_limit,
113 stop_on_exception: self.stop_on_exception,
114 aggregation: self.aggregation.clone(),
115 }
116 }
117}
118
119impl camel_api::OutcomePipeline for SplitSegment {
120 fn clone_box(&self) -> Box<dyn camel_api::OutcomePipeline> {
121 Box::new(self.clone())
122 }
123
124 fn run<'a>(
125 &'a mut self,
126 exchange: camel_api::Exchange,
127 ) -> Pin<Box<dyn Future<Output = camel_api::PipelineOutcome> + Send + 'a>> {
128 let splitter = Arc::clone(&self.splitter);
129 let aggregation = self.aggregation.clone();
130 let parallel = self.parallel;
131 let parallel_limit = self.parallel_limit;
132 let stop_on_exception = self.stop_on_exception;
133 let body = &mut self.body;
134
135 Box::pin(async move {
136 let original = exchange;
137 let fragments = splitter(&original);
138
139 if fragments.is_empty() {
140 return PipelineOutcome::Completed(original);
141 }
142
143 if parallel {
144 parallel_split(
145 fragments,
146 original,
147 body,
148 &aggregation,
149 parallel_limit,
150 stop_on_exception,
151 )
152 .await
153 } else {
154 sequential_split(fragments, original, body, &aggregation, stop_on_exception).await
155 }
156 })
157 }
158}
159
160async fn sequential_split(
163 fragments: Vec<Exchange>,
164 original: Exchange,
165 body: &mut OutcomeSegment,
166 aggregation: &AggregationStrategy,
167 stop_on_exception: bool,
168) -> PipelineOutcome {
169 let mut outputs = Vec::new();
170 let mut last_error: Option<CamelError> = None;
171 for frag in fragments {
172 match body.run(frag).await {
173 PipelineOutcome::Completed(ex) => outputs.push(ex),
174 PipelineOutcome::Stopped(ex) => return PipelineOutcome::Stopped(ex),
175 PipelineOutcome::Failed(err) => {
176 if stop_on_exception {
177 return PipelineOutcome::Failed(err);
178 }
179 last_error = Some(err);
181 }
182 }
183 }
184 if let Some(err) = last_error {
185 return PipelineOutcome::Failed(err);
186 }
187 PipelineOutcome::Completed(aggregate_completed(outputs, original, aggregation.clone()))
188}
189
190async fn parallel_split(
197 fragments: Vec<Exchange>,
198 original: Exchange,
199 body: &mut OutcomeSegment,
200 aggregation: &AggregationStrategy,
201 parallel_limit: Option<usize>,
202 stop_on_exception: bool,
203) -> PipelineOutcome {
204 use tokio::sync::Semaphore;
205
206 let stopped_seen = Arc::new(AtomicBool::new(false));
207 let stopped_idx = Arc::new(AtomicUsize::new(usize::MAX));
208 let aggregation = aggregation.clone();
209 let semaphore = parallel_limit
210 .filter(|&limit| limit > 0)
211 .map(|limit| Arc::new(Semaphore::new(limit)));
212
213 let mut set: JoinSet<(usize, Option<PipelineOutcome>)> = JoinSet::new();
214
215 for (idx, frag) in fragments.into_iter().enumerate() {
216 let mut body = body.clone();
217 let stopped_seen = Arc::clone(&stopped_seen);
218 let stopped_idx = Arc::clone(&stopped_idx);
219 let sem = semaphore.clone();
220 set.spawn(async move {
221 if stopped_seen.load(Ordering::SeqCst) {
226 return (idx, None);
227 }
228 let _permit: Option<tokio::sync::OwnedSemaphorePermit> = match &sem {
230 Some(s) => match std::sync::Arc::clone(s).acquire_owned().await {
231 Ok(p) => Some(p),
232 Err(_) => {
233 return (
234 idx,
235 Some(PipelineOutcome::Failed(CamelError::ProcessorError(
236 "semaphore closed".into(),
237 ))),
238 );
239 }
240 },
241 None => None,
242 };
243 if stopped_seen.load(Ordering::SeqCst) {
246 return (idx, None);
247 }
248 let outcome = body.run(frag).await;
249 if let PipelineOutcome::Stopped(_) = &outcome {
250 loop {
255 let cur = stopped_idx.load(Ordering::SeqCst);
256 if idx >= cur {
257 break; }
259 match stopped_idx.compare_exchange_weak(
260 cur,
261 idx,
262 Ordering::SeqCst,
263 Ordering::SeqCst,
264 ) {
265 Ok(_) => break,
266 Err(actual) => {
267 if actual <= idx {
270 break;
271 }
272 }
273 }
274 }
275 stopped_seen.store(true, Ordering::SeqCst);
276 }
277 (idx, Some(outcome))
278 });
279 }
280
281 let mut results: Vec<(usize, PipelineOutcome)> = Vec::new();
285 while let Some(res) = set.join_next().await {
286 if let Ok((idx, Some(o))) = res {
287 results.push((idx, o));
288 }
289 }
290
291 if stopped_seen.load(Ordering::SeqCst) {
293 let winning_idx = stopped_idx.load(Ordering::SeqCst);
294 if winning_idx == usize::MAX {
295 tracing::warn!(
296 target: "camel.phase4.split",
297 "stopped_seen=true but stopped_idx=usize::MAX — race condition; falling back to pre-split exchange"
298 );
299 return PipelineOutcome::Stopped(original);
300 }
301 let stopped_ex = results
302 .iter()
303 .find(|(idx, _)| *idx == winning_idx)
304 .and_then(|(_, o)| match o {
305 PipelineOutcome::Stopped(ex) => Some(ex.clone()),
306 _ => None,
307 });
308 if let Some(ex) = stopped_ex {
309 return PipelineOutcome::Stopped(ex);
310 }
311 tracing::warn!(
312 target: "camel.phase4.split",
313 winning_idx = winning_idx,
314 "winning_idx not found in results — falling back to pre-split exchange"
315 );
316 return PipelineOutcome::Stopped(original);
317 }
318
319 results.sort_by_key(|(idx, _)| *idx);
323 if stop_on_exception {
324 let mut first_failed: Option<(usize, CamelError)> = None;
325 for (idx, o) in &results {
326 if let PipelineOutcome::Failed(err) = o
327 && first_failed
328 .as_ref()
329 .map(|(i, _)| *i > *idx)
330 .unwrap_or(true)
331 {
332 first_failed = Some((*idx, err.clone()));
333 }
334 }
335 if let Some((_, err)) = first_failed {
336 return PipelineOutcome::Failed(err);
337 }
338 } else {
339 let mut last_error: Option<CamelError> = None;
341 for (_, o) in &results {
342 if let PipelineOutcome::Failed(err) = o {
343 last_error = Some(err.clone());
344 }
345 }
346 if let Some(err) = last_error {
347 return PipelineOutcome::Failed(err);
348 }
349 }
350
351 let completed: Vec<Exchange> = results
353 .into_iter()
354 .filter_map(|(_, o)| match o {
355 PipelineOutcome::Completed(ex) => Some(ex),
356 _ => None,
357 })
358 .collect();
359 PipelineOutcome::Completed(aggregate_completed(completed, original, aggregation))
360}
361
362#[cfg(test)]
365mod tests {
366 use super::*;
367 use camel_api::Message;
368
369 #[derive(Clone)]
373 #[allow(dead_code)]
374 struct CompletedBody;
375 impl camel_api::OutcomePipeline for CompletedBody {
376 fn clone_box(&self) -> Box<dyn camel_api::OutcomePipeline> {
377 Box::new(CompletedBody)
378 }
379 fn run<'a>(
380 &'a mut self,
381 exchange: Exchange,
382 ) -> Pin<Box<dyn Future<Output = PipelineOutcome> + Send + 'a>> {
383 Box::pin(async move { PipelineOutcome::Completed(exchange) })
384 }
385 }
386
387 #[derive(Clone)]
389 #[allow(dead_code)]
390 struct StopBody;
391 impl camel_api::OutcomePipeline for StopBody {
392 fn clone_box(&self) -> Box<dyn camel_api::OutcomePipeline> {
393 Box::new(StopBody)
394 }
395 fn run<'a>(
396 &'a mut self,
397 exchange: Exchange,
398 ) -> Pin<Box<dyn Future<Output = PipelineOutcome> + Send + 'a>> {
399 Box::pin(async move { PipelineOutcome::Stopped(exchange) })
400 }
401 }
402
403 #[derive(Clone)]
405 struct StopOnNthBody {
406 counter: Arc<AtomicUsize>,
407 stop_at: usize,
408 }
409 impl camel_api::OutcomePipeline for StopOnNthBody {
410 fn clone_box(&self) -> Box<dyn camel_api::OutcomePipeline> {
411 Box::new(self.clone())
412 }
413 fn run<'a>(
414 &'a mut self,
415 exchange: Exchange,
416 ) -> Pin<Box<dyn Future<Output = PipelineOutcome> + Send + 'a>> {
417 let count = self.counter.fetch_add(1, Ordering::SeqCst);
418 let stop_at = self.stop_at;
419 Box::pin(async move {
420 if count >= stop_at {
421 PipelineOutcome::Stopped(exchange)
422 } else {
423 PipelineOutcome::Completed(exchange)
424 }
425 })
426 }
427 }
428
429 #[derive(Clone)]
431 struct MutateAndStopBody;
432 impl camel_api::OutcomePipeline for MutateAndStopBody {
433 fn clone_box(&self) -> Box<dyn camel_api::OutcomePipeline> {
434 Box::new(MutateAndStopBody)
435 }
436 fn run<'a>(
437 &'a mut self,
438 mut exchange: Exchange,
439 ) -> Pin<Box<dyn Future<Output = PipelineOutcome> + Send + 'a>> {
440 Box::pin(async move {
441 exchange.input.body = Body::Text("mutated-by-body".to_string());
442 PipelineOutcome::Stopped(exchange)
443 })
444 }
445 }
446
447 #[tokio::test]
450 async fn stop_inside_split_sequential_halts_remaining_fragments() {
451 let invocations = Arc::new(AtomicUsize::new(0));
452 let body = StopOnNthBody {
453 counter: Arc::clone(&invocations),
454 stop_at: 1, };
456
457 let mut seg = SplitSegment {
458 splitter: camel_api::split_body_lines(),
459 body: OutcomeSegment::new(Box::new(body)),
460 parallel: false,
461 parallel_limit: None,
462 stop_on_exception: true,
463 aggregation: AggregationStrategy::LastWins,
464 };
465
466 let ex = Exchange::new(Message::new("a\nb\nc"));
467 let result = camel_api::OutcomePipeline::run(&mut seg, ex).await;
468
469 assert!(matches!(result, PipelineOutcome::Stopped(_)));
470 assert_eq!(invocations.load(Ordering::SeqCst), 2);
472 }
473
474 #[tokio::test]
477 async fn stop_inside_split_sequential_preserves_exchange_mutations() {
478 let mut seg = SplitSegment {
479 splitter: camel_api::split_body_lines(),
480 body: OutcomeSegment::new(Box::new(MutateAndStopBody)),
481 parallel: false,
482 parallel_limit: None,
483 stop_on_exception: true,
484 aggregation: AggregationStrategy::LastWins,
485 };
486
487 let ex = Exchange::new(Message::new("hello"));
488 let result = camel_api::OutcomePipeline::run(&mut seg, ex).await;
489
490 match result {
491 PipelineOutcome::Stopped(ex) => {
492 assert_eq!(
493 ex.input.body.as_text(),
494 Some("mutated-by-body"),
495 "Stopped exchange should carry body mutation"
496 );
497 }
498 other => panic!("Expected Stopped, got {other:?}"),
499 }
500 }
501
502 #[tokio::test(flavor = "multi_thread")]
513 async fn stop_inside_split_parallel_cancels_pending_and_waits_inflight() {
514 use tokio::sync::Barrier;
515
516 let barrier = Arc::new(Barrier::new(3));
517 let fragment1_completed = Arc::new(AtomicBool::new(false));
518 let fragment2_completed = Arc::new(AtomicBool::new(false));
519 let frag1_ok = Arc::clone(&fragment1_completed);
520 let frag2_ok = Arc::clone(&fragment2_completed);
521 let bar = Arc::clone(&barrier);
522
523 let splitter: SplitExpression = Arc::new(|ex: &Exchange| {
525 (0..3)
526 .map(|i| {
527 let mut frag = ex.clone();
528 frag.input.body = Body::Text(format!("frag-{i}"));
529 frag
530 })
531 .collect()
532 });
533
534 struct BarrierDispatchBody {
540 barrier: Arc<Barrier>,
541 f1_completed: Arc<AtomicBool>,
542 f2_completed: Arc<AtomicBool>,
543 }
544 impl Clone for BarrierDispatchBody {
545 fn clone(&self) -> Self {
546 Self {
547 barrier: Arc::clone(&self.barrier),
548 f1_completed: Arc::clone(&self.f1_completed),
549 f2_completed: Arc::clone(&self.f2_completed),
550 }
551 }
552 }
553 impl camel_api::OutcomePipeline for BarrierDispatchBody {
554 fn clone_box(&self) -> Box<dyn camel_api::OutcomePipeline> {
555 Box::new(self.clone())
556 }
557 fn run<'a>(
558 &'a mut self,
559 exchange: Exchange,
560 ) -> Pin<Box<dyn Future<Output = PipelineOutcome> + Send + 'a>> {
561 let bar = Arc::clone(&self.barrier);
562 let f1c = Arc::clone(&self.f1_completed);
563 let f2c = Arc::clone(&self.f2_completed);
564 Box::pin(async move {
565 let body_text = exchange.input.body.as_text().unwrap_or("").to_string();
566
567 bar.wait().await;
571
572 match body_text.as_str() {
573 "frag-0" => PipelineOutcome::Stopped(exchange),
574 "frag-1" => {
575 tokio::time::sleep(std::time::Duration::from_millis(100)).await;
578 f1c.store(true, Ordering::SeqCst);
579 PipelineOutcome::Completed(exchange)
580 }
581 "frag-2" => {
582 f2c.store(true, Ordering::SeqCst);
583 PipelineOutcome::Completed(exchange)
584 }
585 _ => PipelineOutcome::Completed(exchange),
586 }
587 })
588 }
589 }
590
591 let body = BarrierDispatchBody {
592 barrier: bar,
593 f1_completed: frag1_ok,
594 f2_completed: frag2_ok,
595 };
596
597 let mut seg = SplitSegment {
598 splitter,
599 body: OutcomeSegment::new(Box::new(body)),
600 parallel: true,
601 parallel_limit: None,
602 stop_on_exception: true,
603 aggregation: AggregationStrategy::LastWins,
604 };
605
606 let ex = Exchange::new(Message::new("test"));
607 let result = camel_api::OutcomePipeline::run(&mut seg, ex).await;
608
609 assert!(
610 matches!(result, PipelineOutcome::Stopped(_)),
611 "Expected Stopped, got {result:?}"
612 );
613 assert!(
615 fragment1_completed.load(Ordering::SeqCst),
616 "fragment 1 should have completed despite Stop"
617 );
618 assert!(
620 fragment2_completed.load(Ordering::SeqCst),
621 "fragment 2 should have completed despite Stop"
622 );
623 }
624
625 #[tokio::test(flavor = "multi_thread")]
628 async fn stop_inside_split_parallel_lowest_stopped_index_wins() {
629 let splitter: SplitExpression = Arc::new(|ex: &Exchange| {
631 (0..3)
632 .map(|i| {
633 let mut frag = ex.clone();
634 frag.input.body = Body::Text(format!("from-fragment-{i}"));
635 frag
636 })
637 .collect()
638 });
639
640 struct DualStopBody;
642 impl Clone for DualStopBody {
643 fn clone(&self) -> Self {
644 DualStopBody
645 }
646 }
647 impl camel_api::OutcomePipeline for DualStopBody {
648 fn clone_box(&self) -> Box<dyn camel_api::OutcomePipeline> {
649 Box::new(DualStopBody)
650 }
651 fn run<'a>(
652 &'a mut self,
653 exchange: Exchange,
654 ) -> Pin<Box<dyn Future<Output = PipelineOutcome> + Send + 'a>> {
655 let is_frag0 = exchange
656 .input
657 .body
658 .as_text()
659 .map(|s| s == "from-fragment-0")
660 .unwrap_or(false);
661 let is_frag2 = exchange
662 .input
663 .body
664 .as_text()
665 .map(|s| s == "from-fragment-2")
666 .unwrap_or(false);
667 Box::pin(async move {
668 if is_frag0 {
669 return PipelineOutcome::Stopped(exchange);
670 }
671 if is_frag2 {
672 tokio::time::sleep(std::time::Duration::from_millis(10)).await;
675 return PipelineOutcome::Stopped(exchange);
676 }
677 PipelineOutcome::Completed(exchange)
679 })
680 }
681 }
682
683 let mut seg = SplitSegment {
684 splitter,
685 body: OutcomeSegment::new(Box::new(DualStopBody)),
686 parallel: true,
687 parallel_limit: None,
688 stop_on_exception: true,
689 aggregation: AggregationStrategy::LastWins,
690 };
691
692 let ex = Exchange::new(Message::new("test"));
693 let result = camel_api::OutcomePipeline::run(&mut seg, ex).await;
694
695 match result {
696 PipelineOutcome::Stopped(ex) => {
697 assert_eq!(
698 ex.input.body.as_text(),
699 Some("from-fragment-0"),
700 "Lowest stopped index (0) should win, got body {:?}",
701 ex.input.body.as_text()
702 );
703 }
704 other => panic!("Expected Stopped with fragment-0 body, got {other:?}"),
705 }
706 }
707
708 #[tokio::test(flavor = "multi_thread")]
711 async fn split_parallel_limit_enforces_concurrency_cap() {
712 let concurrent = Arc::new(AtomicUsize::new(0));
713 let max_concurrent = Arc::new(AtomicUsize::new(0));
714
715 let splitter: SplitExpression = Arc::new(|ex: &Exchange| {
717 (0..6)
718 .map(|i| {
719 let mut frag = ex.clone();
720 frag.input.body = Body::Text(format!("frag-{i}"));
721 frag
722 })
723 .collect()
724 });
725
726 let c = Arc::clone(&concurrent);
727 let mc = Arc::clone(&max_concurrent);
728 struct LimitedBody {
729 concurrent: Arc<AtomicUsize>,
730 max_concurrent: Arc<AtomicUsize>,
731 }
732 impl Clone for LimitedBody {
733 fn clone(&self) -> Self {
734 Self {
735 concurrent: Arc::clone(&self.concurrent),
736 max_concurrent: Arc::clone(&self.max_concurrent),
737 }
738 }
739 }
740 impl camel_api::OutcomePipeline for LimitedBody {
741 fn clone_box(&self) -> Box<dyn camel_api::OutcomePipeline> {
742 Box::new(self.clone())
743 }
744 fn run<'a>(
745 &'a mut self,
746 exchange: Exchange,
747 ) -> Pin<Box<dyn Future<Output = PipelineOutcome> + Send + 'a>> {
748 let c = Arc::clone(&self.concurrent);
749 let mc = Arc::clone(&self.max_concurrent);
750 Box::pin(async move {
751 let current = c.fetch_add(1, Ordering::SeqCst) + 1;
752 mc.fetch_max(current, Ordering::SeqCst);
753 tokio::task::yield_now().await;
754 c.fetch_sub(1, Ordering::SeqCst);
755 PipelineOutcome::Completed(exchange)
756 })
757 }
758 }
759
760 let mut seg = SplitSegment {
761 splitter,
762 body: OutcomeSegment::new(Box::new(LimitedBody {
763 concurrent: c,
764 max_concurrent: mc,
765 })),
766 parallel: true,
767 parallel_limit: Some(2),
768 stop_on_exception: true,
769 aggregation: AggregationStrategy::LastWins,
770 };
771
772 let ex = Exchange::new(Message::new("test"));
773 let result = camel_api::OutcomePipeline::run(&mut seg, ex).await;
774 assert!(
775 matches!(result, PipelineOutcome::Completed(_)),
776 "Expected Completed, got {result:?}"
777 );
778
779 let observed_max = max_concurrent.load(Ordering::SeqCst);
780 assert!(
781 observed_max <= 2,
782 "parallel_limit=2 but max concurrency was {observed_max}"
783 );
784 }
785
786 #[tokio::test]
789 async fn split_sequential_stop_on_exception_true() {
790 fn make_fail_body(
792 fail_at: usize,
793 counter: Arc<AtomicUsize>,
794 ) -> impl camel_api::OutcomePipeline + Clone {
795 #[derive(Clone)]
796 struct FailAtBody {
797 fail_at: usize,
798 counter: Arc<AtomicUsize>,
799 }
800 impl camel_api::OutcomePipeline for FailAtBody {
801 fn clone_box(&self) -> Box<dyn camel_api::OutcomePipeline> {
802 Box::new(self.clone())
803 }
804 fn run<'a>(
805 &'a mut self,
806 exchange: Exchange,
807 ) -> Pin<Box<dyn Future<Output = PipelineOutcome> + Send + 'a>> {
808 let count = self.counter.fetch_add(1, Ordering::SeqCst);
809 let fail_at = self.fail_at;
810 Box::pin(async move {
811 if count == fail_at {
812 PipelineOutcome::Failed(CamelError::ProcessorError(format!(
813 "fail at {count}"
814 )))
815 } else {
816 PipelineOutcome::Completed(exchange)
817 }
818 })
819 }
820 }
821 FailAtBody { fail_at, counter }
822 }
823
824 let invocations = Arc::new(AtomicUsize::new(0));
825 let body = make_fail_body(1, Arc::clone(&invocations));
826 let mut seg = SplitSegment {
827 splitter: camel_api::split_body_lines(),
828 body: OutcomeSegment::new(Box::new(body)),
829 parallel: false,
830 parallel_limit: None,
831 stop_on_exception: true,
832 aggregation: AggregationStrategy::LastWins,
833 };
834
835 let ex = Exchange::new(Message::new("a\nb\nc\nd\ne"));
836 let result = camel_api::OutcomePipeline::run(&mut seg, ex).await;
837
838 assert!(
839 matches!(result, PipelineOutcome::Failed(_)),
840 "stop_on_exception=true should propagate first failure"
841 );
842 assert_eq!(
845 invocations.load(Ordering::SeqCst),
846 2,
847 "should stop after 2 fragments (0 pass, 1 fail)"
848 );
849 }
850
851 #[tokio::test]
854 async fn split_sequential_stop_on_exception_false() {
855 fn make_fail_body(
857 fail_at: usize,
858 counter: Arc<AtomicUsize>,
859 ) -> impl camel_api::OutcomePipeline + Clone {
860 #[derive(Clone)]
861 struct FailAtBody {
862 fail_at: usize,
863 counter: Arc<AtomicUsize>,
864 }
865 impl camel_api::OutcomePipeline for FailAtBody {
866 fn clone_box(&self) -> Box<dyn camel_api::OutcomePipeline> {
867 Box::new(self.clone())
868 }
869 fn run<'a>(
870 &'a mut self,
871 exchange: Exchange,
872 ) -> Pin<Box<dyn Future<Output = PipelineOutcome> + Send + 'a>> {
873 let count = self.counter.fetch_add(1, Ordering::SeqCst);
874 let fail_at = self.fail_at;
875 Box::pin(async move {
876 if count == fail_at {
877 PipelineOutcome::Failed(CamelError::ProcessorError(format!(
878 "fail at {count}"
879 )))
880 } else {
881 PipelineOutcome::Completed(exchange)
882 }
883 })
884 }
885 }
886 FailAtBody { fail_at, counter }
887 }
888
889 let invocations = Arc::new(AtomicUsize::new(0));
890 let body = make_fail_body(1, Arc::clone(&invocations));
891 let mut seg = SplitSegment {
892 splitter: camel_api::split_body_lines(),
893 body: OutcomeSegment::new(Box::new(body)),
894 parallel: false,
895 parallel_limit: None,
896 stop_on_exception: false,
897 aggregation: AggregationStrategy::LastWins,
898 };
899
900 let ex = Exchange::new(Message::new("a\nb\nc\nd\ne"));
901 let result = camel_api::OutcomePipeline::run(&mut seg, ex).await;
902
903 assert!(
906 matches!(result, PipelineOutcome::Failed(_)),
907 "stop_on_exception=false should still propagate error at end"
908 );
909 assert_eq!(
911 invocations.load(Ordering::SeqCst),
912 5,
913 "all fragments should be processed when stop_on_exception=false"
914 );
915 }
916
917 #[tokio::test(flavor = "multi_thread")]
920 async fn split_parallel_stop_on_exception_true() {
921 let splitter: SplitExpression = Arc::new(|ex: &Exchange| {
922 (0..5)
923 .map(|i| {
924 let mut frag = ex.clone();
925 frag.input.body = Body::Text(format!("frag-{i}"));
926 frag
927 })
928 .collect()
929 });
930
931 let invocations = Arc::new(AtomicUsize::new(0));
933 struct FailBody {
934 counter: Arc<AtomicUsize>,
935 }
936 impl Clone for FailBody {
937 fn clone(&self) -> Self {
938 Self {
939 counter: Arc::clone(&self.counter),
940 }
941 }
942 }
943 impl camel_api::OutcomePipeline for FailBody {
944 fn clone_box(&self) -> Box<dyn camel_api::OutcomePipeline> {
945 Box::new(self.clone())
946 }
947 fn run<'a>(
948 &'a mut self,
949 _exchange: Exchange,
950 ) -> Pin<Box<dyn Future<Output = PipelineOutcome> + Send + 'a>> {
951 let count = self.counter.fetch_add(1, Ordering::SeqCst);
952 Box::pin(async move {
953 PipelineOutcome::Failed(CamelError::ProcessorError(format!("fail {count}")))
954 })
955 }
956 }
957
958 let mut seg = SplitSegment {
959 splitter,
960 body: OutcomeSegment::new(Box::new(FailBody {
961 counter: Arc::clone(&invocations),
962 })),
963 parallel: true,
964 parallel_limit: None,
965 stop_on_exception: true,
966 aggregation: AggregationStrategy::LastWins,
967 };
968
969 let ex = Exchange::new(Message::new("test"));
970 let result = camel_api::OutcomePipeline::run(&mut seg, ex).await;
971
972 assert!(
973 matches!(result, PipelineOutcome::Failed(_)),
974 "stop_on_exception=true should propagate first failure"
975 );
976 assert_eq!(
978 invocations.load(Ordering::SeqCst),
979 5,
980 "all fragments should be spawned"
981 );
982 }
983
984 #[tokio::test(flavor = "multi_thread")]
987 async fn split_parallel_stop_on_exception_false() {
988 let splitter: SplitExpression = Arc::new(|ex: &Exchange| {
989 (0..5)
990 .map(|i| {
991 let mut frag = ex.clone();
992 frag.input.body = Body::Text(format!("frag-{i}"));
993 frag
994 })
995 .collect()
996 });
997
998 let invocations = Arc::new(AtomicUsize::new(0));
1000 struct MixedBody {
1001 counter: Arc<AtomicUsize>,
1002 }
1003 impl Clone for MixedBody {
1004 fn clone(&self) -> Self {
1005 Self {
1006 counter: Arc::clone(&self.counter),
1007 }
1008 }
1009 }
1010 impl camel_api::OutcomePipeline for MixedBody {
1011 fn clone_box(&self) -> Box<dyn camel_api::OutcomePipeline> {
1012 Box::new(self.clone())
1013 }
1014 fn run<'a>(
1015 &'a mut self,
1016 exchange: Exchange,
1017 ) -> Pin<Box<dyn Future<Output = PipelineOutcome> + Send + 'a>> {
1018 let count = self.counter.fetch_add(1, Ordering::SeqCst);
1019 Box::pin(async move {
1020 if count == 1 {
1021 PipelineOutcome::Failed(CamelError::ProcessorError("fail 1".into()))
1022 } else {
1023 PipelineOutcome::Completed(exchange)
1024 }
1025 })
1026 }
1027 }
1028
1029 let mut seg = SplitSegment {
1030 splitter,
1031 body: OutcomeSegment::new(Box::new(MixedBody {
1032 counter: Arc::clone(&invocations),
1033 })),
1034 parallel: true,
1035 parallel_limit: None,
1036 stop_on_exception: false,
1037 aggregation: AggregationStrategy::LastWins,
1038 };
1039
1040 let ex = Exchange::new(Message::new("test"));
1041 let result = camel_api::OutcomePipeline::run(&mut seg, ex).await;
1042
1043 assert!(
1045 matches!(result, PipelineOutcome::Failed(_)),
1046 "stop_on_exception=false should propagate failure at end; got {result:?}"
1047 );
1048 assert_eq!(
1049 invocations.load(Ordering::SeqCst),
1050 5,
1051 "all fragments should be spawned"
1052 );
1053 }
1054}