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 = match splitter(&original) {
140 Ok(fragments) => fragments,
141 Err(err) => return PipelineOutcome::Failed(err),
142 };
143
144 if fragments.is_empty() {
145 return PipelineOutcome::Completed(original);
146 }
147
148 if parallel {
149 parallel_split(
150 fragments,
151 original,
152 body,
153 &aggregation,
154 parallel_limit,
155 stop_on_exception,
156 )
157 .await
158 } else {
159 sequential_split(fragments, original, body, &aggregation, stop_on_exception).await
160 }
161 })
162 }
163}
164
165async fn sequential_split(
168 fragments: Vec<Exchange>,
169 original: Exchange,
170 body: &mut OutcomeSegment,
171 aggregation: &AggregationStrategy,
172 stop_on_exception: bool,
173) -> PipelineOutcome {
174 let mut outputs = Vec::new();
175 let mut last_error: Option<CamelError> = None;
176 for frag in fragments {
177 match body.run(frag).await {
178 PipelineOutcome::Completed(ex) => outputs.push(ex),
179 PipelineOutcome::Stopped(ex) => return PipelineOutcome::Stopped(ex),
180 PipelineOutcome::Failed(err) => {
181 if stop_on_exception {
182 return PipelineOutcome::Failed(err);
183 }
184 last_error = Some(err);
186 }
187 }
188 }
189 if let Some(err) = last_error {
190 return PipelineOutcome::Failed(err);
191 }
192 PipelineOutcome::Completed(aggregate_completed(outputs, original, aggregation.clone()))
193}
194
195async fn parallel_split(
202 fragments: Vec<Exchange>,
203 original: Exchange,
204 body: &mut OutcomeSegment,
205 aggregation: &AggregationStrategy,
206 parallel_limit: Option<usize>,
207 stop_on_exception: bool,
208) -> PipelineOutcome {
209 use tokio::sync::Semaphore;
210
211 let stopped_seen = Arc::new(AtomicBool::new(false));
212 let stopped_idx = Arc::new(AtomicUsize::new(usize::MAX));
213 let aggregation = aggregation.clone();
214 let semaphore = parallel_limit
215 .filter(|&limit| limit > 0)
216 .map(|limit| Arc::new(Semaphore::new(limit)));
217
218 let mut set: JoinSet<(usize, Option<PipelineOutcome>)> = JoinSet::new();
219
220 for (idx, frag) in fragments.into_iter().enumerate() {
221 let mut body = body.clone();
222 let stopped_seen = Arc::clone(&stopped_seen);
223 let stopped_idx = Arc::clone(&stopped_idx);
224 let sem = semaphore.clone();
225 set.spawn(async move {
226 if stopped_seen.load(Ordering::SeqCst) {
231 return (idx, None);
232 }
233 let _permit: Option<tokio::sync::OwnedSemaphorePermit> = match &sem {
235 Some(s) => match std::sync::Arc::clone(s).acquire_owned().await {
236 Ok(p) => Some(p),
237 Err(_) => {
238 return (
239 idx,
240 Some(PipelineOutcome::Failed(CamelError::ProcessorError(
241 "semaphore closed".into(),
242 ))),
243 );
244 }
245 },
246 None => None,
247 };
248 if stopped_seen.load(Ordering::SeqCst) {
251 return (idx, None);
252 }
253 let outcome = body.run(frag).await;
254 if let PipelineOutcome::Stopped(_) = &outcome {
255 loop {
260 let cur = stopped_idx.load(Ordering::SeqCst);
261 if idx >= cur {
262 break; }
264 match stopped_idx.compare_exchange_weak(
265 cur,
266 idx,
267 Ordering::SeqCst,
268 Ordering::SeqCst,
269 ) {
270 Ok(_) => break,
271 Err(actual) => {
272 if actual <= idx {
275 break;
276 }
277 }
278 }
279 }
280 stopped_seen.store(true, Ordering::SeqCst);
281 }
282 (idx, Some(outcome))
283 });
284 }
285
286 let mut results: Vec<(usize, PipelineOutcome)> = Vec::new();
290 while let Some(res) = set.join_next().await {
291 if let Ok((idx, Some(o))) = res {
292 results.push((idx, o));
293 }
294 }
295
296 if stopped_seen.load(Ordering::SeqCst) {
298 let winning_idx = stopped_idx.load(Ordering::SeqCst);
299 if winning_idx == usize::MAX {
300 tracing::warn!(
301 target: "camel.phase4.split",
302 "stopped_seen=true but stopped_idx=usize::MAX — race condition; falling back to pre-split exchange"
303 );
304 return PipelineOutcome::Stopped(original);
305 }
306 let stopped_ex = results
307 .iter()
308 .find(|(idx, _)| *idx == winning_idx)
309 .and_then(|(_, o)| match o {
310 PipelineOutcome::Stopped(ex) => Some(ex.clone()),
311 _ => None,
312 });
313 if let Some(ex) = stopped_ex {
314 return PipelineOutcome::Stopped(ex);
315 }
316 tracing::warn!(
317 target: "camel.phase4.split",
318 winning_idx = winning_idx,
319 "winning_idx not found in results — falling back to pre-split exchange"
320 );
321 return PipelineOutcome::Stopped(original);
322 }
323
324 results.sort_by_key(|(idx, _)| *idx);
328 if stop_on_exception {
329 let mut first_failed: Option<(usize, CamelError)> = None;
330 for (idx, o) in &results {
331 if let PipelineOutcome::Failed(err) = o
332 && first_failed
333 .as_ref()
334 .map(|(i, _)| *i > *idx)
335 .unwrap_or(true)
336 {
337 first_failed = Some((*idx, err.clone()));
338 }
339 }
340 if let Some((_, err)) = first_failed {
341 return PipelineOutcome::Failed(err);
342 }
343 } else {
344 let mut last_error: Option<CamelError> = None;
346 for (_, o) in &results {
347 if let PipelineOutcome::Failed(err) = o {
348 last_error = Some(err.clone());
349 }
350 }
351 if let Some(err) = last_error {
352 return PipelineOutcome::Failed(err);
353 }
354 }
355
356 let completed: Vec<Exchange> = results
358 .into_iter()
359 .filter_map(|(_, o)| match o {
360 PipelineOutcome::Completed(ex) => Some(ex),
361 _ => None,
362 })
363 .collect();
364 PipelineOutcome::Completed(aggregate_completed(completed, original, aggregation))
365}
366
367#[cfg(test)]
370mod tests {
371 use super::*;
372 use camel_api::Message;
373
374 #[derive(Clone)]
378 #[allow(dead_code)]
379 struct CompletedBody;
380 impl camel_api::OutcomePipeline for CompletedBody {
381 fn clone_box(&self) -> Box<dyn camel_api::OutcomePipeline> {
382 Box::new(CompletedBody)
383 }
384 fn run<'a>(
385 &'a mut self,
386 exchange: Exchange,
387 ) -> Pin<Box<dyn Future<Output = PipelineOutcome> + Send + 'a>> {
388 Box::pin(async move { PipelineOutcome::Completed(exchange) })
389 }
390 }
391
392 #[derive(Clone)]
394 #[allow(dead_code)]
395 struct StopBody;
396 impl camel_api::OutcomePipeline for StopBody {
397 fn clone_box(&self) -> Box<dyn camel_api::OutcomePipeline> {
398 Box::new(StopBody)
399 }
400 fn run<'a>(
401 &'a mut self,
402 exchange: Exchange,
403 ) -> Pin<Box<dyn Future<Output = PipelineOutcome> + Send + 'a>> {
404 Box::pin(async move { PipelineOutcome::Stopped(exchange) })
405 }
406 }
407
408 #[derive(Clone)]
410 struct StopOnNthBody {
411 counter: Arc<AtomicUsize>,
412 stop_at: usize,
413 }
414 impl camel_api::OutcomePipeline for StopOnNthBody {
415 fn clone_box(&self) -> Box<dyn camel_api::OutcomePipeline> {
416 Box::new(self.clone())
417 }
418 fn run<'a>(
419 &'a mut self,
420 exchange: Exchange,
421 ) -> Pin<Box<dyn Future<Output = PipelineOutcome> + Send + 'a>> {
422 let count = self.counter.fetch_add(1, Ordering::SeqCst);
423 let stop_at = self.stop_at;
424 Box::pin(async move {
425 if count >= stop_at {
426 PipelineOutcome::Stopped(exchange)
427 } else {
428 PipelineOutcome::Completed(exchange)
429 }
430 })
431 }
432 }
433
434 #[derive(Clone)]
436 struct MutateAndStopBody;
437 impl camel_api::OutcomePipeline for MutateAndStopBody {
438 fn clone_box(&self) -> Box<dyn camel_api::OutcomePipeline> {
439 Box::new(MutateAndStopBody)
440 }
441 fn run<'a>(
442 &'a mut self,
443 mut exchange: Exchange,
444 ) -> Pin<Box<dyn Future<Output = PipelineOutcome> + Send + 'a>> {
445 Box::pin(async move {
446 exchange.input.body = Body::Text("mutated-by-body".to_string());
447 PipelineOutcome::Stopped(exchange)
448 })
449 }
450 }
451
452 #[tokio::test]
455 async fn stop_inside_split_sequential_halts_remaining_fragments() {
456 let invocations = Arc::new(AtomicUsize::new(0));
457 let body = StopOnNthBody {
458 counter: Arc::clone(&invocations),
459 stop_at: 1, };
461
462 let mut seg = SplitSegment {
463 splitter: camel_api::split_body_lines(),
464 body: OutcomeSegment::new(Box::new(body)),
465 parallel: false,
466 parallel_limit: None,
467 stop_on_exception: true,
468 aggregation: AggregationStrategy::LastWins,
469 };
470
471 let ex = Exchange::new(Message::new("a\nb\nc"));
472 let result = camel_api::OutcomePipeline::run(&mut seg, ex).await;
473
474 assert!(matches!(result, PipelineOutcome::Stopped(_)));
475 assert_eq!(invocations.load(Ordering::SeqCst), 2);
477 }
478
479 #[tokio::test]
482 async fn stop_inside_split_sequential_preserves_exchange_mutations() {
483 let mut seg = SplitSegment {
484 splitter: camel_api::split_body_lines(),
485 body: OutcomeSegment::new(Box::new(MutateAndStopBody)),
486 parallel: false,
487 parallel_limit: None,
488 stop_on_exception: true,
489 aggregation: AggregationStrategy::LastWins,
490 };
491
492 let ex = Exchange::new(Message::new("hello"));
493 let result = camel_api::OutcomePipeline::run(&mut seg, ex).await;
494
495 match result {
496 PipelineOutcome::Stopped(ex) => {
497 assert_eq!(
498 ex.input.body.as_text(),
499 Some("mutated-by-body"),
500 "Stopped exchange should carry body mutation"
501 );
502 }
503 other => panic!("Expected Stopped, got {other:?}"),
504 }
505 }
506
507 #[tokio::test(flavor = "multi_thread")]
518 async fn stop_inside_split_parallel_cancels_pending_and_waits_inflight() {
519 use tokio::sync::Barrier;
520
521 let barrier = Arc::new(Barrier::new(3));
522 let fragment1_completed = Arc::new(AtomicBool::new(false));
523 let fragment2_completed = Arc::new(AtomicBool::new(false));
524 let frag1_ok = Arc::clone(&fragment1_completed);
525 let frag2_ok = Arc::clone(&fragment2_completed);
526 let bar = Arc::clone(&barrier);
527
528 let splitter: SplitExpression = Arc::new(|ex: &Exchange| {
530 Ok((0..3)
531 .map(|i| {
532 let mut frag = ex.clone();
533 frag.input.body = Body::Text(format!("frag-{i}"));
534 frag
535 })
536 .collect())
537 });
538
539 struct BarrierDispatchBody {
545 barrier: Arc<Barrier>,
546 f1_completed: Arc<AtomicBool>,
547 f2_completed: Arc<AtomicBool>,
548 }
549 impl Clone for BarrierDispatchBody {
550 fn clone(&self) -> Self {
551 Self {
552 barrier: Arc::clone(&self.barrier),
553 f1_completed: Arc::clone(&self.f1_completed),
554 f2_completed: Arc::clone(&self.f2_completed),
555 }
556 }
557 }
558 impl camel_api::OutcomePipeline for BarrierDispatchBody {
559 fn clone_box(&self) -> Box<dyn camel_api::OutcomePipeline> {
560 Box::new(self.clone())
561 }
562 fn run<'a>(
563 &'a mut self,
564 exchange: Exchange,
565 ) -> Pin<Box<dyn Future<Output = PipelineOutcome> + Send + 'a>> {
566 let bar = Arc::clone(&self.barrier);
567 let f1c = Arc::clone(&self.f1_completed);
568 let f2c = Arc::clone(&self.f2_completed);
569 Box::pin(async move {
570 let body_text = exchange.input.body.as_text().unwrap_or("").to_string();
571
572 bar.wait().await;
576
577 match body_text.as_str() {
578 "frag-0" => PipelineOutcome::Stopped(exchange),
579 "frag-1" => {
580 tokio::time::sleep(std::time::Duration::from_millis(100)).await;
583 f1c.store(true, Ordering::SeqCst);
584 PipelineOutcome::Completed(exchange)
585 }
586 "frag-2" => {
587 f2c.store(true, Ordering::SeqCst);
588 PipelineOutcome::Completed(exchange)
589 }
590 _ => PipelineOutcome::Completed(exchange),
591 }
592 })
593 }
594 }
595
596 let body = BarrierDispatchBody {
597 barrier: bar,
598 f1_completed: frag1_ok,
599 f2_completed: frag2_ok,
600 };
601
602 let mut seg = SplitSegment {
603 splitter,
604 body: OutcomeSegment::new(Box::new(body)),
605 parallel: true,
606 parallel_limit: None,
607 stop_on_exception: true,
608 aggregation: AggregationStrategy::LastWins,
609 };
610
611 let ex = Exchange::new(Message::new("test"));
612 let result = camel_api::OutcomePipeline::run(&mut seg, ex).await;
613
614 assert!(
615 matches!(result, PipelineOutcome::Stopped(_)),
616 "Expected Stopped, got {result:?}"
617 );
618 assert!(
620 fragment1_completed.load(Ordering::SeqCst),
621 "fragment 1 should have completed despite Stop"
622 );
623 assert!(
625 fragment2_completed.load(Ordering::SeqCst),
626 "fragment 2 should have completed despite Stop"
627 );
628 }
629
630 #[tokio::test(flavor = "multi_thread")]
633 async fn stop_inside_split_parallel_lowest_stopped_index_wins() {
634 let splitter: SplitExpression = Arc::new(|ex: &Exchange| {
636 Ok((0..3)
637 .map(|i| {
638 let mut frag = ex.clone();
639 frag.input.body = Body::Text(format!("from-fragment-{i}"));
640 frag
641 })
642 .collect())
643 });
644
645 struct DualStopBody;
647 impl Clone for DualStopBody {
648 fn clone(&self) -> Self {
649 DualStopBody
650 }
651 }
652 impl camel_api::OutcomePipeline for DualStopBody {
653 fn clone_box(&self) -> Box<dyn camel_api::OutcomePipeline> {
654 Box::new(DualStopBody)
655 }
656 fn run<'a>(
657 &'a mut self,
658 exchange: Exchange,
659 ) -> Pin<Box<dyn Future<Output = PipelineOutcome> + Send + 'a>> {
660 let is_frag0 = exchange
661 .input
662 .body
663 .as_text()
664 .map(|s| s == "from-fragment-0")
665 .unwrap_or(false);
666 let is_frag2 = exchange
667 .input
668 .body
669 .as_text()
670 .map(|s| s == "from-fragment-2")
671 .unwrap_or(false);
672 Box::pin(async move {
673 if is_frag0 {
674 return PipelineOutcome::Stopped(exchange);
675 }
676 if is_frag2 {
677 tokio::time::sleep(std::time::Duration::from_millis(10)).await;
680 return PipelineOutcome::Stopped(exchange);
681 }
682 PipelineOutcome::Completed(exchange)
684 })
685 }
686 }
687
688 let mut seg = SplitSegment {
689 splitter,
690 body: OutcomeSegment::new(Box::new(DualStopBody)),
691 parallel: true,
692 parallel_limit: None,
693 stop_on_exception: true,
694 aggregation: AggregationStrategy::LastWins,
695 };
696
697 let ex = Exchange::new(Message::new("test"));
698 let result = camel_api::OutcomePipeline::run(&mut seg, ex).await;
699
700 match result {
701 PipelineOutcome::Stopped(ex) => {
702 assert_eq!(
703 ex.input.body.as_text(),
704 Some("from-fragment-0"),
705 "Lowest stopped index (0) should win, got body {:?}",
706 ex.input.body.as_text()
707 );
708 }
709 other => panic!("Expected Stopped with fragment-0 body, got {other:?}"),
710 }
711 }
712
713 #[tokio::test(flavor = "multi_thread")]
716 async fn split_parallel_limit_enforces_concurrency_cap() {
717 let concurrent = Arc::new(AtomicUsize::new(0));
718 let max_concurrent = Arc::new(AtomicUsize::new(0));
719
720 let splitter: SplitExpression = Arc::new(|ex: &Exchange| {
722 Ok((0..6)
723 .map(|i| {
724 let mut frag = ex.clone();
725 frag.input.body = Body::Text(format!("frag-{i}"));
726 frag
727 })
728 .collect())
729 });
730
731 let c = Arc::clone(&concurrent);
732 let mc = Arc::clone(&max_concurrent);
733 struct LimitedBody {
734 concurrent: Arc<AtomicUsize>,
735 max_concurrent: Arc<AtomicUsize>,
736 }
737 impl Clone for LimitedBody {
738 fn clone(&self) -> Self {
739 Self {
740 concurrent: Arc::clone(&self.concurrent),
741 max_concurrent: Arc::clone(&self.max_concurrent),
742 }
743 }
744 }
745 impl camel_api::OutcomePipeline for LimitedBody {
746 fn clone_box(&self) -> Box<dyn camel_api::OutcomePipeline> {
747 Box::new(self.clone())
748 }
749 fn run<'a>(
750 &'a mut self,
751 exchange: Exchange,
752 ) -> Pin<Box<dyn Future<Output = PipelineOutcome> + Send + 'a>> {
753 let c = Arc::clone(&self.concurrent);
754 let mc = Arc::clone(&self.max_concurrent);
755 Box::pin(async move {
756 let current = c.fetch_add(1, Ordering::SeqCst) + 1;
757 mc.fetch_max(current, Ordering::SeqCst);
758 tokio::task::yield_now().await;
759 c.fetch_sub(1, Ordering::SeqCst);
760 PipelineOutcome::Completed(exchange)
761 })
762 }
763 }
764
765 let mut seg = SplitSegment {
766 splitter,
767 body: OutcomeSegment::new(Box::new(LimitedBody {
768 concurrent: c,
769 max_concurrent: mc,
770 })),
771 parallel: true,
772 parallel_limit: Some(2),
773 stop_on_exception: true,
774 aggregation: AggregationStrategy::LastWins,
775 };
776
777 let ex = Exchange::new(Message::new("test"));
778 let result = camel_api::OutcomePipeline::run(&mut seg, ex).await;
779 assert!(
780 matches!(result, PipelineOutcome::Completed(_)),
781 "Expected Completed, got {result:?}"
782 );
783
784 let observed_max = max_concurrent.load(Ordering::SeqCst);
785 assert!(
786 observed_max <= 2,
787 "parallel_limit=2 but max concurrency was {observed_max}"
788 );
789 }
790
791 #[tokio::test]
794 async fn split_sequential_stop_on_exception_true() {
795 fn make_fail_body(
797 fail_at: usize,
798 counter: Arc<AtomicUsize>,
799 ) -> impl camel_api::OutcomePipeline + Clone {
800 #[derive(Clone)]
801 struct FailAtBody {
802 fail_at: usize,
803 counter: Arc<AtomicUsize>,
804 }
805 impl camel_api::OutcomePipeline for FailAtBody {
806 fn clone_box(&self) -> Box<dyn camel_api::OutcomePipeline> {
807 Box::new(self.clone())
808 }
809 fn run<'a>(
810 &'a mut self,
811 exchange: Exchange,
812 ) -> Pin<Box<dyn Future<Output = PipelineOutcome> + Send + 'a>> {
813 let count = self.counter.fetch_add(1, Ordering::SeqCst);
814 let fail_at = self.fail_at;
815 Box::pin(async move {
816 if count == fail_at {
817 PipelineOutcome::Failed(CamelError::ProcessorError(format!(
818 "fail at {count}"
819 )))
820 } else {
821 PipelineOutcome::Completed(exchange)
822 }
823 })
824 }
825 }
826 FailAtBody { fail_at, counter }
827 }
828
829 let invocations = Arc::new(AtomicUsize::new(0));
830 let body = make_fail_body(1, Arc::clone(&invocations));
831 let mut seg = SplitSegment {
832 splitter: camel_api::split_body_lines(),
833 body: OutcomeSegment::new(Box::new(body)),
834 parallel: false,
835 parallel_limit: None,
836 stop_on_exception: true,
837 aggregation: AggregationStrategy::LastWins,
838 };
839
840 let ex = Exchange::new(Message::new("a\nb\nc\nd\ne"));
841 let result = camel_api::OutcomePipeline::run(&mut seg, ex).await;
842
843 assert!(
844 matches!(result, PipelineOutcome::Failed(_)),
845 "stop_on_exception=true should propagate first failure"
846 );
847 assert_eq!(
850 invocations.load(Ordering::SeqCst),
851 2,
852 "should stop after 2 fragments (0 pass, 1 fail)"
853 );
854 }
855
856 #[tokio::test]
859 async fn split_sequential_stop_on_exception_false() {
860 fn make_fail_body(
862 fail_at: usize,
863 counter: Arc<AtomicUsize>,
864 ) -> impl camel_api::OutcomePipeline + Clone {
865 #[derive(Clone)]
866 struct FailAtBody {
867 fail_at: usize,
868 counter: Arc<AtomicUsize>,
869 }
870 impl camel_api::OutcomePipeline for FailAtBody {
871 fn clone_box(&self) -> Box<dyn camel_api::OutcomePipeline> {
872 Box::new(self.clone())
873 }
874 fn run<'a>(
875 &'a mut self,
876 exchange: Exchange,
877 ) -> Pin<Box<dyn Future<Output = PipelineOutcome> + Send + 'a>> {
878 let count = self.counter.fetch_add(1, Ordering::SeqCst);
879 let fail_at = self.fail_at;
880 Box::pin(async move {
881 if count == fail_at {
882 PipelineOutcome::Failed(CamelError::ProcessorError(format!(
883 "fail at {count}"
884 )))
885 } else {
886 PipelineOutcome::Completed(exchange)
887 }
888 })
889 }
890 }
891 FailAtBody { fail_at, counter }
892 }
893
894 let invocations = Arc::new(AtomicUsize::new(0));
895 let body = make_fail_body(1, Arc::clone(&invocations));
896 let mut seg = SplitSegment {
897 splitter: camel_api::split_body_lines(),
898 body: OutcomeSegment::new(Box::new(body)),
899 parallel: false,
900 parallel_limit: None,
901 stop_on_exception: false,
902 aggregation: AggregationStrategy::LastWins,
903 };
904
905 let ex = Exchange::new(Message::new("a\nb\nc\nd\ne"));
906 let result = camel_api::OutcomePipeline::run(&mut seg, ex).await;
907
908 assert!(
911 matches!(result, PipelineOutcome::Failed(_)),
912 "stop_on_exception=false should still propagate error at end"
913 );
914 assert_eq!(
916 invocations.load(Ordering::SeqCst),
917 5,
918 "all fragments should be processed when stop_on_exception=false"
919 );
920 }
921
922 #[tokio::test(flavor = "multi_thread")]
925 async fn split_parallel_stop_on_exception_true() {
926 let splitter: SplitExpression = Arc::new(|ex: &Exchange| {
927 Ok((0..5)
928 .map(|i| {
929 let mut frag = ex.clone();
930 frag.input.body = Body::Text(format!("frag-{i}"));
931 frag
932 })
933 .collect())
934 });
935
936 let invocations = Arc::new(AtomicUsize::new(0));
938 struct FailBody {
939 counter: Arc<AtomicUsize>,
940 }
941 impl Clone for FailBody {
942 fn clone(&self) -> Self {
943 Self {
944 counter: Arc::clone(&self.counter),
945 }
946 }
947 }
948 impl camel_api::OutcomePipeline for FailBody {
949 fn clone_box(&self) -> Box<dyn camel_api::OutcomePipeline> {
950 Box::new(self.clone())
951 }
952 fn run<'a>(
953 &'a mut self,
954 _exchange: Exchange,
955 ) -> Pin<Box<dyn Future<Output = PipelineOutcome> + Send + 'a>> {
956 let count = self.counter.fetch_add(1, Ordering::SeqCst);
957 Box::pin(async move {
958 PipelineOutcome::Failed(CamelError::ProcessorError(format!("fail {count}")))
959 })
960 }
961 }
962
963 let mut seg = SplitSegment {
964 splitter,
965 body: OutcomeSegment::new(Box::new(FailBody {
966 counter: Arc::clone(&invocations),
967 })),
968 parallel: true,
969 parallel_limit: None,
970 stop_on_exception: true,
971 aggregation: AggregationStrategy::LastWins,
972 };
973
974 let ex = Exchange::new(Message::new("test"));
975 let result = camel_api::OutcomePipeline::run(&mut seg, ex).await;
976
977 assert!(
978 matches!(result, PipelineOutcome::Failed(_)),
979 "stop_on_exception=true should propagate first failure"
980 );
981 assert_eq!(
983 invocations.load(Ordering::SeqCst),
984 5,
985 "all fragments should be spawned"
986 );
987 }
988
989 #[tokio::test(flavor = "multi_thread")]
992 async fn split_parallel_stop_on_exception_false() {
993 let splitter: SplitExpression = Arc::new(|ex: &Exchange| {
994 Ok((0..5)
995 .map(|i| {
996 let mut frag = ex.clone();
997 frag.input.body = Body::Text(format!("frag-{i}"));
998 frag
999 })
1000 .collect())
1001 });
1002
1003 let invocations = Arc::new(AtomicUsize::new(0));
1005 struct MixedBody {
1006 counter: Arc<AtomicUsize>,
1007 }
1008 impl Clone for MixedBody {
1009 fn clone(&self) -> Self {
1010 Self {
1011 counter: Arc::clone(&self.counter),
1012 }
1013 }
1014 }
1015 impl camel_api::OutcomePipeline for MixedBody {
1016 fn clone_box(&self) -> Box<dyn camel_api::OutcomePipeline> {
1017 Box::new(self.clone())
1018 }
1019 fn run<'a>(
1020 &'a mut self,
1021 exchange: Exchange,
1022 ) -> Pin<Box<dyn Future<Output = PipelineOutcome> + Send + 'a>> {
1023 let count = self.counter.fetch_add(1, Ordering::SeqCst);
1024 Box::pin(async move {
1025 if count == 1 {
1026 PipelineOutcome::Failed(CamelError::ProcessorError("fail 1".into()))
1027 } else {
1028 PipelineOutcome::Completed(exchange)
1029 }
1030 })
1031 }
1032 }
1033
1034 let mut seg = SplitSegment {
1035 splitter,
1036 body: OutcomeSegment::new(Box::new(MixedBody {
1037 counter: Arc::clone(&invocations),
1038 })),
1039 parallel: true,
1040 parallel_limit: None,
1041 stop_on_exception: false,
1042 aggregation: AggregationStrategy::LastWins,
1043 };
1044
1045 let ex = Exchange::new(Message::new("test"));
1046 let result = camel_api::OutcomePipeline::run(&mut seg, ex).await;
1047
1048 assert!(
1050 matches!(result, PipelineOutcome::Failed(_)),
1051 "stop_on_exception=false should propagate failure at end; got {result:?}"
1052 );
1053 assert_eq!(
1054 invocations.load(Ordering::SeqCst),
1055 5,
1056 "all fragments should be spawned"
1057 );
1058 }
1059
1060 #[tokio::test]
1063 async fn test_split_segment_expression_error_is_failed() {
1064 #[derive(Clone)]
1066 struct RecordingBody {
1067 counter: Arc<AtomicUsize>,
1068 }
1069 impl camel_api::OutcomePipeline for RecordingBody {
1070 fn clone_box(&self) -> Box<dyn camel_api::OutcomePipeline> {
1071 Box::new(self.clone())
1072 }
1073 fn run<'a>(
1074 &'a mut self,
1075 exchange: Exchange,
1076 ) -> Pin<Box<dyn Future<Output = PipelineOutcome> + Send + 'a>> {
1077 self.counter.fetch_add(1, Ordering::SeqCst);
1078 Box::pin(async move { PipelineOutcome::Completed(exchange) })
1079 }
1080 }
1081
1082 let invocations = Arc::new(AtomicUsize::new(0));
1083 let splitter: SplitExpression = Arc::new(|_| {
1084 Err(CamelError::TypeConversionFailed(
1085 "declarative split requires a text or array value, got number; add an unmarshal step before split"
1086 .to_string(),
1087 ))
1088 });
1089
1090 let mut seg = SplitSegment {
1091 splitter,
1092 body: OutcomeSegment::new(Box::new(RecordingBody {
1093 counter: Arc::clone(&invocations),
1094 })),
1095 parallel: false,
1096 parallel_limit: None,
1097 stop_on_exception: true,
1098 aggregation: AggregationStrategy::LastWins,
1099 };
1100
1101 let ex = Exchange::new(Message::new("anything"));
1102 let result = camel_api::OutcomePipeline::run(&mut seg, ex).await;
1103
1104 match result {
1105 PipelineOutcome::Failed(err) => {
1106 let msg = err.to_string();
1107 assert!(
1108 msg.contains("declarative split"),
1109 "carried error should mention 'declarative split': {msg}"
1110 );
1111 }
1112 other => panic!("Expected Failed, got {other:?}"),
1113 }
1114 assert_eq!(
1115 invocations.load(Ordering::SeqCst),
1116 0,
1117 "body segment must record zero invocations when the split expression errors"
1118 );
1119 }
1120}