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