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, MetricsCollector, 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
28const QUEUE_DEPTH_SAMPLE_INTERVAL: Duration = Duration::from_millis(250);
32
33pub const CAMEL_AGGREGATOR_PENDING: &str = "CamelAggregatorPending";
34pub const CAMEL_AGGREGATED_SIZE: &str = "CamelAggregatedSize";
35pub const CAMEL_AGGREGATED_KEY: &str = "CamelAggregatedKey";
36pub const CAMEL_AGGREGATED_COMPLETION_REASON: &str = "CamelAggregatedCompletionReason";
37
38struct Bucket {
40 exchanges: Vec<Exchange>,
41 last_updated: Instant,
42}
43
44impl Bucket {
45 fn new() -> Self {
46 Self {
47 exchanges: Vec::new(),
48 last_updated: Instant::now(),
49 }
50 }
51
52 fn push(&mut self, exchange: Exchange) {
53 self.exchanges.push(exchange);
54 self.last_updated = Instant::now();
55 }
56
57 fn is_expired(&self, ttl: Duration) -> bool {
58 Instant::now().duration_since(self.last_updated) >= ttl
59 }
60}
61
62#[derive(Clone)]
63pub struct AggregatorService {
64 config: AggregatorConfig,
65 buckets: Arc<Mutex<HashMap<String, Bucket>>>,
66 timeout_tasks: Arc<Mutex<HashMap<String, CancellationToken>>>,
67 timeout_handles: Arc<Mutex<HashMap<String, JoinHandle<()>>>>,
68 late_tx: mpsc::Sender<Exchange>,
69 language_registry: SharedLanguageRegistry,
70 sweep_cancel: Arc<Mutex<CancellationToken>>,
77 sweep_handle: Arc<Mutex<Option<JoinHandle<()>>>>,
85 queue_metrics: Option<(Arc<dyn MetricsCollector>, String)>,
90 clone_guard: Arc<()>,
94 cached_key: Arc<Mutex<Option<(serde_json::Value, String)>>>,
105 #[cfg(test)]
109 key_serializations: Arc<std::sync::atomic::AtomicUsize>,
110}
111
112impl std::fmt::Debug for AggregatorService {
113 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
114 f.debug_struct("AggregatorService").finish_non_exhaustive()
115 }
116}
117
118impl Drop for AggregatorService {
119 fn drop(&mut self) {
120 if Arc::strong_count(&self.clone_guard) > 1 {
130 return;
131 }
132 if let Some(handle) = self
133 .sweep_handle
134 .lock()
135 .unwrap_or_else(|e| e.into_inner())
136 .take()
137 {
138 handle.abort();
139 }
140 }
141}
142
143impl AggregatorService {
144 pub fn new(
155 config: AggregatorConfig,
156 late_tx: mpsc::Sender<Exchange>,
157 language_registry: SharedLanguageRegistry,
158 route_cancel: CancellationToken,
159 ) -> Self {
160 config.validate().expect(
162 "AggregatorService::new: config failed validation \
165 (need max_buckets, completionTimeout, or bucket_ttl)",
166 );
167
168 let has_timeout = has_timeout_condition(&config.completion);
172 if !has_timeout && config.bucket_ttl.is_none() {
173 tracing::warn!(
174 "Aggregator configured with Size/Predicate completion and no \
175 bucket_ttl: buckets accumulate until max_buckets is reached"
176 );
177 }
178
179 let buckets: Arc<Mutex<HashMap<String, Bucket>>> = Arc::new(Mutex::new(HashMap::new()));
182
183 Self {
192 config,
193 buckets,
194 timeout_tasks: Arc::new(Mutex::new(HashMap::new())),
195 timeout_handles: Arc::new(Mutex::new(HashMap::new())),
196 late_tx,
197 language_registry,
198 sweep_cancel: Arc::new(Mutex::new(route_cancel)),
199 sweep_handle: Arc::new(Mutex::new(None)),
200 queue_metrics: None,
201 clone_guard: Arc::new(()),
202 cached_key: Arc::new(Mutex::new(None)),
203 #[cfg(test)]
204 key_serializations: Arc::new(std::sync::atomic::AtomicUsize::new(0)),
205 }
206 }
207
208 pub fn with_queue_metrics(
213 mut self,
214 metrics: Arc<dyn MetricsCollector>,
215 label: impl Into<String>,
216 ) -> Self {
217 self.queue_metrics = Some((metrics, label.into()));
218 self
219 }
220
221 pub fn config(&self) -> &AggregatorConfig {
222 &self.config
223 }
224
225 pub fn has_timeout(&self) -> bool {
226 has_timeout_condition(&self.config.completion)
227 }
228
229 pub fn force_complete_all(&self) {
230 let mut buckets_guard = self.buckets.lock().unwrap_or_else(|e| e.into_inner());
231 let keys: Vec<String> = buckets_guard.keys().cloned().collect();
232
233 for key in keys {
234 if let Some(bucket) = buckets_guard.remove(&key) {
235 if self.config.force_completion_on_stop {
236 cancel_timeout_task_with_handle(
237 &key,
238 &self.timeout_tasks,
239 &self.timeout_handles,
240 );
241 match aggregate(bucket.exchanges, &self.config.strategy) {
242 Ok(mut result) => {
243 result.set_property(
244 CAMEL_AGGREGATED_COMPLETION_REASON,
245 serde_json::json!(CompletionReason::Stop.as_str()),
246 );
247 if self.late_tx.try_send(result).is_err() {
248 tracing::warn!(
249 key = %key,
250 "aggregator force-complete emit dropped: late channel full"
251 );
252 }
253 }
254 Err(e) => {
255 tracing::warn!(
257 key = %key,
258 error = %e,
259 "aggregation failed in force_complete_all"
260 );
261 }
262 }
263 } else {
264 cancel_timeout_task_with_handle(
265 &key,
266 &self.timeout_tasks,
267 &self.timeout_handles,
268 );
269 }
270 }
271 }
272 }
273
274 pub fn release_unarmed_buckets(&self) {
291 let mut buckets_guard = self.buckets.lock().unwrap_or_else(|e| e.into_inner());
292 let timeout_guard = self.timeout_tasks.lock().unwrap_or_else(|e| e.into_inner());
293 let before = buckets_guard.len();
294 buckets_guard.retain(|key, _| timeout_guard.contains_key(key));
295 let released = before - buckets_guard.len();
296 if released > 0 {
297 tracing::debug!(
298 released,
299 armed = buckets_guard.len(),
300 "aggregator released unarmed buckets on consumer exit"
301 );
302 }
303 }
304
305 pub(crate) async fn shutdown_inner(&self) {
308 {
310 let mut guard = self.timeout_tasks.lock().unwrap_or_else(|e| e.into_inner());
311 for token in guard.values() {
312 token.cancel();
313 }
314 guard.clear();
315 };
316
317 let handles: Vec<JoinHandle<()>> = {
319 let mut guard = self
320 .timeout_handles
321 .lock()
322 .unwrap_or_else(|e| e.into_inner());
323 guard.drain().map(|(_, handle)| handle).collect()
324 };
325
326 if handles.is_empty() {
327 return;
328 }
329
330 let _ = tokio::time::timeout(Duration::from_secs(5), async {
332 for handle in handles {
333 let _ = handle.await;
334 }
335 })
336 .await;
337 }
338}
339
340#[async_trait]
341impl StepLifecycle for AggregatorService {
342 fn name(&self) -> &'static str {
343 "aggregator"
344 }
345
346 async fn shutdown(&self, reason: StepShutdownReason) -> Result<(), CamelError> {
347 tracing::debug!(reason = ?reason, "Aggregator shutdown via StepLifecycle");
348
349 self.sweep_cancel
352 .lock()
353 .unwrap_or_else(|e| e.into_inner())
354 .cancel();
355
356 if let Some(handle) = self
359 .sweep_handle
360 .lock()
361 .unwrap_or_else(|e| e.into_inner())
362 .take()
363 {
364 handle.abort();
365 }
366
367 self.shutdown_inner().await;
368 Ok(())
369 }
370
371 async fn start(&self) -> Result<(), CamelError> {
375 *self.sweep_cancel.lock().unwrap_or_else(|e| e.into_inner()) = CancellationToken::new();
376
377 if let Some(handle) = self
378 .sweep_handle
379 .lock()
380 .unwrap_or_else(|e| e.into_inner())
381 .take()
382 {
383 handle.abort();
384 }
385
386 Ok(())
387 }
388}
389
390pub fn has_timeout_condition(mode: &CompletionMode) -> bool {
391 match mode {
392 CompletionMode::Single(CompletionCondition::Timeout(_)) => true,
393 CompletionMode::Any(conditions) => conditions
394 .iter()
395 .any(|c| matches!(c, CompletionCondition::Timeout(_))),
396 _ => false,
397 }
398}
399
400impl Service<Exchange> for AggregatorService {
401 type Response = Exchange;
402 type Error = CamelError;
403 type Future = Pin<Box<dyn Future<Output = Result<Exchange, CamelError>> + Send>>;
404
405 fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), CamelError>> {
406 let ttl = self.config.bucket_ttl;
415 if ttl.is_none() && self.queue_metrics.is_none() {
416 return Poll::Ready(Ok(()));
417 }
418 let mut g = self.sweep_handle.lock().unwrap_or_else(|e| e.into_inner());
419 if g.is_none() {
420 let interval = match ttl {
421 Some(ttl) => std::cmp::max(ttl / 2, Duration::from_millis(50)),
422 None => QUEUE_DEPTH_SAMPLE_INTERVAL,
423 };
424 let buckets = Arc::clone(&self.buckets);
425 let cancel = self
426 .sweep_cancel
427 .lock()
428 .unwrap_or_else(|e| e.into_inner())
429 .clone();
430 let queue_metrics = self.queue_metrics.clone();
431 *g = Some(tokio::spawn(async move {
432 loop {
433 tokio::select! {
434 _ = cancel.cancelled() => break,
435 _ = tokio::time::sleep(interval) => {
436 let mut guard =
437 buckets.lock().unwrap_or_else(|e| e.into_inner());
438 if let Some(ttl) = ttl {
439 guard.retain(|_, b| !b.is_expired(ttl));
440 }
441 if let Some((metrics, label)) = queue_metrics.as_ref() {
444 metrics.set_queue_depth(label, guard.len());
445 }
446 }
447 }
448 }
449 }));
450 }
451 Poll::Ready(Ok(()))
452 }
453
454 fn call(&mut self, exchange: Exchange) -> Self::Future {
455 let config = self.config.clone();
456 let buckets = Arc::clone(&self.buckets);
457 let timeout_tasks = Arc::clone(&self.timeout_tasks);
458 let timeout_handles = Arc::clone(&self.timeout_handles);
459 let late_tx = self.late_tx.clone();
460 let language_registry = Arc::clone(&self.language_registry);
461 let cached_key = Arc::clone(&self.cached_key);
462 #[cfg(test)]
463 let key_serializations = Arc::clone(&self.key_serializations);
464
465 Box::pin(async move {
466 let key_value =
467 extract_correlation_key(&exchange, &config.correlation, &language_registry).await?;
468
469 let key_is_memoizable = matches!(key_value, serde_json::Value::String(_));
480 let key_str = if key_is_memoizable {
481 let mut memo = cached_key.lock().unwrap_or_else(|e| e.into_inner());
482 if let Some((v, s)) = memo.as_ref()
483 && *v == key_value
484 {
485 s.clone()
486 } else {
487 #[cfg(test)]
488 key_serializations.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
489 let s = serde_json::to_string(&key_value)
490 .map_err(|e| CamelError::ProcessorError(e.to_string()))?;
491 *memo = Some((key_value.clone(), s.clone()));
492 s
493 }
494 } else {
495 #[cfg(test)]
496 key_serializations.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
497 serde_json::to_string(&key_value)
498 .map_err(|e| CamelError::ProcessorError(e.to_string()))?
499 };
500
501 let predicate_satisfied =
505 evaluate_completion_predicate(&config.completion, &exchange, &language_registry)
506 .await?;
507
508 let completed_bucket = {
509 let mut guard = buckets.lock().unwrap_or_else(|e| e.into_inner());
510
511 if let Some(ttl) = config.bucket_ttl {
512 guard.retain(|_, bucket| !bucket.is_expired(ttl));
513 }
514
515 if let Some(max) = config.max_buckets
516 && !guard.contains_key(&key_str)
517 && guard.len() >= max
518 {
519 tracing::warn!(
520 max_buckets = max,
521 correlation_key = %key_str,
522 "Aggregator reached max buckets limit, rejecting new correlation key"
523 );
524 return Err(CamelError::ProcessorError(format!(
525 "Aggregator reached maximum {} buckets",
526 max
527 )));
528 }
529
530 if let Some(max_size) = config.max_bucket_size
534 && let Some(existing) = guard.get(&key_str)
535 && existing.exchanges.len() >= max_size
536 {
537 tracing::warn!(
538 max_bucket_size = max_size,
539 correlation_key = %key_str,
540 "Aggregator reached per-bucket size limit, rejecting exchange"
541 );
542 return Err(CamelError::ProcessorError(format!(
543 "Aggregator bucket reached maximum {max_size} exchanges"
544 )));
545 }
546
547 let bucket = match guard.get_mut(&key_str) {
548 Some(b) => b,
549 None => guard.entry(key_str.clone()).or_insert_with(Bucket::new),
550 };
551 bucket.push(exchange);
552
553 let (is_complete, reason) = check_sync_completion(
554 &config.completion,
555 &bucket.exchanges,
556 predicate_satisfied,
557 );
558
559 let exchanges = if is_complete {
560 guard.remove(&key_str).map(|b| b.exchanges)
561 } else {
562 None
563 };
564 (exchanges, reason)
565 };
566
567 if completed_bucket.0.is_none() && has_timeout_condition(&config.completion) {
568 let timeout_dur = extract_timeout_duration(&config.completion);
569 if let Some(timeout) = timeout_dur {
570 let live_count = timeout_handles
576 .lock()
577 .unwrap_or_else(|e| e.into_inner())
578 .len();
579 if live_count >= config.max_timeout_tasks {
580 tracing::warn!(
581 live_timeout_tasks = live_count,
582 max_timeout_tasks = config.max_timeout_tasks,
583 correlation_key = %key_str,
584 "Aggregator timeout-task cap reached; bucket will rely on \
585 bucket_ttl eviction instead of a dedicated timeout task"
586 );
587 } else {
588 {
590 let tt_guard = timeout_tasks.lock().unwrap_or_else(|e| e.into_inner());
591 if let Some(existing) = tt_guard.get(&key_str) {
592 existing.cancel();
593 }
594 }
595 {
597 let mut hh = timeout_handles.lock().unwrap_or_else(|e| e.into_inner());
598 if let Some(old) = hh.remove(&key_str) {
599 old.abort();
600 }
601 }
602 let cancel = CancellationToken::new();
603 timeout_tasks
604 .lock()
605 .unwrap_or_else(|e| e.into_inner())
606 .insert(key_str.clone(), cancel.clone());
607 let handle = spawn_timeout_task(
608 key_str.clone(),
609 timeout,
610 cancel,
611 buckets.clone(),
612 timeout_tasks.clone(),
613 timeout_handles.clone(),
614 late_tx,
615 config.strategy.clone(),
616 config.discard_on_timeout,
617 );
618 timeout_handles
619 .lock()
620 .unwrap_or_else(|e| e.into_inner())
621 .insert(key_str.clone(), handle);
622 }
623 }
624 }
625
626 if let Some(exchanges) = completed_bucket.0 {
627 cancel_timeout_task_with_handle(&key_str, &timeout_tasks, &timeout_handles);
628 let reason = completed_bucket.1;
629 let size = exchanges.len();
630 let mut result = aggregate(exchanges, &config.strategy)?;
631 result.set_property(CAMEL_AGGREGATED_SIZE, serde_json::json!(size as u64));
632 result.set_property(CAMEL_AGGREGATED_KEY, key_value);
633 result.set_property(
634 CAMEL_AGGREGATED_COMPLETION_REASON,
635 serde_json::json!(reason.as_str()),
636 );
637 Ok(result)
638 } else {
639 let mut pending = Exchange::new(Message {
640 headers: Default::default(),
641 body: Body::Empty,
642 });
643 pending.set_property(CAMEL_AGGREGATOR_PENDING, serde_json::json!(true));
644 Ok(pending)
645 }
646 })
647 }
648}
649
650async fn extract_correlation_key(
651 exchange: &Exchange,
652 strategy: &CorrelationStrategy,
653 registry: &SharedLanguageRegistry,
654) -> Result<serde_json::Value, CamelError> {
655 match strategy {
656 CorrelationStrategy::HeaderName(h) => {
657 exchange.input.headers.get(h).cloned().ok_or_else(|| {
658 CamelError::ProcessorError(format!(
659 "Aggregator: missing correlation key header '{}'",
660 h
661 ))
662 })
663 }
664 CorrelationStrategy::Expression { expr, language } => {
665 let expression = {
666 let reg = registry.lock().unwrap_or_else(|e| e.into_inner());
667 let lang = reg.get(language).ok_or_else(|| {
668 CamelError::ProcessorError(format!(
669 "Aggregator: language '{}' not found in registry",
670 language
671 ))
672 })?;
673 lang.create_expression(expr)
674 .map_err(|e| CamelError::ProcessorError(e.to_string()))?
675 };
676 let value = expression
677 .evaluate(exchange)
678 .await
679 .map_err(|e| CamelError::ProcessorError(e.to_string()))?;
680 if value.is_null() {
681 return Err(CamelError::ProcessorError(format!(
682 "Aggregator: correlation expression '{}' evaluated to null",
683 expr
684 )));
685 }
686 Ok(value)
687 }
688 CorrelationStrategy::Fn(f) => f(exchange).map(serde_json::Value::String).ok_or_else(|| {
689 CamelError::ProcessorError("Aggregator: correlation function returned None".to_string())
690 }),
691 _ => Err(CamelError::ProcessorError(
693 "Aggregator: unsupported correlation strategy".to_string(),
694 )),
695 }
696}
697
698fn iter_predicate_exprs(mode: &CompletionMode) -> Vec<(&String, &String)> {
700 let mut out = Vec::new();
701 match mode {
702 CompletionMode::Single(CompletionCondition::PredicateExpr { expr, language }) => {
703 out.push((expr, language));
704 }
705 CompletionMode::Any(conds) => {
706 for c in conds {
707 if let CompletionCondition::PredicateExpr { expr, language } = c {
708 out.push((expr, language));
709 }
710 }
711 }
712 _ => {}
713 }
714 out
715}
716
717async fn evaluate_completion_predicate(
722 mode: &CompletionMode,
723 incoming: &Exchange,
724 registry: &SharedLanguageRegistry,
725) -> Result<bool, CamelError> {
726 let mut satisfied = false;
727 for (expr, language) in iter_predicate_exprs(mode) {
728 let expression = {
729 let reg = registry.lock().unwrap_or_else(|e| e.into_inner());
730 let lang = reg.get(language).ok_or_else(|| {
731 CamelError::ProcessorError(format!(
732 "Aggregator: language '{}' not found in registry",
733 language
734 ))
735 })?;
736 lang.create_expression(expr)
737 .map_err(|e| CamelError::ProcessorError(e.to_string()))?
738 }; let value = expression
740 .evaluate(incoming)
741 .await
742 .map_err(|e| CamelError::ProcessorError(e.to_string()))?;
743 if value.as_bool().unwrap_or(false) {
744 satisfied = true;
745 }
746 }
747 Ok(satisfied)
748}
749
750fn check_sync_completion(
751 mode: &CompletionMode,
752 exchanges: &[Exchange],
753 predicate_satisfied: bool,
754) -> (bool, CompletionReason) {
755 match mode {
756 CompletionMode::Single(cond) => check_single(cond, exchanges, predicate_satisfied),
757 CompletionMode::Any(conditions) => {
758 for cond in conditions {
759 if let CompletionCondition::Timeout(_) = cond {
760 continue;
761 }
762 let (done, reason) = check_single(cond, exchanges, predicate_satisfied);
763 if done {
764 return (true, reason);
765 }
766 }
767 (false, CompletionReason::Size)
768 }
769 _ => (false, CompletionReason::Size),
771 }
772}
773
774fn check_single(
775 cond: &CompletionCondition,
776 exchanges: &[Exchange],
777 predicate_satisfied: bool,
778) -> (bool, CompletionReason) {
779 match cond {
780 CompletionCondition::Size(n) => (exchanges.len() >= *n, CompletionReason::Size),
781 CompletionCondition::Predicate(pred) => (pred(exchanges), CompletionReason::Predicate),
782 CompletionCondition::PredicateExpr { .. } => {
783 (predicate_satisfied, CompletionReason::Predicate)
784 }
785 CompletionCondition::Timeout(_) => (false, CompletionReason::Timeout),
786 _ => (false, CompletionReason::Size),
788 }
789}
790
791fn extract_timeout_duration(mode: &CompletionMode) -> Option<Duration> {
792 match mode {
793 CompletionMode::Single(CompletionCondition::Timeout(d)) => Some(*d),
794 CompletionMode::Any(conditions) => conditions.iter().find_map(|c| {
795 if let CompletionCondition::Timeout(d) = c {
796 Some(*d)
797 } else {
798 None
799 }
800 }),
801 _ => None,
802 }
803}
804
805fn cancel_timeout_task(key: &str, timeout_tasks: &Arc<Mutex<HashMap<String, CancellationToken>>>) {
806 let mut guard = timeout_tasks.lock().unwrap_or_else(|e| e.into_inner());
807 if let Some(token) = guard.remove(key) {
808 token.cancel();
809 }
810}
811
812fn cancel_timeout_task_with_handle(
814 key: &str,
815 timeout_tasks: &Arc<Mutex<HashMap<String, CancellationToken>>>,
816 timeout_handles: &Arc<Mutex<HashMap<String, JoinHandle<()>>>>,
817) {
818 cancel_timeout_task(key, timeout_tasks);
819 let mut guard = timeout_handles.lock().unwrap_or_else(|e| e.into_inner());
820 guard.remove(key);
821}
822
823#[allow(clippy::too_many_arguments)]
824fn spawn_timeout_task(
825 key: String,
826 timeout: Duration,
827 cancel: CancellationToken,
828 buckets: Arc<Mutex<HashMap<String, Bucket>>>,
829 timeout_tasks: Arc<Mutex<HashMap<String, CancellationToken>>>,
830 timeout_handles: Arc<Mutex<HashMap<String, JoinHandle<()>>>>,
831 late_tx: mpsc::Sender<Exchange>,
832 strategy: AggregationStrategy,
833 discard: bool,
834) -> JoinHandle<()> {
835 let cancel_clone = cancel.clone();
836 tokio::spawn(async move {
837 tokio::select! {
838 _ = tokio::time::sleep(timeout) => {
839 let should_proceed = {
840 let mut tt_guard = timeout_tasks.lock().unwrap_or_else(|e| e.into_inner());
841 if cancel_clone.is_cancelled() {
842 false
843 } else {
844 tt_guard.remove(&key);
845 true
846 }
847 };
848 if !should_proceed {
849 return;
850 }
851 {
854 let mut hh = timeout_handles.lock().unwrap_or_else(|e| e.into_inner());
855 hh.remove(&key);
856 }
857 let bucket_exchanges = {
858 let mut guard = buckets.lock().unwrap_or_else(|e| e.into_inner());
859 guard.remove(&key).map(|b| b.exchanges)
860 };
861 if let Some(exchanges) = bucket_exchanges
862 && !discard
863 {
864 match aggregate(exchanges, &strategy) {
865 Ok(mut result) => {
866 result.set_property(
867 CAMEL_AGGREGATED_COMPLETION_REASON,
868 serde_json::json!(CompletionReason::Timeout.as_str()),
869 );
870 if late_tx.try_send(result).is_err() {
871 tracing::warn!(
872 key = %key,
873 "aggregator timeout emit dropped: late channel full"
874 );
875 }
876 }
877 Err(e) => {
878 tracing::warn!(
880 key = %key,
881 error = %e,
882 "aggregation failed in timeout task"
883 );
884 }
885 }
886 }
887 }
888 _ = cancel_clone.cancelled() => {}
889 }
890 })
891}
892
893fn aggregate(
894 exchanges: Vec<Exchange>,
895 strategy: &AggregationStrategy,
896) -> Result<Exchange, CamelError> {
897 match strategy {
898 AggregationStrategy::CollectAll => {
899 let bodies: Vec<serde_json::Value> = exchanges
900 .into_iter()
901 .map(|e| match e.input.body {
902 Body::Json(v) => v,
903 Body::Text(s) => serde_json::Value::String(s),
904 Body::Xml(s) => serde_json::Value::String(s),
905 Body::Bytes(b) => {
906 serde_json::Value::String(String::from_utf8_lossy(&b).into_owned())
907 }
908 Body::Stream(s) => serde_json::json!({
909 "_stream": {
910 "origin": s.metadata.origin,
911 "placeholder": true,
912 "hint": "Materialize exchange body with .into_bytes() before aggregation if content needed"
913 }
914 }),
915 _ => serde_json::Value::Null,
917 })
918 .collect();
919 Ok(Exchange::new(Message {
920 headers: Default::default(),
921 body: Body::Json(serde_json::Value::Array(bodies)),
922 }))
923 }
924 AggregationStrategy::Custom(f) => {
925 let mut iter = exchanges.into_iter();
926 let first = iter.next().ok_or_else(|| {
927 CamelError::ProcessorError("Aggregator: empty bucket".to_string())
928 })?;
929 Ok(iter.fold(first, |acc, next| f(acc, next)))
930 }
931 _ => Err(CamelError::ProcessorError(
933 "Aggregator: unsupported aggregation strategy".to_string(),
934 )),
935 }
936}
937
938#[cfg(test)]
939mod tests {
940 use super::*;
941 use std::collections::HashMap;
942
943 use camel_api::{
944 StepLifecycle, StepShutdownReason,
945 aggregator::{AggregationStrategy, AggregatorConfig},
946 body::Body,
947 exchange::Exchange,
948 message::Message,
949 };
950 use tokio::sync::mpsc;
951 use tokio_util::sync::CancellationToken;
952 use tower::ServiceExt;
953
954 fn make_exchange(header: &str, value: &str, body: &str) -> Exchange {
955 let mut msg = Message {
956 headers: Default::default(),
957 body: Body::Text(body.to_string()),
958 };
959 msg.headers
960 .insert(header.to_string(), serde_json::json!(value));
961 Exchange::new(msg)
962 }
963
964 fn config_size(n: usize) -> AggregatorConfig {
965 AggregatorConfig::correlate_by("orderId")
966 .complete_when_size(n)
967 .build()
968 .unwrap()
969 }
970
971 fn new_test_svc(config: AggregatorConfig) -> AggregatorService {
972 let (tx, _rx) = mpsc::channel(256);
973 let registry: SharedLanguageRegistry = Arc::new(std::sync::Mutex::new(HashMap::new()));
974 let cancel = CancellationToken::new();
975 AggregatorService::new(config, tx, registry, cancel)
976 }
977
978 fn new_test_svc_with_registry(
982 config: AggregatorConfig,
983 registry: SharedLanguageRegistry,
984 ) -> AggregatorService {
985 let (tx, _rx) = mpsc::channel(256);
986 let cancel = CancellationToken::new();
987 AggregatorService::new(config, tx, registry, cancel)
988 }
989
990 #[tokio::test]
991 async fn test_pending_exchange_not_yet_complete() {
992 let mut svc = new_test_svc(config_size(3));
993 let ex = make_exchange("orderId", "A", "first");
994 let result = svc.ready().await.unwrap().call(ex).await.unwrap();
995 assert!(matches!(result.input.body, Body::Empty));
996 assert_eq!(
997 result.property(CAMEL_AGGREGATOR_PENDING),
998 Some(&serde_json::json!(true))
999 );
1000 }
1001
1002 #[tokio::test]
1003 async fn test_completes_on_size() {
1004 let mut svc = new_test_svc(config_size(3));
1005 for _ in 0..2 {
1006 let ex = make_exchange("orderId", "A", "item");
1007 let r = svc.ready().await.unwrap().call(ex).await.unwrap();
1008 assert!(matches!(r.input.body, Body::Empty));
1009 }
1010 let ex = make_exchange("orderId", "A", "last");
1011 let result = svc.ready().await.unwrap().call(ex).await.unwrap();
1012 assert!(result.property(CAMEL_AGGREGATOR_PENDING).is_none());
1013 assert_eq!(
1014 result.property(CAMEL_AGGREGATED_SIZE),
1015 Some(&serde_json::json!(3u64))
1016 );
1017 }
1018
1019 #[tokio::test]
1020 async fn test_collect_all_produces_json_array() {
1021 let mut svc = new_test_svc(config_size(2));
1022 svc.ready()
1023 .await
1024 .unwrap()
1025 .call(make_exchange("orderId", "A", "alpha"))
1026 .await
1027 .unwrap();
1028 let result = svc
1029 .ready()
1030 .await
1031 .unwrap()
1032 .call(make_exchange("orderId", "A", "beta"))
1033 .await
1034 .unwrap();
1035 let Body::Json(v) = &result.input.body else {
1036 panic!("expected Body::Json")
1037 };
1038 let arr = v.as_array().unwrap();
1039 assert_eq!(arr.len(), 2);
1040 assert_eq!(arr[0], serde_json::json!("alpha"));
1041 assert_eq!(arr[1], serde_json::json!("beta"));
1042 }
1043
1044 #[tokio::test]
1045 async fn test_two_keys_independent_buckets() {
1046 let mut svc = new_test_svc(config_size(3));
1048 svc.ready()
1049 .await
1050 .unwrap()
1051 .call(make_exchange("orderId", "A", "a1"))
1052 .await
1053 .unwrap();
1054 svc.ready()
1055 .await
1056 .unwrap()
1057 .call(make_exchange("orderId", "B", "b1"))
1058 .await
1059 .unwrap();
1060 svc.ready()
1061 .await
1062 .unwrap()
1063 .call(make_exchange("orderId", "A", "a2"))
1064 .await
1065 .unwrap();
1066 let ra = svc
1068 .ready()
1069 .await
1070 .unwrap()
1071 .call(make_exchange("orderId", "A", "a3"))
1072 .await
1073 .unwrap();
1074 assert!(matches!(ra.input.body, Body::Json(_)));
1076 let rb = svc
1078 .ready()
1079 .await
1080 .unwrap()
1081 .call(make_exchange("orderId", "B", "b_check"))
1082 .await
1083 .unwrap();
1084 assert!(matches!(rb.input.body, Body::Empty));
1085 }
1086
1087 #[tokio::test]
1088 async fn test_bucket_resets_after_completion() {
1089 let mut svc = new_test_svc(config_size(2));
1090 svc.ready()
1091 .await
1092 .unwrap()
1093 .call(make_exchange("orderId", "A", "x"))
1094 .await
1095 .unwrap();
1096 svc.ready()
1097 .await
1098 .unwrap()
1099 .call(make_exchange("orderId", "A", "x"))
1100 .await
1101 .unwrap(); let r = svc
1104 .ready()
1105 .await
1106 .unwrap()
1107 .call(make_exchange("orderId", "A", "new"))
1108 .await
1109 .unwrap();
1110 assert!(matches!(r.input.body, Body::Empty)); }
1112
1113 #[tokio::test]
1114 async fn test_completion_size_1_emits_immediately() {
1115 let mut svc = new_test_svc(config_size(1));
1116 let ex = make_exchange("orderId", "A", "solo");
1117 let result = svc.ready().await.unwrap().call(ex).await.unwrap();
1118 assert!(result.property(CAMEL_AGGREGATOR_PENDING).is_none());
1119 }
1120
1121 #[tokio::test]
1122 async fn test_custom_aggregation_strategy() {
1123 use camel_api::aggregator::AggregationFn;
1124 use std::sync::Arc;
1125
1126 let f: AggregationFn = Arc::new(|mut acc: Exchange, next: Exchange| {
1127 let combined = format!(
1128 "{}+{}",
1129 acc.input.body.as_text().unwrap_or(""),
1130 next.input.body.as_text().unwrap_or("")
1131 );
1132 acc.input.body = Body::Text(combined);
1133 acc
1134 });
1135 let config = AggregatorConfig::correlate_by("key")
1136 .complete_when_size(2)
1137 .strategy(AggregationStrategy::Custom(f))
1138 .build()
1139 .unwrap();
1140 let mut svc = new_test_svc(config);
1141 svc.ready()
1142 .await
1143 .unwrap()
1144 .call(make_exchange("key", "X", "hello"))
1145 .await
1146 .unwrap();
1147 let result = svc
1148 .ready()
1149 .await
1150 .unwrap()
1151 .call(make_exchange("key", "X", "world"))
1152 .await
1153 .unwrap();
1154 assert_eq!(result.input.body.as_text(), Some("hello+world"));
1155 }
1156
1157 #[tokio::test]
1158 async fn test_completion_predicate() {
1159 let config = AggregatorConfig::correlate_by("key")
1160 .complete_when(|bucket| {
1161 bucket
1162 .iter()
1163 .any(|e| e.input.body.as_text() == Some("DONE"))
1164 })
1165 .build()
1166 .unwrap();
1167 let mut svc = new_test_svc(config);
1168 svc.ready()
1169 .await
1170 .unwrap()
1171 .call(make_exchange("key", "K", "first"))
1172 .await
1173 .unwrap();
1174 svc.ready()
1175 .await
1176 .unwrap()
1177 .call(make_exchange("key", "K", "second"))
1178 .await
1179 .unwrap();
1180 let result = svc
1181 .ready()
1182 .await
1183 .unwrap()
1184 .call(make_exchange("key", "K", "DONE"))
1185 .await
1186 .unwrap();
1187 assert!(result.property(CAMEL_AGGREGATOR_PENDING).is_none());
1188 }
1189
1190 #[tokio::test]
1191 async fn test_missing_header_returns_error() {
1192 let mut svc = new_test_svc(config_size(2));
1193 let msg = Message {
1194 headers: Default::default(),
1195 body: Body::Text("no key".into()),
1196 };
1197 let ex = Exchange::new(msg);
1198 let result = svc.ready().await.unwrap().call(ex).await;
1199 assert!(result.is_err());
1200 assert!(matches!(
1201 result.unwrap_err(),
1202 camel_api::CamelError::ProcessorError(_)
1203 ));
1204 }
1205
1206 #[tokio::test]
1207 async fn test_cloned_service_shares_state() {
1208 let svc1 = new_test_svc(config_size(2));
1209 let mut svc2 = svc1.clone();
1210 svc1.clone()
1212 .ready()
1213 .await
1214 .unwrap()
1215 .call(make_exchange("orderId", "A", "from-svc1"))
1216 .await
1217 .unwrap();
1218 let result = svc2
1220 .ready()
1221 .await
1222 .unwrap()
1223 .call(make_exchange("orderId", "A", "from-svc2"))
1224 .await
1225 .unwrap();
1226 assert!(result.property(CAMEL_AGGREGATOR_PENDING).is_none());
1227 }
1228
1229 #[tokio::test]
1230 async fn test_camel_aggregated_key_property_set() {
1231 let mut svc = new_test_svc(config_size(1));
1232 let ex = make_exchange("orderId", "ORDER-42", "body");
1233 let result = svc.ready().await.unwrap().call(ex).await.unwrap();
1234 assert_eq!(
1235 result.property(CAMEL_AGGREGATED_KEY),
1236 Some(&serde_json::json!("ORDER-42"))
1237 );
1238 }
1239
1240 #[tokio::test]
1241 async fn test_aggregator_enforces_max_buckets() {
1242 let config = AggregatorConfig::correlate_by("orderId")
1243 .complete_when_size(2)
1244 .max_buckets(3)
1245 .build()
1246 .unwrap();
1247
1248 let mut svc = new_test_svc(config);
1249
1250 for i in 0..3 {
1252 let ex = make_exchange("orderId", &format!("key-{}", i), "body");
1253 let _ = svc.ready().await.unwrap().call(ex).await.unwrap();
1254 }
1255
1256 let ex = make_exchange("orderId", "key-4", "body");
1258 let result = svc.ready().await.unwrap().call(ex).await;
1259
1260 assert!(result.is_err(), "Should reject when max buckets reached");
1261 let err = result.unwrap_err().to_string();
1262 assert!(
1263 err.contains("maximum"),
1264 "Error message should contain 'maximum': {}",
1265 err
1266 );
1267 }
1268
1269 #[tokio::test]
1270 async fn test_max_buckets_allows_existing_key() {
1271 let config = AggregatorConfig::correlate_by("orderId")
1272 .complete_when_size(5) .max_buckets(2)
1274 .build()
1275 .unwrap();
1276
1277 let mut svc = new_test_svc(config);
1278
1279 let ex1 = make_exchange("orderId", "key-A", "body1");
1281 let _ = svc.ready().await.unwrap().call(ex1).await.unwrap();
1282 let ex2 = make_exchange("orderId", "key-B", "body2");
1283 let _ = svc.ready().await.unwrap().call(ex2).await.unwrap();
1284
1285 let ex3 = make_exchange("orderId", "key-A", "body3");
1287 let result = svc.ready().await.unwrap().call(ex3).await;
1288 assert!(
1289 result.is_ok(),
1290 "Should allow adding to existing bucket even at max limit"
1291 );
1292 }
1293
1294 #[tokio::test]
1298 async fn test_aggregator_enforces_max_bucket_size() {
1299 let config = AggregatorConfig::correlate_by("orderId")
1300 .complete_when(|_| false)
1302 .max_bucket_size(3)
1303 .build()
1304 .unwrap();
1305
1306 let mut svc = new_test_svc(config);
1307
1308 for _ in 0..3 {
1309 let ex = make_exchange("orderId", "hot-key", "body");
1310 let r = svc.ready().await.unwrap().call(ex).await;
1311 assert!(r.is_ok(), "first 3 exchanges accepted: {r:?}");
1312 }
1313
1314 let ex = make_exchange("orderId", "hot-key", "body");
1315 let result = svc.ready().await.unwrap().call(ex).await;
1316 assert!(result.is_err(), "4th exchange on hot key must be rejected");
1317 let err = result.unwrap_err().to_string();
1318 assert!(
1319 err.contains("maximum"),
1320 "error should mention the per-bucket maximum: {err}"
1321 );
1322
1323 let ex = make_exchange("orderId", "other-key", "body");
1325 let result = svc.ready().await.unwrap().call(ex).await;
1326 assert!(result.is_ok(), "other keys still accepted: {result:?}");
1327 }
1328
1329 #[tokio::test]
1330 async fn test_bucket_ttl_eviction() {
1331 let config = AggregatorConfig::correlate_by("orderId")
1332 .complete_when_size(10) .bucket_ttl(Duration::from_millis(50))
1334 .build()
1335 .unwrap();
1336
1337 let mut svc = new_test_svc(config);
1338
1339 let ex1 = make_exchange("orderId", "key-A", "body1");
1341 let _ = svc.ready().await.unwrap().call(ex1).await.unwrap();
1342
1343 tokio::time::sleep(Duration::from_millis(100)).await;
1345
1346 let ex2 = make_exchange("orderId", "key-B", "body2");
1348 let _ = svc.ready().await.unwrap().call(ex2).await.unwrap();
1349
1350 let ex3 = make_exchange("orderId", "key-A", "body3");
1353 let result = svc.ready().await.unwrap().call(ex3).await;
1354 assert!(result.is_ok(), "Should be able to recreate evicted bucket");
1355 }
1356
1357 #[tokio::test(start_paused = true)]
1358 async fn test_timeout_completes_bucket() {
1359 let config = AggregatorConfig::correlate_by("key")
1360 .complete_on_timeout(Duration::from_millis(100))
1361 .build()
1362 .unwrap();
1363 let mut svc = new_test_svc(config);
1364 let ex = make_exchange("key", "A", "data");
1365 let result = svc.ready().await.unwrap().call(ex).await.unwrap();
1366 assert!(result.property(CAMEL_AGGREGATOR_PENDING).is_some());
1367
1368 tokio::time::sleep(Duration::from_millis(200)).await;
1369
1370 assert_eq!(
1371 svc.buckets.lock().unwrap().len(),
1372 0,
1373 "bucket should be removed after timeout"
1374 );
1375 }
1376
1377 #[tokio::test(start_paused = true)]
1378 async fn test_timeout_resets_on_new_exchange() {
1379 let config = AggregatorConfig::correlate_by("key")
1380 .complete_on_timeout(Duration::from_millis(150))
1381 .build()
1382 .unwrap();
1383 let mut svc = new_test_svc(config);
1384
1385 let ex1 = make_exchange("key", "A", "first");
1386 let _ = svc.ready().await.unwrap().call(ex1).await.unwrap();
1387
1388 tokio::time::sleep(Duration::from_millis(100)).await;
1389
1390 let ex2 = make_exchange("key", "A", "second");
1391 let _ = svc.ready().await.unwrap().call(ex2).await.unwrap();
1392
1393 tokio::time::sleep(Duration::from_millis(100)).await;
1394
1395 assert_eq!(
1396 svc.buckets.lock().unwrap().len(),
1397 1,
1398 "bucket should still exist — timeout was reset"
1399 );
1400
1401 tokio::time::sleep(Duration::from_millis(100)).await;
1402
1403 assert_eq!(
1404 svc.buckets.lock().unwrap().len(),
1405 0,
1406 "bucket should be gone after timeout fires"
1407 );
1408 }
1409
1410 #[tokio::test]
1411 async fn test_composable_size_and_timeout() {
1412 let config = AggregatorConfig::correlate_by("key")
1413 .complete_on_size_or_timeout(2, Duration::from_millis(200))
1414 .build()
1415 .unwrap();
1416 let mut svc = new_test_svc(config);
1417
1418 let ex1 = make_exchange("key", "A", "first");
1419 let _ = svc.ready().await.unwrap().call(ex1).await.unwrap();
1420 assert!(svc.buckets.lock().unwrap().contains_key("\"A\""));
1421
1422 let ex2 = make_exchange("key", "A", "second");
1423 let result = svc.ready().await.unwrap().call(ex2).await.unwrap();
1424 assert!(result.property(CAMEL_AGGREGATOR_PENDING).is_none());
1425 assert_eq!(
1426 result.property(CAMEL_AGGREGATED_COMPLETION_REASON),
1427 Some(&serde_json::json!("size"))
1428 );
1429 }
1430
1431 #[tokio::test(start_paused = true)]
1432 async fn test_discard_on_timeout() {
1433 let config = AggregatorConfig::correlate_by("key")
1434 .complete_on_timeout(Duration::from_millis(50))
1435 .discard_on_timeout(true)
1436 .build()
1437 .unwrap();
1438 let (tx, mut rx) = mpsc::channel(256);
1439 let registry: SharedLanguageRegistry = Arc::new(std::sync::Mutex::new(HashMap::new()));
1440 let cancel = CancellationToken::new();
1441 let mut svc = AggregatorService::new(config, tx, registry, cancel);
1442
1443 let ex = make_exchange("key", "A", "data");
1444 let _ = svc.ready().await.unwrap().call(ex).await.unwrap();
1445
1446 tokio::time::sleep(Duration::from_millis(100)).await;
1447
1448 assert!(
1449 rx.try_recv().is_err(),
1450 "no emit expected with discard_on_timeout"
1451 );
1452 assert_eq!(svc.buckets.lock().unwrap().len(), 0);
1453 assert!(
1454 svc.timeout_tasks.lock().unwrap().is_empty(),
1455 "timeout task should be cleaned up"
1456 );
1457 }
1458
1459 #[tokio::test]
1460 async fn test_force_completion_on_stop() {
1461 let config = AggregatorConfig::correlate_by("key")
1462 .complete_when_size(10)
1463 .force_completion_on_stop(true)
1464 .build()
1465 .unwrap();
1466 let (tx, mut rx) = mpsc::channel(256);
1467 let registry: SharedLanguageRegistry = Arc::new(std::sync::Mutex::new(HashMap::new()));
1468 let cancel = CancellationToken::new();
1469 let svc = AggregatorService::new(config, tx, registry, cancel);
1470
1471 let mut call_svc = svc.clone();
1472 let ex = make_exchange("key", "A", "data");
1473 let _ = call_svc.ready().await.unwrap().call(ex).await.unwrap();
1474
1475 svc.force_complete_all();
1476
1477 let result = rx.try_recv().expect("should emit on force-complete");
1478 assert!(
1479 result.input.body.as_text().is_some() || matches!(result.input.body, Body::Json(_))
1480 );
1481 assert_eq!(
1482 result.property(CAMEL_AGGREGATED_COMPLETION_REASON),
1483 Some(&serde_json::json!("stop"))
1484 );
1485 }
1486
1487 #[tokio::test]
1488 async fn test_completion_reason_property_size() {
1489 let config = AggregatorConfig::correlate_by("key")
1490 .complete_when_size(1)
1491 .build()
1492 .unwrap();
1493 let mut svc = new_test_svc(config);
1494 let ex = make_exchange("key", "X", "body");
1495 let result = svc.ready().await.unwrap().call(ex).await.unwrap();
1496 assert_eq!(
1497 result.property(CAMEL_AGGREGATED_COMPLETION_REASON),
1498 Some(&serde_json::json!("size"))
1499 );
1500 }
1501
1502 #[tokio::test]
1503 async fn test_completion_reason_property_predicate() {
1504 let config = AggregatorConfig::correlate_by("key")
1505 .complete_when(|_| true)
1506 .build()
1507 .unwrap();
1508 let mut svc = new_test_svc(config);
1509 let ex = make_exchange("key", "X", "body");
1510 let result = svc.ready().await.unwrap().call(ex).await.unwrap();
1511 assert_eq!(
1512 result.property(CAMEL_AGGREGATED_COMPLETION_REASON),
1513 Some(&serde_json::json!("predicate"))
1514 );
1515 }
1516
1517 #[tokio::test(start_paused = true)]
1518 async fn test_size_completes_before_timeout() {
1519 let config = AggregatorConfig::correlate_by("key")
1520 .complete_on_size_or_timeout(2, Duration::from_millis(200))
1521 .build()
1522 .unwrap();
1523 let mut svc = new_test_svc(config);
1524
1525 let ex1 = make_exchange("key", "A", "first");
1526 let _ = svc.ready().await.unwrap().call(ex1).await.unwrap();
1527
1528 let ex2 = make_exchange("key", "A", "second");
1529 let result = svc.ready().await.unwrap().call(ex2).await.unwrap();
1530
1531 assert!(result.property(CAMEL_AGGREGATOR_PENDING).is_none());
1532 assert_eq!(
1533 result.property(CAMEL_AGGREGATED_COMPLETION_REASON),
1534 Some(&serde_json::json!("size"))
1535 );
1536 assert_eq!(svc.buckets.lock().unwrap().len(), 0);
1537
1538 tokio::time::sleep(Duration::from_millis(300)).await;
1539 assert_eq!(
1540 svc.buckets.lock().unwrap().len(),
1541 0,
1542 "no re-fire after timeout"
1543 );
1544 }
1545
1546 #[tokio::test(start_paused = true)]
1547 async fn test_concurrent_timeout_fire_and_new_exchange() {
1548 let config = AggregatorConfig::correlate_by("key")
1549 .complete_on_size_or_timeout(2, Duration::from_millis(100))
1550 .build()
1551 .unwrap();
1552 let (tx, mut rx) = mpsc::channel(256);
1553 let registry: SharedLanguageRegistry = Arc::new(std::sync::Mutex::new(HashMap::new()));
1554 let cancel = CancellationToken::new();
1555 let mut svc = AggregatorService::new(config, tx, registry, cancel);
1556
1557 let ex = make_exchange("key", "A", "data");
1558 let _ = svc.ready().await.unwrap().call(ex).await.unwrap();
1559
1560 tokio::time::sleep(Duration::from_millis(150)).await;
1562
1563 let ex2 = make_exchange("key", "A", "data2");
1565 let result = svc.ready().await.unwrap().call(ex2).await.unwrap();
1566 assert!(
1567 result.property(CAMEL_AGGREGATOR_PENDING).is_some(),
1568 "should be pending in new bucket"
1569 );
1570
1571 let mut late_count = 0;
1573 while rx.try_recv().is_ok() {
1574 late_count += 1;
1575 }
1576 assert_eq!(
1577 late_count, 1,
1578 "exactly 1 late emit from the timed-out bucket"
1579 );
1580 }
1581
1582 #[tokio::test(start_paused = true)]
1583 async fn test_late_channel_full_drops_with_warning() {
1584 let config = AggregatorConfig::correlate_by("key")
1585 .complete_on_timeout(Duration::from_millis(50))
1586 .build()
1587 .unwrap();
1588 let (tx, mut rx) = mpsc::channel(1);
1589 rx.close();
1590 let registry: SharedLanguageRegistry = Arc::new(std::sync::Mutex::new(HashMap::new()));
1591 let cancel = CancellationToken::new();
1592 let mut svc = AggregatorService::new(config, tx, registry, cancel);
1593
1594 let ex = make_exchange("key", "A", "data");
1595 let _ = svc.ready().await.unwrap().call(ex).await.unwrap();
1596
1597 tokio::time::sleep(Duration::from_millis(100)).await;
1598 assert_eq!(
1599 svc.buckets.lock().unwrap().len(),
1600 0,
1601 "bucket removed despite channel closed"
1602 );
1603 }
1604
1605 #[tokio::test]
1611 async fn test_da3_force_complete_all_drops_on_saturated_channel() {
1612 let config = AggregatorConfig::correlate_by("k")
1613 .complete_when_size(10)
1614 .force_completion_on_stop(true)
1615 .build()
1616 .unwrap();
1617 let (late_tx, mut late_rx) = mpsc::channel::<Exchange>(1);
1619 late_tx
1623 .try_send(make_exchange("k", "99", "dummy"))
1624 .expect("pre-fill succeeds");
1625
1626 let registry: SharedLanguageRegistry = Arc::new(std::sync::Mutex::new(HashMap::new()));
1627 let cancel = CancellationToken::new();
1628 let mut svc = AggregatorService::new(config, late_tx, registry, cancel);
1629
1630 for v in ["1", "2", "3"] {
1633 let ex = make_exchange("k", v, "body");
1634 let _ = svc.ready().await.unwrap().call(ex).await.unwrap();
1635 }
1636 assert_eq!(svc.buckets.lock().unwrap().len(), 3);
1637
1638 svc.force_complete_all();
1640
1641 let pre_fill = late_rx
1643 .try_recv()
1644 .expect("pre-fill should still be in channel");
1645 assert_eq!(
1646 pre_fill.input.headers.get("k"),
1647 Some(&serde_json::json!("99"))
1648 );
1649
1650 assert!(matches!(
1653 late_rx.try_recv(),
1654 Err(mpsc::error::TryRecvError::Empty)
1655 ));
1656
1657 assert!(svc.buckets.lock().unwrap().is_empty());
1659 }
1660
1661 #[tokio::test]
1662 async fn test_aggregate_stream_bodies_creates_valid_json() {
1663 use bytes::Bytes;
1664 use camel_api::{Body, StreamBody, StreamMetadata};
1665 use futures::stream;
1666 use tokio::sync::Mutex;
1667
1668 let chunks = vec![Ok(Bytes::from("test"))];
1669 let stream_body = StreamBody {
1670 stream: Arc::new(Mutex::new(Some(Box::pin(stream::iter(chunks))))),
1671 metadata: StreamMetadata {
1672 origin: Some("file:///test.txt".to_string()),
1673 ..Default::default()
1674 },
1675 };
1676
1677 let ex1 = Exchange::new(Message {
1678 headers: Default::default(),
1679 body: Body::Stream(stream_body),
1680 });
1681
1682 let exchanges = vec![ex1];
1683 let result = aggregate(exchanges, &AggregationStrategy::CollectAll);
1684
1685 let exchange = result.expect("Expected Ok result");
1686 assert!(
1687 matches!(exchange.input.body, Body::Json(_)),
1688 "Expected Json body"
1689 );
1690
1691 if let Body::Json(value) = exchange.input.body {
1692 let json_str = serde_json::to_string(&value).unwrap();
1693 let parsed: serde_json::Value = serde_json::from_str(&json_str).unwrap();
1694
1695 assert!(parsed.is_array(), "Result should be an array");
1696 let arr = parsed.as_array().unwrap();
1697 assert!(arr[0].is_object(), "First element should be an object");
1698 assert!(
1699 arr[0]["_stream"].is_object(),
1700 "Should contain _stream object"
1701 );
1702 assert_eq!(arr[0]["_stream"]["origin"], "file:///test.txt");
1703 assert_eq!(
1704 arr[0]["_stream"]["placeholder"], true,
1705 "placeholder flag should be true"
1706 );
1707 }
1708 }
1709
1710 #[tokio::test]
1711 async fn test_aggregate_stream_bodies_with_none_origin() {
1712 use bytes::Bytes;
1713 use camel_api::{Body, StreamBody, StreamMetadata};
1714 use futures::stream;
1715 use tokio::sync::Mutex;
1716
1717 let chunks = vec![Ok(Bytes::from("test"))];
1718 let stream_body = StreamBody {
1719 stream: Arc::new(Mutex::new(Some(Box::pin(stream::iter(chunks))))),
1720 metadata: StreamMetadata {
1721 origin: None,
1722 ..Default::default()
1723 },
1724 };
1725
1726 let ex1 = Exchange::new(Message {
1727 headers: Default::default(),
1728 body: Body::Stream(stream_body),
1729 });
1730
1731 let exchanges = vec![ex1];
1732 let result = aggregate(exchanges, &AggregationStrategy::CollectAll);
1733
1734 let exchange = result.expect("Expected Ok result");
1735 assert!(
1736 matches!(exchange.input.body, Body::Json(_)),
1737 "Expected Json body"
1738 );
1739
1740 if let Body::Json(value) = exchange.input.body {
1741 let json_str = serde_json::to_string(&value).unwrap();
1742 let parsed: serde_json::Value = serde_json::from_str(&json_str).unwrap();
1743
1744 assert!(parsed.is_array(), "Result should be an array");
1745 let arr = parsed.as_array().unwrap();
1746 assert!(arr[0].is_object(), "First element should be an object");
1747 assert!(
1748 arr[0]["_stream"].is_object(),
1749 "Should contain _stream object"
1750 );
1751 assert_eq!(
1752 arr[0]["_stream"]["origin"],
1753 serde_json::Value::Null,
1754 "origin should be null when None"
1755 );
1756 assert_eq!(
1757 arr[0]["_stream"]["placeholder"], true,
1758 "placeholder flag should be true"
1759 );
1760 }
1761 }
1762
1763 #[tokio::test]
1764 async fn timeout_completion_clears_handle_from_map() {
1765 let config = AggregatorConfig::correlate_by("key")
1770 .complete_on_timeout(Duration::from_millis(50))
1771 .build()
1772 .unwrap();
1773 let (tx, _rx) = mpsc::channel(256);
1774 let registry: SharedLanguageRegistry = Arc::new(std::sync::Mutex::new(HashMap::new()));
1775 let cancel = CancellationToken::new();
1776 let svc = AggregatorService::new(config, tx, registry, cancel);
1777
1778 let mut call_svc = svc.clone();
1780 let ex = make_exchange("key", "A", "data");
1781 let _ = call_svc.ready().await.unwrap().call(ex).await.unwrap();
1782 assert!(
1783 !svc.timeout_handles.lock().unwrap().is_empty(),
1784 "handle should exist while timeout pending"
1785 );
1786
1787 tokio::time::sleep(Duration::from_millis(200)).await;
1789
1790 assert!(
1791 svc.timeout_handles.lock().unwrap().is_empty(),
1792 "handle should be cleared from map after natural timeout completion (was leak)"
1793 );
1794 }
1795
1796 #[tokio::test]
1797 async fn aggregator_shutdown_via_trait_dispatch() {
1798 let config = AggregatorConfig::correlate_by("key")
1801 .complete_when_size(10)
1802 .build()
1803 .unwrap();
1804 let (tx, _rx) = mpsc::channel(256);
1805 let registry: SharedLanguageRegistry = Arc::new(std::sync::Mutex::new(HashMap::new()));
1806 let cancel = CancellationToken::new();
1807 let svc = AggregatorService::new(config, tx, registry, cancel);
1808
1809 let step: Arc<dyn StepLifecycle> = Arc::new(svc);
1810 step.shutdown(StepShutdownReason::RouteStop)
1811 .await
1812 .expect("first shutdown should succeed");
1813 step.shutdown(StepShutdownReason::RouteStop)
1814 .await
1815 .expect("second shutdown (idempotent) should succeed");
1816 }
1817
1818 #[tokio::test(start_paused = true)]
1819 async fn test_shutdown_awaits_timeout_handles() {
1820 let config = AggregatorConfig::correlate_by("key")
1821 .complete_on_timeout(Duration::from_millis(100))
1822 .build()
1823 .unwrap();
1824 let (tx, _rx) = mpsc::channel(256);
1825 let registry: SharedLanguageRegistry = Arc::new(std::sync::Mutex::new(HashMap::new()));
1826 let cancel = CancellationToken::new();
1827 let svc = AggregatorService::new(config, tx, registry, cancel);
1828
1829 let mut call_svc = svc.clone();
1831 let ex = make_exchange("key", "A", "data");
1832 let _ = call_svc.ready().await.unwrap().call(ex).await.unwrap();
1833
1834 assert!(
1836 !svc.timeout_handles.lock().unwrap().is_empty(),
1837 "should have a timeout handle"
1838 );
1839
1840 svc.shutdown_inner().await;
1843
1844 assert!(
1845 svc.timeout_handles.lock().unwrap().is_empty(),
1846 "all handles should be cleaned up after shutdown"
1847 );
1848 }
1849
1850 #[tokio::test]
1857 async fn test_unique_key_flood_stays_bounded_by_default() {
1858 let config = AggregatorConfig::correlate_by("orderId")
1860 .complete_when_size(1_000_000) .build()
1862 .unwrap();
1863 let mut svc = new_test_svc(config);
1864
1865 for i in 0..10_000usize {
1868 let ex = make_exchange("orderId", &format!("key-{i}"), "body");
1869 let result = svc.ready().await.unwrap().call(ex).await;
1870 assert!(result.is_ok(), "key {i} should be accepted under the cap");
1871 }
1872 let ex = make_exchange("orderId", "key-10001", "body");
1873 let result = svc.ready().await.unwrap().call(ex).await;
1874 assert!(
1875 result.is_err(),
1876 "10_001st unique key must be rejected by the max_buckets cap"
1877 );
1878 let err = result.unwrap_err().to_string();
1879 assert!(
1880 err.contains("maximum") || err.contains("max"),
1881 "error should mention cap: {err}"
1882 );
1883 }
1884
1885 #[tokio::test]
1891 async fn test_background_sweep_spawns_on_first_poll_not_construction() {
1892 let config = AggregatorConfig::correlate_by("key")
1893 .complete_when_size(10_000)
1894 .bucket_ttl(Duration::from_millis(50))
1895 .build()
1896 .unwrap();
1897 let cancel = CancellationToken::new();
1898 let (tx, _rx) = mpsc::channel(8);
1899 let registry: SharedLanguageRegistry = Arc::new(std::sync::Mutex::new(HashMap::new()));
1900 let mut svc = AggregatorService::new(config, tx, registry, cancel.clone());
1901
1902 assert!(
1904 svc.sweep_handle
1905 .lock()
1906 .unwrap_or_else(|e| e.into_inner())
1907 .is_none(),
1908 "sweep must NOT be spawned at construction (runtime-free new)"
1909 );
1910
1911 let _ = svc.ready().await.unwrap();
1914 let sweep_present = svc
1915 .sweep_handle
1916 .lock()
1917 .unwrap_or_else(|e| e.into_inner())
1918 .is_some();
1919 assert!(
1920 sweep_present,
1921 "sweep handle should be Some after first poll when bucket_ttl is set"
1922 );
1923
1924 cancel.cancel();
1926 tokio::time::sleep(Duration::from_millis(50)).await;
1928 }
1929
1930 struct QueueDepthRecorder(Mutex<Vec<(String, usize)>>);
1933 impl MetricsCollector for QueueDepthRecorder {
1934 fn record_exchange_duration(&self, _: &str, _: std::time::Duration) {}
1935 fn increment_errors(&self, _: &str, _: &str) {}
1936 fn increment_exchanges(&self, _: &str) {}
1937 fn set_queue_depth(&self, queue: &str, depth: usize) {
1938 self.0
1939 .lock()
1940 .unwrap_or_else(|e| e.into_inner())
1941 .push((queue.to_string(), depth));
1942 }
1943 fn record_circuit_breaker_change(&self, _: &str, _: &str, _: &str) {}
1944 }
1945
1946 async fn await_depth_sample(
1949 recorder: &QueueDepthRecorder,
1950 label: &str,
1951 pred: impl Fn(usize) -> bool,
1952 ) {
1953 let deadline = std::time::Instant::now() + Duration::from_secs(2);
1954 loop {
1955 let matched = recorder
1956 .0
1957 .lock()
1958 .unwrap_or_else(|e| e.into_inner())
1959 .iter()
1960 .any(|(q, d)| q == label && pred(*d));
1961 if matched {
1962 return;
1963 }
1964 assert!(
1965 std::time::Instant::now() < deadline,
1966 "no queue-depth sample for '{label}' matched within 2s"
1967 );
1968 tokio::time::sleep(Duration::from_millis(20)).await;
1969 }
1970 }
1971
1972 #[tokio::test]
1977 async fn test_sweep_reports_queue_depth() {
1978 let config = AggregatorConfig::correlate_by("orderId")
1979 .complete_when_size(3)
1980 .bucket_ttl(Duration::from_millis(100))
1984 .build()
1985 .unwrap();
1986 let cancel = CancellationToken::new();
1987 let (tx, _rx) = mpsc::channel(8);
1988 let registry: SharedLanguageRegistry = Arc::new(std::sync::Mutex::new(HashMap::new()));
1989 let recorder = Arc::new(QueueDepthRecorder(Mutex::new(Vec::new())));
1990 let mut svc = AggregatorService::new(config, tx, registry, cancel).with_queue_metrics(
1991 Arc::clone(&recorder) as Arc<dyn MetricsCollector>,
1992 "aggregator:t",
1993 );
1994
1995 let _ = svc.ready().await.unwrap();
1997
1998 let ex = make_exchange("orderId", "g1", "partial");
2000 let _ = svc.ready().await.unwrap().call(ex).await;
2001
2002 await_depth_sample(&recorder, "aggregator:t", |d| d > 0).await;
2003
2004 let ex = make_exchange("orderId", "g1", "b");
2006 let _ = svc.ready().await.unwrap().call(ex).await;
2007 let ex = make_exchange("orderId", "g1", "c");
2008 let _ = svc.ready().await.unwrap().call(ex).await;
2009
2010 await_depth_sample(&recorder, "aggregator:t", |d| d == 0).await;
2011
2012 svc.shutdown(StepShutdownReason::RouteStop).await.unwrap();
2013 }
2014
2015 #[tokio::test]
2020 async fn test_no_ttl_aggregator_reports_queue_depth() {
2021 use camel_api::aggregator::CorrelationStrategy;
2022
2023 let config = AggregatorConfig {
2024 header_name: "orderId".into(),
2025 completion: CompletionMode::Single(CompletionCondition::Size(3)),
2026 correlation: CorrelationStrategy::HeaderName("orderId".into()),
2027 strategy: AggregationStrategy::CollectAll,
2028 max_buckets: Some(100),
2029 max_bucket_size: Some(100),
2030 bucket_ttl: None,
2031 force_completion_on_stop: false,
2032 discard_on_timeout: false,
2033 max_timeout_tasks: 64,
2034 };
2035 let cancel = CancellationToken::new();
2036 let (tx, _rx) = mpsc::channel(8);
2037 let registry: SharedLanguageRegistry = Arc::new(std::sync::Mutex::new(HashMap::new()));
2038 let recorder = Arc::new(QueueDepthRecorder(Mutex::new(Vec::new())));
2039 let mut svc = AggregatorService::new(config, tx, registry, cancel).with_queue_metrics(
2040 Arc::clone(&recorder) as Arc<dyn MetricsCollector>,
2041 "aggregator:nottl",
2042 );
2043
2044 let _ = svc.ready().await.unwrap();
2045 assert!(
2046 svc.sweep_handle
2047 .lock()
2048 .unwrap_or_else(|e| e.into_inner())
2049 .is_some(),
2050 "metrics-only config (bucket_ttl = None) must still spawn the sweep"
2051 );
2052
2053 let ex = make_exchange("orderId", "g1", "partial");
2055 let _ = svc.ready().await.unwrap().call(ex).await;
2056 await_depth_sample(&recorder, "aggregator:nottl", |d| d > 0).await;
2057
2058 let ex = make_exchange("orderId", "g1", "b");
2060 let _ = svc.ready().await.unwrap().call(ex).await;
2061 let ex = make_exchange("orderId", "g1", "c");
2062 let _ = svc.ready().await.unwrap().call(ex).await;
2063 await_depth_sample(&recorder, "aggregator:nottl", |d| d == 0).await;
2064
2065 svc.shutdown(StepShutdownReason::RouteStop).await.unwrap();
2066 }
2067
2068 #[tokio::test]
2073 async fn test_transient_clone_drop_keeps_sweep_sampling() {
2074 let config = AggregatorConfig::correlate_by("orderId")
2075 .complete_when_size(3)
2076 .bucket_ttl(Duration::from_millis(100))
2077 .build()
2078 .unwrap();
2079 let cancel = CancellationToken::new();
2080 let (tx, _rx) = mpsc::channel(8);
2081 let registry: SharedLanguageRegistry = Arc::new(std::sync::Mutex::new(HashMap::new()));
2082 let recorder = Arc::new(QueueDepthRecorder(Mutex::new(Vec::new())));
2083 let mut svc = AggregatorService::new(config, tx, registry, cancel).with_queue_metrics(
2084 Arc::clone(&recorder) as Arc<dyn MetricsCollector>,
2085 "aggregator:clone",
2086 );
2087
2088 let _ = svc.ready().await.unwrap();
2089
2090 drop(svc.clone());
2092 assert!(
2093 svc.sweep_handle
2094 .lock()
2095 .unwrap_or_else(|e| e.into_inner())
2096 .is_some(),
2097 "transient clone drop must not abort the shared sweep"
2098 );
2099
2100 let ex = make_exchange("orderId", "g1", "partial");
2102 let _ = svc.ready().await.unwrap().call(ex).await;
2103 await_depth_sample(&recorder, "aggregator:clone", |d| d > 0).await;
2104
2105 svc.shutdown(StepShutdownReason::RouteStop).await.unwrap();
2106 }
2107
2108 #[tokio::test]
2111 async fn test_aggregator_timeout_task_cap_no_panic_under_flood() {
2112 use camel_api::aggregator::CorrelationStrategy;
2115
2116 let config = AggregatorConfig {
2117 header_name: "k".into(),
2118 completion: CompletionMode::Any(vec![
2119 CompletionCondition::Size(999),
2120 CompletionCondition::Timeout(Duration::from_secs(30)),
2121 ]),
2122 correlation: CorrelationStrategy::HeaderName("k".into()),
2123 strategy: AggregationStrategy::CollectAll,
2124 max_buckets: Some(50),
2125 max_bucket_size: Some(50),
2126 bucket_ttl: Some(Duration::from_secs(30)),
2127 force_completion_on_stop: false,
2128 discard_on_timeout: false,
2129 max_timeout_tasks: 2,
2130 };
2131 let (late_tx, mut late_rx) = mpsc::channel(64);
2132 let registry: SharedLanguageRegistry = Arc::new(std::sync::Mutex::new(HashMap::new()));
2133 let cancel = CancellationToken::new();
2134 let svc = AggregatorService::new(config, late_tx, registry, cancel);
2135
2136 for i in 0..20u64 {
2138 let mut ex = Exchange::new(Message {
2139 headers: HashMap::from([("k".to_string(), serde_json::json!(i))]),
2140 body: Body::Text(i.to_string()),
2141 });
2142 ex.input
2143 .headers
2144 .insert("k".to_string(), serde_json::json!(i));
2145 let outcome = tokio::time::timeout(Duration::from_secs(2), async {
2146 let mut s = svc.clone();
2147 use tower::ServiceExt;
2148 s.ready().await.unwrap().call(ex).await
2149 })
2150 .await;
2151 assert!(outcome.is_ok(), "call {} hung/panicked under task cap", i);
2152 let res = outcome.unwrap().unwrap();
2154 assert_eq!(
2155 res.properties
2156 .get(CAMEL_AGGREGATOR_PENDING)
2157 .and_then(|v| v.as_bool()),
2158 Some(true),
2159 "exchange {} should be pending",
2160 i
2161 );
2162 }
2163 let _ = late_rx.try_recv();
2165 }
2166
2167 #[tokio::test]
2175 async fn test_release_unarmed_buckets_discards_cap_exceeded_keeps_armed() {
2176 let config = AggregatorConfig::correlate_by("k")
2177 .complete_on_timeout(Duration::from_millis(400))
2178 .max_timeout_tasks(1)
2179 .build()
2180 .unwrap();
2181 let (late_tx, mut late_rx) = mpsc::channel(8);
2182 let registry: SharedLanguageRegistry = Arc::new(std::sync::Mutex::new(HashMap::new()));
2183 let cancel = CancellationToken::new();
2184 let svc = AggregatorService::new(config, late_tx, registry, cancel);
2185
2186 let a = make_exchange("k", "a", "body-a");
2189 let mut sa = svc.clone();
2190 let _ = sa.ready().await.unwrap().call(a).await.unwrap();
2191 let b = make_exchange("k", "b", "body-b");
2192 let mut sb = svc.clone();
2193 let _ = sb.ready().await.unwrap().call(b).await.unwrap();
2194
2195 {
2196 let armed = svc.timeout_tasks.lock().unwrap_or_else(|e| e.into_inner());
2197 assert_eq!(armed.len(), 1, "cap=1 must arm exactly one timeout task");
2198 assert!(
2199 armed.contains_key("\"a\""),
2200 "first key must hold the armed slot, got {armed:?}"
2201 );
2202 }
2203
2204 svc.release_unarmed_buckets();
2206 {
2207 let buckets = svc.buckets.lock().unwrap_or_else(|e| e.into_inner());
2208 assert!(
2209 buckets.contains_key("\"a\""),
2210 "armed bucket must survive the release"
2211 );
2212 assert!(
2213 !buckets.contains_key("\"b\""),
2214 "cap-exceeded (unarmed) bucket must be released, not orphaned"
2215 );
2216 }
2217
2218 let emitted = tokio::time::timeout(Duration::from_secs(2), late_rx.recv())
2221 .await
2222 .expect("armed bucket must emit on its timeout")
2223 .expect("late channel must stay open");
2224 assert_eq!(
2225 emitted.properties.get(CAMEL_AGGREGATED_COMPLETION_REASON),
2226 Some(&serde_json::json!("timeout"))
2227 );
2228
2229 assert!(
2233 tokio::time::timeout(Duration::from_millis(600), late_rx.recv())
2234 .await
2235 .is_err(),
2236 "released unarmed bucket must not emit"
2237 );
2238
2239 svc.shutdown(StepShutdownReason::RouteStop).await.unwrap();
2240 }
2241
2242 #[tokio::test]
2243 async fn evaluate_completion_predicate_or_combines_any() {
2244 use camel_api::aggregator::{CompletionCondition, CompletionMode};
2245 use camel_language_api::Language;
2246 use std::collections::HashMap;
2247
2248 let registry: SharedLanguageRegistry = Arc::new(std::sync::Mutex::new(HashMap::new()));
2250 registry.lock().unwrap().insert(
2251 "simple".to_string(),
2252 Arc::new(camel_language_simple::SimpleLanguage::new()) as Arc<dyn Language>,
2253 );
2254
2255 let incoming = make_exchange("k", "X", "hello");
2256
2257 let mode = CompletionMode::Any(vec![
2259 CompletionCondition::PredicateExpr {
2260 expr: "${body} == 'NOPE'".to_string(),
2261 language: "simple".to_string(),
2262 },
2263 CompletionCondition::PredicateExpr {
2264 expr: "${body} == 'hello'".to_string(),
2265 language: "simple".to_string(),
2266 },
2267 ]);
2268
2269 let satisfied = evaluate_completion_predicate(&mode, &incoming, ®istry)
2270 .await
2271 .expect("eval must succeed");
2272 assert!(satisfied, "second predicate matches → OR true");
2273 }
2274
2275 #[tokio::test]
2276 async fn evaluate_completion_predicate_no_predicate_skips_registry() {
2277 use camel_api::aggregator::{CompletionCondition, CompletionMode};
2278 use std::collections::HashMap;
2279
2280 let mode = CompletionMode::Single(CompletionCondition::Size(3));
2284 let registry: SharedLanguageRegistry = Arc::new(std::sync::Mutex::new(HashMap::new()));
2285 let incoming = make_exchange("k", "X", "hello");
2286 let result = evaluate_completion_predicate(&mode, &incoming, ®istry).await;
2287 assert!(result.is_ok(), "fast path must not error: {:?}", result);
2288 assert!(!result.unwrap(), "no predicate → not satisfied");
2289 }
2290
2291 #[tokio::test]
2292 async fn evaluate_completion_predicate_unregistered_language_errors() {
2293 use camel_api::aggregator::{CompletionCondition, CompletionMode};
2294
2295 let mode = CompletionMode::Single(CompletionCondition::PredicateExpr {
2296 expr: "${body} == 'DONE'".to_string(),
2297 language: "nonexistent-lang".to_string(),
2298 });
2299 let registry: SharedLanguageRegistry = Arc::new(std::sync::Mutex::new(HashMap::new()));
2300 let incoming = make_exchange("k", "X", "hello");
2301 let result = evaluate_completion_predicate(&mode, &incoming, ®istry).await;
2302 assert!(result.is_err(), "unregistered language must error");
2303 }
2304
2305 #[tokio::test]
2306 async fn evaluate_completion_predicate_all_miss_returns_false() {
2307 use camel_api::aggregator::{CompletionCondition, CompletionMode};
2308 use camel_language_api::Language;
2309 use std::collections::HashMap;
2310
2311 let registry: SharedLanguageRegistry = Arc::new(std::sync::Mutex::new(HashMap::new()));
2312 registry.lock().unwrap().insert(
2313 "simple".to_string(),
2314 Arc::new(camel_language_simple::SimpleLanguage::new()) as Arc<dyn Language>,
2315 );
2316
2317 let mode = CompletionMode::Single(CompletionCondition::PredicateExpr {
2318 expr: "${body} == 'NOPE'".to_string(),
2319 language: "simple".to_string(),
2320 });
2321 let incoming = make_exchange("k", "X", "hello");
2322 let result = evaluate_completion_predicate(&mode, &incoming, ®istry).await;
2323 assert!(result.is_ok(), "eval must succeed: {:?}", result);
2324 assert!(!result.unwrap(), "predicate does not match");
2325 }
2326
2327 #[tokio::test]
2328 async fn completion_predicate_expr_completes_on_match() {
2329 use camel_api::aggregator::{CompletionCondition, CompletionMode};
2330 use camel_language_api::Language;
2331 use std::collections::HashMap;
2332
2333 let registry: SharedLanguageRegistry = Arc::new(std::sync::Mutex::new(HashMap::new()));
2335 registry.lock().unwrap().insert(
2336 "simple".to_string(),
2337 Arc::new(camel_language_simple::SimpleLanguage::new()) as Arc<dyn Language>,
2338 );
2339
2340 let mut config = AggregatorConfig::correlate_by("key")
2345 .complete_when_size(999) .build()
2347 .expect("config builds");
2348 config.completion = CompletionMode::Single(CompletionCondition::PredicateExpr {
2349 expr: "${body} == 'DONE'".to_string(),
2350 language: "simple".to_string(),
2351 });
2352
2353 let mut svc = new_test_svc_with_registry(config, registry);
2354
2355 let r = svc
2357 .ready()
2358 .await
2359 .unwrap()
2360 .call(make_exchange("key", "K", "first"))
2361 .await
2362 .unwrap();
2363 assert!(r.property(CAMEL_AGGREGATOR_PENDING).is_some());
2364
2365 let r = svc
2367 .ready()
2368 .await
2369 .unwrap()
2370 .call(make_exchange("key", "K", "DONE"))
2371 .await
2372 .unwrap();
2373 assert!(r.property(CAMEL_AGGREGATOR_PENDING).is_none());
2374 assert_eq!(
2375 r.property(CAMEL_AGGREGATED_COMPLETION_REASON),
2376 Some(&serde_json::json!("predicate"))
2377 );
2378 }
2379
2380 #[tokio::test]
2392 async fn test_da1_strategy_receives_two_exchanges_first_message_preserved() {
2393 use camel_api::aggregator::{AggregationFn, AggregationStrategy};
2394 use std::sync::Arc;
2395
2396 let recorded: Arc<std::sync::Mutex<Vec<(String, String)>>> =
2397 Arc::new(std::sync::Mutex::new(Vec::new()));
2398 let recorded_for_closure = Arc::clone(&recorded);
2399
2400 let f: AggregationFn = Arc::new(move |old: Exchange, new: Exchange| {
2401 let old_body = old.input.body.as_text().unwrap_or("").to_string();
2402 let new_body = new.input.body.as_text().unwrap_or("").to_string();
2403 recorded_for_closure
2404 .lock()
2405 .expect("recorded mutex poisoned")
2406 .push((old_body, new_body));
2407 new
2408 });
2409
2410 let config = AggregatorConfig::correlate_by("k")
2411 .complete_when_size(2)
2412 .strategy(AggregationStrategy::Custom(f))
2413 .build()
2414 .unwrap();
2415 let mut svc = new_test_svc(config);
2416
2417 let first = svc
2419 .ready()
2420 .await
2421 .unwrap()
2422 .call(make_exchange("k", "1", "A"))
2423 .await
2424 .unwrap();
2425 assert!(
2426 first.property(CAMEL_AGGREGATOR_PENDING).is_some(),
2427 "first message must leave the bucket pending (size < 2)"
2428 );
2429 assert!(
2430 recorded.lock().expect("recorded mutex poisoned").is_empty(),
2431 "strategy must NOT be invoked on the first message of a bucket"
2432 );
2433
2434 let _completed = svc
2437 .ready()
2438 .await
2439 .unwrap()
2440 .call(make_exchange("k", "1", "B"))
2441 .await
2442 .unwrap();
2443
2444 let recorded = recorded.lock().expect("recorded mutex poisoned");
2445 assert_eq!(
2446 recorded.len(),
2447 1,
2448 "strategy must be invoked exactly once across the two-message bucket, got {recorded:?}"
2449 );
2450 assert_eq!(
2451 recorded[0],
2452 ("A".to_string(), "B".to_string()),
2453 "strategy must observe the first message as `old` and the second as `new`, \
2454 with both bodies preserved unchanged"
2455 );
2456 }
2457
2458 #[tokio::test]
2461 async fn sweep_shutdown_cancels_task() {
2462 let config = AggregatorConfig::correlate_by("key")
2463 .complete_when_size(10)
2464 .bucket_ttl(Duration::from_millis(100))
2465 .build()
2466 .unwrap();
2467 let (tx, _rx) = mpsc::channel(256);
2468 let registry: SharedLanguageRegistry = Arc::new(std::sync::Mutex::new(HashMap::new()));
2469 let cancel = CancellationToken::new();
2470 let mut svc = AggregatorService::new(config, tx, registry, cancel);
2471
2472 let _ = svc.ready().await.unwrap();
2473
2474 assert!(
2475 svc.sweep_handle
2476 .lock()
2477 .unwrap_or_else(|e| e.into_inner())
2478 .is_some(),
2479 "sweep handle should be Some after poll_ready"
2480 );
2481
2482 svc.shutdown(StepShutdownReason::RouteStop).await.unwrap();
2483
2484 assert!(
2485 svc.sweep_handle
2486 .lock()
2487 .unwrap_or_else(|e| e.into_inner())
2488 .is_none(),
2489 "sweep handle should be None after shutdown (taken + aborted)"
2490 );
2491 assert!(
2492 svc.sweep_cancel
2493 .lock()
2494 .unwrap_or_else(|e| e.into_inner())
2495 .is_cancelled(),
2496 "sweep_cancel token should be cancelled after shutdown"
2497 );
2498
2499 tokio::time::sleep(Duration::from_millis(50)).await;
2500 }
2501
2502 #[tokio::test]
2503 async fn sweep_start_respawns_after_shutdown() {
2504 let config = AggregatorConfig::correlate_by("key")
2505 .complete_when_size(10)
2506 .bucket_ttl(Duration::from_millis(100))
2507 .build()
2508 .unwrap();
2509 let (tx, _rx) = mpsc::channel(256);
2510 let registry: SharedLanguageRegistry = Arc::new(std::sync::Mutex::new(HashMap::new()));
2511 let cancel = CancellationToken::new();
2512 let mut svc = AggregatorService::new(config, tx, registry, cancel);
2513
2514 let _ = svc.ready().await.unwrap();
2515 svc.shutdown(StepShutdownReason::RouteStop).await.unwrap();
2516
2517 svc.start().await.unwrap();
2518 let _ = svc.ready().await.unwrap();
2519
2520 assert!(
2521 svc.sweep_handle
2522 .lock()
2523 .unwrap_or_else(|e| e.into_inner())
2524 .is_some(),
2525 "sweep handle should be Some after start + poll_ready"
2526 );
2527 assert!(
2528 !svc.sweep_cancel
2529 .lock()
2530 .unwrap_or_else(|e| e.into_inner())
2531 .is_cancelled(),
2532 "sweep_cancel should be a fresh uncancelled token after start"
2533 );
2534 }
2535
2536 #[tokio::test]
2537 async fn sweep_shutdown_hotswap_cancels_task() {
2538 let config = AggregatorConfig::correlate_by("key")
2539 .complete_when_size(10)
2540 .bucket_ttl(Duration::from_millis(100))
2541 .build()
2542 .unwrap();
2543 let (tx, _rx) = mpsc::channel(256);
2544 let registry: SharedLanguageRegistry = Arc::new(std::sync::Mutex::new(HashMap::new()));
2545 let cancel = CancellationToken::new();
2546 let mut svc = AggregatorService::new(config, tx, registry, cancel);
2547
2548 let _ = svc.ready().await.unwrap();
2549
2550 assert!(
2551 svc.sweep_handle
2552 .lock()
2553 .unwrap_or_else(|e| e.into_inner())
2554 .is_some(),
2555 "sweep handle should be Some after poll_ready"
2556 );
2557
2558 svc.shutdown(StepShutdownReason::HotSwap).await.unwrap();
2559
2560 assert!(
2561 svc.sweep_handle
2562 .lock()
2563 .unwrap_or_else(|e| e.into_inner())
2564 .is_none(),
2565 "sweep handle should be None after HotSwap shutdown"
2566 );
2567 assert!(
2568 svc.sweep_cancel
2569 .lock()
2570 .unwrap_or_else(|e| e.into_inner())
2571 .is_cancelled(),
2572 "sweep_cancel token should be cancelled after HotSwap shutdown"
2573 );
2574
2575 tokio::time::sleep(Duration::from_millis(50)).await;
2576 }
2577
2578 #[tokio::test]
2586 async fn constant_key_skips_reserialization() {
2587 let mut svc = new_test_svc(config_size(3));
2588 let mut result = None;
2589 for body in ["first", "second", "third"] {
2590 result = Some(
2591 svc.ready()
2592 .await
2593 .unwrap()
2594 .call(make_exchange("orderId", "A", body))
2595 .await
2596 .unwrap(),
2597 );
2598 }
2599 let result = result.unwrap();
2600 assert_eq!(
2601 result.property(CAMEL_AGGREGATED_SIZE),
2602 Some(&serde_json::json!(3u64)),
2603 "all 3 fragments must aggregate out of a single bucket"
2604 );
2605 assert!(svc.buckets.lock().unwrap().is_empty());
2606 assert_eq!(
2607 svc.key_serializations
2608 .load(std::sync::atomic::Ordering::Relaxed),
2609 1,
2610 "constant scalar key must serialize exactly once"
2611 );
2612 }
2613
2614 #[tokio::test]
2618 async fn divergent_keys_keep_serde_semantics() {
2619 let mut svc = new_test_svc(config_size(10)); for key in ["k1", "k2", "k1"] {
2621 svc.ready()
2622 .await
2623 .unwrap()
2624 .call(make_exchange("orderId", key, "body"))
2625 .await
2626 .unwrap();
2627 }
2628 let guard = svc.buckets.lock().unwrap();
2629 assert_eq!(guard.len(), 2, "k1/k2/k1 → exactly two buckets");
2630 for key in ["k1", "k2"] {
2631 let expected = serde_json::to_string(&serde_json::json!(key)).unwrap();
2632 assert!(
2633 guard.contains_key(expected.as_str()),
2634 "bucket name must be byte-identical to serde_json::to_string: \
2635 expected {expected}, have {:?}",
2636 guard.keys().collect::<Vec<_>>()
2637 );
2638 }
2639 }
2640
2641 #[tokio::test]
2649 async fn object_keys_bypass_cache() {
2650 let mut svc = new_test_svc(config_size(10)); let obj_a = serde_json::json!({"a": 1, "b": 2});
2652 let obj_b = serde_json::json!({"b": 2, "a": 1});
2653 for obj in [&obj_a, &obj_b, &obj_a] {
2654 let mut msg = Message {
2655 headers: Default::default(),
2656 body: Body::Text("body".into()),
2657 };
2658 msg.headers.insert("orderId".to_string(), obj.clone());
2659 svc.ready()
2660 .await
2661 .unwrap()
2662 .call(Exchange::new(msg))
2663 .await
2664 .unwrap();
2665 }
2666 let expected_keys: std::collections::HashSet<String> = [&obj_a, &obj_b]
2667 .into_iter()
2668 .map(|o| serde_json::to_string(o).unwrap())
2669 .collect();
2670 let guard = svc.buckets.lock().unwrap();
2671 let actual_keys: std::collections::HashSet<String> = guard.keys().cloned().collect();
2672 assert_eq!(
2673 actual_keys, expected_keys,
2674 "object keys must bucket exactly per serde_json::to_string (cache bypassed)"
2675 );
2676 assert_eq!(
2677 svc.key_serializations
2678 .load(std::sync::atomic::Ordering::Relaxed),
2679 3,
2680 "each object fragment must serialize (memo never consulted for objects)"
2681 );
2682 }
2683
2684 #[tokio::test]
2690 async fn float_zero_sign_keys_stay_distinct() {
2691 let mut svc = new_test_svc(config_size(10)); for key in [0.0_f64, -0.0, 0.0] {
2693 let mut msg = Message {
2694 headers: Default::default(),
2695 body: Body::Text("body".into()),
2696 };
2697 msg.headers
2698 .insert("orderId".to_string(), serde_json::json!(key));
2699 svc.ready()
2700 .await
2701 .unwrap()
2702 .call(Exchange::new(msg))
2703 .await
2704 .unwrap();
2705 }
2706 let guard = svc.buckets.lock().unwrap();
2707 assert_eq!(guard.len(), 2, "±0.0 must stay in distinct buckets");
2708 for key in [0.0_f64, -0.0] {
2709 let expected = serde_json::to_string(&serde_json::json!(key)).unwrap();
2710 assert!(
2711 guard.contains_key(expected.as_str()),
2712 "bucket name must be byte-identical to serde_json::to_string: \
2713 expected {expected}, have {:?}",
2714 guard.keys().collect::<Vec<_>>()
2715 );
2716 }
2717 assert_eq!(
2718 svc.key_serializations
2719 .load(std::sync::atomic::Ordering::Relaxed),
2720 3,
2721 "number keys serialize per fragment (never memoized)"
2722 );
2723 }
2724}