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
88 for _ in 0..limit {
89 if let Some((uri, mut endpoint)) = iter.next() {
90 let mut cloned = original_for_aggregate.clone();
91 cloned.set_property(CAMEL_SLIP_ENDPOINT, Value::String(uri));
92 join_set.spawn(async move { endpoint.ready().await?.call(cloned).await });
93 }
94 }
95
96 while let Some(result) = join_set.join_next().await {
97 match result {
98 Ok(Ok(ex)) => results.push(ex),
99 Ok(Err(e)) if config.stop_on_exception => {
100 join_set.abort_all();
101 return Err(e);
102 }
103 _ => {}
104 }
105
106 if let Some((uri, mut endpoint)) = iter.next() {
107 let mut cloned = original_for_aggregate.clone();
108 cloned.set_property(CAMEL_SLIP_ENDPOINT, Value::String(uri));
109 join_set.spawn(async move { endpoint.ready().await?.call(cloned).await });
110 }
111 }
112
113 exchange = aggregate_results(config.strategy, original_for_aggregate, results);
114 } else {
115 let mut results: Vec<Exchange> = Vec::new();
116 let original_for_aggregate = exchange.clone();
117 for uri in &uris {
118 let endpoint = match pipeline.resolve(uri)? {
119 Some(e) => e,
120 None => continue,
121 };
122 exchange.set_property(CAMEL_SLIP_ENDPOINT, Value::String(uri.to_string()));
123 let mut endpoint = endpoint;
124 let result = endpoint.ready().await?.call(exchange.clone()).await;
125 match result {
126 Ok(ex) => {
127 results.push(ex.clone());
128 exchange = ex;
129 }
130 Err(e) if config.stop_on_exception => return Err(e),
131 Err(_) => continue,
132 }
133 }
134 exchange = aggregate_results(config.strategy, original_for_aggregate, results);
135 }
136
137 Ok(exchange)
138 })
139 }
140}
141
142fn aggregate_results(
143 strategy: camel_api::MulticastStrategy,
144 original: Exchange,
145 results: Vec<Exchange>,
146) -> Exchange {
147 match strategy {
148 camel_api::MulticastStrategy::LastWins => results.into_iter().last().unwrap_or(original),
149 camel_api::MulticastStrategy::Original => original,
150 camel_api::MulticastStrategy::CollectAll => {
151 let bodies: Vec<Value> = results
152 .iter()
153 .map(|ex| match &ex.input.body {
154 Body::Text(s) => Value::String(s.clone()),
155 Body::Json(v) => v.clone(),
156 Body::Xml(s) => Value::String(s.clone()),
157 Body::Bytes(b) => Value::String(String::from_utf8_lossy(b).into_owned()),
158 Body::Empty => Value::Null,
159 Body::Stream(s) => serde_json::json!({
160 "_stream": {
161 "origin": s.metadata.origin,
162 "placeholder": true,
163 "hint": "Materialize exchange body with .into_bytes() before recipient-list aggregation"
164 }
165 }),
166 })
167 .collect();
168 let mut result = results.into_iter().last().unwrap_or(original);
169 result.input.body = camel_api::Body::from(Value::Array(bodies));
170 result
171 }
172 camel_api::MulticastStrategy::Custom(fn_) => {
173 results.into_iter().fold(original, |acc, ex| fn_(acc, ex))
174 }
175 }
176}
177
178#[cfg(test)]
179mod tests {
180 use super::*;
181 use camel_api::MulticastStrategy;
182 use camel_api::{BoxProcessor, BoxProcessorExt, CamelError, Message};
183 use std::collections::HashMap;
184 use std::sync::Arc;
185 use std::sync::atomic::{AtomicUsize, Ordering};
186 use std::time::{Duration, Instant};
187 use tokio::sync::Mutex;
188 use tokio::time::sleep;
189
190 fn mock_resolver() -> camel_api::EndpointResolver {
191 Arc::new(|uri: &str| {
192 if uri.starts_with("mock:") {
193 Some(BoxProcessor::from_fn(|ex| Box::pin(async move { Ok(ex) })))
194 } else {
195 None
196 }
197 })
198 }
199
200 #[tokio::test]
201 async fn recipient_list_single_destination() {
202 let call_count = Arc::new(AtomicUsize::new(0));
203 let count_clone = call_count.clone();
204
205 let resolver = Arc::new(move |uri: &str| {
206 if uri == "mock:a" {
207 let count = count_clone.clone();
208 Some(BoxProcessor::from_fn(move |ex| {
209 count.fetch_add(1, Ordering::SeqCst);
210 Box::pin(async move { Ok(ex) })
211 }))
212 } else {
213 None
214 }
215 });
216
217 let config = RecipientListConfig::new(Arc::new(|_ex: &Exchange| "mock:a".to_string()));
218
219 let mut svc = RecipientListService::new(config, resolver).unwrap();
220 let ex = Exchange::new(Message::new("test"));
221 let result = svc.ready().await.unwrap().call(ex).await;
222
223 assert!(result.is_ok());
224 assert_eq!(call_count.load(Ordering::SeqCst), 1);
225 }
226
227 #[tokio::test]
228 async fn recipient_list_multiple_destinations() {
229 let call_count = Arc::new(AtomicUsize::new(0));
230 let count_clone = call_count.clone();
231
232 let resolver = Arc::new(move |uri: &str| {
233 if uri.starts_with("mock:") {
234 let count = count_clone.clone();
235 Some(BoxProcessor::from_fn(move |ex| {
236 count.fetch_add(1, Ordering::SeqCst);
237 Box::pin(async move { Ok(ex) })
238 }))
239 } else {
240 None
241 }
242 });
243
244 let config = RecipientListConfig::new(Arc::new(|_ex: &Exchange| {
245 "mock:a,mock:b,mock:c".to_string()
246 }));
247
248 let mut svc = RecipientListService::new(config, resolver).unwrap();
249 let ex = Exchange::new(Message::new("test"));
250 let result = svc.ready().await.unwrap().call(ex).await;
251
252 assert!(result.is_ok());
253 assert_eq!(call_count.load(Ordering::SeqCst), 3);
254 }
255
256 #[tokio::test]
257 async fn recipient_list_empty_expression() {
258 let config = RecipientListConfig::new(Arc::new(|_ex: &Exchange| String::new()));
259
260 let mut svc = RecipientListService::new(config, mock_resolver()).unwrap();
261 let ex = Exchange::new(Message::new("test"));
262 let result = svc.ready().await.unwrap().call(ex).await;
263
264 assert!(result.is_ok());
265 }
266
267 #[tokio::test]
268 async fn recipient_list_invalid_endpoint_error() {
269 let config =
270 RecipientListConfig::new(Arc::new(|_ex: &Exchange| "invalid:endpoint".to_string()));
271
272 let mut svc = RecipientListService::new(config, mock_resolver()).unwrap();
273 let ex = Exchange::new(Message::new("test"));
274 let result = svc.ready().await.unwrap().call(ex).await;
275
276 assert!(result.is_err());
277 assert!(result.unwrap_err().to_string().contains("Invalid endpoint"));
278 }
279
280 #[tokio::test]
281 async fn recipient_list_custom_delimiter() {
282 use std::sync::Mutex;
283
284 let order: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(Vec::new()));
285
286 let resolver = {
287 let order = order.clone();
288 Arc::new(move |uri: &str| {
289 let order = order.clone();
290 let uri = uri.to_string();
291 Some(BoxProcessor::from_fn(move |ex| {
292 order.lock().unwrap().push(uri.clone());
293 Box::pin(async move { Ok(ex) })
294 }))
295 })
296 };
297
298 let config = RecipientListConfig::new(Arc::new(|_ex: &Exchange| {
299 "mock:x|mock:y|mock:z".to_string()
300 }))
301 .delimiter("|");
302
303 let mut svc = RecipientListService::new(config, resolver).unwrap();
304 let ex = Exchange::new(Message::new("test"));
305 svc.ready().await.unwrap().call(ex).await.unwrap();
306
307 let order = order.lock().unwrap();
308 assert_eq!(*order, vec!["mock:x", "mock:y", "mock:z"]);
309 }
310
311 #[tokio::test]
312 async fn recipient_list_expression_evaluated_once() {
313 let expr_count = Arc::new(AtomicUsize::new(0));
314 let expr_count_clone = expr_count.clone();
315
316 let config = RecipientListConfig::new(Arc::new(move |_ex: &Exchange| {
317 expr_count_clone.fetch_add(1, Ordering::SeqCst);
318 "mock:a,mock:b".to_string()
319 }));
320
321 let mut svc = RecipientListService::new(config, mock_resolver()).unwrap();
322 let ex = Exchange::new(Message::new("test"));
323 svc.ready().await.unwrap().call(ex).await.unwrap();
324
325 assert_eq!(
326 expr_count.load(Ordering::SeqCst),
327 1,
328 "Expression must be evaluated exactly once"
329 );
330 }
331
332 #[tokio::test]
333 async fn recipient_list_ignores_empty_uri_tokens() {
334 let call_count = Arc::new(AtomicUsize::new(0));
335 let call_count_clone = call_count.clone();
336
337 let resolver = Arc::new(move |uri: &str| {
338 if uri.starts_with("mock:") {
339 let count = call_count_clone.clone();
340 Some(BoxProcessor::from_fn(move |ex| {
341 count.fetch_add(1, Ordering::SeqCst);
342 Box::pin(async move { Ok(ex) })
343 }))
344 } else {
345 None
346 }
347 });
348
349 let config = RecipientListConfig::new(Arc::new(|_ex: &Exchange| {
350 " ,mock:a, ,mock:b,, ".to_string()
351 }));
352
353 let mut svc = RecipientListService::new(config, resolver).unwrap();
354 let ex = Exchange::new(Message::new("test"));
355 let result = svc.ready().await.unwrap().call(ex).await;
356 assert!(result.is_ok());
357 assert_eq!(call_count.load(Ordering::SeqCst), 2);
358 }
359
360 #[tokio::test]
361 async fn recipient_list_mutation_between_steps() {
362 let resolver = Arc::new(|uri: &str| {
363 if uri == "mock:mutate" {
364 Some(BoxProcessor::from_fn(|mut ex| {
365 ex.input.body = camel_api::Body::Text("mutated".to_string());
366 Box::pin(async move { Ok(ex) })
367 }))
368 } else if uri == "mock:verify" {
369 Some(BoxProcessor::from_fn(|ex| {
370 let body = ex.input.body.as_text().unwrap_or("").to_string();
371 assert_eq!(body, "mutated");
372 Box::pin(async move { Ok(ex) })
373 }))
374 } else {
375 None
376 }
377 });
378
379 let config = RecipientListConfig::new(Arc::new(|_ex: &Exchange| {
380 "mock:mutate,mock:verify".to_string()
381 }));
382
383 let mut svc = RecipientListService::new(config, resolver).unwrap();
384 let ex = Exchange::new(Message::new("original"));
385 let result = svc.ready().await.unwrap().call(ex).await;
386
387 assert!(result.is_ok());
388 }
389
390 #[tokio::test]
391 async fn recipient_list_parallel_executes_concurrently() {
392 let records: Arc<Mutex<Vec<(String, Instant, Instant)>>> = Arc::new(Mutex::new(Vec::new()));
393
394 let resolver = {
395 let records = records.clone();
396 Arc::new(move |uri: &str| {
397 if uri.starts_with("mock:") {
398 let records = records.clone();
399 let uri = uri.to_string();
400 Some(BoxProcessor::from_fn(move |ex| {
401 let records = records.clone();
402 let uri = uri.clone();
403 Box::pin(async move {
404 let start = Instant::now();
405 sleep(Duration::from_millis(100)).await;
406 let end = Instant::now();
407 records.lock().await.push((uri, start, end));
408 Ok(ex)
409 })
410 }))
411 } else {
412 None
413 }
414 })
415 };
416
417 let config = RecipientListConfig::new(Arc::new(|_ex: &Exchange| {
418 "mock:a,mock:b,mock:c".to_string()
419 }))
420 .parallel(true);
421
422 let mut svc = RecipientListService::new(config, resolver).unwrap();
423 let ex = Exchange::new(Message::new("test"));
424 svc.ready().await.unwrap().call(ex).await.unwrap();
425
426 let records = records.lock().await;
427 assert_eq!(records.len(), 3);
428
429 let mut overlap_found = false;
430 for i in 0..records.len() {
431 for j in (i + 1)..records.len() {
432 let (_, a_start, a_end) = records[i];
433 let (_, b_start, b_end) = records[j];
434 if a_start < b_end && b_start < a_end {
435 overlap_found = true;
436 break;
437 }
438 }
439 if overlap_found {
440 break;
441 }
442 }
443
444 assert!(overlap_found);
445 }
446
447 #[tokio::test]
448 async fn recipient_list_parallel_stop_on_exception_returns_error() {
449 let resolver = Arc::new(|uri: &str| {
450 if uri == "mock:err" {
451 Some(BoxProcessor::from_fn(|_ex| {
452 Box::pin(async { Err(CamelError::ProcessorError("boom".to_string())) })
453 }))
454 } else if uri.starts_with("mock:") {
455 Some(BoxProcessor::from_fn(|ex| Box::pin(async move { Ok(ex) })))
456 } else {
457 None
458 }
459 });
460
461 let config = RecipientListConfig::new(Arc::new(|_ex: &Exchange| {
462 "mock:a,mock:err,mock:c".to_string()
463 }))
464 .parallel(true)
465 .stop_on_exception(true);
466
467 let mut svc = RecipientListService::new(config, resolver).unwrap();
468 let ex = Exchange::new(Message::new("test"));
469 let result = svc.ready().await.unwrap().call(ex).await;
470 assert!(matches!(result, Err(CamelError::ProcessorError(msg)) if msg == "boom"));
471 }
472
473 #[tokio::test]
474 async fn recipient_list_parallel_limit_respects_limit() {
475 let config = RecipientListConfig::new(Arc::new(|_ex: &Exchange| {
476 "mock:a,mock:b,mock:c,mock:d".to_string()
477 }))
478 .parallel(true)
479 .parallel_limit(2);
480
481 let resolver = Arc::new(|uri: &str| {
482 if uri.starts_with("mock:") {
483 Some(BoxProcessor::from_fn(|ex| {
484 Box::pin(async move {
485 sleep(Duration::from_millis(100)).await;
486 Ok(ex)
487 })
488 }))
489 } else {
490 None
491 }
492 });
493
494 let mut svc = RecipientListService::new(config, resolver).unwrap();
495 let ex = Exchange::new(Message::new("test"));
496 let start = Instant::now();
497 svc.ready().await.unwrap().call(ex).await.unwrap();
498 let elapsed = start.elapsed();
499
500 assert!(elapsed >= Duration::from_millis(180));
501 assert!(elapsed < Duration::from_millis(350));
502 }
503
504 #[tokio::test]
505 async fn recipient_list_collect_all_strategy() {
506 let resolver = Arc::new(|uri: &str| {
507 if uri == "mock:a" {
508 Some(BoxProcessor::from_fn(|mut ex| {
509 ex.input.body = Body::Text("a".to_string());
510 Box::pin(async move { Ok(ex) })
511 }))
512 } else if uri == "mock:b" {
513 Some(BoxProcessor::from_fn(|mut ex| {
514 ex.input.body = Body::Text("b".to_string());
515 Box::pin(async move { Ok(ex) })
516 }))
517 } else if uri == "mock:c" {
518 Some(BoxProcessor::from_fn(|mut ex| {
519 ex.input.body = Body::Text("c".to_string());
520 Box::pin(async move { Ok(ex) })
521 }))
522 } else {
523 None
524 }
525 });
526
527 let config = RecipientListConfig::new(Arc::new(|_ex: &Exchange| {
528 "mock:a,mock:b,mock:c".to_string()
529 }))
530 .strategy(MulticastStrategy::CollectAll);
531
532 let mut svc = RecipientListService::new(config, resolver).unwrap();
533 let ex = Exchange::new(Message::new("seed"));
534 let result = svc.ready().await.unwrap().call(ex).await.unwrap();
535
536 assert_eq!(
537 result.input.body,
538 Body::from(Value::Array(vec![
539 Value::String("a".to_string()),
540 Value::String("b".to_string()),
541 Value::String("c".to_string()),
542 ]))
543 );
544 }
545
546 #[tokio::test]
547 async fn recipient_list_original_strategy() {
548 let resolver = Arc::new(|uri: &str| {
549 if uri.starts_with("mock:") {
550 let label = uri.to_string();
551 Some(BoxProcessor::from_fn(move |mut ex| {
552 let label = label.clone();
553 ex.input.body = Body::Text(format!("mutated-{label}"));
554 Box::pin(async move { Ok(ex) })
555 }))
556 } else {
557 None
558 }
559 });
560
561 let config = RecipientListConfig::new(Arc::new(|_ex: &Exchange| {
562 "mock:a,mock:b,mock:c".to_string()
563 }))
564 .strategy(MulticastStrategy::Original);
565
566 let mut svc = RecipientListService::new(config, resolver).unwrap();
567 let ex = Exchange::new(Message::new("original"));
568 let result = svc.ready().await.unwrap().call(ex).await.unwrap();
569
570 assert_eq!(result.input.body.as_text(), Some("original"));
571 }
572
573 #[tokio::test]
580 async fn test_huge_recipient_list_is_capped() {
581 let call_count = Arc::new(AtomicUsize::new(0));
582 let count_clone = call_count.clone();
583
584 let resolver = Arc::new(move |uri: &str| {
585 if uri.starts_with("mock:") {
586 let count = count_clone.clone();
587 Some(BoxProcessor::from_fn(move |ex| {
588 count.fetch_add(1, Ordering::SeqCst);
589 Box::pin(async move { Ok(ex) })
590 }))
591 } else {
592 None
593 }
594 });
595
596 let mut many = String::with_capacity(8 * 1_000_000);
598 for i in 0..1_000_000 {
599 if i > 0 {
600 many.push(',');
601 }
602 many.push_str(&format!("mock:k{i}"));
603 }
604
605 let config = RecipientListConfig::new(Arc::new(|ex: &Exchange| {
611 ex.input
612 .header("CamelRecipients")
613 .and_then(|v| v.as_str().map(|s| s.to_string()))
614 .unwrap_or_default()
615 }))
616 .max_recipients(4);
617
618 let mut svc = RecipientListService::new(config, resolver).unwrap();
619 let mut ex = Exchange::new(Message::new("test"));
620 ex.input.set_header("CamelRecipients", Value::String(many));
621 let result = svc.ready().await.unwrap().call(ex).await;
622 assert!(result.is_ok(), "capped execution should still succeed");
623 assert_eq!(
624 call_count.load(Ordering::SeqCst),
625 4,
626 "must resolve at most max_recipients (4) endpoints"
627 );
628 }
629
630 #[tokio::test]
631 async fn recipient_list_last_wins_strategy() {
632 let payloads: Arc<HashMap<String, String>> = Arc::new(HashMap::from([
633 ("mock:a".to_string(), "first".to_string()),
634 ("mock:b".to_string(), "second".to_string()),
635 ("mock:c".to_string(), "third".to_string()),
636 ]));
637
638 let resolver = {
639 let payloads = payloads.clone();
640 Arc::new(move |uri: &str| {
641 if let Some(payload) = payloads.get(uri) {
642 let payload = payload.clone();
643 Some(BoxProcessor::from_fn(move |mut ex| {
644 let payload = payload.clone();
645 ex.input.body = Body::Text(payload);
646 Box::pin(async move { Ok(ex) })
647 }))
648 } else {
649 None
650 }
651 })
652 };
653
654 let config = RecipientListConfig::new(Arc::new(|_ex: &Exchange| {
655 "mock:a,mock:b,mock:c".to_string()
656 }))
657 .strategy(MulticastStrategy::LastWins);
658
659 let mut svc = RecipientListService::new(config, resolver).unwrap();
660 let ex = Exchange::new(Message::new("seed"));
661 let result = svc.ready().await.unwrap().call(ex).await.unwrap();
662
663 assert_eq!(result.input.body.as_text(), Some("third"));
664 }
665}