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