1use std::collections::HashMap;
2use std::future::Future;
3use std::pin::Pin;
4use std::sync::{Arc, Mutex};
5use std::task::{Context, Poll};
6use std::time::{Duration, Instant};
7
8use tokio::sync::mpsc;
9use tokio::task::JoinHandle;
10use tokio_util::sync::CancellationToken;
11use tower::Service;
12
13use async_trait::async_trait;
14use camel_api::{
15 CamelError, StepLifecycle, StepShutdownReason,
16 aggregator::{
17 AggregationStrategy, AggregatorConfig, CompletionCondition, CompletionMode,
18 CompletionReason, CorrelationStrategy,
19 },
20 body::Body,
21 exchange::Exchange,
22 message::Message,
23};
24use camel_language_api::Language;
25
26pub type SharedLanguageRegistry = Arc<std::sync::Mutex<HashMap<String, Arc<dyn Language>>>>;
27
28pub const CAMEL_AGGREGATOR_PENDING: &str = "CamelAggregatorPending";
29pub const CAMEL_AGGREGATED_SIZE: &str = "CamelAggregatedSize";
30pub const CAMEL_AGGREGATED_KEY: &str = "CamelAggregatedKey";
31pub const CAMEL_AGGREGATED_COMPLETION_REASON: &str = "CamelAggregatedCompletionReason";
32
33struct Bucket {
35 exchanges: Vec<Exchange>,
36 last_updated: Instant,
37}
38
39impl Bucket {
40 fn new() -> Self {
41 Self {
42 exchanges: Vec::new(),
43 last_updated: Instant::now(),
44 }
45 }
46
47 fn push(&mut self, exchange: Exchange) {
48 self.exchanges.push(exchange);
49 self.last_updated = Instant::now();
50 }
51
52 fn is_expired(&self, ttl: Duration) -> bool {
53 Instant::now().duration_since(self.last_updated) >= ttl
54 }
55}
56
57#[derive(Clone)]
58pub struct AggregatorService {
59 config: AggregatorConfig,
60 buckets: Arc<Mutex<HashMap<String, Bucket>>>,
61 timeout_tasks: Arc<Mutex<HashMap<String, CancellationToken>>>,
62 timeout_handles: Arc<Mutex<HashMap<String, JoinHandle<()>>>>,
63 late_tx: mpsc::Sender<Exchange>,
64 language_registry: SharedLanguageRegistry,
65 sweep_cancel: Arc<Mutex<CancellationToken>>,
72 sweep_handle: Arc<Mutex<Option<JoinHandle<()>>>>,
78}
79
80impl std::fmt::Debug for AggregatorService {
81 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
82 f.debug_struct("AggregatorService").finish_non_exhaustive()
83 }
84}
85
86impl Drop for AggregatorService {
87 fn drop(&mut self) {
88 if let Some(handle) = self
94 .sweep_handle
95 .lock()
96 .unwrap_or_else(|e| e.into_inner())
97 .take()
98 {
99 handle.abort();
100 }
101 }
102}
103
104impl AggregatorService {
105 pub fn new(
115 config: AggregatorConfig,
116 late_tx: mpsc::Sender<Exchange>,
117 language_registry: SharedLanguageRegistry,
118 route_cancel: CancellationToken,
119 ) -> Self {
120 config.validate().expect(
122 "AggregatorService::new: config failed validation \
125 (need max_buckets, completionTimeout, or bucket_ttl)",
126 );
127
128 let has_timeout = has_timeout_condition(&config.completion);
132 if !has_timeout && config.bucket_ttl.is_none() {
133 tracing::warn!(
134 "Aggregator configured with Size/Predicate completion and no \
135 bucket_ttl: buckets accumulate until max_buckets is reached"
136 );
137 }
138
139 let buckets: Arc<Mutex<HashMap<String, Bucket>>> = Arc::new(Mutex::new(HashMap::new()));
142
143 Self {
152 config,
153 buckets,
154 timeout_tasks: Arc::new(Mutex::new(HashMap::new())),
155 timeout_handles: Arc::new(Mutex::new(HashMap::new())),
156 late_tx,
157 language_registry,
158 sweep_cancel: Arc::new(Mutex::new(route_cancel)),
159 sweep_handle: Arc::new(Mutex::new(None)),
160 }
161 }
162
163 pub fn config(&self) -> &AggregatorConfig {
164 &self.config
165 }
166
167 pub fn has_timeout(&self) -> bool {
168 has_timeout_condition(&self.config.completion)
169 }
170
171 pub fn force_complete_all(&self) {
172 let mut buckets_guard = self.buckets.lock().unwrap_or_else(|e| e.into_inner());
173 let keys: Vec<String> = buckets_guard.keys().cloned().collect();
174
175 for key in keys {
176 if let Some(bucket) = buckets_guard.remove(&key) {
177 if self.config.force_completion_on_stop {
178 cancel_timeout_task_with_handle(
179 &key,
180 &self.timeout_tasks,
181 &self.timeout_handles,
182 );
183 match aggregate(bucket.exchanges, &self.config.strategy) {
184 Ok(mut result) => {
185 result.set_property(
186 CAMEL_AGGREGATED_COMPLETION_REASON,
187 serde_json::json!(CompletionReason::Stop.as_str()),
188 );
189 if self.late_tx.try_send(result).is_err() {
190 tracing::warn!(
191 key = %key,
192 "aggregator force-complete emit dropped: late channel full"
193 );
194 }
195 }
196 Err(e) => {
197 tracing::warn!(
199 key = %key,
200 error = %e,
201 "aggregation failed in force_complete_all"
202 );
203 }
204 }
205 } else {
206 cancel_timeout_task_with_handle(
207 &key,
208 &self.timeout_tasks,
209 &self.timeout_handles,
210 );
211 }
212 }
213 }
214 }
215
216 pub(crate) async fn shutdown_inner(&self) {
219 {
221 let mut guard = self.timeout_tasks.lock().unwrap_or_else(|e| e.into_inner());
222 for token in guard.values() {
223 token.cancel();
224 }
225 guard.clear();
226 };
227
228 let handles: Vec<JoinHandle<()>> = {
230 let mut guard = self
231 .timeout_handles
232 .lock()
233 .unwrap_or_else(|e| e.into_inner());
234 guard.drain().map(|(_, handle)| handle).collect()
235 };
236
237 if handles.is_empty() {
238 return;
239 }
240
241 let _ = tokio::time::timeout(Duration::from_secs(5), async {
243 for handle in handles {
244 let _ = handle.await;
245 }
246 })
247 .await;
248 }
249}
250
251#[async_trait]
252impl StepLifecycle for AggregatorService {
253 fn name(&self) -> &'static str {
254 "aggregator"
255 }
256
257 async fn shutdown(&self, reason: StepShutdownReason) -> Result<(), CamelError> {
258 tracing::debug!(reason = ?reason, "Aggregator shutdown via StepLifecycle");
259
260 self.sweep_cancel
263 .lock()
264 .unwrap_or_else(|e| e.into_inner())
265 .cancel();
266
267 if let Some(handle) = self
270 .sweep_handle
271 .lock()
272 .unwrap_or_else(|e| e.into_inner())
273 .take()
274 {
275 handle.abort();
276 }
277
278 self.shutdown_inner().await;
279 Ok(())
280 }
281
282 async fn start(&self) -> Result<(), CamelError> {
286 *self.sweep_cancel.lock().unwrap_or_else(|e| e.into_inner()) = CancellationToken::new();
287
288 if let Some(handle) = self
289 .sweep_handle
290 .lock()
291 .unwrap_or_else(|e| e.into_inner())
292 .take()
293 {
294 handle.abort();
295 }
296
297 Ok(())
298 }
299}
300
301pub fn has_timeout_condition(mode: &CompletionMode) -> bool {
302 match mode {
303 CompletionMode::Single(CompletionCondition::Timeout(_)) => true,
304 CompletionMode::Any(conditions) => conditions
305 .iter()
306 .any(|c| matches!(c, CompletionCondition::Timeout(_))),
307 _ => false,
308 }
309}
310
311impl Service<Exchange> for AggregatorService {
312 type Response = Exchange;
313 type Error = CamelError;
314 type Future = Pin<Box<dyn Future<Output = Result<Exchange, CamelError>> + Send>>;
315
316 fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), CamelError>> {
317 if let Some(ttl) = self.config.bucket_ttl {
321 let mut g = self.sweep_handle.lock().unwrap_or_else(|e| e.into_inner());
322 if g.is_none() {
323 let interval = std::cmp::max(ttl / 2, Duration::from_millis(50));
324 let buckets = Arc::clone(&self.buckets);
325 let cancel = self
326 .sweep_cancel
327 .lock()
328 .unwrap_or_else(|e| e.into_inner())
329 .clone();
330 *g = Some(tokio::spawn(async move {
331 loop {
332 tokio::select! {
333 _ = cancel.cancelled() => break,
334 _ = tokio::time::sleep(interval) => {
335 let mut guard =
336 buckets.lock().unwrap_or_else(|e| e.into_inner());
337 guard.retain(|_, b| !b.is_expired(ttl));
338 }
339 }
340 }
341 }));
342 }
343 }
344 Poll::Ready(Ok(()))
345 }
346
347 fn call(&mut self, exchange: Exchange) -> Self::Future {
348 let config = self.config.clone();
349 let buckets = Arc::clone(&self.buckets);
350 let timeout_tasks = Arc::clone(&self.timeout_tasks);
351 let timeout_handles = Arc::clone(&self.timeout_handles);
352 let late_tx = self.late_tx.clone();
353 let language_registry = Arc::clone(&self.language_registry);
354
355 Box::pin(async move {
356 let key_value =
357 extract_correlation_key(&exchange, &config.correlation, &language_registry).await?;
358
359 let key_str = serde_json::to_string(&key_value)
360 .map_err(|e| CamelError::ProcessorError(e.to_string()))?;
361
362 let predicate_satisfied =
366 evaluate_completion_predicate(&config.completion, &exchange, &language_registry)
367 .await?;
368
369 let completed_bucket = {
370 let mut guard = buckets.lock().unwrap_or_else(|e| e.into_inner());
371
372 if let Some(ttl) = config.bucket_ttl {
373 guard.retain(|_, bucket| !bucket.is_expired(ttl));
374 }
375
376 if let Some(max) = config.max_buckets
377 && !guard.contains_key(&key_str)
378 && guard.len() >= max
379 {
380 tracing::warn!(
381 max_buckets = max,
382 correlation_key = %key_str,
383 "Aggregator reached max buckets limit, rejecting new correlation key"
384 );
385 return Err(CamelError::ProcessorError(format!(
386 "Aggregator reached maximum {} buckets",
387 max
388 )));
389 }
390
391 let bucket = guard.entry(key_str.clone()).or_insert_with(Bucket::new);
392 bucket.push(exchange);
393
394 let (is_complete, reason) = check_sync_completion(
395 &config.completion,
396 &bucket.exchanges,
397 predicate_satisfied,
398 );
399
400 if is_complete {
401 let exchanges = guard.remove(&key_str).map(|b| b.exchanges);
402 (exchanges, reason)
403 } else {
404 (None, CompletionReason::Size) }
406 };
407
408 if completed_bucket.0.is_none() && has_timeout_condition(&config.completion) {
409 let timeout_dur = extract_timeout_duration(&config.completion);
410 if let Some(timeout) = timeout_dur {
411 let live_count = timeout_handles
417 .lock()
418 .unwrap_or_else(|e| e.into_inner())
419 .len();
420 if live_count >= config.max_timeout_tasks {
421 tracing::warn!(
422 live_timeout_tasks = live_count,
423 max_timeout_tasks = config.max_timeout_tasks,
424 correlation_key = %key_str,
425 "Aggregator timeout-task cap reached; bucket will rely on \
426 bucket_ttl eviction instead of a dedicated timeout task"
427 );
428 } else {
429 {
431 let tt_guard = timeout_tasks.lock().unwrap_or_else(|e| e.into_inner());
432 if let Some(existing) = tt_guard.get(&key_str) {
433 existing.cancel();
434 }
435 }
436 {
438 let mut hh = timeout_handles.lock().unwrap_or_else(|e| e.into_inner());
439 if let Some(old) = hh.remove(&key_str) {
440 old.abort();
441 }
442 }
443 let cancel = CancellationToken::new();
444 timeout_tasks
445 .lock()
446 .unwrap_or_else(|e| e.into_inner())
447 .insert(key_str.clone(), cancel.clone());
448 let handle = spawn_timeout_task(
449 key_str.clone(),
450 timeout,
451 cancel,
452 buckets.clone(),
453 timeout_tasks.clone(),
454 timeout_handles.clone(),
455 late_tx,
456 config.strategy.clone(),
457 config.discard_on_timeout,
458 );
459 timeout_handles
460 .lock()
461 .unwrap_or_else(|e| e.into_inner())
462 .insert(key_str.clone(), handle);
463 }
464 }
465 }
466
467 if let Some(exchanges) = completed_bucket.0 {
468 cancel_timeout_task_with_handle(&key_str, &timeout_tasks, &timeout_handles);
469 let reason = completed_bucket.1;
470 let size = exchanges.len();
471 let mut result = aggregate(exchanges, &config.strategy)?;
472 result.set_property(CAMEL_AGGREGATED_SIZE, serde_json::json!(size as u64));
473 result.set_property(CAMEL_AGGREGATED_KEY, key_value);
474 result.set_property(
475 CAMEL_AGGREGATED_COMPLETION_REASON,
476 serde_json::json!(reason.as_str()),
477 );
478 Ok(result)
479 } else {
480 let mut pending = Exchange::new(Message {
481 headers: Default::default(),
482 body: Body::Empty,
483 });
484 pending.set_property(CAMEL_AGGREGATOR_PENDING, serde_json::json!(true));
485 Ok(pending)
486 }
487 })
488 }
489}
490
491async fn extract_correlation_key(
492 exchange: &Exchange,
493 strategy: &CorrelationStrategy,
494 registry: &SharedLanguageRegistry,
495) -> Result<serde_json::Value, CamelError> {
496 match strategy {
497 CorrelationStrategy::HeaderName(h) => {
498 exchange.input.headers.get(h).cloned().ok_or_else(|| {
499 CamelError::ProcessorError(format!(
500 "Aggregator: missing correlation key header '{}'",
501 h
502 ))
503 })
504 }
505 CorrelationStrategy::Expression { expr, language } => {
506 let expression = {
507 let reg = registry.lock().unwrap_or_else(|e| e.into_inner());
508 let lang = reg.get(language).ok_or_else(|| {
509 CamelError::ProcessorError(format!(
510 "Aggregator: language '{}' not found in registry",
511 language
512 ))
513 })?;
514 lang.create_expression(expr)
515 .map_err(|e| CamelError::ProcessorError(e.to_string()))?
516 };
517 let value = expression
518 .evaluate(exchange)
519 .await
520 .map_err(|e| CamelError::ProcessorError(e.to_string()))?;
521 if value.is_null() {
522 return Err(CamelError::ProcessorError(format!(
523 "Aggregator: correlation expression '{}' evaluated to null",
524 expr
525 )));
526 }
527 Ok(value)
528 }
529 CorrelationStrategy::Fn(f) => f(exchange).map(serde_json::Value::String).ok_or_else(|| {
530 CamelError::ProcessorError("Aggregator: correlation function returned None".to_string())
531 }),
532 _ => Err(CamelError::ProcessorError(
534 "Aggregator: unsupported correlation strategy".to_string(),
535 )),
536 }
537}
538
539fn iter_predicate_exprs(mode: &CompletionMode) -> Vec<(&String, &String)> {
541 let mut out = Vec::new();
542 match mode {
543 CompletionMode::Single(CompletionCondition::PredicateExpr { expr, language }) => {
544 out.push((expr, language));
545 }
546 CompletionMode::Any(conds) => {
547 for c in conds {
548 if let CompletionCondition::PredicateExpr { expr, language } = c {
549 out.push((expr, language));
550 }
551 }
552 }
553 _ => {}
554 }
555 out
556}
557
558async fn evaluate_completion_predicate(
563 mode: &CompletionMode,
564 incoming: &Exchange,
565 registry: &SharedLanguageRegistry,
566) -> Result<bool, CamelError> {
567 let mut satisfied = false;
568 for (expr, language) in iter_predicate_exprs(mode) {
569 let expression = {
570 let reg = registry.lock().unwrap_or_else(|e| e.into_inner());
571 let lang = reg.get(language).ok_or_else(|| {
572 CamelError::ProcessorError(format!(
573 "Aggregator: language '{}' not found in registry",
574 language
575 ))
576 })?;
577 lang.create_expression(expr)
578 .map_err(|e| CamelError::ProcessorError(e.to_string()))?
579 }; let value = expression
581 .evaluate(incoming)
582 .await
583 .map_err(|e| CamelError::ProcessorError(e.to_string()))?;
584 if value.as_bool().unwrap_or(false) {
585 satisfied = true;
586 }
587 }
588 Ok(satisfied)
589}
590
591fn check_sync_completion(
592 mode: &CompletionMode,
593 exchanges: &[Exchange],
594 predicate_satisfied: bool,
595) -> (bool, CompletionReason) {
596 match mode {
597 CompletionMode::Single(cond) => check_single(cond, exchanges, predicate_satisfied),
598 CompletionMode::Any(conditions) => {
599 for cond in conditions {
600 if let CompletionCondition::Timeout(_) = cond {
601 continue;
602 }
603 let (done, reason) = check_single(cond, exchanges, predicate_satisfied);
604 if done {
605 return (true, reason);
606 }
607 }
608 (false, CompletionReason::Size)
609 }
610 _ => (false, CompletionReason::Size),
612 }
613}
614
615fn check_single(
616 cond: &CompletionCondition,
617 exchanges: &[Exchange],
618 predicate_satisfied: bool,
619) -> (bool, CompletionReason) {
620 match cond {
621 CompletionCondition::Size(n) => (exchanges.len() >= *n, CompletionReason::Size),
622 CompletionCondition::Predicate(pred) => (pred(exchanges), CompletionReason::Predicate),
623 CompletionCondition::PredicateExpr { .. } => {
624 (predicate_satisfied, CompletionReason::Predicate)
625 }
626 CompletionCondition::Timeout(_) => (false, CompletionReason::Timeout),
627 _ => (false, CompletionReason::Size),
629 }
630}
631
632fn extract_timeout_duration(mode: &CompletionMode) -> Option<Duration> {
633 match mode {
634 CompletionMode::Single(CompletionCondition::Timeout(d)) => Some(*d),
635 CompletionMode::Any(conditions) => conditions.iter().find_map(|c| {
636 if let CompletionCondition::Timeout(d) = c {
637 Some(*d)
638 } else {
639 None
640 }
641 }),
642 _ => None,
643 }
644}
645
646fn cancel_timeout_task(key: &str, timeout_tasks: &Arc<Mutex<HashMap<String, CancellationToken>>>) {
647 let mut guard = timeout_tasks.lock().unwrap_or_else(|e| e.into_inner());
648 if let Some(token) = guard.remove(key) {
649 token.cancel();
650 }
651}
652
653fn cancel_timeout_task_with_handle(
655 key: &str,
656 timeout_tasks: &Arc<Mutex<HashMap<String, CancellationToken>>>,
657 timeout_handles: &Arc<Mutex<HashMap<String, JoinHandle<()>>>>,
658) {
659 cancel_timeout_task(key, timeout_tasks);
660 let mut guard = timeout_handles.lock().unwrap_or_else(|e| e.into_inner());
661 guard.remove(key);
662}
663
664#[allow(clippy::too_many_arguments)]
665fn spawn_timeout_task(
666 key: String,
667 timeout: Duration,
668 cancel: CancellationToken,
669 buckets: Arc<Mutex<HashMap<String, Bucket>>>,
670 timeout_tasks: Arc<Mutex<HashMap<String, CancellationToken>>>,
671 timeout_handles: Arc<Mutex<HashMap<String, JoinHandle<()>>>>,
672 late_tx: mpsc::Sender<Exchange>,
673 strategy: AggregationStrategy,
674 discard: bool,
675) -> JoinHandle<()> {
676 let cancel_clone = cancel.clone();
677 tokio::spawn(async move {
678 tokio::select! {
679 _ = tokio::time::sleep(timeout) => {
680 let should_proceed = {
681 let mut tt_guard = timeout_tasks.lock().unwrap_or_else(|e| e.into_inner());
682 if cancel_clone.is_cancelled() {
683 false
684 } else {
685 tt_guard.remove(&key);
686 true
687 }
688 };
689 if !should_proceed {
690 return;
691 }
692 {
695 let mut hh = timeout_handles.lock().unwrap_or_else(|e| e.into_inner());
696 hh.remove(&key);
697 }
698 let bucket_exchanges = {
699 let mut guard = buckets.lock().unwrap_or_else(|e| e.into_inner());
700 guard.remove(&key).map(|b| b.exchanges)
701 };
702 if let Some(exchanges) = bucket_exchanges
703 && !discard
704 {
705 match aggregate(exchanges, &strategy) {
706 Ok(mut result) => {
707 result.set_property(
708 CAMEL_AGGREGATED_COMPLETION_REASON,
709 serde_json::json!(CompletionReason::Timeout.as_str()),
710 );
711 if late_tx.try_send(result).is_err() {
712 tracing::warn!(
713 key = %key,
714 "aggregator timeout emit dropped: late channel full"
715 );
716 }
717 }
718 Err(e) => {
719 tracing::warn!(
721 key = %key,
722 error = %e,
723 "aggregation failed in timeout task"
724 );
725 }
726 }
727 }
728 }
729 _ = cancel_clone.cancelled() => {}
730 }
731 })
732}
733
734fn aggregate(
735 exchanges: Vec<Exchange>,
736 strategy: &AggregationStrategy,
737) -> Result<Exchange, CamelError> {
738 match strategy {
739 AggregationStrategy::CollectAll => {
740 let bodies: Vec<serde_json::Value> = exchanges
741 .into_iter()
742 .map(|e| match e.input.body {
743 Body::Json(v) => v,
744 Body::Text(s) => serde_json::Value::String(s),
745 Body::Xml(s) => serde_json::Value::String(s),
746 Body::Bytes(b) => {
747 serde_json::Value::String(String::from_utf8_lossy(&b).into_owned())
748 }
749 Body::Stream(s) => serde_json::json!({
750 "_stream": {
751 "origin": s.metadata.origin,
752 "placeholder": true,
753 "hint": "Materialize exchange body with .into_bytes() before aggregation if content needed"
754 }
755 }),
756 _ => serde_json::Value::Null,
758 })
759 .collect();
760 Ok(Exchange::new(Message {
761 headers: Default::default(),
762 body: Body::Json(serde_json::Value::Array(bodies)),
763 }))
764 }
765 AggregationStrategy::Custom(f) => {
766 let mut iter = exchanges.into_iter();
767 let first = iter.next().ok_or_else(|| {
768 CamelError::ProcessorError("Aggregator: empty bucket".to_string())
769 })?;
770 Ok(iter.fold(first, |acc, next| f(acc, next)))
771 }
772 _ => Err(CamelError::ProcessorError(
774 "Aggregator: unsupported aggregation strategy".to_string(),
775 )),
776 }
777}
778
779#[cfg(test)]
780mod tests {
781 use super::*;
782 use std::collections::HashMap;
783
784 use camel_api::{
785 StepLifecycle, StepShutdownReason,
786 aggregator::{AggregationStrategy, AggregatorConfig},
787 body::Body,
788 exchange::Exchange,
789 message::Message,
790 };
791 use tokio::sync::mpsc;
792 use tokio_util::sync::CancellationToken;
793 use tower::ServiceExt;
794
795 fn make_exchange(header: &str, value: &str, body: &str) -> Exchange {
796 let mut msg = Message {
797 headers: Default::default(),
798 body: Body::Text(body.to_string()),
799 };
800 msg.headers
801 .insert(header.to_string(), serde_json::json!(value));
802 Exchange::new(msg)
803 }
804
805 fn config_size(n: usize) -> AggregatorConfig {
806 AggregatorConfig::correlate_by("orderId")
807 .complete_when_size(n)
808 .build()
809 .unwrap()
810 }
811
812 fn new_test_svc(config: AggregatorConfig) -> AggregatorService {
813 let (tx, _rx) = mpsc::channel(256);
814 let registry: SharedLanguageRegistry = Arc::new(std::sync::Mutex::new(HashMap::new()));
815 let cancel = CancellationToken::new();
816 AggregatorService::new(config, tx, registry, cancel)
817 }
818
819 fn new_test_svc_with_registry(
823 config: AggregatorConfig,
824 registry: SharedLanguageRegistry,
825 ) -> AggregatorService {
826 let (tx, _rx) = mpsc::channel(256);
827 let cancel = CancellationToken::new();
828 AggregatorService::new(config, tx, registry, cancel)
829 }
830
831 #[tokio::test]
832 async fn test_pending_exchange_not_yet_complete() {
833 let mut svc = new_test_svc(config_size(3));
834 let ex = make_exchange("orderId", "A", "first");
835 let result = svc.ready().await.unwrap().call(ex).await.unwrap();
836 assert!(matches!(result.input.body, Body::Empty));
837 assert_eq!(
838 result.property(CAMEL_AGGREGATOR_PENDING),
839 Some(&serde_json::json!(true))
840 );
841 }
842
843 #[tokio::test]
844 async fn test_completes_on_size() {
845 let mut svc = new_test_svc(config_size(3));
846 for _ in 0..2 {
847 let ex = make_exchange("orderId", "A", "item");
848 let r = svc.ready().await.unwrap().call(ex).await.unwrap();
849 assert!(matches!(r.input.body, Body::Empty));
850 }
851 let ex = make_exchange("orderId", "A", "last");
852 let result = svc.ready().await.unwrap().call(ex).await.unwrap();
853 assert!(result.property(CAMEL_AGGREGATOR_PENDING).is_none());
854 assert_eq!(
855 result.property(CAMEL_AGGREGATED_SIZE),
856 Some(&serde_json::json!(3u64))
857 );
858 }
859
860 #[tokio::test]
861 async fn test_collect_all_produces_json_array() {
862 let mut svc = new_test_svc(config_size(2));
863 svc.ready()
864 .await
865 .unwrap()
866 .call(make_exchange("orderId", "A", "alpha"))
867 .await
868 .unwrap();
869 let result = svc
870 .ready()
871 .await
872 .unwrap()
873 .call(make_exchange("orderId", "A", "beta"))
874 .await
875 .unwrap();
876 let Body::Json(v) = &result.input.body else {
877 panic!("expected Body::Json")
878 };
879 let arr = v.as_array().unwrap();
880 assert_eq!(arr.len(), 2);
881 assert_eq!(arr[0], serde_json::json!("alpha"));
882 assert_eq!(arr[1], serde_json::json!("beta"));
883 }
884
885 #[tokio::test]
886 async fn test_two_keys_independent_buckets() {
887 let mut svc = new_test_svc(config_size(3));
889 svc.ready()
890 .await
891 .unwrap()
892 .call(make_exchange("orderId", "A", "a1"))
893 .await
894 .unwrap();
895 svc.ready()
896 .await
897 .unwrap()
898 .call(make_exchange("orderId", "B", "b1"))
899 .await
900 .unwrap();
901 svc.ready()
902 .await
903 .unwrap()
904 .call(make_exchange("orderId", "A", "a2"))
905 .await
906 .unwrap();
907 let ra = svc
909 .ready()
910 .await
911 .unwrap()
912 .call(make_exchange("orderId", "A", "a3"))
913 .await
914 .unwrap();
915 assert!(matches!(ra.input.body, Body::Json(_)));
917 let rb = svc
919 .ready()
920 .await
921 .unwrap()
922 .call(make_exchange("orderId", "B", "b_check"))
923 .await
924 .unwrap();
925 assert!(matches!(rb.input.body, Body::Empty));
926 }
927
928 #[tokio::test]
929 async fn test_bucket_resets_after_completion() {
930 let mut svc = new_test_svc(config_size(2));
931 svc.ready()
932 .await
933 .unwrap()
934 .call(make_exchange("orderId", "A", "x"))
935 .await
936 .unwrap();
937 svc.ready()
938 .await
939 .unwrap()
940 .call(make_exchange("orderId", "A", "x"))
941 .await
942 .unwrap(); let r = svc
945 .ready()
946 .await
947 .unwrap()
948 .call(make_exchange("orderId", "A", "new"))
949 .await
950 .unwrap();
951 assert!(matches!(r.input.body, Body::Empty)); }
953
954 #[tokio::test]
955 async fn test_completion_size_1_emits_immediately() {
956 let mut svc = new_test_svc(config_size(1));
957 let ex = make_exchange("orderId", "A", "solo");
958 let result = svc.ready().await.unwrap().call(ex).await.unwrap();
959 assert!(result.property(CAMEL_AGGREGATOR_PENDING).is_none());
960 }
961
962 #[tokio::test]
963 async fn test_custom_aggregation_strategy() {
964 use camel_api::aggregator::AggregationFn;
965 use std::sync::Arc;
966
967 let f: AggregationFn = Arc::new(|mut acc: Exchange, next: Exchange| {
968 let combined = format!(
969 "{}+{}",
970 acc.input.body.as_text().unwrap_or(""),
971 next.input.body.as_text().unwrap_or("")
972 );
973 acc.input.body = Body::Text(combined);
974 acc
975 });
976 let config = AggregatorConfig::correlate_by("key")
977 .complete_when_size(2)
978 .strategy(AggregationStrategy::Custom(f))
979 .build()
980 .unwrap();
981 let mut svc = new_test_svc(config);
982 svc.ready()
983 .await
984 .unwrap()
985 .call(make_exchange("key", "X", "hello"))
986 .await
987 .unwrap();
988 let result = svc
989 .ready()
990 .await
991 .unwrap()
992 .call(make_exchange("key", "X", "world"))
993 .await
994 .unwrap();
995 assert_eq!(result.input.body.as_text(), Some("hello+world"));
996 }
997
998 #[tokio::test]
999 async fn test_completion_predicate() {
1000 let config = AggregatorConfig::correlate_by("key")
1001 .complete_when(|bucket| {
1002 bucket
1003 .iter()
1004 .any(|e| e.input.body.as_text() == Some("DONE"))
1005 })
1006 .build()
1007 .unwrap();
1008 let mut svc = new_test_svc(config);
1009 svc.ready()
1010 .await
1011 .unwrap()
1012 .call(make_exchange("key", "K", "first"))
1013 .await
1014 .unwrap();
1015 svc.ready()
1016 .await
1017 .unwrap()
1018 .call(make_exchange("key", "K", "second"))
1019 .await
1020 .unwrap();
1021 let result = svc
1022 .ready()
1023 .await
1024 .unwrap()
1025 .call(make_exchange("key", "K", "DONE"))
1026 .await
1027 .unwrap();
1028 assert!(result.property(CAMEL_AGGREGATOR_PENDING).is_none());
1029 }
1030
1031 #[tokio::test]
1032 async fn test_missing_header_returns_error() {
1033 let mut svc = new_test_svc(config_size(2));
1034 let msg = Message {
1035 headers: Default::default(),
1036 body: Body::Text("no key".into()),
1037 };
1038 let ex = Exchange::new(msg);
1039 let result = svc.ready().await.unwrap().call(ex).await;
1040 assert!(result.is_err());
1041 assert!(matches!(
1042 result.unwrap_err(),
1043 camel_api::CamelError::ProcessorError(_)
1044 ));
1045 }
1046
1047 #[tokio::test]
1048 async fn test_cloned_service_shares_state() {
1049 let svc1 = new_test_svc(config_size(2));
1050 let mut svc2 = svc1.clone();
1051 svc1.clone()
1053 .ready()
1054 .await
1055 .unwrap()
1056 .call(make_exchange("orderId", "A", "from-svc1"))
1057 .await
1058 .unwrap();
1059 let result = svc2
1061 .ready()
1062 .await
1063 .unwrap()
1064 .call(make_exchange("orderId", "A", "from-svc2"))
1065 .await
1066 .unwrap();
1067 assert!(result.property(CAMEL_AGGREGATOR_PENDING).is_none());
1068 }
1069
1070 #[tokio::test]
1071 async fn test_camel_aggregated_key_property_set() {
1072 let mut svc = new_test_svc(config_size(1));
1073 let ex = make_exchange("orderId", "ORDER-42", "body");
1074 let result = svc.ready().await.unwrap().call(ex).await.unwrap();
1075 assert_eq!(
1076 result.property(CAMEL_AGGREGATED_KEY),
1077 Some(&serde_json::json!("ORDER-42"))
1078 );
1079 }
1080
1081 #[tokio::test]
1082 async fn test_aggregator_enforces_max_buckets() {
1083 let config = AggregatorConfig::correlate_by("orderId")
1084 .complete_when_size(2)
1085 .max_buckets(3)
1086 .build()
1087 .unwrap();
1088
1089 let mut svc = new_test_svc(config);
1090
1091 for i in 0..3 {
1093 let ex = make_exchange("orderId", &format!("key-{}", i), "body");
1094 let _ = svc.ready().await.unwrap().call(ex).await.unwrap();
1095 }
1096
1097 let ex = make_exchange("orderId", "key-4", "body");
1099 let result = svc.ready().await.unwrap().call(ex).await;
1100
1101 assert!(result.is_err(), "Should reject when max buckets reached");
1102 let err = result.unwrap_err().to_string();
1103 assert!(
1104 err.contains("maximum"),
1105 "Error message should contain 'maximum': {}",
1106 err
1107 );
1108 }
1109
1110 #[tokio::test]
1111 async fn test_max_buckets_allows_existing_key() {
1112 let config = AggregatorConfig::correlate_by("orderId")
1113 .complete_when_size(5) .max_buckets(2)
1115 .build()
1116 .unwrap();
1117
1118 let mut svc = new_test_svc(config);
1119
1120 let ex1 = make_exchange("orderId", "key-A", "body1");
1122 let _ = svc.ready().await.unwrap().call(ex1).await.unwrap();
1123 let ex2 = make_exchange("orderId", "key-B", "body2");
1124 let _ = svc.ready().await.unwrap().call(ex2).await.unwrap();
1125
1126 let ex3 = make_exchange("orderId", "key-A", "body3");
1128 let result = svc.ready().await.unwrap().call(ex3).await;
1129 assert!(
1130 result.is_ok(),
1131 "Should allow adding to existing bucket even at max limit"
1132 );
1133 }
1134
1135 #[tokio::test]
1136 async fn test_bucket_ttl_eviction() {
1137 let config = AggregatorConfig::correlate_by("orderId")
1138 .complete_when_size(10) .bucket_ttl(Duration::from_millis(50))
1140 .build()
1141 .unwrap();
1142
1143 let mut svc = new_test_svc(config);
1144
1145 let ex1 = make_exchange("orderId", "key-A", "body1");
1147 let _ = svc.ready().await.unwrap().call(ex1).await.unwrap();
1148
1149 tokio::time::sleep(Duration::from_millis(100)).await;
1151
1152 let ex2 = make_exchange("orderId", "key-B", "body2");
1154 let _ = svc.ready().await.unwrap().call(ex2).await.unwrap();
1155
1156 let ex3 = make_exchange("orderId", "key-A", "body3");
1159 let result = svc.ready().await.unwrap().call(ex3).await;
1160 assert!(result.is_ok(), "Should be able to recreate evicted bucket");
1161 }
1162
1163 #[tokio::test(start_paused = true)]
1164 async fn test_timeout_completes_bucket() {
1165 let config = AggregatorConfig::correlate_by("key")
1166 .complete_on_timeout(Duration::from_millis(100))
1167 .build()
1168 .unwrap();
1169 let mut svc = new_test_svc(config);
1170 let ex = make_exchange("key", "A", "data");
1171 let result = svc.ready().await.unwrap().call(ex).await.unwrap();
1172 assert!(result.property(CAMEL_AGGREGATOR_PENDING).is_some());
1173
1174 tokio::time::sleep(Duration::from_millis(200)).await;
1175
1176 assert_eq!(
1177 svc.buckets.lock().unwrap().len(),
1178 0,
1179 "bucket should be removed after timeout"
1180 );
1181 }
1182
1183 #[tokio::test(start_paused = true)]
1184 async fn test_timeout_resets_on_new_exchange() {
1185 let config = AggregatorConfig::correlate_by("key")
1186 .complete_on_timeout(Duration::from_millis(150))
1187 .build()
1188 .unwrap();
1189 let mut svc = new_test_svc(config);
1190
1191 let ex1 = make_exchange("key", "A", "first");
1192 let _ = svc.ready().await.unwrap().call(ex1).await.unwrap();
1193
1194 tokio::time::sleep(Duration::from_millis(100)).await;
1195
1196 let ex2 = make_exchange("key", "A", "second");
1197 let _ = svc.ready().await.unwrap().call(ex2).await.unwrap();
1198
1199 tokio::time::sleep(Duration::from_millis(100)).await;
1200
1201 assert_eq!(
1202 svc.buckets.lock().unwrap().len(),
1203 1,
1204 "bucket should still exist — timeout was reset"
1205 );
1206
1207 tokio::time::sleep(Duration::from_millis(100)).await;
1208
1209 assert_eq!(
1210 svc.buckets.lock().unwrap().len(),
1211 0,
1212 "bucket should be gone after timeout fires"
1213 );
1214 }
1215
1216 #[tokio::test]
1217 async fn test_composable_size_and_timeout() {
1218 let config = AggregatorConfig::correlate_by("key")
1219 .complete_on_size_or_timeout(2, Duration::from_millis(200))
1220 .build()
1221 .unwrap();
1222 let mut svc = new_test_svc(config);
1223
1224 let ex1 = make_exchange("key", "A", "first");
1225 let _ = svc.ready().await.unwrap().call(ex1).await.unwrap();
1226 assert!(svc.buckets.lock().unwrap().contains_key("\"A\""));
1227
1228 let ex2 = make_exchange("key", "A", "second");
1229 let result = svc.ready().await.unwrap().call(ex2).await.unwrap();
1230 assert!(result.property(CAMEL_AGGREGATOR_PENDING).is_none());
1231 assert_eq!(
1232 result.property(CAMEL_AGGREGATED_COMPLETION_REASON),
1233 Some(&serde_json::json!("size"))
1234 );
1235 }
1236
1237 #[tokio::test(start_paused = true)]
1238 async fn test_discard_on_timeout() {
1239 let config = AggregatorConfig::correlate_by("key")
1240 .complete_on_timeout(Duration::from_millis(50))
1241 .discard_on_timeout(true)
1242 .build()
1243 .unwrap();
1244 let (tx, mut rx) = mpsc::channel(256);
1245 let registry: SharedLanguageRegistry = Arc::new(std::sync::Mutex::new(HashMap::new()));
1246 let cancel = CancellationToken::new();
1247 let mut svc = AggregatorService::new(config, tx, registry, cancel);
1248
1249 let ex = make_exchange("key", "A", "data");
1250 let _ = svc.ready().await.unwrap().call(ex).await.unwrap();
1251
1252 tokio::time::sleep(Duration::from_millis(100)).await;
1253
1254 assert!(
1255 rx.try_recv().is_err(),
1256 "no emit expected with discard_on_timeout"
1257 );
1258 assert_eq!(svc.buckets.lock().unwrap().len(), 0);
1259 assert!(
1260 svc.timeout_tasks.lock().unwrap().is_empty(),
1261 "timeout task should be cleaned up"
1262 );
1263 }
1264
1265 #[tokio::test]
1266 async fn test_force_completion_on_stop() {
1267 let config = AggregatorConfig::correlate_by("key")
1268 .complete_when_size(10)
1269 .force_completion_on_stop(true)
1270 .build()
1271 .unwrap();
1272 let (tx, mut rx) = mpsc::channel(256);
1273 let registry: SharedLanguageRegistry = Arc::new(std::sync::Mutex::new(HashMap::new()));
1274 let cancel = CancellationToken::new();
1275 let svc = AggregatorService::new(config, tx, registry, cancel);
1276
1277 let mut call_svc = svc.clone();
1278 let ex = make_exchange("key", "A", "data");
1279 let _ = call_svc.ready().await.unwrap().call(ex).await.unwrap();
1280
1281 svc.force_complete_all();
1282
1283 let result = rx.try_recv().expect("should emit on force-complete");
1284 assert!(
1285 result.input.body.as_text().is_some() || matches!(result.input.body, Body::Json(_))
1286 );
1287 assert_eq!(
1288 result.property(CAMEL_AGGREGATED_COMPLETION_REASON),
1289 Some(&serde_json::json!("stop"))
1290 );
1291 }
1292
1293 #[tokio::test]
1294 async fn test_completion_reason_property_size() {
1295 let config = AggregatorConfig::correlate_by("key")
1296 .complete_when_size(1)
1297 .build()
1298 .unwrap();
1299 let mut svc = new_test_svc(config);
1300 let ex = make_exchange("key", "X", "body");
1301 let result = svc.ready().await.unwrap().call(ex).await.unwrap();
1302 assert_eq!(
1303 result.property(CAMEL_AGGREGATED_COMPLETION_REASON),
1304 Some(&serde_json::json!("size"))
1305 );
1306 }
1307
1308 #[tokio::test]
1309 async fn test_completion_reason_property_predicate() {
1310 let config = AggregatorConfig::correlate_by("key")
1311 .complete_when(|_| true)
1312 .build()
1313 .unwrap();
1314 let mut svc = new_test_svc(config);
1315 let ex = make_exchange("key", "X", "body");
1316 let result = svc.ready().await.unwrap().call(ex).await.unwrap();
1317 assert_eq!(
1318 result.property(CAMEL_AGGREGATED_COMPLETION_REASON),
1319 Some(&serde_json::json!("predicate"))
1320 );
1321 }
1322
1323 #[tokio::test(start_paused = true)]
1324 async fn test_size_completes_before_timeout() {
1325 let config = AggregatorConfig::correlate_by("key")
1326 .complete_on_size_or_timeout(2, Duration::from_millis(200))
1327 .build()
1328 .unwrap();
1329 let mut svc = new_test_svc(config);
1330
1331 let ex1 = make_exchange("key", "A", "first");
1332 let _ = svc.ready().await.unwrap().call(ex1).await.unwrap();
1333
1334 let ex2 = make_exchange("key", "A", "second");
1335 let result = svc.ready().await.unwrap().call(ex2).await.unwrap();
1336
1337 assert!(result.property(CAMEL_AGGREGATOR_PENDING).is_none());
1338 assert_eq!(
1339 result.property(CAMEL_AGGREGATED_COMPLETION_REASON),
1340 Some(&serde_json::json!("size"))
1341 );
1342 assert_eq!(svc.buckets.lock().unwrap().len(), 0);
1343
1344 tokio::time::sleep(Duration::from_millis(300)).await;
1345 assert_eq!(
1346 svc.buckets.lock().unwrap().len(),
1347 0,
1348 "no re-fire after timeout"
1349 );
1350 }
1351
1352 #[tokio::test(start_paused = true)]
1353 async fn test_concurrent_timeout_fire_and_new_exchange() {
1354 let config = AggregatorConfig::correlate_by("key")
1355 .complete_on_size_or_timeout(2, Duration::from_millis(100))
1356 .build()
1357 .unwrap();
1358 let (tx, mut rx) = mpsc::channel(256);
1359 let registry: SharedLanguageRegistry = Arc::new(std::sync::Mutex::new(HashMap::new()));
1360 let cancel = CancellationToken::new();
1361 let mut svc = AggregatorService::new(config, tx, registry, cancel);
1362
1363 let ex = make_exchange("key", "A", "data");
1364 let _ = svc.ready().await.unwrap().call(ex).await.unwrap();
1365
1366 tokio::time::sleep(Duration::from_millis(150)).await;
1368
1369 let ex2 = make_exchange("key", "A", "data2");
1371 let result = svc.ready().await.unwrap().call(ex2).await.unwrap();
1372 assert!(
1373 result.property(CAMEL_AGGREGATOR_PENDING).is_some(),
1374 "should be pending in new bucket"
1375 );
1376
1377 let mut late_count = 0;
1379 while rx.try_recv().is_ok() {
1380 late_count += 1;
1381 }
1382 assert_eq!(
1383 late_count, 1,
1384 "exactly 1 late emit from the timed-out bucket"
1385 );
1386 }
1387
1388 #[tokio::test(start_paused = true)]
1389 async fn test_late_channel_full_drops_with_warning() {
1390 let config = AggregatorConfig::correlate_by("key")
1391 .complete_on_timeout(Duration::from_millis(50))
1392 .build()
1393 .unwrap();
1394 let (tx, mut rx) = mpsc::channel(1);
1395 rx.close();
1396 let registry: SharedLanguageRegistry = Arc::new(std::sync::Mutex::new(HashMap::new()));
1397 let cancel = CancellationToken::new();
1398 let mut svc = AggregatorService::new(config, tx, registry, cancel);
1399
1400 let ex = make_exchange("key", "A", "data");
1401 let _ = svc.ready().await.unwrap().call(ex).await.unwrap();
1402
1403 tokio::time::sleep(Duration::from_millis(100)).await;
1404 assert_eq!(
1405 svc.buckets.lock().unwrap().len(),
1406 0,
1407 "bucket removed despite channel closed"
1408 );
1409 }
1410
1411 #[tokio::test]
1417 async fn test_da3_force_complete_all_drops_on_saturated_channel() {
1418 let config = AggregatorConfig::correlate_by("k")
1419 .complete_when_size(10)
1420 .force_completion_on_stop(true)
1421 .build()
1422 .unwrap();
1423 let (late_tx, mut late_rx) = mpsc::channel::<Exchange>(1);
1425 late_tx
1429 .try_send(make_exchange("k", "99", "dummy"))
1430 .expect("pre-fill succeeds");
1431
1432 let registry: SharedLanguageRegistry = Arc::new(std::sync::Mutex::new(HashMap::new()));
1433 let cancel = CancellationToken::new();
1434 let mut svc = AggregatorService::new(config, late_tx, registry, cancel);
1435
1436 for v in ["1", "2", "3"] {
1439 let ex = make_exchange("k", v, "body");
1440 let _ = svc.ready().await.unwrap().call(ex).await.unwrap();
1441 }
1442 assert_eq!(svc.buckets.lock().unwrap().len(), 3);
1443
1444 svc.force_complete_all();
1446
1447 let pre_fill = late_rx
1449 .try_recv()
1450 .expect("pre-fill should still be in channel");
1451 assert_eq!(
1452 pre_fill.input.headers.get("k"),
1453 Some(&serde_json::json!("99"))
1454 );
1455
1456 assert!(matches!(
1459 late_rx.try_recv(),
1460 Err(mpsc::error::TryRecvError::Empty)
1461 ));
1462
1463 assert!(svc.buckets.lock().unwrap().is_empty());
1465 }
1466
1467 #[tokio::test]
1468 async fn test_aggregate_stream_bodies_creates_valid_json() {
1469 use bytes::Bytes;
1470 use camel_api::{Body, StreamBody, StreamMetadata};
1471 use futures::stream;
1472 use tokio::sync::Mutex;
1473
1474 let chunks = vec![Ok(Bytes::from("test"))];
1475 let stream_body = StreamBody {
1476 stream: Arc::new(Mutex::new(Some(Box::pin(stream::iter(chunks))))),
1477 metadata: StreamMetadata {
1478 origin: Some("file:///test.txt".to_string()),
1479 ..Default::default()
1480 },
1481 };
1482
1483 let ex1 = Exchange::new(Message {
1484 headers: Default::default(),
1485 body: Body::Stream(stream_body),
1486 });
1487
1488 let exchanges = vec![ex1];
1489 let result = aggregate(exchanges, &AggregationStrategy::CollectAll);
1490
1491 let exchange = result.expect("Expected Ok result");
1492 assert!(
1493 matches!(exchange.input.body, Body::Json(_)),
1494 "Expected Json body"
1495 );
1496
1497 if let Body::Json(value) = exchange.input.body {
1498 let json_str = serde_json::to_string(&value).unwrap();
1499 let parsed: serde_json::Value = serde_json::from_str(&json_str).unwrap();
1500
1501 assert!(parsed.is_array(), "Result should be an array");
1502 let arr = parsed.as_array().unwrap();
1503 assert!(arr[0].is_object(), "First element should be an object");
1504 assert!(
1505 arr[0]["_stream"].is_object(),
1506 "Should contain _stream object"
1507 );
1508 assert_eq!(arr[0]["_stream"]["origin"], "file:///test.txt");
1509 assert_eq!(
1510 arr[0]["_stream"]["placeholder"], true,
1511 "placeholder flag should be true"
1512 );
1513 }
1514 }
1515
1516 #[tokio::test]
1517 async fn test_aggregate_stream_bodies_with_none_origin() {
1518 use bytes::Bytes;
1519 use camel_api::{Body, StreamBody, StreamMetadata};
1520 use futures::stream;
1521 use tokio::sync::Mutex;
1522
1523 let chunks = vec![Ok(Bytes::from("test"))];
1524 let stream_body = StreamBody {
1525 stream: Arc::new(Mutex::new(Some(Box::pin(stream::iter(chunks))))),
1526 metadata: StreamMetadata {
1527 origin: None,
1528 ..Default::default()
1529 },
1530 };
1531
1532 let ex1 = Exchange::new(Message {
1533 headers: Default::default(),
1534 body: Body::Stream(stream_body),
1535 });
1536
1537 let exchanges = vec![ex1];
1538 let result = aggregate(exchanges, &AggregationStrategy::CollectAll);
1539
1540 let exchange = result.expect("Expected Ok result");
1541 assert!(
1542 matches!(exchange.input.body, Body::Json(_)),
1543 "Expected Json body"
1544 );
1545
1546 if let Body::Json(value) = exchange.input.body {
1547 let json_str = serde_json::to_string(&value).unwrap();
1548 let parsed: serde_json::Value = serde_json::from_str(&json_str).unwrap();
1549
1550 assert!(parsed.is_array(), "Result should be an array");
1551 let arr = parsed.as_array().unwrap();
1552 assert!(arr[0].is_object(), "First element should be an object");
1553 assert!(
1554 arr[0]["_stream"].is_object(),
1555 "Should contain _stream object"
1556 );
1557 assert_eq!(
1558 arr[0]["_stream"]["origin"],
1559 serde_json::Value::Null,
1560 "origin should be null when None"
1561 );
1562 assert_eq!(
1563 arr[0]["_stream"]["placeholder"], true,
1564 "placeholder flag should be true"
1565 );
1566 }
1567 }
1568
1569 #[tokio::test]
1570 async fn timeout_completion_clears_handle_from_map() {
1571 let config = AggregatorConfig::correlate_by("key")
1576 .complete_on_timeout(Duration::from_millis(50))
1577 .build()
1578 .unwrap();
1579 let (tx, _rx) = mpsc::channel(256);
1580 let registry: SharedLanguageRegistry = Arc::new(std::sync::Mutex::new(HashMap::new()));
1581 let cancel = CancellationToken::new();
1582 let svc = AggregatorService::new(config, tx, registry, cancel);
1583
1584 let mut call_svc = svc.clone();
1586 let ex = make_exchange("key", "A", "data");
1587 let _ = call_svc.ready().await.unwrap().call(ex).await.unwrap();
1588 assert!(
1589 !svc.timeout_handles.lock().unwrap().is_empty(),
1590 "handle should exist while timeout pending"
1591 );
1592
1593 tokio::time::sleep(Duration::from_millis(200)).await;
1595
1596 assert!(
1597 svc.timeout_handles.lock().unwrap().is_empty(),
1598 "handle should be cleared from map after natural timeout completion (was leak)"
1599 );
1600 }
1601
1602 #[tokio::test]
1603 async fn aggregator_shutdown_via_trait_dispatch() {
1604 let config = AggregatorConfig::correlate_by("key")
1607 .complete_when_size(10)
1608 .build()
1609 .unwrap();
1610 let (tx, _rx) = mpsc::channel(256);
1611 let registry: SharedLanguageRegistry = Arc::new(std::sync::Mutex::new(HashMap::new()));
1612 let cancel = CancellationToken::new();
1613 let svc = AggregatorService::new(config, tx, registry, cancel);
1614
1615 let step: Arc<dyn StepLifecycle> = Arc::new(svc);
1616 step.shutdown(StepShutdownReason::RouteStop)
1617 .await
1618 .expect("first shutdown should succeed");
1619 step.shutdown(StepShutdownReason::RouteStop)
1620 .await
1621 .expect("second shutdown (idempotent) should succeed");
1622 }
1623
1624 #[tokio::test(start_paused = true)]
1625 async fn test_shutdown_awaits_timeout_handles() {
1626 let config = AggregatorConfig::correlate_by("key")
1627 .complete_on_timeout(Duration::from_millis(100))
1628 .build()
1629 .unwrap();
1630 let (tx, _rx) = mpsc::channel(256);
1631 let registry: SharedLanguageRegistry = Arc::new(std::sync::Mutex::new(HashMap::new()));
1632 let cancel = CancellationToken::new();
1633 let svc = AggregatorService::new(config, tx, registry, cancel);
1634
1635 let mut call_svc = svc.clone();
1637 let ex = make_exchange("key", "A", "data");
1638 let _ = call_svc.ready().await.unwrap().call(ex).await.unwrap();
1639
1640 assert!(
1642 !svc.timeout_handles.lock().unwrap().is_empty(),
1643 "should have a timeout handle"
1644 );
1645
1646 svc.shutdown_inner().await;
1649
1650 assert!(
1651 svc.timeout_handles.lock().unwrap().is_empty(),
1652 "all handles should be cleaned up after shutdown"
1653 );
1654 }
1655
1656 #[tokio::test]
1663 async fn test_unique_key_flood_stays_bounded_by_default() {
1664 let config = AggregatorConfig::correlate_by("orderId")
1666 .complete_when_size(1_000_000) .build()
1668 .unwrap();
1669 let mut svc = new_test_svc(config);
1670
1671 for i in 0..10_000usize {
1674 let ex = make_exchange("orderId", &format!("key-{i}"), "body");
1675 let result = svc.ready().await.unwrap().call(ex).await;
1676 assert!(result.is_ok(), "key {i} should be accepted under the cap");
1677 }
1678 let ex = make_exchange("orderId", "key-10001", "body");
1679 let result = svc.ready().await.unwrap().call(ex).await;
1680 assert!(
1681 result.is_err(),
1682 "10_001st unique key must be rejected by the max_buckets cap"
1683 );
1684 let err = result.unwrap_err().to_string();
1685 assert!(
1686 err.contains("maximum") || err.contains("max"),
1687 "error should mention cap: {err}"
1688 );
1689 }
1690
1691 #[tokio::test]
1697 async fn test_background_sweep_spawns_on_first_poll_not_construction() {
1698 let config = AggregatorConfig::correlate_by("key")
1699 .complete_when_size(10_000)
1700 .bucket_ttl(Duration::from_millis(50))
1701 .build()
1702 .unwrap();
1703 let cancel = CancellationToken::new();
1704 let (tx, _rx) = mpsc::channel(8);
1705 let registry: SharedLanguageRegistry = Arc::new(std::sync::Mutex::new(HashMap::new()));
1706 let mut svc = AggregatorService::new(config, tx, registry, cancel.clone());
1707
1708 assert!(
1710 svc.sweep_handle
1711 .lock()
1712 .unwrap_or_else(|e| e.into_inner())
1713 .is_none(),
1714 "sweep must NOT be spawned at construction (runtime-free new)"
1715 );
1716
1717 let _ = svc.ready().await.unwrap();
1720 let sweep_present = svc
1721 .sweep_handle
1722 .lock()
1723 .unwrap_or_else(|e| e.into_inner())
1724 .is_some();
1725 assert!(
1726 sweep_present,
1727 "sweep handle should be Some after first poll when bucket_ttl is set"
1728 );
1729
1730 cancel.cancel();
1732 tokio::time::sleep(Duration::from_millis(50)).await;
1734 }
1735
1736 #[tokio::test]
1739 async fn test_aggregator_timeout_task_cap_no_panic_under_flood() {
1740 use camel_api::aggregator::CorrelationStrategy;
1743
1744 let config = AggregatorConfig {
1745 header_name: "k".into(),
1746 completion: CompletionMode::Any(vec![
1747 CompletionCondition::Size(999),
1748 CompletionCondition::Timeout(Duration::from_secs(30)),
1749 ]),
1750 correlation: CorrelationStrategy::HeaderName("k".into()),
1751 strategy: AggregationStrategy::CollectAll,
1752 max_buckets: Some(50),
1753 bucket_ttl: Some(Duration::from_secs(30)),
1754 force_completion_on_stop: false,
1755 discard_on_timeout: false,
1756 max_timeout_tasks: 2,
1757 };
1758 let (late_tx, mut late_rx) = mpsc::channel(64);
1759 let registry: SharedLanguageRegistry = Arc::new(std::sync::Mutex::new(HashMap::new()));
1760 let cancel = CancellationToken::new();
1761 let svc = AggregatorService::new(config, late_tx, registry, cancel);
1762
1763 for i in 0..20u64 {
1765 let mut ex = Exchange::new(Message {
1766 headers: HashMap::from([("k".to_string(), serde_json::json!(i))]),
1767 body: Body::Text(i.to_string()),
1768 });
1769 ex.input
1770 .headers
1771 .insert("k".to_string(), serde_json::json!(i));
1772 let outcome = tokio::time::timeout(Duration::from_secs(2), async {
1773 let mut s = svc.clone();
1774 use tower::ServiceExt;
1775 s.ready().await.unwrap().call(ex).await
1776 })
1777 .await;
1778 assert!(outcome.is_ok(), "call {} hung/panicked under task cap", i);
1779 let res = outcome.unwrap().unwrap();
1781 assert_eq!(
1782 res.properties
1783 .get(CAMEL_AGGREGATOR_PENDING)
1784 .and_then(|v| v.as_bool()),
1785 Some(true),
1786 "exchange {} should be pending",
1787 i
1788 );
1789 }
1790 let _ = late_rx.try_recv();
1792 }
1793
1794 #[tokio::test]
1795 async fn evaluate_completion_predicate_or_combines_any() {
1796 use camel_api::aggregator::{CompletionCondition, CompletionMode};
1797 use camel_language_api::Language;
1798 use std::collections::HashMap;
1799
1800 let registry: SharedLanguageRegistry = Arc::new(std::sync::Mutex::new(HashMap::new()));
1802 registry.lock().unwrap().insert(
1803 "simple".to_string(),
1804 Arc::new(camel_language_simple::SimpleLanguage::new()) as Arc<dyn Language>,
1805 );
1806
1807 let incoming = make_exchange("k", "X", "hello");
1808
1809 let mode = CompletionMode::Any(vec![
1811 CompletionCondition::PredicateExpr {
1812 expr: "${body} == 'NOPE'".to_string(),
1813 language: "simple".to_string(),
1814 },
1815 CompletionCondition::PredicateExpr {
1816 expr: "${body} == 'hello'".to_string(),
1817 language: "simple".to_string(),
1818 },
1819 ]);
1820
1821 let satisfied = evaluate_completion_predicate(&mode, &incoming, ®istry)
1822 .await
1823 .expect("eval must succeed");
1824 assert!(satisfied, "second predicate matches → OR true");
1825 }
1826
1827 #[tokio::test]
1828 async fn evaluate_completion_predicate_no_predicate_skips_registry() {
1829 use camel_api::aggregator::{CompletionCondition, CompletionMode};
1830 use std::collections::HashMap;
1831
1832 let mode = CompletionMode::Single(CompletionCondition::Size(3));
1836 let registry: SharedLanguageRegistry = Arc::new(std::sync::Mutex::new(HashMap::new()));
1837 let incoming = make_exchange("k", "X", "hello");
1838 let result = evaluate_completion_predicate(&mode, &incoming, ®istry).await;
1839 assert!(result.is_ok(), "fast path must not error: {:?}", result);
1840 assert!(!result.unwrap(), "no predicate → not satisfied");
1841 }
1842
1843 #[tokio::test]
1844 async fn evaluate_completion_predicate_unregistered_language_errors() {
1845 use camel_api::aggregator::{CompletionCondition, CompletionMode};
1846
1847 let mode = CompletionMode::Single(CompletionCondition::PredicateExpr {
1848 expr: "${body} == 'DONE'".to_string(),
1849 language: "nonexistent-lang".to_string(),
1850 });
1851 let registry: SharedLanguageRegistry = Arc::new(std::sync::Mutex::new(HashMap::new()));
1852 let incoming = make_exchange("k", "X", "hello");
1853 let result = evaluate_completion_predicate(&mode, &incoming, ®istry).await;
1854 assert!(result.is_err(), "unregistered language must error");
1855 }
1856
1857 #[tokio::test]
1858 async fn evaluate_completion_predicate_all_miss_returns_false() {
1859 use camel_api::aggregator::{CompletionCondition, CompletionMode};
1860 use camel_language_api::Language;
1861 use std::collections::HashMap;
1862
1863 let registry: SharedLanguageRegistry = Arc::new(std::sync::Mutex::new(HashMap::new()));
1864 registry.lock().unwrap().insert(
1865 "simple".to_string(),
1866 Arc::new(camel_language_simple::SimpleLanguage::new()) as Arc<dyn Language>,
1867 );
1868
1869 let mode = CompletionMode::Single(CompletionCondition::PredicateExpr {
1870 expr: "${body} == 'NOPE'".to_string(),
1871 language: "simple".to_string(),
1872 });
1873 let incoming = make_exchange("k", "X", "hello");
1874 let result = evaluate_completion_predicate(&mode, &incoming, ®istry).await;
1875 assert!(result.is_ok(), "eval must succeed: {:?}", result);
1876 assert!(!result.unwrap(), "predicate does not match");
1877 }
1878
1879 #[tokio::test]
1880 async fn completion_predicate_expr_completes_on_match() {
1881 use camel_api::aggregator::{CompletionCondition, CompletionMode};
1882 use camel_language_api::Language;
1883 use std::collections::HashMap;
1884
1885 let registry: SharedLanguageRegistry = Arc::new(std::sync::Mutex::new(HashMap::new()));
1887 registry.lock().unwrap().insert(
1888 "simple".to_string(),
1889 Arc::new(camel_language_simple::SimpleLanguage::new()) as Arc<dyn Language>,
1890 );
1891
1892 let mut config = AggregatorConfig::correlate_by("key")
1897 .complete_when_size(999) .build()
1899 .expect("config builds");
1900 config.completion = CompletionMode::Single(CompletionCondition::PredicateExpr {
1901 expr: "${body} == 'DONE'".to_string(),
1902 language: "simple".to_string(),
1903 });
1904
1905 let mut svc = new_test_svc_with_registry(config, registry);
1906
1907 let r = svc
1909 .ready()
1910 .await
1911 .unwrap()
1912 .call(make_exchange("key", "K", "first"))
1913 .await
1914 .unwrap();
1915 assert!(r.property(CAMEL_AGGREGATOR_PENDING).is_some());
1916
1917 let r = svc
1919 .ready()
1920 .await
1921 .unwrap()
1922 .call(make_exchange("key", "K", "DONE"))
1923 .await
1924 .unwrap();
1925 assert!(r.property(CAMEL_AGGREGATOR_PENDING).is_none());
1926 assert_eq!(
1927 r.property(CAMEL_AGGREGATED_COMPLETION_REASON),
1928 Some(&serde_json::json!("predicate"))
1929 );
1930 }
1931
1932 #[tokio::test]
1944 async fn test_da1_strategy_receives_two_exchanges_first_message_preserved() {
1945 use camel_api::aggregator::{AggregationFn, AggregationStrategy};
1946 use std::sync::Arc;
1947
1948 let recorded: Arc<std::sync::Mutex<Vec<(String, String)>>> =
1949 Arc::new(std::sync::Mutex::new(Vec::new()));
1950 let recorded_for_closure = Arc::clone(&recorded);
1951
1952 let f: AggregationFn = Arc::new(move |old: Exchange, new: Exchange| {
1953 let old_body = old.input.body.as_text().unwrap_or("").to_string();
1954 let new_body = new.input.body.as_text().unwrap_or("").to_string();
1955 recorded_for_closure
1956 .lock()
1957 .expect("recorded mutex poisoned")
1958 .push((old_body, new_body));
1959 new
1960 });
1961
1962 let config = AggregatorConfig::correlate_by("k")
1963 .complete_when_size(2)
1964 .strategy(AggregationStrategy::Custom(f))
1965 .build()
1966 .unwrap();
1967 let mut svc = new_test_svc(config);
1968
1969 let first = svc
1971 .ready()
1972 .await
1973 .unwrap()
1974 .call(make_exchange("k", "1", "A"))
1975 .await
1976 .unwrap();
1977 assert!(
1978 first.property(CAMEL_AGGREGATOR_PENDING).is_some(),
1979 "first message must leave the bucket pending (size < 2)"
1980 );
1981 assert!(
1982 recorded.lock().expect("recorded mutex poisoned").is_empty(),
1983 "strategy must NOT be invoked on the first message of a bucket"
1984 );
1985
1986 let _completed = svc
1989 .ready()
1990 .await
1991 .unwrap()
1992 .call(make_exchange("k", "1", "B"))
1993 .await
1994 .unwrap();
1995
1996 let recorded = recorded.lock().expect("recorded mutex poisoned");
1997 assert_eq!(
1998 recorded.len(),
1999 1,
2000 "strategy must be invoked exactly once across the two-message bucket, got {recorded:?}"
2001 );
2002 assert_eq!(
2003 recorded[0],
2004 ("A".to_string(), "B".to_string()),
2005 "strategy must observe the first message as `old` and the second as `new`, \
2006 with both bodies preserved unchanged"
2007 );
2008 }
2009
2010 #[tokio::test]
2013 async fn sweep_shutdown_cancels_task() {
2014 let config = AggregatorConfig::correlate_by("key")
2015 .complete_when_size(10)
2016 .bucket_ttl(Duration::from_millis(100))
2017 .build()
2018 .unwrap();
2019 let (tx, _rx) = mpsc::channel(256);
2020 let registry: SharedLanguageRegistry = Arc::new(std::sync::Mutex::new(HashMap::new()));
2021 let cancel = CancellationToken::new();
2022 let mut svc = AggregatorService::new(config, tx, registry, cancel);
2023
2024 let _ = svc.ready().await.unwrap();
2025
2026 assert!(
2027 svc.sweep_handle
2028 .lock()
2029 .unwrap_or_else(|e| e.into_inner())
2030 .is_some(),
2031 "sweep handle should be Some after poll_ready"
2032 );
2033
2034 svc.shutdown(StepShutdownReason::RouteStop).await.unwrap();
2035
2036 assert!(
2037 svc.sweep_handle
2038 .lock()
2039 .unwrap_or_else(|e| e.into_inner())
2040 .is_none(),
2041 "sweep handle should be None after shutdown (taken + aborted)"
2042 );
2043 assert!(
2044 svc.sweep_cancel
2045 .lock()
2046 .unwrap_or_else(|e| e.into_inner())
2047 .is_cancelled(),
2048 "sweep_cancel token should be cancelled after shutdown"
2049 );
2050
2051 tokio::time::sleep(Duration::from_millis(50)).await;
2052 }
2053
2054 #[tokio::test]
2055 async fn sweep_start_respawns_after_shutdown() {
2056 let config = AggregatorConfig::correlate_by("key")
2057 .complete_when_size(10)
2058 .bucket_ttl(Duration::from_millis(100))
2059 .build()
2060 .unwrap();
2061 let (tx, _rx) = mpsc::channel(256);
2062 let registry: SharedLanguageRegistry = Arc::new(std::sync::Mutex::new(HashMap::new()));
2063 let cancel = CancellationToken::new();
2064 let mut svc = AggregatorService::new(config, tx, registry, cancel);
2065
2066 let _ = svc.ready().await.unwrap();
2067 svc.shutdown(StepShutdownReason::RouteStop).await.unwrap();
2068
2069 svc.start().await.unwrap();
2070 let _ = svc.ready().await.unwrap();
2071
2072 assert!(
2073 svc.sweep_handle
2074 .lock()
2075 .unwrap_or_else(|e| e.into_inner())
2076 .is_some(),
2077 "sweep handle should be Some after start + poll_ready"
2078 );
2079 assert!(
2080 !svc.sweep_cancel
2081 .lock()
2082 .unwrap_or_else(|e| e.into_inner())
2083 .is_cancelled(),
2084 "sweep_cancel should be a fresh uncancelled token after start"
2085 );
2086 }
2087
2088 #[tokio::test]
2089 async fn sweep_shutdown_hotswap_cancels_task() {
2090 let config = AggregatorConfig::correlate_by("key")
2091 .complete_when_size(10)
2092 .bucket_ttl(Duration::from_millis(100))
2093 .build()
2094 .unwrap();
2095 let (tx, _rx) = mpsc::channel(256);
2096 let registry: SharedLanguageRegistry = Arc::new(std::sync::Mutex::new(HashMap::new()));
2097 let cancel = CancellationToken::new();
2098 let mut svc = AggregatorService::new(config, tx, registry, cancel);
2099
2100 let _ = svc.ready().await.unwrap();
2101
2102 assert!(
2103 svc.sweep_handle
2104 .lock()
2105 .unwrap_or_else(|e| e.into_inner())
2106 .is_some(),
2107 "sweep handle should be Some after poll_ready"
2108 );
2109
2110 svc.shutdown(StepShutdownReason::HotSwap).await.unwrap();
2111
2112 assert!(
2113 svc.sweep_handle
2114 .lock()
2115 .unwrap_or_else(|e| e.into_inner())
2116 .is_none(),
2117 "sweep handle should be None after HotSwap shutdown"
2118 );
2119 assert!(
2120 svc.sweep_cancel
2121 .lock()
2122 .unwrap_or_else(|e| e.into_inner())
2123 .is_cancelled(),
2124 "sweep_cancel token should be cancelled after HotSwap shutdown"
2125 );
2126
2127 tokio::time::sleep(Duration::from_millis(50)).await;
2128 }
2129}