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, SplitSource,
13 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: SplitSource,
88 pub body: OutcomeSegment,
90 pub parallel: bool,
92 pub parallel_limit: Option<usize>,
94 pub stop_on_exception: bool,
104 pub max_fragments: usize,
111 pub aggregation: AggregationStrategy,
113}
114
115impl Clone for SplitSegment {
116 fn clone(&self) -> Self {
117 Self {
118 splitter: self.splitter.clone(),
119 body: self.body.clone(),
120 parallel: self.parallel,
121 parallel_limit: self.parallel_limit,
122 stop_on_exception: self.stop_on_exception,
123 max_fragments: self.max_fragments,
124 aggregation: self.aggregation.clone(),
125 }
126 }
127}
128
129impl camel_api::OutcomePipeline for SplitSegment {
130 fn clone_box(&self) -> Box<dyn camel_api::OutcomePipeline> {
131 Box::new(self.clone())
132 }
133
134 fn run<'a>(
135 &'a mut self,
136 exchange: camel_api::Exchange,
137 ) -> Pin<Box<dyn Future<Output = camel_api::PipelineOutcome> + Send + 'a>> {
138 let splitter = self.splitter.clone();
139 let aggregation = self.aggregation.clone();
140 let parallel = self.parallel;
141 let parallel_limit = self.parallel_limit;
142 let stop_on_exception = self.stop_on_exception;
143 let max_fragments = self.max_fragments;
144 let body = &mut self.body;
145
146 Box::pin(async move {
147 let original = exchange;
148 let mut fragments = match splitter.split(&original).await {
151 Ok(fragments) => fragments,
152 Err(err) => return PipelineOutcome::Failed(err),
153 };
154
155 if fragments.is_empty() {
156 return PipelineOutcome::Completed(original);
157 }
158
159 if fragments.len() > max_fragments {
160 return PipelineOutcome::Failed(CamelError::ProcessorError(format!(
161 "Splitter produced {} fragments, exceeding max_fragments {}",
162 fragments.len(),
163 max_fragments
164 )));
165 }
166
167 let total = fragments.len();
172 for (i, frag) in fragments.iter_mut().enumerate() {
173 frag.set_property(CAMEL_SPLIT_INDEX, Value::from(i as u64));
174 frag.set_property(CAMEL_SPLIT_SIZE, Value::from(total as u64));
175 frag.set_property(CAMEL_SPLIT_COMPLETE, Value::Bool(i == total - 1));
176 }
177
178 if parallel {
179 parallel_split(
180 fragments,
181 original,
182 body,
183 &aggregation,
184 parallel_limit,
185 stop_on_exception,
186 )
187 .await
188 } else {
189 sequential_split(fragments, original, body, &aggregation, stop_on_exception).await
190 }
191 })
192 }
193}
194
195async fn sequential_split(
198 fragments: Vec<Exchange>,
199 original: Exchange,
200 body: &mut OutcomeSegment,
201 aggregation: &AggregationStrategy,
202 stop_on_exception: bool,
203) -> PipelineOutcome {
204 let mut outputs = Vec::new();
205 let mut last_error: Option<CamelError> = None;
206 for frag in fragments {
207 match body.run(frag).await {
208 PipelineOutcome::Completed(ex) => outputs.push(ex),
209 PipelineOutcome::Stopped(ex) => return PipelineOutcome::Stopped(ex),
210 PipelineOutcome::Failed(err) => {
211 if stop_on_exception {
212 return PipelineOutcome::Failed(err);
213 }
214 last_error = Some(err);
216 }
217 }
218 }
219 if let Some(err) = last_error {
220 return PipelineOutcome::Failed(err);
221 }
222 PipelineOutcome::Completed(aggregate_completed(outputs, original, aggregation.clone()))
223}
224
225async fn parallel_split(
232 fragments: Vec<Exchange>,
233 original: Exchange,
234 body: &mut OutcomeSegment,
235 aggregation: &AggregationStrategy,
236 parallel_limit: Option<usize>,
237 stop_on_exception: bool,
238) -> PipelineOutcome {
239 use tokio::sync::Semaphore;
240
241 let stopped_seen = Arc::new(AtomicBool::new(false));
242 let stopped_idx = Arc::new(AtomicUsize::new(usize::MAX));
243 let aggregation = aggregation.clone();
244 let semaphore = parallel_limit
245 .filter(|&limit| limit > 0)
246 .map(|limit| Arc::new(Semaphore::new(limit)));
247
248 let mut set: JoinSet<(usize, Option<PipelineOutcome>)> = JoinSet::new();
249
250 for (idx, frag) in fragments.into_iter().enumerate() {
251 let mut body = body.clone();
252 let stopped_seen = Arc::clone(&stopped_seen);
253 let stopped_idx = Arc::clone(&stopped_idx);
254 let sem = semaphore.clone();
255 set.spawn(async move {
256 if stopped_seen.load(Ordering::SeqCst) {
261 return (idx, None);
262 }
263 let _permit: Option<tokio::sync::OwnedSemaphorePermit> = match &sem {
265 Some(s) => match std::sync::Arc::clone(s).acquire_owned().await {
266 Ok(p) => Some(p),
267 Err(_) => {
268 return (
269 idx,
270 Some(PipelineOutcome::Failed(CamelError::ProcessorError(
271 "semaphore closed".into(),
272 ))),
273 );
274 }
275 },
276 None => None,
277 };
278 if stopped_seen.load(Ordering::SeqCst) {
281 return (idx, None);
282 }
283 let outcome = body.run(frag).await;
284 if let PipelineOutcome::Stopped(_) = &outcome {
285 loop {
290 let cur = stopped_idx.load(Ordering::SeqCst);
291 if idx >= cur {
292 break; }
294 match stopped_idx.compare_exchange_weak(
295 cur,
296 idx,
297 Ordering::SeqCst,
298 Ordering::SeqCst,
299 ) {
300 Ok(_) => break,
301 Err(actual) => {
302 if actual <= idx {
305 break;
306 }
307 }
308 }
309 }
310 stopped_seen.store(true, Ordering::SeqCst);
311 }
312 (idx, Some(outcome))
313 });
314 }
315
316 let mut results: Vec<(usize, PipelineOutcome)> = Vec::new();
320 while let Some(res) = set.join_next().await {
321 if let Ok((idx, Some(o))) = res {
322 results.push((idx, o));
323 }
324 }
325
326 if stopped_seen.load(Ordering::SeqCst) {
328 let winning_idx = stopped_idx.load(Ordering::SeqCst);
329 if winning_idx == usize::MAX {
330 tracing::warn!(
331 target: "camel.phase4.split",
332 "stopped_seen=true but stopped_idx=usize::MAX — race condition; falling back to pre-split exchange"
333 );
334 return PipelineOutcome::Stopped(original);
335 }
336 let stopped_ex = results
337 .iter()
338 .find(|(idx, _)| *idx == winning_idx)
339 .and_then(|(_, o)| match o {
340 PipelineOutcome::Stopped(ex) => Some(ex.branch_clone()),
341 _ => None,
342 });
343 if let Some(ex) = stopped_ex {
344 return PipelineOutcome::Stopped(ex);
345 }
346 tracing::warn!(
347 target: "camel.phase4.split",
348 winning_idx = winning_idx,
349 "winning_idx not found in results — falling back to pre-split exchange"
350 );
351 return PipelineOutcome::Stopped(original);
352 }
353
354 results.sort_by_key(|(idx, _)| *idx);
358 if stop_on_exception {
359 let mut first_failed: Option<(usize, CamelError)> = None;
360 for (idx, o) in &results {
361 if let PipelineOutcome::Failed(err) = o
362 && first_failed
363 .as_ref()
364 .map(|(i, _)| *i > *idx)
365 .unwrap_or(true)
366 {
367 first_failed = Some((*idx, err.clone()));
368 }
369 }
370 if let Some((_, err)) = first_failed {
371 return PipelineOutcome::Failed(err);
372 }
373 } else {
374 let mut last_error: Option<CamelError> = None;
376 for (_, o) in &results {
377 if let PipelineOutcome::Failed(err) = o {
378 last_error = Some(err.clone());
379 }
380 }
381 if let Some(err) = last_error {
382 return PipelineOutcome::Failed(err);
383 }
384 }
385
386 let completed: Vec<Exchange> = results
388 .into_iter()
389 .filter_map(|(_, o)| match o {
390 PipelineOutcome::Completed(ex) => Some(ex),
391 _ => None,
392 })
393 .collect();
394 PipelineOutcome::Completed(aggregate_completed(completed, original, aggregation))
395}
396
397#[cfg(test)]
400mod tests {
401 use super::*;
402 use camel_api::Message;
403
404 #[derive(Clone)]
408 #[allow(dead_code)]
409 struct CompletedBody;
410 impl camel_api::OutcomePipeline for CompletedBody {
411 fn clone_box(&self) -> Box<dyn camel_api::OutcomePipeline> {
412 Box::new(CompletedBody)
413 }
414 fn run<'a>(
415 &'a mut self,
416 exchange: Exchange,
417 ) -> Pin<Box<dyn Future<Output = PipelineOutcome> + Send + 'a>> {
418 Box::pin(async move { PipelineOutcome::Completed(exchange) })
419 }
420 }
421
422 #[derive(Clone)]
424 #[allow(dead_code)]
425 struct StopBody;
426 impl camel_api::OutcomePipeline for StopBody {
427 fn clone_box(&self) -> Box<dyn camel_api::OutcomePipeline> {
428 Box::new(StopBody)
429 }
430 fn run<'a>(
431 &'a mut self,
432 exchange: Exchange,
433 ) -> Pin<Box<dyn Future<Output = PipelineOutcome> + Send + 'a>> {
434 Box::pin(async move { PipelineOutcome::Stopped(exchange) })
435 }
436 }
437
438 #[derive(Clone)]
440 struct StopOnNthBody {
441 counter: Arc<AtomicUsize>,
442 stop_at: usize,
443 }
444 impl camel_api::OutcomePipeline for StopOnNthBody {
445 fn clone_box(&self) -> Box<dyn camel_api::OutcomePipeline> {
446 Box::new(self.clone())
447 }
448 fn run<'a>(
449 &'a mut self,
450 exchange: Exchange,
451 ) -> Pin<Box<dyn Future<Output = PipelineOutcome> + Send + 'a>> {
452 let count = self.counter.fetch_add(1, Ordering::SeqCst);
453 let stop_at = self.stop_at;
454 Box::pin(async move {
455 if count >= stop_at {
456 PipelineOutcome::Stopped(exchange)
457 } else {
458 PipelineOutcome::Completed(exchange)
459 }
460 })
461 }
462 }
463
464 #[derive(Clone)]
466 struct MutateAndStopBody;
467 impl camel_api::OutcomePipeline for MutateAndStopBody {
468 fn clone_box(&self) -> Box<dyn camel_api::OutcomePipeline> {
469 Box::new(MutateAndStopBody)
470 }
471 fn run<'a>(
472 &'a mut self,
473 mut exchange: Exchange,
474 ) -> Pin<Box<dyn Future<Output = PipelineOutcome> + Send + 'a>> {
475 Box::pin(async move {
476 exchange.input.body = Body::Text("mutated-by-body".to_string());
477 PipelineOutcome::Stopped(exchange)
478 })
479 }
480 }
481
482 #[tokio::test]
485 async fn stop_inside_split_sequential_halts_remaining_fragments() {
486 let invocations = Arc::new(AtomicUsize::new(0));
487 let body = StopOnNthBody {
488 counter: Arc::clone(&invocations),
489 stop_at: 1, };
491
492 let mut seg = SplitSegment {
493 splitter: camel_api::SplitSource::Sync(camel_api::split_body_lines()),
494 body: OutcomeSegment::new(Box::new(body)),
495 parallel: false,
496 parallel_limit: None,
497 stop_on_exception: true,
498 max_fragments: camel_api::splitter::DEFAULT_MAX_FRAGMENTS,
499 aggregation: AggregationStrategy::LastWins,
500 };
501
502 let ex = Exchange::new(Message::new("a\nb\nc"));
503 let result = camel_api::OutcomePipeline::run(&mut seg, ex).await;
504
505 assert!(matches!(result, PipelineOutcome::Stopped(_)));
506 assert_eq!(invocations.load(Ordering::SeqCst), 2);
508 }
509
510 #[tokio::test]
513 async fn stop_inside_split_sequential_preserves_exchange_mutations() {
514 let mut seg = SplitSegment {
515 splitter: camel_api::SplitSource::Sync(camel_api::split_body_lines()),
516 body: OutcomeSegment::new(Box::new(MutateAndStopBody)),
517 parallel: false,
518 parallel_limit: None,
519 stop_on_exception: true,
520 max_fragments: camel_api::splitter::DEFAULT_MAX_FRAGMENTS,
521 aggregation: AggregationStrategy::LastWins,
522 };
523
524 let ex = Exchange::new(Message::new("hello"));
525 let result = camel_api::OutcomePipeline::run(&mut seg, ex).await;
526
527 match result {
528 PipelineOutcome::Stopped(ex) => {
529 assert_eq!(
530 ex.input.body.as_text(),
531 Some("mutated-by-body"),
532 "Stopped exchange should carry body mutation"
533 );
534 }
535 other => panic!("Expected Stopped, got {other:?}"),
536 }
537 }
538
539 #[tokio::test(flavor = "multi_thread")]
550 async fn stop_inside_split_parallel_cancels_pending_and_waits_inflight() {
551 use tokio::sync::Barrier;
552
553 let barrier = Arc::new(Barrier::new(3));
554 let fragment1_completed = Arc::new(AtomicBool::new(false));
555 let fragment2_completed = Arc::new(AtomicBool::new(false));
556 let frag1_ok = Arc::clone(&fragment1_completed);
557 let frag2_ok = Arc::clone(&fragment2_completed);
558 let bar = Arc::clone(&barrier);
559
560 let splitter = SplitSource::Sync(Arc::new(|ex: &Exchange| {
562 Ok((0..3)
563 .map(|i| {
564 let mut frag = ex.clone();
565 frag.input.body = Body::Text(format!("frag-{i}"));
566 frag
567 })
568 .collect())
569 }));
570
571 struct BarrierDispatchBody {
577 barrier: Arc<Barrier>,
578 f1_completed: Arc<AtomicBool>,
579 f2_completed: Arc<AtomicBool>,
580 }
581 impl Clone for BarrierDispatchBody {
582 fn clone(&self) -> Self {
583 Self {
584 barrier: Arc::clone(&self.barrier),
585 f1_completed: Arc::clone(&self.f1_completed),
586 f2_completed: Arc::clone(&self.f2_completed),
587 }
588 }
589 }
590 impl camel_api::OutcomePipeline for BarrierDispatchBody {
591 fn clone_box(&self) -> Box<dyn camel_api::OutcomePipeline> {
592 Box::new(self.clone())
593 }
594 fn run<'a>(
595 &'a mut self,
596 exchange: Exchange,
597 ) -> Pin<Box<dyn Future<Output = PipelineOutcome> + Send + 'a>> {
598 let bar = Arc::clone(&self.barrier);
599 let f1c = Arc::clone(&self.f1_completed);
600 let f2c = Arc::clone(&self.f2_completed);
601 Box::pin(async move {
602 let body_text = exchange.input.body.as_text().unwrap_or("").to_string();
603
604 bar.wait().await;
608
609 match body_text.as_str() {
610 "frag-0" => PipelineOutcome::Stopped(exchange),
611 "frag-1" => {
612 tokio::time::sleep(std::time::Duration::from_millis(100)).await;
615 f1c.store(true, Ordering::SeqCst);
616 PipelineOutcome::Completed(exchange)
617 }
618 "frag-2" => {
619 f2c.store(true, Ordering::SeqCst);
620 PipelineOutcome::Completed(exchange)
621 }
622 _ => PipelineOutcome::Completed(exchange),
623 }
624 })
625 }
626 }
627
628 let body = BarrierDispatchBody {
629 barrier: bar,
630 f1_completed: frag1_ok,
631 f2_completed: frag2_ok,
632 };
633
634 let mut seg = SplitSegment {
635 splitter,
636 body: OutcomeSegment::new(Box::new(body)),
637 parallel: true,
638 parallel_limit: None,
639 stop_on_exception: true,
640 max_fragments: camel_api::splitter::DEFAULT_MAX_FRAGMENTS,
641 aggregation: AggregationStrategy::LastWins,
642 };
643
644 let ex = Exchange::new(Message::new("test"));
645 let result = camel_api::OutcomePipeline::run(&mut seg, ex).await;
646
647 assert!(
648 matches!(result, PipelineOutcome::Stopped(_)),
649 "Expected Stopped, got {result:?}"
650 );
651 assert!(
653 fragment1_completed.load(Ordering::SeqCst),
654 "fragment 1 should have completed despite Stop"
655 );
656 assert!(
658 fragment2_completed.load(Ordering::SeqCst),
659 "fragment 2 should have completed despite Stop"
660 );
661 }
662
663 #[tokio::test(flavor = "multi_thread")]
666 async fn stop_inside_split_parallel_lowest_stopped_index_wins() {
667 let splitter = SplitSource::Sync(Arc::new(|ex: &Exchange| {
669 Ok((0..3)
670 .map(|i| {
671 let mut frag = ex.clone();
672 frag.input.body = Body::Text(format!("from-fragment-{i}"));
673 frag
674 })
675 .collect())
676 }));
677
678 struct DualStopBody;
680 impl Clone for DualStopBody {
681 fn clone(&self) -> Self {
682 DualStopBody
683 }
684 }
685 impl camel_api::OutcomePipeline for DualStopBody {
686 fn clone_box(&self) -> Box<dyn camel_api::OutcomePipeline> {
687 Box::new(DualStopBody)
688 }
689 fn run<'a>(
690 &'a mut self,
691 exchange: Exchange,
692 ) -> Pin<Box<dyn Future<Output = PipelineOutcome> + Send + 'a>> {
693 let is_frag0 = exchange
694 .input
695 .body
696 .as_text()
697 .map(|s| s == "from-fragment-0")
698 .unwrap_or(false);
699 let is_frag2 = exchange
700 .input
701 .body
702 .as_text()
703 .map(|s| s == "from-fragment-2")
704 .unwrap_or(false);
705 Box::pin(async move {
706 if is_frag0 {
707 return PipelineOutcome::Stopped(exchange);
708 }
709 if is_frag2 {
710 tokio::time::sleep(std::time::Duration::from_millis(10)).await;
713 return PipelineOutcome::Stopped(exchange);
714 }
715 PipelineOutcome::Completed(exchange)
717 })
718 }
719 }
720
721 let mut seg = SplitSegment {
722 splitter,
723 body: OutcomeSegment::new(Box::new(DualStopBody)),
724 parallel: true,
725 parallel_limit: None,
726 stop_on_exception: true,
727 max_fragments: camel_api::splitter::DEFAULT_MAX_FRAGMENTS,
728 aggregation: AggregationStrategy::LastWins,
729 };
730
731 let ex = Exchange::new(Message::new("test"));
732 let result = camel_api::OutcomePipeline::run(&mut seg, ex).await;
733
734 match result {
735 PipelineOutcome::Stopped(ex) => {
736 assert_eq!(
737 ex.input.body.as_text(),
738 Some("from-fragment-0"),
739 "Lowest stopped index (0) should win, got body {:?}",
740 ex.input.body.as_text()
741 );
742 }
743 other => panic!("Expected Stopped with fragment-0 body, got {other:?}"),
744 }
745 }
746
747 #[tokio::test(flavor = "multi_thread")]
750 async fn split_parallel_limit_enforces_concurrency_cap() {
751 let concurrent = Arc::new(AtomicUsize::new(0));
752 let max_concurrent = Arc::new(AtomicUsize::new(0));
753
754 let splitter = SplitSource::Sync(Arc::new(|ex: &Exchange| {
756 Ok((0..6)
757 .map(|i| {
758 let mut frag = ex.clone();
759 frag.input.body = Body::Text(format!("frag-{i}"));
760 frag
761 })
762 .collect())
763 }));
764
765 let c = Arc::clone(&concurrent);
766 let mc = Arc::clone(&max_concurrent);
767 struct LimitedBody {
768 concurrent: Arc<AtomicUsize>,
769 max_concurrent: Arc<AtomicUsize>,
770 }
771 impl Clone for LimitedBody {
772 fn clone(&self) -> Self {
773 Self {
774 concurrent: Arc::clone(&self.concurrent),
775 max_concurrent: Arc::clone(&self.max_concurrent),
776 }
777 }
778 }
779 impl camel_api::OutcomePipeline for LimitedBody {
780 fn clone_box(&self) -> Box<dyn camel_api::OutcomePipeline> {
781 Box::new(self.clone())
782 }
783 fn run<'a>(
784 &'a mut self,
785 exchange: Exchange,
786 ) -> Pin<Box<dyn Future<Output = PipelineOutcome> + Send + 'a>> {
787 let c = Arc::clone(&self.concurrent);
788 let mc = Arc::clone(&self.max_concurrent);
789 Box::pin(async move {
790 let current = c.fetch_add(1, Ordering::SeqCst) + 1;
791 mc.fetch_max(current, Ordering::SeqCst);
792 tokio::task::yield_now().await;
793 c.fetch_sub(1, Ordering::SeqCst);
794 PipelineOutcome::Completed(exchange)
795 })
796 }
797 }
798
799 let mut seg = SplitSegment {
800 splitter,
801 body: OutcomeSegment::new(Box::new(LimitedBody {
802 concurrent: c,
803 max_concurrent: mc,
804 })),
805 parallel: true,
806 parallel_limit: Some(2),
807 stop_on_exception: true,
808 max_fragments: camel_api::splitter::DEFAULT_MAX_FRAGMENTS,
809 aggregation: AggregationStrategy::LastWins,
810 };
811
812 let ex = Exchange::new(Message::new("test"));
813 let result = camel_api::OutcomePipeline::run(&mut seg, ex).await;
814 assert!(
815 matches!(result, PipelineOutcome::Completed(_)),
816 "Expected Completed, got {result:?}"
817 );
818
819 let observed_max = max_concurrent.load(Ordering::SeqCst);
820 assert!(
821 observed_max <= 2,
822 "parallel_limit=2 but max concurrency was {observed_max}"
823 );
824 }
825
826 #[tokio::test]
829 async fn split_sequential_stop_on_exception_true() {
830 fn make_fail_body(
832 fail_at: usize,
833 counter: Arc<AtomicUsize>,
834 ) -> impl camel_api::OutcomePipeline + Clone {
835 #[derive(Clone)]
836 struct FailAtBody {
837 fail_at: usize,
838 counter: Arc<AtomicUsize>,
839 }
840 impl camel_api::OutcomePipeline for FailAtBody {
841 fn clone_box(&self) -> Box<dyn camel_api::OutcomePipeline> {
842 Box::new(self.clone())
843 }
844 fn run<'a>(
845 &'a mut self,
846 exchange: Exchange,
847 ) -> Pin<Box<dyn Future<Output = PipelineOutcome> + Send + 'a>> {
848 let count = self.counter.fetch_add(1, Ordering::SeqCst);
849 let fail_at = self.fail_at;
850 Box::pin(async move {
851 if count == fail_at {
852 PipelineOutcome::Failed(CamelError::ProcessorError(format!(
853 "fail at {count}"
854 )))
855 } else {
856 PipelineOutcome::Completed(exchange)
857 }
858 })
859 }
860 }
861 FailAtBody { fail_at, counter }
862 }
863
864 let invocations = Arc::new(AtomicUsize::new(0));
865 let body = make_fail_body(1, Arc::clone(&invocations));
866 let mut seg = SplitSegment {
867 splitter: camel_api::SplitSource::Sync(camel_api::split_body_lines()),
868 body: OutcomeSegment::new(Box::new(body)),
869 parallel: false,
870 parallel_limit: None,
871 stop_on_exception: true,
872 max_fragments: camel_api::splitter::DEFAULT_MAX_FRAGMENTS,
873 aggregation: AggregationStrategy::LastWins,
874 };
875
876 let ex = Exchange::new(Message::new("a\nb\nc\nd\ne"));
877 let result = camel_api::OutcomePipeline::run(&mut seg, ex).await;
878
879 assert!(
880 matches!(result, PipelineOutcome::Failed(_)),
881 "stop_on_exception=true should propagate first failure"
882 );
883 assert_eq!(
886 invocations.load(Ordering::SeqCst),
887 2,
888 "should stop after 2 fragments (0 pass, 1 fail)"
889 );
890 }
891
892 #[tokio::test]
895 async fn split_sequential_stop_on_exception_false() {
896 fn make_fail_body(
898 fail_at: usize,
899 counter: Arc<AtomicUsize>,
900 ) -> impl camel_api::OutcomePipeline + Clone {
901 #[derive(Clone)]
902 struct FailAtBody {
903 fail_at: usize,
904 counter: Arc<AtomicUsize>,
905 }
906 impl camel_api::OutcomePipeline for FailAtBody {
907 fn clone_box(&self) -> Box<dyn camel_api::OutcomePipeline> {
908 Box::new(self.clone())
909 }
910 fn run<'a>(
911 &'a mut self,
912 exchange: Exchange,
913 ) -> Pin<Box<dyn Future<Output = PipelineOutcome> + Send + 'a>> {
914 let count = self.counter.fetch_add(1, Ordering::SeqCst);
915 let fail_at = self.fail_at;
916 Box::pin(async move {
917 if count == fail_at {
918 PipelineOutcome::Failed(CamelError::ProcessorError(format!(
919 "fail at {count}"
920 )))
921 } else {
922 PipelineOutcome::Completed(exchange)
923 }
924 })
925 }
926 }
927 FailAtBody { fail_at, counter }
928 }
929
930 let invocations = Arc::new(AtomicUsize::new(0));
931 let body = make_fail_body(1, Arc::clone(&invocations));
932 let mut seg = SplitSegment {
933 splitter: camel_api::SplitSource::Sync(camel_api::split_body_lines()),
934 body: OutcomeSegment::new(Box::new(body)),
935 parallel: false,
936 parallel_limit: None,
937 stop_on_exception: false,
938 max_fragments: camel_api::splitter::DEFAULT_MAX_FRAGMENTS,
939 aggregation: AggregationStrategy::LastWins,
940 };
941
942 let ex = Exchange::new(Message::new("a\nb\nc\nd\ne"));
943 let result = camel_api::OutcomePipeline::run(&mut seg, ex).await;
944
945 assert!(
948 matches!(result, PipelineOutcome::Failed(_)),
949 "stop_on_exception=false should still propagate error at end"
950 );
951 assert_eq!(
953 invocations.load(Ordering::SeqCst),
954 5,
955 "all fragments should be processed when stop_on_exception=false"
956 );
957 }
958
959 #[tokio::test(flavor = "multi_thread")]
962 async fn split_parallel_stop_on_exception_true() {
963 let splitter = SplitSource::Sync(Arc::new(|ex: &Exchange| {
964 Ok((0..5)
965 .map(|i| {
966 let mut frag = ex.clone();
967 frag.input.body = Body::Text(format!("frag-{i}"));
968 frag
969 })
970 .collect())
971 }));
972
973 let invocations = Arc::new(AtomicUsize::new(0));
975 struct FailBody {
976 counter: Arc<AtomicUsize>,
977 }
978 impl Clone for FailBody {
979 fn clone(&self) -> Self {
980 Self {
981 counter: Arc::clone(&self.counter),
982 }
983 }
984 }
985 impl camel_api::OutcomePipeline for FailBody {
986 fn clone_box(&self) -> Box<dyn camel_api::OutcomePipeline> {
987 Box::new(self.clone())
988 }
989 fn run<'a>(
990 &'a mut self,
991 _exchange: Exchange,
992 ) -> Pin<Box<dyn Future<Output = PipelineOutcome> + Send + 'a>> {
993 let count = self.counter.fetch_add(1, Ordering::SeqCst);
994 Box::pin(async move {
995 PipelineOutcome::Failed(CamelError::ProcessorError(format!("fail {count}")))
996 })
997 }
998 }
999
1000 let mut seg = SplitSegment {
1001 splitter,
1002 body: OutcomeSegment::new(Box::new(FailBody {
1003 counter: Arc::clone(&invocations),
1004 })),
1005 parallel: true,
1006 parallel_limit: None,
1007 stop_on_exception: true,
1008 max_fragments: camel_api::splitter::DEFAULT_MAX_FRAGMENTS,
1009 aggregation: AggregationStrategy::LastWins,
1010 };
1011
1012 let ex = Exchange::new(Message::new("test"));
1013 let result = camel_api::OutcomePipeline::run(&mut seg, ex).await;
1014
1015 assert!(
1016 matches!(result, PipelineOutcome::Failed(_)),
1017 "stop_on_exception=true should propagate first failure"
1018 );
1019 assert_eq!(
1021 invocations.load(Ordering::SeqCst),
1022 5,
1023 "all fragments should be spawned"
1024 );
1025 }
1026
1027 #[tokio::test(flavor = "multi_thread")]
1030 async fn split_parallel_stop_on_exception_false() {
1031 let splitter = SplitSource::Sync(Arc::new(|ex: &Exchange| {
1032 Ok((0..5)
1033 .map(|i| {
1034 let mut frag = ex.clone();
1035 frag.input.body = Body::Text(format!("frag-{i}"));
1036 frag
1037 })
1038 .collect())
1039 }));
1040
1041 let invocations = Arc::new(AtomicUsize::new(0));
1043 struct MixedBody {
1044 counter: Arc<AtomicUsize>,
1045 }
1046 impl Clone for MixedBody {
1047 fn clone(&self) -> Self {
1048 Self {
1049 counter: Arc::clone(&self.counter),
1050 }
1051 }
1052 }
1053 impl camel_api::OutcomePipeline for MixedBody {
1054 fn clone_box(&self) -> Box<dyn camel_api::OutcomePipeline> {
1055 Box::new(self.clone())
1056 }
1057 fn run<'a>(
1058 &'a mut self,
1059 exchange: Exchange,
1060 ) -> Pin<Box<dyn Future<Output = PipelineOutcome> + Send + 'a>> {
1061 let count = self.counter.fetch_add(1, Ordering::SeqCst);
1062 Box::pin(async move {
1063 if count == 1 {
1064 PipelineOutcome::Failed(CamelError::ProcessorError("fail 1".into()))
1065 } else {
1066 PipelineOutcome::Completed(exchange)
1067 }
1068 })
1069 }
1070 }
1071
1072 let mut seg = SplitSegment {
1073 splitter,
1074 body: OutcomeSegment::new(Box::new(MixedBody {
1075 counter: Arc::clone(&invocations),
1076 })),
1077 parallel: true,
1078 parallel_limit: None,
1079 stop_on_exception: false,
1080 max_fragments: camel_api::splitter::DEFAULT_MAX_FRAGMENTS,
1081 aggregation: AggregationStrategy::LastWins,
1082 };
1083
1084 let ex = Exchange::new(Message::new("test"));
1085 let result = camel_api::OutcomePipeline::run(&mut seg, ex).await;
1086
1087 assert!(
1089 matches!(result, PipelineOutcome::Failed(_)),
1090 "stop_on_exception=false should propagate failure at end; got {result:?}"
1091 );
1092 assert_eq!(
1093 invocations.load(Ordering::SeqCst),
1094 5,
1095 "all fragments should be spawned"
1096 );
1097 }
1098
1099 #[tokio::test]
1102 async fn test_split_segment_expression_error_is_failed() {
1103 #[derive(Clone)]
1105 struct RecordingBody {
1106 counter: Arc<AtomicUsize>,
1107 }
1108 impl camel_api::OutcomePipeline for RecordingBody {
1109 fn clone_box(&self) -> Box<dyn camel_api::OutcomePipeline> {
1110 Box::new(self.clone())
1111 }
1112 fn run<'a>(
1113 &'a mut self,
1114 exchange: Exchange,
1115 ) -> Pin<Box<dyn Future<Output = PipelineOutcome> + Send + 'a>> {
1116 self.counter.fetch_add(1, Ordering::SeqCst);
1117 Box::pin(async move { PipelineOutcome::Completed(exchange) })
1118 }
1119 }
1120
1121 let invocations = Arc::new(AtomicUsize::new(0));
1122 let splitter = SplitSource::Sync(Arc::new(|_| {
1123 Err(CamelError::TypeConversionFailed(
1124 "declarative split requires a text or array value, got number; add an unmarshal step before split"
1125 .to_string(),
1126 ))
1127 }));
1128
1129 let mut seg = SplitSegment {
1130 splitter,
1131 body: OutcomeSegment::new(Box::new(RecordingBody {
1132 counter: Arc::clone(&invocations),
1133 })),
1134 parallel: false,
1135 parallel_limit: None,
1136 stop_on_exception: true,
1137 max_fragments: camel_api::splitter::DEFAULT_MAX_FRAGMENTS,
1138 aggregation: AggregationStrategy::LastWins,
1139 };
1140
1141 let ex = Exchange::new(Message::new("anything"));
1142 let result = camel_api::OutcomePipeline::run(&mut seg, ex).await;
1143
1144 match result {
1145 PipelineOutcome::Failed(err) => {
1146 let msg = err.to_string();
1147 assert!(
1148 msg.contains("declarative split"),
1149 "carried error should mention 'declarative split': {msg}"
1150 );
1151 }
1152 other => panic!("Expected Failed, got {other:?}"),
1153 }
1154 assert_eq!(
1155 invocations.load(Ordering::SeqCst),
1156 0,
1157 "body segment must record zero invocations when the split expression errors"
1158 );
1159 }
1160
1161 #[tokio::test]
1162 async fn split_segment_wrong_type_body_fails_loud() {
1163 let invocations = Arc::new(AtomicUsize::new(0));
1166
1167 #[derive(Clone)]
1168 struct RecordingBody {
1169 counter: Arc<AtomicUsize>,
1170 }
1171 impl camel_api::OutcomePipeline for RecordingBody {
1172 fn clone_box(&self) -> Box<dyn camel_api::OutcomePipeline> {
1173 Box::new(self.clone())
1174 }
1175 fn run<'a>(
1176 &'a mut self,
1177 exchange: Exchange,
1178 ) -> Pin<Box<dyn Future<Output = PipelineOutcome> + Send + 'a>> {
1179 self.counter.fetch_add(1, Ordering::SeqCst);
1180 Box::pin(async move { PipelineOutcome::Completed(exchange) })
1181 }
1182 }
1183
1184 let mut seg = SplitSegment {
1185 splitter: SplitSource::Sync(camel_api::split_body_json_array()),
1186 body: OutcomeSegment::new(Box::new(RecordingBody {
1187 counter: Arc::clone(&invocations),
1188 })),
1189 parallel: false,
1190 parallel_limit: None,
1191 stop_on_exception: true,
1192 max_fragments: camel_api::splitter::DEFAULT_MAX_FRAGMENTS,
1193 aggregation: AggregationStrategy::LastWins,
1194 };
1195
1196 let ex = Exchange::new(Message::new(Body::Text("a,b".to_string())));
1197 let result = camel_api::OutcomePipeline::run(&mut seg, ex).await;
1198
1199 match result {
1200 PipelineOutcome::Failed(err) => {
1201 let msg = err.to_string();
1202 for needle in [
1203 "body_json_array",
1204 "text",
1205 "json (array)",
1206 "add an unmarshal step before split",
1207 ] {
1208 assert!(msg.contains(needle), "message '{msg}' missing '{needle}'");
1209 }
1210 }
1211 other => panic!("wrong body type must fail loud, got {other:?}"),
1212 }
1213 assert_eq!(
1214 invocations.load(Ordering::SeqCst),
1215 0,
1216 "body segment must record zero invocations when the split expression errors"
1217 );
1218 }
1219
1220 #[tokio::test]
1221 async fn split_segment_async_error_propagates_before_fragments() {
1222 use camel_api::BoxValueFuture;
1223
1224 let invocations = Arc::new(AtomicUsize::new(0));
1225
1226 #[derive(Clone)]
1227 struct RecordingBody {
1228 counter: Arc<AtomicUsize>,
1229 }
1230 impl camel_api::OutcomePipeline for RecordingBody {
1231 fn clone_box(&self) -> Box<dyn camel_api::OutcomePipeline> {
1232 Box::new(self.clone())
1233 }
1234 fn run<'a>(
1235 &'a mut self,
1236 exchange: Exchange,
1237 ) -> Pin<Box<dyn Future<Output = PipelineOutcome> + Send + 'a>> {
1238 self.counter.fetch_add(1, Ordering::SeqCst);
1239 Box::pin(async move { PipelineOutcome::Completed(exchange) })
1240 }
1241 }
1242
1243 let splitter = SplitSource::Async(Arc::new(|_: &Exchange| {
1244 Box::pin(async { Err(CamelError::ProcessorError("split boom".into())) })
1245 as BoxValueFuture
1246 }));
1247
1248 let mut seg = SplitSegment {
1249 splitter,
1250 body: OutcomeSegment::new(Box::new(RecordingBody {
1251 counter: Arc::clone(&invocations),
1252 })),
1253 parallel: false,
1254 parallel_limit: None,
1255 stop_on_exception: true,
1256 max_fragments: camel_api::splitter::DEFAULT_MAX_FRAGMENTS,
1257 aggregation: AggregationStrategy::LastWins,
1258 };
1259
1260 let ex = Exchange::new(Message::new("a\nb\nc"));
1261 let result = camel_api::OutcomePipeline::run(&mut seg, ex).await;
1262
1263 assert!(
1264 matches!(result, PipelineOutcome::Failed(CamelError::ProcessorError(ref e)) if e.contains("split boom")),
1265 "an async split-expression failure must propagate; got {result:?}"
1266 );
1267 assert_eq!(
1268 invocations.load(Ordering::SeqCst),
1269 0,
1270 "no fragment may reach the sub-pipeline when the split expression fails"
1271 );
1272 }
1273 #[derive(Clone)]
1279 struct MetadataCaptureBody {
1280 triples: Arc<std::sync::Mutex<Vec<(u64, u64, bool)>>>,
1281 }
1282 impl camel_api::OutcomePipeline for MetadataCaptureBody {
1283 fn clone_box(&self) -> Box<dyn camel_api::OutcomePipeline> {
1284 Box::new(self.clone())
1285 }
1286 fn run<'a>(
1287 &'a mut self,
1288 exchange: Exchange,
1289 ) -> Pin<Box<dyn Future<Output = PipelineOutcome> + Send + 'a>> {
1290 let triples = Arc::clone(&self.triples);
1291 Box::pin(async move {
1292 let index = exchange
1293 .property(crate::splitter::CAMEL_SPLIT_INDEX)
1294 .and_then(|v| v.as_u64())
1295 .unwrap_or(u64::MAX);
1296 let size = exchange
1297 .property(crate::splitter::CAMEL_SPLIT_SIZE)
1298 .and_then(|v| v.as_u64())
1299 .unwrap_or(u64::MAX);
1300 let complete = exchange
1301 .property(crate::splitter::CAMEL_SPLIT_COMPLETE)
1302 .and_then(|v| v.as_bool())
1303 .unwrap_or(false);
1304 triples.lock().unwrap().push((index, size, complete));
1305 PipelineOutcome::Completed(exchange)
1306 })
1307 }
1308 }
1309
1310 fn n_fragment_splitter(n: u64) -> SplitSource {
1312 SplitSource::Sync(Arc::new(move |ex: &Exchange| {
1313 Ok((0..n)
1314 .map(|i| {
1315 let mut frag = ex.clone();
1316 frag.input.body = Body::Text(format!("frag-{i}"));
1317 frag
1318 })
1319 .collect())
1320 }))
1321 }
1322
1323 #[tokio::test]
1324 async fn sequential_split_stamps_fragment_metadata() {
1325 let triples = Arc::new(std::sync::Mutex::new(Vec::new()));
1326
1327 let mut seg = SplitSegment {
1328 splitter: n_fragment_splitter(3),
1329 body: OutcomeSegment::new(Box::new(MetadataCaptureBody {
1330 triples: Arc::clone(&triples),
1331 })),
1332 parallel: false,
1333 parallel_limit: None,
1334 stop_on_exception: true,
1335 max_fragments: camel_api::splitter::DEFAULT_MAX_FRAGMENTS,
1336 aggregation: AggregationStrategy::LastWins,
1337 };
1338
1339 let ex = Exchange::new(Message::new("test"));
1340 let result = camel_api::OutcomePipeline::run(&mut seg, ex).await;
1341 assert!(
1342 matches!(result, PipelineOutcome::Completed(_)),
1343 "Expected Completed, got {result:?}"
1344 );
1345
1346 assert_eq!(
1347 *triples.lock().unwrap(),
1348 vec![(0, 3, false), (1, 3, false), (2, 3, true)]
1349 );
1350 }
1351
1352 #[tokio::test(flavor = "multi_thread")]
1353 async fn parallel_split_stamps_fragment_metadata() {
1354 let triples = Arc::new(std::sync::Mutex::new(Vec::new()));
1355
1356 let mut seg = SplitSegment {
1357 splitter: n_fragment_splitter(3),
1358 body: OutcomeSegment::new(Box::new(MetadataCaptureBody {
1359 triples: Arc::clone(&triples),
1360 })),
1361 parallel: true,
1362 parallel_limit: None,
1363 stop_on_exception: true,
1364 max_fragments: camel_api::splitter::DEFAULT_MAX_FRAGMENTS,
1365 aggregation: AggregationStrategy::LastWins,
1366 };
1367
1368 let ex = Exchange::new(Message::new("test"));
1369 let result = camel_api::OutcomePipeline::run(&mut seg, ex).await;
1370 assert!(
1371 matches!(result, PipelineOutcome::Completed(_)),
1372 "Expected Completed, got {result:?}"
1373 );
1374
1375 let mut got = triples.lock().unwrap().clone();
1376 got.sort_by_key(|(idx, _, _)| *idx);
1377 assert_eq!(got, vec![(0, 3, false), (1, 3, false), (2, 3, true)]);
1378 }
1379
1380 #[tokio::test]
1381 async fn single_fragment_is_complete() {
1382 let triples = Arc::new(std::sync::Mutex::new(Vec::new()));
1383
1384 let mut seg = SplitSegment {
1385 splitter: n_fragment_splitter(1),
1386 body: OutcomeSegment::new(Box::new(MetadataCaptureBody {
1387 triples: Arc::clone(&triples),
1388 })),
1389 parallel: false,
1390 parallel_limit: None,
1391 stop_on_exception: true,
1392 max_fragments: camel_api::splitter::DEFAULT_MAX_FRAGMENTS,
1393 aggregation: AggregationStrategy::LastWins,
1394 };
1395
1396 let ex = Exchange::new(Message::new("test"));
1397 let result = camel_api::OutcomePipeline::run(&mut seg, ex).await;
1398 assert!(
1399 matches!(result, PipelineOutcome::Completed(_)),
1400 "Expected Completed, got {result:?}"
1401 );
1402
1403 assert_eq!(*triples.lock().unwrap(), vec![(0, 1, true)]);
1404 }
1405
1406 #[tokio::test]
1409 async fn split_segment_empty_fragments_returns_original_unchanged() {
1410 let mut seg = SplitSegment {
1412 splitter: SplitSource::Sync(camel_api::split_body_lines()),
1413 body: OutcomeSegment::new(Box::new(CompletedBody)),
1414 parallel: false,
1415 parallel_limit: None,
1416 stop_on_exception: true,
1417 max_fragments: camel_api::splitter::DEFAULT_MAX_FRAGMENTS,
1418 aggregation: AggregationStrategy::LastWins,
1419 };
1420
1421 let mut ex = Exchange::new(Message::default()); ex.set_property("marker", Value::Bool(true));
1423
1424 let result = camel_api::OutcomePipeline::run(&mut seg, ex).await;
1425 match result {
1426 PipelineOutcome::Completed(out) => {
1427 assert!(out.input.body.is_empty());
1428 assert_eq!(out.property("marker"), Some(&Value::Bool(true)));
1429 }
1430 other => panic!("Expected Completed(original), got {other:?}"),
1431 }
1432 }
1433
1434 #[derive(Clone)]
1436 struct MetadataJsonBody;
1437 impl camel_api::OutcomePipeline for MetadataJsonBody {
1438 fn clone_box(&self) -> Box<dyn camel_api::OutcomePipeline> {
1439 Box::new(MetadataJsonBody)
1440 }
1441 fn run<'a>(
1442 &'a mut self,
1443 exchange: Exchange,
1444 ) -> Pin<Box<dyn Future<Output = PipelineOutcome> + Send + 'a>> {
1445 Box::pin(async move {
1446 let index = exchange
1447 .property(crate::splitter::CAMEL_SPLIT_INDEX)
1448 .cloned();
1449 let size = exchange
1450 .property(crate::splitter::CAMEL_SPLIT_SIZE)
1451 .cloned();
1452 let complete = exchange
1453 .property(crate::splitter::CAMEL_SPLIT_COMPLETE)
1454 .cloned();
1455 let mut out = exchange;
1456 out.input.body = Body::Json(serde_json::json!({
1457 "index": index,
1458 "size": size,
1459 "complete": complete,
1460 }));
1461 PipelineOutcome::Completed(out)
1462 })
1463 }
1464 }
1465
1466 #[tokio::test]
1467 async fn split_segment_metadata_properties() {
1468 let mut seg = SplitSegment {
1471 splitter: SplitSource::Sync(camel_api::split_body_lines()),
1472 body: OutcomeSegment::new(Box::new(MetadataJsonBody)),
1473 parallel: false,
1474 parallel_limit: None,
1475 stop_on_exception: true,
1476 max_fragments: camel_api::splitter::DEFAULT_MAX_FRAGMENTS,
1477 aggregation: AggregationStrategy::CollectAll,
1478 };
1479
1480 let ex = Exchange::new(Message::new("x\ny\nz"));
1481 let result = camel_api::OutcomePipeline::run(&mut seg, ex).await;
1482
1483 let expected = serde_json::json!([
1484 {"index": 0, "size": 3, "complete": false},
1485 {"index": 1, "size": 3, "complete": false},
1486 {"index": 2, "size": 3, "complete": true},
1487 ]);
1488 match result {
1489 PipelineOutcome::Completed(out) => match &out.input.body {
1490 Body::Json(v) => assert_eq!(*v, expected),
1491 other => panic!("expected JSON body, got {other:?}"),
1492 },
1493 other => panic!("Expected Completed, got {other:?}"),
1494 }
1495 }
1496
1497 #[tokio::test]
1498 async fn split_segment_json_array_body() {
1499 let mut seg = SplitSegment {
1501 splitter: SplitSource::Sync(camel_api::split_body_json_array()),
1502 body: OutcomeSegment::new(Box::new(CompletedBody)),
1503 parallel: false,
1504 parallel_limit: None,
1505 stop_on_exception: true,
1506 max_fragments: camel_api::splitter::DEFAULT_MAX_FRAGMENTS,
1507 aggregation: AggregationStrategy::CollectAll,
1508 };
1509
1510 let msg = Message::new(Body::Json(serde_json::json!([1, 2, 3])));
1511 let parent = Exchange::new(msg);
1512
1513 let result = camel_api::OutcomePipeline::run(&mut seg, parent).await;
1514 match result {
1515 PipelineOutcome::Completed(out) => match &out.input.body {
1516 Body::Json(serde_json::Value::Array(arr)) => {
1517 assert_eq!(
1518 arr,
1519 &vec![
1520 serde_json::json!(1),
1521 serde_json::json!(2),
1522 serde_json::json!(3)
1523 ]
1524 );
1525 }
1526 other => panic!("expected JSON array body, got {other:?}"),
1527 },
1528 other => panic!("Expected Completed, got {other:?}"),
1529 }
1530 }
1531
1532 #[derive(Clone)]
1534 struct CorrelationIdBody;
1535 impl camel_api::OutcomePipeline for CorrelationIdBody {
1536 fn clone_box(&self) -> Box<dyn camel_api::OutcomePipeline> {
1537 Box::new(CorrelationIdBody)
1538 }
1539 fn run<'a>(
1540 &'a mut self,
1541 mut exchange: Exchange,
1542 ) -> Pin<Box<dyn Future<Output = PipelineOutcome> + Send + 'a>> {
1543 Box::pin(async move {
1544 exchange.input.body = Body::Text(exchange.correlation_id().to_string());
1545 PipelineOutcome::Completed(exchange)
1546 })
1547 }
1548 }
1549
1550 #[tokio::test]
1551 async fn split_segment_each_fragment_has_unique_correlation_id() {
1552 let mut seg = SplitSegment {
1553 splitter: SplitSource::Sync(camel_api::split_body_lines()),
1554 body: OutcomeSegment::new(Box::new(CorrelationIdBody)),
1555 parallel: false,
1556 parallel_limit: None,
1557 stop_on_exception: true,
1558 max_fragments: camel_api::splitter::DEFAULT_MAX_FRAGMENTS,
1559 aggregation: AggregationStrategy::CollectAll,
1560 };
1561
1562 let ex = Exchange::new(Message::new("a\nb\nc\nd"));
1563 let result = camel_api::OutcomePipeline::run(&mut seg, ex).await;
1564
1565 let ids: Vec<String> = match result {
1566 PipelineOutcome::Completed(out) => match &out.input.body {
1567 Body::Json(serde_json::Value::Array(arr)) => arr
1568 .iter()
1569 .map(|v| v.as_str().unwrap_or("").to_string())
1570 .collect(),
1571 other => panic!("expected JSON array of ids, got {other:?}"),
1572 },
1573 other => panic!("Expected Completed, got {other:?}"),
1574 };
1575 assert_eq!(ids.len(), 4, "should have 4 fragments");
1576 let unique: std::collections::HashSet<&String> = ids.iter().collect();
1577 assert_eq!(unique.len(), 4, "fragment correlation_ids must be unique");
1578 }
1579
1580 #[derive(Clone)]
1584 struct CountingBody {
1585 counter: Arc<AtomicUsize>,
1586 }
1587 impl camel_api::OutcomePipeline for CountingBody {
1588 fn clone_box(&self) -> Box<dyn camel_api::OutcomePipeline> {
1589 Box::new(self.clone())
1590 }
1591 fn run<'a>(
1592 &'a mut self,
1593 exchange: Exchange,
1594 ) -> Pin<Box<dyn Future<Output = PipelineOutcome> + Send + 'a>> {
1595 self.counter.fetch_add(1, Ordering::SeqCst);
1596 Box::pin(async move { PipelineOutcome::Completed(exchange) })
1597 }
1598 }
1599
1600 fn counting_body(counter: &Arc<AtomicUsize>) -> OutcomeSegment {
1601 OutcomeSegment::new(Box::new(CountingBody {
1602 counter: Arc::clone(counter),
1603 }))
1604 }
1605
1606 #[tokio::test]
1607 async fn split_rejects_above_custom_cap() {
1608 let invocations = Arc::new(AtomicUsize::new(0));
1609 let mut seg = SplitSegment {
1610 splitter: n_fragment_splitter(5),
1611 body: counting_body(&invocations),
1612 parallel: false,
1613 parallel_limit: None,
1614 stop_on_exception: true,
1615 max_fragments: 4,
1616 aggregation: AggregationStrategy::LastWins,
1617 };
1618
1619 let result =
1620 camel_api::OutcomePipeline::run(&mut seg, Exchange::new(Message::new("test"))).await;
1621
1622 match result {
1623 PipelineOutcome::Failed(err) => assert!(
1624 err.to_string()
1625 .contains("Splitter produced 5 fragments, exceeding max_fragments 4"),
1626 "error should name the produced count and cap, got {err}"
1627 ),
1628 other => panic!("expected Failed, got {other:?}"),
1629 }
1630 assert_eq!(
1631 invocations.load(Ordering::SeqCst),
1632 0,
1633 "no fragment may run when the cap is exceeded"
1634 );
1635 }
1636
1637 #[tokio::test]
1638 async fn split_accepts_at_and_below_custom_cap() {
1639 let below = Arc::new(AtomicUsize::new(0));
1640 let mut below_seg = SplitSegment {
1641 splitter: n_fragment_splitter(3),
1642 body: counting_body(&below),
1643 parallel: false,
1644 parallel_limit: None,
1645 stop_on_exception: true,
1646 max_fragments: 4,
1647 aggregation: AggregationStrategy::LastWins,
1648 };
1649 let below_result =
1650 camel_api::OutcomePipeline::run(&mut below_seg, Exchange::new(Message::new("test")))
1651 .await;
1652 assert!(
1653 matches!(below_result, PipelineOutcome::Completed(_)),
1654 "cap-1 must complete, got {below_result:?}"
1655 );
1656 assert_eq!(below.load(Ordering::SeqCst), 3, "cap-1 should run 3 bodies");
1657
1658 let at = Arc::new(AtomicUsize::new(0));
1659 let mut at_seg = SplitSegment {
1660 splitter: n_fragment_splitter(4),
1661 body: counting_body(&at),
1662 parallel: false,
1663 parallel_limit: None,
1664 stop_on_exception: true,
1665 max_fragments: 4,
1666 aggregation: AggregationStrategy::LastWins,
1667 };
1668 let at_result =
1669 camel_api::OutcomePipeline::run(&mut at_seg, Exchange::new(Message::new("test"))).await;
1670 assert!(
1671 matches!(at_result, PipelineOutcome::Completed(_)),
1672 "exactly-at-cap must complete, got {at_result:?}"
1673 );
1674 assert_eq!(at.load(Ordering::SeqCst), 4, "cap should run 4 bodies");
1675 }
1676
1677 #[tokio::test(flavor = "multi_thread")]
1678 async fn split_rejects_before_spawning_parallel() {
1679 let invocations = Arc::new(AtomicUsize::new(0));
1680 let mut seg = SplitSegment {
1681 splitter: n_fragment_splitter(3),
1682 body: counting_body(&invocations),
1683 parallel: true,
1684 parallel_limit: None,
1685 stop_on_exception: true,
1686 max_fragments: 2,
1687 aggregation: AggregationStrategy::LastWins,
1688 };
1689
1690 let result =
1691 camel_api::OutcomePipeline::run(&mut seg, Exchange::new(Message::new("test"))).await;
1692
1693 match result {
1694 PipelineOutcome::Failed(err) => assert!(
1695 err.to_string()
1696 .contains("Splitter produced 3 fragments, exceeding max_fragments 2"),
1697 "error should name the produced count and cap, got {err}"
1698 ),
1699 other => panic!("expected Failed, got {other:?}"),
1700 }
1701 assert_eq!(
1702 invocations.load(Ordering::SeqCst),
1703 0,
1704 "no parallel task may spawn when the cap is exceeded"
1705 );
1706 }
1707
1708 #[tokio::test]
1709 async fn split_rejects_before_processing_sequential() {
1710 let invocations = Arc::new(AtomicUsize::new(0));
1711 let mut seg = SplitSegment {
1712 splitter: n_fragment_splitter(3),
1713 body: counting_body(&invocations),
1714 parallel: false,
1715 parallel_limit: None,
1716 stop_on_exception: true,
1717 max_fragments: 2,
1718 aggregation: AggregationStrategy::LastWins,
1719 };
1720
1721 let result =
1722 camel_api::OutcomePipeline::run(&mut seg, Exchange::new(Message::new("test"))).await;
1723
1724 match result {
1725 PipelineOutcome::Failed(err) => assert!(
1726 err.to_string()
1727 .contains("Splitter produced 3 fragments, exceeding max_fragments 2"),
1728 "error should name the produced count and cap, got {err}"
1729 ),
1730 other => panic!("expected Failed, got {other:?}"),
1731 }
1732 assert_eq!(
1733 invocations.load(Ordering::SeqCst),
1734 0,
1735 "no fragment may run when the cap is exceeded"
1736 );
1737 }
1738
1739 #[tokio::test]
1740 async fn split_empty_fragments_pass_through_with_cap() {
1741 let invocations = Arc::new(AtomicUsize::new(0));
1742 let splitter = SplitSource::Sync(
1743 Arc::new(|_: &Exchange| Ok(Vec::new())) as camel_api::SplitExpression
1744 );
1745 let mut seg = SplitSegment {
1746 splitter,
1747 body: counting_body(&invocations),
1748 parallel: false,
1749 parallel_limit: None,
1750 stop_on_exception: true,
1751 max_fragments: 4,
1752 aggregation: AggregationStrategy::LastWins,
1753 };
1754
1755 let result =
1756 camel_api::OutcomePipeline::run(&mut seg, Exchange::new(Message::new("original")))
1757 .await;
1758
1759 match result {
1760 PipelineOutcome::Completed(out) => {
1761 assert_eq!(
1762 out.input.body.as_text(),
1763 Some("original"),
1764 "empty split must pass the original exchange through"
1765 );
1766 }
1767 other => panic!("expected Completed, got {other:?}"),
1768 }
1769 assert_eq!(
1770 invocations.load(Ordering::SeqCst),
1771 0,
1772 "an empty split must not invoke the body"
1773 );
1774 }
1775
1776 #[tokio::test]
1777 async fn split_default_cap_respected() {
1778 assert_eq!(camel_api::splitter::DEFAULT_MAX_FRAGMENTS, 100_000);
1779
1780 let invocations = Arc::new(AtomicUsize::new(0));
1781 let mut seg = SplitSegment {
1782 splitter: n_fragment_splitter(3),
1783 body: counting_body(&invocations),
1784 parallel: false,
1785 parallel_limit: None,
1786 stop_on_exception: true,
1787 max_fragments: camel_api::splitter::DEFAULT_MAX_FRAGMENTS,
1788 aggregation: AggregationStrategy::LastWins,
1789 };
1790
1791 let result =
1792 camel_api::OutcomePipeline::run(&mut seg, Exchange::new(Message::new("test"))).await;
1793
1794 assert!(
1795 matches!(result, PipelineOutcome::Completed(_)),
1796 "3 fragments are well below the default cap, got {result:?}"
1797 );
1798 assert_eq!(invocations.load(Ordering::SeqCst), 3, "all 3 bodies run");
1799 }
1800
1801 #[tokio::test]
1802 async fn split_default_cap_plus_one_rejected() {
1803 let invocations = Arc::new(AtomicUsize::new(0));
1804 let mut seg = SplitSegment {
1805 splitter: n_fragment_splitter(100_001),
1806 body: counting_body(&invocations),
1807 parallel: false,
1808 parallel_limit: None,
1809 stop_on_exception: true,
1810 max_fragments: camel_api::splitter::DEFAULT_MAX_FRAGMENTS,
1811 aggregation: AggregationStrategy::LastWins,
1812 };
1813
1814 let result =
1815 camel_api::OutcomePipeline::run(&mut seg, Exchange::new(Message::new("test"))).await;
1816
1817 match result {
1818 PipelineOutcome::Failed(err) => assert!(
1819 err.to_string().contains("exceeding max_fragments 100000"),
1820 "default cap error should name 100000, got {err}"
1821 ),
1822 other => panic!("expected Failed, got {other:?}"),
1823 }
1824 assert_eq!(
1825 invocations.load(Ordering::SeqCst),
1826 0,
1827 "no fragment may run when the default cap is exceeded"
1828 );
1829 }
1830
1831 #[tokio::test]
1832 async fn split_zero_cap_fails_closed_non_empty() {
1833 let invocations = Arc::new(AtomicUsize::new(0));
1834 let mut seg = SplitSegment {
1835 splitter: n_fragment_splitter(2),
1836 body: counting_body(&invocations),
1837 parallel: false,
1838 parallel_limit: None,
1839 stop_on_exception: true,
1840 max_fragments: 0,
1841 aggregation: AggregationStrategy::LastWins,
1842 };
1843
1844 let result =
1845 camel_api::OutcomePipeline::run(&mut seg, Exchange::new(Message::new("test"))).await;
1846
1847 match result {
1848 PipelineOutcome::Failed(err) => assert!(
1849 err.to_string().contains("exceeding max_fragments 0"),
1850 "zero cap must fail closed for non-empty splits, got {err}"
1851 ),
1852 other => panic!("expected Failed, got {other:?}"),
1853 }
1854 assert_eq!(
1855 invocations.load(Ordering::SeqCst),
1856 0,
1857 "zero cap must reject before processing"
1858 );
1859 }
1860}