1use std::future::Future;
2use std::pin::Pin;
3use std::task::{Context, Poll};
4
5use tokio::task::JoinSet;
6use tower::Service;
7use tower::ServiceExt;
8
9use camel_api::endpoint_pipeline::{CAMEL_SLIP_ENDPOINT, EndpointPipelineConfig};
10use camel_api::recipient_list::RecipientListConfig;
11use camel_api::{Body, CamelError, Exchange, Value};
12
13use crate::endpoint_pipeline::EndpointPipelineService;
14
15#[derive(Clone)]
16pub struct RecipientListService {
17 config: RecipientListConfig,
18 pipeline: EndpointPipelineService,
19}
20
21impl RecipientListService {
22 pub fn new(
23 config: RecipientListConfig,
24 endpoint_resolver: camel_api::EndpointResolver,
25 ) -> Result<Self, CamelError> {
26 config.validate()?;
27 let pipeline_config = EndpointPipelineConfig {
28 cache_size: EndpointPipelineConfig::from_signed(1000),
29 ignore_invalid_endpoints: false,
30 };
31 Ok(Self {
32 config,
33 pipeline: EndpointPipelineService::new(endpoint_resolver, pipeline_config),
34 })
35 }
36}
37
38impl Service<Exchange> for RecipientListService {
39 type Response = Exchange;
40 type Error = CamelError;
41 type Future = Pin<Box<dyn Future<Output = Result<Exchange, CamelError>> + Send>>;
42
43 fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
44 Poll::Ready(Ok(()))
45 }
46
47 fn call(&mut self, mut exchange: Exchange) -> Self::Future {
48 let config = self.config.clone();
49 let pipeline = self.pipeline.clone();
50
51 Box::pin(async move {
52 let uris_raw = (config.expression)(&exchange);
53 if uris_raw.is_empty() {
54 return Ok(exchange);
55 }
56
57 let cap = config.max_recipients;
63 let uris: Vec<&str> = uris_raw
64 .split(&config.delimiter)
65 .map(|s| s.trim())
66 .filter(|s| !s.is_empty())
67 .take(cap)
68 .collect();
69 if uris.is_empty() {
70 return Ok(exchange);
71 }
72
73 if config.parallel {
74 let original_for_aggregate = exchange.clone();
75 let mut endpoints_to_call = Vec::with_capacity(uris.len());
76 for uri in &uris {
77 if let Some(endpoint) = pipeline.resolve(uri)? {
78 endpoints_to_call.push((uri.to_string(), endpoint));
79 }
80 }
81
82 let mut results: Vec<Exchange> = Vec::with_capacity(endpoints_to_call.len());
83 let mut join_set = JoinSet::new();
84 let mut iter = endpoints_to_call.into_iter();
85 let raw_limit = config.parallel_limit.unwrap_or(results.capacity());
86 let limit = raw_limit.max(1).min(results.capacity().max(1));
87 let mut last_parallel_error: Option<CamelError> = None;
88
89 for _ in 0..limit {
90 if let Some((uri, mut endpoint)) = iter.next() {
91 let mut cloned = original_for_aggregate.clone();
92 cloned.set_property(CAMEL_SLIP_ENDPOINT, Value::String(uri));
93 join_set.spawn(async move { endpoint.ready().await?.call(cloned).await });
94 }
95 }
96
97 while let Some(result) = join_set.join_next().await {
98 match result {
99 Ok(Ok(ex)) => results.push(ex),
100 Ok(Err(e)) if config.stop_on_exception => {
101 join_set.abort_all();
102 return Err(e);
103 }
104 Ok(Err(e)) => {
105 last_parallel_error = Some(e);
109 }
110 Err(join_err) if join_err.is_panic() => {
111 last_parallel_error = Some(CamelError::ProcessorError(format!(
118 "recipient task panicked: {join_err}"
119 )));
120 }
121 Err(_) => {} }
123
124 if let Some((uri, mut endpoint)) = iter.next() {
125 let mut cloned = original_for_aggregate.clone();
126 cloned.set_property(CAMEL_SLIP_ENDPOINT, Value::String(uri));
127 join_set.spawn(async move { endpoint.ready().await?.call(cloned).await });
128 }
129 }
130
131 let zero_success_error = if results.is_empty() {
135 last_parallel_error
136 } else {
137 None
138 };
139 if let Some(err) = zero_success_error {
140 return Err(err);
141 }
142
143 exchange = aggregate_results(config.strategy, original_for_aggregate, results);
144 } else {
145 let mut results: Vec<Exchange> = Vec::new();
146 let mut last_error: Option<CamelError> = None;
147 let original_for_aggregate = exchange.clone();
148 for uri in &uris {
149 let endpoint = match pipeline.resolve(uri)? {
150 Some(e) => e,
151 None => continue,
152 };
153 exchange.set_property(CAMEL_SLIP_ENDPOINT, Value::String(uri.to_string()));
154 let mut endpoint = endpoint;
155 let result = endpoint.ready().await?.call(exchange.clone()).await;
156 match result {
157 Ok(ex) => {
158 results.push(ex.clone());
159 exchange = ex;
160 }
161 Err(e) if config.stop_on_exception => return Err(e),
162 Err(e) => {
163 last_error = Some(e);
166 continue;
167 }
168 }
169 }
170 let zero_success_error = if results.is_empty() { last_error } else { None };
175 if let Some(err) = zero_success_error {
176 return Err(err);
177 }
178 exchange = aggregate_results(config.strategy, original_for_aggregate, results);
179 }
180
181 Ok(exchange)
182 })
183 }
184}
185
186fn aggregate_results(
187 strategy: camel_api::MulticastStrategy,
188 original: Exchange,
189 results: Vec<Exchange>,
190) -> Exchange {
191 match strategy {
192 camel_api::MulticastStrategy::LastWins => results.into_iter().last().unwrap_or(original),
193 camel_api::MulticastStrategy::CollectAll => {
194 let bodies: Vec<Value> = results
195 .iter()
196 .map(|ex| match &ex.input.body {
197 Body::Text(s) => Value::String(s.clone()),
198 Body::Json(v) => v.clone(),
199 Body::Xml(s) => Value::String(s.clone()),
200 Body::Bytes(b) => Value::String(String::from_utf8_lossy(b).into_owned()),
201 Body::Stream(s) => serde_json::json!({
202 "_stream": {
203 "origin": s.metadata.origin,
204 "placeholder": true,
205 "hint": "Materialize exchange body with .into_bytes() before recipient-list aggregation"
206 }
207 }),
208 _ => Value::Null,
210 })
211 .collect();
212 let mut result = results.into_iter().last().unwrap_or(original);
213 result.input.body = camel_api::Body::from(Value::Array(bodies));
214 result
215 }
216 camel_api::MulticastStrategy::Custom(fn_) => {
217 results.into_iter().fold(original, |acc, ex| fn_(acc, ex))
218 }
219 _ => original,
221 }
222}
223
224#[cfg(test)]
225mod tests {
226 use super::*;
227 use camel_api::MulticastStrategy;
228 use camel_api::{BoxProcessor, BoxProcessorExt, CamelError, Message};
229 use std::collections::HashMap;
230 use std::sync::Arc;
231 use std::sync::atomic::{AtomicUsize, Ordering};
232 use std::time::{Duration, Instant};
233 use tokio::sync::Mutex;
234 use tokio::time::sleep;
235
236 fn mock_resolver() -> camel_api::EndpointResolver {
237 Arc::new(|uri: &str| {
238 if uri.starts_with("mock:") {
239 Some(BoxProcessor::from_fn(|ex| Box::pin(async move { Ok(ex) })))
240 } else {
241 None
242 }
243 })
244 }
245
246 #[tokio::test]
247 async fn recipient_list_single_destination() {
248 let call_count = Arc::new(AtomicUsize::new(0));
249 let count_clone = call_count.clone();
250
251 let resolver = Arc::new(move |uri: &str| {
252 if uri == "mock:a" {
253 let count = count_clone.clone();
254 Some(BoxProcessor::from_fn(move |ex| {
255 count.fetch_add(1, Ordering::SeqCst);
256 Box::pin(async move { Ok(ex) })
257 }))
258 } else {
259 None
260 }
261 });
262
263 let config = RecipientListConfig::new(Arc::new(|_ex: &Exchange| "mock:a".to_string()));
264
265 let mut svc = RecipientListService::new(config, resolver).unwrap();
266 let ex = Exchange::new(Message::new("test"));
267 let result = svc.ready().await.unwrap().call(ex).await;
268
269 assert!(result.is_ok());
270 assert_eq!(call_count.load(Ordering::SeqCst), 1);
271 }
272
273 #[tokio::test]
274 async fn recipient_list_multiple_destinations() {
275 let call_count = Arc::new(AtomicUsize::new(0));
276 let count_clone = call_count.clone();
277
278 let resolver = Arc::new(move |uri: &str| {
279 if uri.starts_with("mock:") {
280 let count = count_clone.clone();
281 Some(BoxProcessor::from_fn(move |ex| {
282 count.fetch_add(1, Ordering::SeqCst);
283 Box::pin(async move { Ok(ex) })
284 }))
285 } else {
286 None
287 }
288 });
289
290 let config = RecipientListConfig::new(Arc::new(|_ex: &Exchange| {
291 "mock:a,mock:b,mock:c".to_string()
292 }));
293
294 let mut svc = RecipientListService::new(config, resolver).unwrap();
295 let ex = Exchange::new(Message::new("test"));
296 let result = svc.ready().await.unwrap().call(ex).await;
297
298 assert!(result.is_ok());
299 assert_eq!(call_count.load(Ordering::SeqCst), 3);
300 }
301
302 #[tokio::test]
303 async fn recipient_list_empty_expression() {
304 let config = RecipientListConfig::new(Arc::new(|_ex: &Exchange| String::new()));
305
306 let mut svc = RecipientListService::new(config, mock_resolver()).unwrap();
307 let ex = Exchange::new(Message::new("test"));
308 let result = svc.ready().await.unwrap().call(ex).await;
309
310 assert!(result.is_ok());
311 }
312
313 #[tokio::test]
314 async fn recipient_list_invalid_endpoint_error() {
315 let config =
316 RecipientListConfig::new(Arc::new(|_ex: &Exchange| "invalid:endpoint".to_string()));
317
318 let mut svc = RecipientListService::new(config, mock_resolver()).unwrap();
319 let ex = Exchange::new(Message::new("test"));
320 let result = svc.ready().await.unwrap().call(ex).await;
321
322 assert!(result.is_err());
323 assert!(result.unwrap_err().to_string().contains("Invalid endpoint"));
324 }
325
326 #[tokio::test]
327 async fn recipient_list_custom_delimiter() {
328 use std::sync::Mutex;
329
330 let order: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(Vec::new()));
331
332 let resolver = {
333 let order = order.clone();
334 Arc::new(move |uri: &str| {
335 let order = order.clone();
336 let uri = uri.to_string();
337 Some(BoxProcessor::from_fn(move |ex| {
338 order.lock().unwrap().push(uri.clone());
339 Box::pin(async move { Ok(ex) })
340 }))
341 })
342 };
343
344 let config = RecipientListConfig::new(Arc::new(|_ex: &Exchange| {
345 "mock:x|mock:y|mock:z".to_string()
346 }))
347 .delimiter("|");
348
349 let mut svc = RecipientListService::new(config, resolver).unwrap();
350 let ex = Exchange::new(Message::new("test"));
351 svc.ready().await.unwrap().call(ex).await.unwrap();
352
353 let order = order.lock().unwrap();
354 assert_eq!(*order, vec!["mock:x", "mock:y", "mock:z"]);
355 }
356
357 #[tokio::test]
358 async fn recipient_list_expression_evaluated_once() {
359 let expr_count = Arc::new(AtomicUsize::new(0));
360 let expr_count_clone = expr_count.clone();
361
362 let config = RecipientListConfig::new(Arc::new(move |_ex: &Exchange| {
363 expr_count_clone.fetch_add(1, Ordering::SeqCst);
364 "mock:a,mock:b".to_string()
365 }));
366
367 let mut svc = RecipientListService::new(config, mock_resolver()).unwrap();
368 let ex = Exchange::new(Message::new("test"));
369 svc.ready().await.unwrap().call(ex).await.unwrap();
370
371 assert_eq!(
372 expr_count.load(Ordering::SeqCst),
373 1,
374 "Expression must be evaluated exactly once"
375 );
376 }
377
378 #[tokio::test]
379 async fn recipient_list_ignores_empty_uri_tokens() {
380 let call_count = Arc::new(AtomicUsize::new(0));
381 let call_count_clone = call_count.clone();
382
383 let resolver = Arc::new(move |uri: &str| {
384 if uri.starts_with("mock:") {
385 let count = call_count_clone.clone();
386 Some(BoxProcessor::from_fn(move |ex| {
387 count.fetch_add(1, Ordering::SeqCst);
388 Box::pin(async move { Ok(ex) })
389 }))
390 } else {
391 None
392 }
393 });
394
395 let config = RecipientListConfig::new(Arc::new(|_ex: &Exchange| {
396 " ,mock:a, ,mock:b,, ".to_string()
397 }));
398
399 let mut svc = RecipientListService::new(config, resolver).unwrap();
400 let ex = Exchange::new(Message::new("test"));
401 let result = svc.ready().await.unwrap().call(ex).await;
402 assert!(result.is_ok());
403 assert_eq!(call_count.load(Ordering::SeqCst), 2);
404 }
405
406 #[tokio::test]
407 async fn recipient_list_mutation_between_steps() {
408 let resolver = Arc::new(|uri: &str| {
409 if uri == "mock:mutate" {
410 Some(BoxProcessor::from_fn(|mut ex| {
411 ex.input.body = camel_api::Body::Text("mutated".to_string());
412 Box::pin(async move { Ok(ex) })
413 }))
414 } else if uri == "mock:verify" {
415 Some(BoxProcessor::from_fn(|ex| {
416 let body = ex.input.body.as_text().unwrap_or("").to_string();
417 assert_eq!(body, "mutated");
418 Box::pin(async move { Ok(ex) })
419 }))
420 } else {
421 None
422 }
423 });
424
425 let config = RecipientListConfig::new(Arc::new(|_ex: &Exchange| {
426 "mock:mutate,mock:verify".to_string()
427 }));
428
429 let mut svc = RecipientListService::new(config, resolver).unwrap();
430 let ex = Exchange::new(Message::new("original"));
431 let result = svc.ready().await.unwrap().call(ex).await;
432
433 assert!(result.is_ok());
434 }
435
436 #[tokio::test]
437 async fn recipient_list_parallel_executes_concurrently() {
438 let records: Arc<Mutex<Vec<(String, Instant, Instant)>>> = Arc::new(Mutex::new(Vec::new()));
439
440 let resolver = {
441 let records = records.clone();
442 Arc::new(move |uri: &str| {
443 if uri.starts_with("mock:") {
444 let records = records.clone();
445 let uri = uri.to_string();
446 Some(BoxProcessor::from_fn(move |ex| {
447 let records = records.clone();
448 let uri = uri.clone();
449 Box::pin(async move {
450 let start = Instant::now();
451 sleep(Duration::from_millis(100)).await;
452 let end = Instant::now();
453 records.lock().await.push((uri, start, end));
454 Ok(ex)
455 })
456 }))
457 } else {
458 None
459 }
460 })
461 };
462
463 let config = RecipientListConfig::new(Arc::new(|_ex: &Exchange| {
464 "mock:a,mock:b,mock:c".to_string()
465 }))
466 .parallel(true);
467
468 let mut svc = RecipientListService::new(config, resolver).unwrap();
469 let ex = Exchange::new(Message::new("test"));
470 svc.ready().await.unwrap().call(ex).await.unwrap();
471
472 let records = records.lock().await;
473 assert_eq!(records.len(), 3);
474
475 let mut overlap_found = false;
476 for i in 0..records.len() {
477 for j in (i + 1)..records.len() {
478 let (_, a_start, a_end) = records[i];
479 let (_, b_start, b_end) = records[j];
480 if a_start < b_end && b_start < a_end {
481 overlap_found = true;
482 break;
483 }
484 }
485 if overlap_found {
486 break;
487 }
488 }
489
490 assert!(overlap_found);
491 }
492
493 #[tokio::test]
494 async fn recipient_list_parallel_stop_on_exception_returns_error() {
495 let resolver = Arc::new(|uri: &str| {
496 if uri == "mock:err" {
497 Some(BoxProcessor::from_fn(|_ex| {
498 Box::pin(async { Err(CamelError::ProcessorError("boom".to_string())) })
499 }))
500 } else if uri.starts_with("mock:") {
501 Some(BoxProcessor::from_fn(|ex| Box::pin(async move { Ok(ex) })))
502 } else {
503 None
504 }
505 });
506
507 let config = RecipientListConfig::new(Arc::new(|_ex: &Exchange| {
508 "mock:a,mock:err,mock:c".to_string()
509 }))
510 .parallel(true)
511 .stop_on_exception(true);
512
513 let mut svc = RecipientListService::new(config, resolver).unwrap();
514 let ex = Exchange::new(Message::new("test"));
515 let result = svc.ready().await.unwrap().call(ex).await;
516 assert!(matches!(result, Err(CamelError::ProcessorError(msg)) if msg == "boom"));
517 }
518
519 #[tokio::test]
520 async fn recipient_list_parallel_limit_respects_limit() {
521 let config = RecipientListConfig::new(Arc::new(|_ex: &Exchange| {
522 "mock:a,mock:b,mock:c,mock:d".to_string()
523 }))
524 .parallel(true)
525 .parallel_limit(2);
526
527 let resolver = Arc::new(|uri: &str| {
528 if uri.starts_with("mock:") {
529 Some(BoxProcessor::from_fn(|ex| {
530 Box::pin(async move {
531 sleep(Duration::from_millis(100)).await;
532 Ok(ex)
533 })
534 }))
535 } else {
536 None
537 }
538 });
539
540 let mut svc = RecipientListService::new(config, resolver).unwrap();
541 let ex = Exchange::new(Message::new("test"));
542 let start = Instant::now();
543 svc.ready().await.unwrap().call(ex).await.unwrap();
544 let elapsed = start.elapsed();
545
546 assert!(elapsed >= Duration::from_millis(180));
547 assert!(elapsed < Duration::from_millis(350));
548 }
549
550 #[tokio::test]
551 async fn recipient_list_collect_all_strategy() {
552 let resolver = Arc::new(|uri: &str| {
553 if uri == "mock:a" {
554 Some(BoxProcessor::from_fn(|mut ex| {
555 ex.input.body = Body::Text("a".to_string());
556 Box::pin(async move { Ok(ex) })
557 }))
558 } else if uri == "mock:b" {
559 Some(BoxProcessor::from_fn(|mut ex| {
560 ex.input.body = Body::Text("b".to_string());
561 Box::pin(async move { Ok(ex) })
562 }))
563 } else if uri == "mock:c" {
564 Some(BoxProcessor::from_fn(|mut ex| {
565 ex.input.body = Body::Text("c".to_string());
566 Box::pin(async move { Ok(ex) })
567 }))
568 } else {
569 None
570 }
571 });
572
573 let config = RecipientListConfig::new(Arc::new(|_ex: &Exchange| {
574 "mock:a,mock:b,mock:c".to_string()
575 }))
576 .strategy(MulticastStrategy::CollectAll);
577
578 let mut svc = RecipientListService::new(config, resolver).unwrap();
579 let ex = Exchange::new(Message::new("seed"));
580 let result = svc.ready().await.unwrap().call(ex).await.unwrap();
581
582 assert_eq!(
583 result.input.body,
584 Body::from(Value::Array(vec![
585 Value::String("a".to_string()),
586 Value::String("b".to_string()),
587 Value::String("c".to_string()),
588 ]))
589 );
590 }
591
592 #[tokio::test]
593 async fn recipient_list_original_strategy() {
594 let resolver = Arc::new(|uri: &str| {
595 if uri.starts_with("mock:") {
596 let label = uri.to_string();
597 Some(BoxProcessor::from_fn(move |mut ex| {
598 let label = label.clone();
599 ex.input.body = Body::Text(format!("mutated-{label}"));
600 Box::pin(async move { Ok(ex) })
601 }))
602 } else {
603 None
604 }
605 });
606
607 let config = RecipientListConfig::new(Arc::new(|_ex: &Exchange| {
608 "mock:a,mock:b,mock:c".to_string()
609 }))
610 .strategy(MulticastStrategy::Original);
611
612 let mut svc = RecipientListService::new(config, resolver).unwrap();
613 let ex = Exchange::new(Message::new("original"));
614 let result = svc.ready().await.unwrap().call(ex).await.unwrap();
615
616 assert_eq!(result.input.body.as_text(), Some("original"));
617 }
618
619 #[tokio::test]
626 async fn test_huge_recipient_list_is_capped() {
627 let call_count = Arc::new(AtomicUsize::new(0));
628 let count_clone = call_count.clone();
629
630 let resolver = Arc::new(move |uri: &str| {
631 if uri.starts_with("mock:") {
632 let count = count_clone.clone();
633 Some(BoxProcessor::from_fn(move |ex| {
634 count.fetch_add(1, Ordering::SeqCst);
635 Box::pin(async move { Ok(ex) })
636 }))
637 } else {
638 None
639 }
640 });
641
642 let mut many = String::with_capacity(8 * 1_000_000);
644 for i in 0..1_000_000 {
645 if i > 0 {
646 many.push(',');
647 }
648 many.push_str(&format!("mock:k{i}"));
649 }
650
651 let config = RecipientListConfig::new(Arc::new(|ex: &Exchange| {
657 ex.input
658 .header("CamelRecipients")
659 .and_then(|v| v.as_str().map(|s| s.to_string()))
660 .unwrap_or_default()
661 }))
662 .max_recipients(4);
663
664 let mut svc = RecipientListService::new(config, resolver).unwrap();
665 let mut ex = Exchange::new(Message::new("test"));
666 ex.input.set_header("CamelRecipients", Value::String(many));
667 let result = svc.ready().await.unwrap().call(ex).await;
668 assert!(result.is_ok(), "capped execution should still succeed");
669 assert_eq!(
670 call_count.load(Ordering::SeqCst),
671 4,
672 "must resolve at most max_recipients (4) endpoints"
673 );
674 }
675
676 #[tokio::test]
677 async fn recipient_list_last_wins_strategy() {
678 let payloads: Arc<HashMap<String, String>> = Arc::new(HashMap::from([
679 ("mock:a".to_string(), "first".to_string()),
680 ("mock:b".to_string(), "second".to_string()),
681 ("mock:c".to_string(), "third".to_string()),
682 ]));
683
684 let resolver = {
685 let payloads = payloads.clone();
686 Arc::new(move |uri: &str| {
687 if let Some(payload) = payloads.get(uri) {
688 let payload = payload.clone();
689 Some(BoxProcessor::from_fn(move |mut ex| {
690 let payload = payload.clone();
691 ex.input.body = Body::Text(payload);
692 Box::pin(async move { Ok(ex) })
693 }))
694 } else {
695 None
696 }
697 })
698 };
699
700 let config = RecipientListConfig::new(Arc::new(|_ex: &Exchange| {
701 "mock:a,mock:b,mock:c".to_string()
702 }))
703 .strategy(MulticastStrategy::LastWins);
704
705 let mut svc = RecipientListService::new(config, resolver).unwrap();
706 let ex = Exchange::new(Message::new("seed"));
707 let result = svc.ready().await.unwrap().call(ex).await.unwrap();
708
709 assert_eq!(result.input.body.as_text(), Some("third"));
710 }
711
712 fn err_resolver(uri_to_err: Vec<(&'static str, CamelError)>) -> camel_api::EndpointResolver {
715 Arc::new(move |uri: &str| {
716 for (pattern, err) in &uri_to_err {
717 if uri == *pattern {
718 let err = err.clone();
719 return Some(BoxProcessor::from_fn(move |_ex| {
720 let err = err.clone();
721 Box::pin(async move { Err(err) })
722 }));
723 }
724 }
725 None
726 })
727 }
728
729 #[tokio::test]
730 async fn recipient_list_sequential_all_failed_returns_err() {
731 let resolver = err_resolver(vec![(
734 "mock:a",
735 CamelError::Config(String::from("seq-all-failed")),
736 )]);
737 let config = RecipientListConfig::new(Arc::new(|_ex: &Exchange| "mock:a".to_string()))
738 .strategy(MulticastStrategy::LastWins);
739
740 let mut svc = RecipientListService::new(config, resolver).unwrap();
741 let mut ex = Exchange::new(Message::new("timer:t tick #1"));
742 ex.input.body = Body::Text(String::from("timer:t tick #1"));
743 let result = svc.ready().await.unwrap().call(ex).await;
744
745 assert!(
746 result.is_err(),
747 "zero-success recipient_list must return Err, not Ok(original)"
748 );
749 assert!(
750 matches!(result, Err(CamelError::Config(m)) if m == "seq-all-failed"),
751 "returned error must carry the iteration-last error"
752 );
753 }
754
755 #[tokio::test]
756 async fn recipient_list_parallel_all_failed_returns_err() {
757 let resolver = err_resolver(vec![
760 ("mock:a", CamelError::Config(String::from("par-err-a"))),
761 ("mock:b", CamelError::Config(String::from("par-err-b"))),
762 ]);
763 let config =
764 RecipientListConfig::new(Arc::new(|_ex: &Exchange| "mock:a,mock:b".to_string()))
765 .strategy(MulticastStrategy::LastWins)
766 .parallel(true);
767
768 let mut svc = RecipientListService::new(config, resolver).unwrap();
769 let ex = Exchange::new(Message::new("inbound"));
770 let result = svc.ready().await.unwrap().call(ex).await;
771
772 assert!(
773 result.is_err(),
774 "zero-success parallel recipient_list must return Err, not Ok(original)"
775 );
776 }
777
778 #[tokio::test]
779 async fn recipient_list_parallel_last_error_is_join_next_order() {
780 let (tx, rx) = tokio::sync::oneshot::channel::<()>();
786 let rx = Arc::new(tokio::sync::Mutex::new(Some(rx)));
787 let resolver: camel_api::EndpointResolver = Arc::new(move |uri: &str| {
788 if uri == "mock:a" {
789 Some(BoxProcessor::from_fn(|_ex| {
790 Box::pin(async move { Err(CamelError::Config(String::from("par-err-a"))) })
791 }))
792 } else if uri == "mock:b" {
793 let rx = rx.clone();
794 Some(BoxProcessor::from_fn(move |_ex| {
795 let rx = rx.clone();
796 Box::pin(async move {
797 let mut lock = rx.lock().await;
799 if let Some(rx) = lock.take() {
800 let _ = rx.await;
801 }
802 Err(CamelError::Config(String::from("par-err-b")))
803 })
804 }))
805 } else {
806 None
807 }
808 });
809 let config =
810 RecipientListConfig::new(Arc::new(|_ex: &Exchange| "mock:a,mock:b".to_string()))
811 .strategy(MulticastStrategy::LastWins)
812 .parallel(true);
813
814 let mut svc = RecipientListService::new(config, resolver).unwrap();
815 let ex = Exchange::new(Message::new("inbound"));
816
817 let join = tokio::spawn(async move { svc.ready().await.unwrap().call(ex).await });
820 for _ in 0..10 {
822 tokio::task::yield_now().await;
823 }
824 let _ = tx.send(());
825 let result = join.await.unwrap();
826
827 assert!(
828 matches!(result, Err(CamelError::Config(ref m)) if m == "par-err-b"),
829 "representative error must be the last failing task to complete (mock:b), got: {result:?}"
830 );
831 }
832
833 #[tokio::test]
834 async fn recipient_list_partial_success_aggregates_and_returns_ok() {
835 let call_count = Arc::new(AtomicUsize::new(0));
838 let ok_count = call_count.clone();
839 let resolver: camel_api::EndpointResolver = Arc::new(move |uri: &str| {
840 if uri == "mock:ok" {
841 let c = ok_count.clone();
842 Some(BoxProcessor::from_fn(move |mut ex| {
843 c.fetch_add(1, Ordering::SeqCst);
844 ex.input.body = Body::Text(String::from("ok-body"));
845 Box::pin(async move { Ok(ex) })
846 }))
847 } else if uri == "mock:fail" {
848 Some(BoxProcessor::from_fn(|_ex| {
849 Box::pin(async move { Err(CamelError::Config(String::from("partial-fail"))) })
850 }))
851 } else {
852 None
853 }
854 });
855 let config =
856 RecipientListConfig::new(Arc::new(|_ex: &Exchange| "mock:fail,mock:ok".to_string()))
857 .strategy(MulticastStrategy::LastWins);
858
859 let mut svc = RecipientListService::new(config, resolver).unwrap();
860 let ex = Exchange::new(Message::new("inbound"));
861 let result = svc.ready().await.unwrap().call(ex).await;
862
863 assert!(
864 result.is_ok(),
865 "partial success must return Ok, got: {result:?}"
866 );
867 assert_eq!(call_count.load(Ordering::SeqCst), 1);
868 assert_eq!(result.unwrap().input.body.as_text(), Some("ok-body"));
869 }
870
871 #[tokio::test]
872 async fn recipient_list_parallel_all_panic_returns_err() {
873 let resolver: camel_api::EndpointResolver = Arc::new(|uri: &str| {
879 if uri.starts_with("mock:panic") {
880 Some(BoxProcessor::from_fn(|_ex| {
881 Box::pin(async move {
882 panic!("recipient panicked");
883 })
884 }))
885 } else {
886 None
887 }
888 });
889 let config = RecipientListConfig::new(Arc::new(|_ex: &Exchange| {
890 "mock:panic1,mock:panic2".to_string()
891 }))
892 .strategy(MulticastStrategy::LastWins)
893 .parallel(true);
894
895 let mut svc = RecipientListService::new(config, resolver).unwrap();
896 let ex = Exchange::new(Message::new("inbound"));
897 let result = svc.ready().await.unwrap().call(ex).await;
898
899 assert!(
900 result.is_err(),
901 "all-panic parallel recipient_list must return Err, not Ok(original); got: {result:?}"
902 );
903 assert!(
904 matches!(result, Err(CamelError::ProcessorError(_))),
905 "panic must surface as a ProcessorError representative"
906 );
907 }
908}