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(crate) async fn shutdown_inner(&self) {
277 {
279 let mut guard = self.timeout_tasks.lock().unwrap_or_else(|e| e.into_inner());
280 for token in guard.values() {
281 token.cancel();
282 }
283 guard.clear();
284 };
285
286 let handles: Vec<JoinHandle<()>> = {
288 let mut guard = self
289 .timeout_handles
290 .lock()
291 .unwrap_or_else(|e| e.into_inner());
292 guard.drain().map(|(_, handle)| handle).collect()
293 };
294
295 if handles.is_empty() {
296 return;
297 }
298
299 let _ = tokio::time::timeout(Duration::from_secs(5), async {
301 for handle in handles {
302 let _ = handle.await;
303 }
304 })
305 .await;
306 }
307}
308
309#[async_trait]
310impl StepLifecycle for AggregatorService {
311 fn name(&self) -> &'static str {
312 "aggregator"
313 }
314
315 async fn shutdown(&self, reason: StepShutdownReason) -> Result<(), CamelError> {
316 tracing::debug!(reason = ?reason, "Aggregator shutdown via StepLifecycle");
317
318 self.sweep_cancel
321 .lock()
322 .unwrap_or_else(|e| e.into_inner())
323 .cancel();
324
325 if let Some(handle) = self
328 .sweep_handle
329 .lock()
330 .unwrap_or_else(|e| e.into_inner())
331 .take()
332 {
333 handle.abort();
334 }
335
336 self.shutdown_inner().await;
337 Ok(())
338 }
339
340 async fn start(&self) -> Result<(), CamelError> {
344 *self.sweep_cancel.lock().unwrap_or_else(|e| e.into_inner()) = CancellationToken::new();
345
346 if let Some(handle) = self
347 .sweep_handle
348 .lock()
349 .unwrap_or_else(|e| e.into_inner())
350 .take()
351 {
352 handle.abort();
353 }
354
355 Ok(())
356 }
357}
358
359pub fn has_timeout_condition(mode: &CompletionMode) -> bool {
360 match mode {
361 CompletionMode::Single(CompletionCondition::Timeout(_)) => true,
362 CompletionMode::Any(conditions) => conditions
363 .iter()
364 .any(|c| matches!(c, CompletionCondition::Timeout(_))),
365 _ => false,
366 }
367}
368
369impl Service<Exchange> for AggregatorService {
370 type Response = Exchange;
371 type Error = CamelError;
372 type Future = Pin<Box<dyn Future<Output = Result<Exchange, CamelError>> + Send>>;
373
374 fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), CamelError>> {
375 let ttl = self.config.bucket_ttl;
384 if ttl.is_none() && self.queue_metrics.is_none() {
385 return Poll::Ready(Ok(()));
386 }
387 let mut g = self.sweep_handle.lock().unwrap_or_else(|e| e.into_inner());
388 if g.is_none() {
389 let interval = match ttl {
390 Some(ttl) => std::cmp::max(ttl / 2, Duration::from_millis(50)),
391 None => QUEUE_DEPTH_SAMPLE_INTERVAL,
392 };
393 let buckets = Arc::clone(&self.buckets);
394 let cancel = self
395 .sweep_cancel
396 .lock()
397 .unwrap_or_else(|e| e.into_inner())
398 .clone();
399 let queue_metrics = self.queue_metrics.clone();
400 *g = Some(tokio::spawn(async move {
401 loop {
402 tokio::select! {
403 _ = cancel.cancelled() => break,
404 _ = tokio::time::sleep(interval) => {
405 let mut guard =
406 buckets.lock().unwrap_or_else(|e| e.into_inner());
407 if let Some(ttl) = ttl {
408 guard.retain(|_, b| !b.is_expired(ttl));
409 }
410 if let Some((metrics, label)) = queue_metrics.as_ref() {
413 metrics.set_queue_depth(label, guard.len());
414 }
415 }
416 }
417 }
418 }));
419 }
420 Poll::Ready(Ok(()))
421 }
422
423 fn call(&mut self, exchange: Exchange) -> Self::Future {
424 let config = self.config.clone();
425 let buckets = Arc::clone(&self.buckets);
426 let timeout_tasks = Arc::clone(&self.timeout_tasks);
427 let timeout_handles = Arc::clone(&self.timeout_handles);
428 let late_tx = self.late_tx.clone();
429 let language_registry = Arc::clone(&self.language_registry);
430 let cached_key = Arc::clone(&self.cached_key);
431 #[cfg(test)]
432 let key_serializations = Arc::clone(&self.key_serializations);
433
434 Box::pin(async move {
435 let key_value =
436 extract_correlation_key(&exchange, &config.correlation, &language_registry).await?;
437
438 let key_is_memoizable = matches!(key_value, serde_json::Value::String(_));
449 let key_str = if key_is_memoizable {
450 let mut memo = cached_key.lock().unwrap_or_else(|e| e.into_inner());
451 if let Some((v, s)) = memo.as_ref()
452 && *v == key_value
453 {
454 s.clone()
455 } else {
456 #[cfg(test)]
457 key_serializations.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
458 let s = serde_json::to_string(&key_value)
459 .map_err(|e| CamelError::ProcessorError(e.to_string()))?;
460 *memo = Some((key_value.clone(), s.clone()));
461 s
462 }
463 } else {
464 #[cfg(test)]
465 key_serializations.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
466 serde_json::to_string(&key_value)
467 .map_err(|e| CamelError::ProcessorError(e.to_string()))?
468 };
469
470 let predicate_satisfied =
474 evaluate_completion_predicate(&config.completion, &exchange, &language_registry)
475 .await?;
476
477 let completed_bucket = {
478 let mut guard = buckets.lock().unwrap_or_else(|e| e.into_inner());
479
480 if let Some(ttl) = config.bucket_ttl {
481 guard.retain(|_, bucket| !bucket.is_expired(ttl));
482 }
483
484 if let Some(max) = config.max_buckets
485 && !guard.contains_key(&key_str)
486 && guard.len() >= max
487 {
488 tracing::warn!(
489 max_buckets = max,
490 correlation_key = %key_str,
491 "Aggregator reached max buckets limit, rejecting new correlation key"
492 );
493 return Err(CamelError::ProcessorError(format!(
494 "Aggregator reached maximum {} buckets",
495 max
496 )));
497 }
498
499 if let Some(max_size) = config.max_bucket_size
503 && let Some(existing) = guard.get(&key_str)
504 && existing.exchanges.len() >= max_size
505 {
506 tracing::warn!(
507 max_bucket_size = max_size,
508 correlation_key = %key_str,
509 "Aggregator reached per-bucket size limit, rejecting exchange"
510 );
511 return Err(CamelError::ProcessorError(format!(
512 "Aggregator bucket reached maximum {max_size} exchanges"
513 )));
514 }
515
516 let bucket = match guard.get_mut(&key_str) {
517 Some(b) => b,
518 None => guard.entry(key_str.clone()).or_insert_with(Bucket::new),
519 };
520 bucket.push(exchange);
521
522 let (is_complete, reason) = check_sync_completion(
523 &config.completion,
524 &bucket.exchanges,
525 predicate_satisfied,
526 );
527
528 let exchanges = if is_complete {
529 guard.remove(&key_str).map(|b| b.exchanges)
530 } else {
531 None
532 };
533 (exchanges, reason)
534 };
535
536 if completed_bucket.0.is_none() && has_timeout_condition(&config.completion) {
537 let timeout_dur = extract_timeout_duration(&config.completion);
538 if let Some(timeout) = timeout_dur {
539 let live_count = timeout_handles
545 .lock()
546 .unwrap_or_else(|e| e.into_inner())
547 .len();
548 if live_count >= config.max_timeout_tasks {
549 tracing::warn!(
550 live_timeout_tasks = live_count,
551 max_timeout_tasks = config.max_timeout_tasks,
552 correlation_key = %key_str,
553 "Aggregator timeout-task cap reached; bucket will rely on \
554 bucket_ttl eviction instead of a dedicated timeout task"
555 );
556 } else {
557 {
559 let tt_guard = timeout_tasks.lock().unwrap_or_else(|e| e.into_inner());
560 if let Some(existing) = tt_guard.get(&key_str) {
561 existing.cancel();
562 }
563 }
564 {
566 let mut hh = timeout_handles.lock().unwrap_or_else(|e| e.into_inner());
567 if let Some(old) = hh.remove(&key_str) {
568 old.abort();
569 }
570 }
571 let cancel = CancellationToken::new();
572 timeout_tasks
573 .lock()
574 .unwrap_or_else(|e| e.into_inner())
575 .insert(key_str.clone(), cancel.clone());
576 let handle = spawn_timeout_task(
577 key_str.clone(),
578 timeout,
579 cancel,
580 buckets.clone(),
581 timeout_tasks.clone(),
582 timeout_handles.clone(),
583 late_tx,
584 config.strategy.clone(),
585 config.discard_on_timeout,
586 );
587 timeout_handles
588 .lock()
589 .unwrap_or_else(|e| e.into_inner())
590 .insert(key_str.clone(), handle);
591 }
592 }
593 }
594
595 if let Some(exchanges) = completed_bucket.0 {
596 cancel_timeout_task_with_handle(&key_str, &timeout_tasks, &timeout_handles);
597 let reason = completed_bucket.1;
598 let size = exchanges.len();
599 let mut result = aggregate(exchanges, &config.strategy)?;
600 result.set_property(CAMEL_AGGREGATED_SIZE, serde_json::json!(size as u64));
601 result.set_property(CAMEL_AGGREGATED_KEY, key_value);
602 result.set_property(
603 CAMEL_AGGREGATED_COMPLETION_REASON,
604 serde_json::json!(reason.as_str()),
605 );
606 Ok(result)
607 } else {
608 let mut pending = Exchange::new(Message {
609 headers: Default::default(),
610 body: Body::Empty,
611 });
612 pending.set_property(CAMEL_AGGREGATOR_PENDING, serde_json::json!(true));
613 Ok(pending)
614 }
615 })
616 }
617}
618
619async fn extract_correlation_key(
620 exchange: &Exchange,
621 strategy: &CorrelationStrategy,
622 registry: &SharedLanguageRegistry,
623) -> Result<serde_json::Value, CamelError> {
624 match strategy {
625 CorrelationStrategy::HeaderName(h) => {
626 exchange.input.headers.get(h).cloned().ok_or_else(|| {
627 CamelError::ProcessorError(format!(
628 "Aggregator: missing correlation key header '{}'",
629 h
630 ))
631 })
632 }
633 CorrelationStrategy::Expression { expr, language } => {
634 let expression = {
635 let reg = registry.lock().unwrap_or_else(|e| e.into_inner());
636 let lang = reg.get(language).ok_or_else(|| {
637 CamelError::ProcessorError(format!(
638 "Aggregator: language '{}' not found in registry",
639 language
640 ))
641 })?;
642 lang.create_expression(expr)
643 .map_err(|e| CamelError::ProcessorError(e.to_string()))?
644 };
645 let value = expression
646 .evaluate(exchange)
647 .await
648 .map_err(|e| CamelError::ProcessorError(e.to_string()))?;
649 if value.is_null() {
650 return Err(CamelError::ProcessorError(format!(
651 "Aggregator: correlation expression '{}' evaluated to null",
652 expr
653 )));
654 }
655 Ok(value)
656 }
657 CorrelationStrategy::Fn(f) => f(exchange).map(serde_json::Value::String).ok_or_else(|| {
658 CamelError::ProcessorError("Aggregator: correlation function returned None".to_string())
659 }),
660 _ => Err(CamelError::ProcessorError(
662 "Aggregator: unsupported correlation strategy".to_string(),
663 )),
664 }
665}
666
667fn iter_predicate_exprs(mode: &CompletionMode) -> Vec<(&String, &String)> {
669 let mut out = Vec::new();
670 match mode {
671 CompletionMode::Single(CompletionCondition::PredicateExpr { expr, language }) => {
672 out.push((expr, language));
673 }
674 CompletionMode::Any(conds) => {
675 for c in conds {
676 if let CompletionCondition::PredicateExpr { expr, language } = c {
677 out.push((expr, language));
678 }
679 }
680 }
681 _ => {}
682 }
683 out
684}
685
686async fn evaluate_completion_predicate(
691 mode: &CompletionMode,
692 incoming: &Exchange,
693 registry: &SharedLanguageRegistry,
694) -> Result<bool, CamelError> {
695 let mut satisfied = false;
696 for (expr, language) in iter_predicate_exprs(mode) {
697 let expression = {
698 let reg = registry.lock().unwrap_or_else(|e| e.into_inner());
699 let lang = reg.get(language).ok_or_else(|| {
700 CamelError::ProcessorError(format!(
701 "Aggregator: language '{}' not found in registry",
702 language
703 ))
704 })?;
705 lang.create_expression(expr)
706 .map_err(|e| CamelError::ProcessorError(e.to_string()))?
707 }; let value = expression
709 .evaluate(incoming)
710 .await
711 .map_err(|e| CamelError::ProcessorError(e.to_string()))?;
712 if value.as_bool().unwrap_or(false) {
713 satisfied = true;
714 }
715 }
716 Ok(satisfied)
717}
718
719fn check_sync_completion(
720 mode: &CompletionMode,
721 exchanges: &[Exchange],
722 predicate_satisfied: bool,
723) -> (bool, CompletionReason) {
724 match mode {
725 CompletionMode::Single(cond) => check_single(cond, exchanges, predicate_satisfied),
726 CompletionMode::Any(conditions) => {
727 for cond in conditions {
728 if let CompletionCondition::Timeout(_) = cond {
729 continue;
730 }
731 let (done, reason) = check_single(cond, exchanges, predicate_satisfied);
732 if done {
733 return (true, reason);
734 }
735 }
736 (false, CompletionReason::Size)
737 }
738 _ => (false, CompletionReason::Size),
740 }
741}
742
743fn check_single(
744 cond: &CompletionCondition,
745 exchanges: &[Exchange],
746 predicate_satisfied: bool,
747) -> (bool, CompletionReason) {
748 match cond {
749 CompletionCondition::Size(n) => (exchanges.len() >= *n, CompletionReason::Size),
750 CompletionCondition::Predicate(pred) => (pred(exchanges), CompletionReason::Predicate),
751 CompletionCondition::PredicateExpr { .. } => {
752 (predicate_satisfied, CompletionReason::Predicate)
753 }
754 CompletionCondition::Timeout(_) => (false, CompletionReason::Timeout),
755 _ => (false, CompletionReason::Size),
757 }
758}
759
760fn extract_timeout_duration(mode: &CompletionMode) -> Option<Duration> {
761 match mode {
762 CompletionMode::Single(CompletionCondition::Timeout(d)) => Some(*d),
763 CompletionMode::Any(conditions) => conditions.iter().find_map(|c| {
764 if let CompletionCondition::Timeout(d) = c {
765 Some(*d)
766 } else {
767 None
768 }
769 }),
770 _ => None,
771 }
772}
773
774fn cancel_timeout_task(key: &str, timeout_tasks: &Arc<Mutex<HashMap<String, CancellationToken>>>) {
775 let mut guard = timeout_tasks.lock().unwrap_or_else(|e| e.into_inner());
776 if let Some(token) = guard.remove(key) {
777 token.cancel();
778 }
779}
780
781fn cancel_timeout_task_with_handle(
783 key: &str,
784 timeout_tasks: &Arc<Mutex<HashMap<String, CancellationToken>>>,
785 timeout_handles: &Arc<Mutex<HashMap<String, JoinHandle<()>>>>,
786) {
787 cancel_timeout_task(key, timeout_tasks);
788 let mut guard = timeout_handles.lock().unwrap_or_else(|e| e.into_inner());
789 guard.remove(key);
790}
791
792#[allow(clippy::too_many_arguments)]
793fn spawn_timeout_task(
794 key: String,
795 timeout: Duration,
796 cancel: CancellationToken,
797 buckets: Arc<Mutex<HashMap<String, Bucket>>>,
798 timeout_tasks: Arc<Mutex<HashMap<String, CancellationToken>>>,
799 timeout_handles: Arc<Mutex<HashMap<String, JoinHandle<()>>>>,
800 late_tx: mpsc::Sender<Exchange>,
801 strategy: AggregationStrategy,
802 discard: bool,
803) -> JoinHandle<()> {
804 let cancel_clone = cancel.clone();
805 tokio::spawn(async move {
806 tokio::select! {
807 _ = tokio::time::sleep(timeout) => {
808 let should_proceed = {
809 let mut tt_guard = timeout_tasks.lock().unwrap_or_else(|e| e.into_inner());
810 if cancel_clone.is_cancelled() {
811 false
812 } else {
813 tt_guard.remove(&key);
814 true
815 }
816 };
817 if !should_proceed {
818 return;
819 }
820 {
823 let mut hh = timeout_handles.lock().unwrap_or_else(|e| e.into_inner());
824 hh.remove(&key);
825 }
826 let bucket_exchanges = {
827 let mut guard = buckets.lock().unwrap_or_else(|e| e.into_inner());
828 guard.remove(&key).map(|b| b.exchanges)
829 };
830 if let Some(exchanges) = bucket_exchanges
831 && !discard
832 {
833 match aggregate(exchanges, &strategy) {
834 Ok(mut result) => {
835 result.set_property(
836 CAMEL_AGGREGATED_COMPLETION_REASON,
837 serde_json::json!(CompletionReason::Timeout.as_str()),
838 );
839 if late_tx.try_send(result).is_err() {
840 tracing::warn!(
841 key = %key,
842 "aggregator timeout emit dropped: late channel full"
843 );
844 }
845 }
846 Err(e) => {
847 tracing::warn!(
849 key = %key,
850 error = %e,
851 "aggregation failed in timeout task"
852 );
853 }
854 }
855 }
856 }
857 _ = cancel_clone.cancelled() => {}
858 }
859 })
860}
861
862fn aggregate(
863 exchanges: Vec<Exchange>,
864 strategy: &AggregationStrategy,
865) -> Result<Exchange, CamelError> {
866 match strategy {
867 AggregationStrategy::CollectAll => {
868 let bodies: Vec<serde_json::Value> = exchanges
869 .into_iter()
870 .map(|e| match e.input.body {
871 Body::Json(v) => v,
872 Body::Text(s) => serde_json::Value::String(s),
873 Body::Xml(s) => serde_json::Value::String(s),
874 Body::Bytes(b) => {
875 serde_json::Value::String(String::from_utf8_lossy(&b).into_owned())
876 }
877 Body::Stream(s) => serde_json::json!({
878 "_stream": {
879 "origin": s.metadata.origin,
880 "placeholder": true,
881 "hint": "Materialize exchange body with .into_bytes() before aggregation if content needed"
882 }
883 }),
884 _ => serde_json::Value::Null,
886 })
887 .collect();
888 Ok(Exchange::new(Message {
889 headers: Default::default(),
890 body: Body::Json(serde_json::Value::Array(bodies)),
891 }))
892 }
893 AggregationStrategy::Custom(f) => {
894 let mut iter = exchanges.into_iter();
895 let first = iter.next().ok_or_else(|| {
896 CamelError::ProcessorError("Aggregator: empty bucket".to_string())
897 })?;
898 Ok(iter.fold(first, |acc, next| f(acc, next)))
899 }
900 _ => Err(CamelError::ProcessorError(
902 "Aggregator: unsupported aggregation strategy".to_string(),
903 )),
904 }
905}
906
907#[cfg(test)]
908mod tests {
909 use super::*;
910 use std::collections::HashMap;
911
912 use camel_api::{
913 StepLifecycle, StepShutdownReason,
914 aggregator::{AggregationStrategy, AggregatorConfig},
915 body::Body,
916 exchange::Exchange,
917 message::Message,
918 };
919 use tokio::sync::mpsc;
920 use tokio_util::sync::CancellationToken;
921 use tower::ServiceExt;
922
923 fn make_exchange(header: &str, value: &str, body: &str) -> Exchange {
924 let mut msg = Message {
925 headers: Default::default(),
926 body: Body::Text(body.to_string()),
927 };
928 msg.headers
929 .insert(header.to_string(), serde_json::json!(value));
930 Exchange::new(msg)
931 }
932
933 fn config_size(n: usize) -> AggregatorConfig {
934 AggregatorConfig::correlate_by("orderId")
935 .complete_when_size(n)
936 .build()
937 .unwrap()
938 }
939
940 fn new_test_svc(config: AggregatorConfig) -> AggregatorService {
941 let (tx, _rx) = mpsc::channel(256);
942 let registry: SharedLanguageRegistry = Arc::new(std::sync::Mutex::new(HashMap::new()));
943 let cancel = CancellationToken::new();
944 AggregatorService::new(config, tx, registry, cancel)
945 }
946
947 fn new_test_svc_with_registry(
951 config: AggregatorConfig,
952 registry: SharedLanguageRegistry,
953 ) -> AggregatorService {
954 let (tx, _rx) = mpsc::channel(256);
955 let cancel = CancellationToken::new();
956 AggregatorService::new(config, tx, registry, cancel)
957 }
958
959 #[tokio::test]
960 async fn test_pending_exchange_not_yet_complete() {
961 let mut svc = new_test_svc(config_size(3));
962 let ex = make_exchange("orderId", "A", "first");
963 let result = svc.ready().await.unwrap().call(ex).await.unwrap();
964 assert!(matches!(result.input.body, Body::Empty));
965 assert_eq!(
966 result.property(CAMEL_AGGREGATOR_PENDING),
967 Some(&serde_json::json!(true))
968 );
969 }
970
971 #[tokio::test]
972 async fn test_completes_on_size() {
973 let mut svc = new_test_svc(config_size(3));
974 for _ in 0..2 {
975 let ex = make_exchange("orderId", "A", "item");
976 let r = svc.ready().await.unwrap().call(ex).await.unwrap();
977 assert!(matches!(r.input.body, Body::Empty));
978 }
979 let ex = make_exchange("orderId", "A", "last");
980 let result = svc.ready().await.unwrap().call(ex).await.unwrap();
981 assert!(result.property(CAMEL_AGGREGATOR_PENDING).is_none());
982 assert_eq!(
983 result.property(CAMEL_AGGREGATED_SIZE),
984 Some(&serde_json::json!(3u64))
985 );
986 }
987
988 #[tokio::test]
989 async fn test_collect_all_produces_json_array() {
990 let mut svc = new_test_svc(config_size(2));
991 svc.ready()
992 .await
993 .unwrap()
994 .call(make_exchange("orderId", "A", "alpha"))
995 .await
996 .unwrap();
997 let result = svc
998 .ready()
999 .await
1000 .unwrap()
1001 .call(make_exchange("orderId", "A", "beta"))
1002 .await
1003 .unwrap();
1004 let Body::Json(v) = &result.input.body else {
1005 panic!("expected Body::Json")
1006 };
1007 let arr = v.as_array().unwrap();
1008 assert_eq!(arr.len(), 2);
1009 assert_eq!(arr[0], serde_json::json!("alpha"));
1010 assert_eq!(arr[1], serde_json::json!("beta"));
1011 }
1012
1013 #[tokio::test]
1014 async fn test_two_keys_independent_buckets() {
1015 let mut svc = new_test_svc(config_size(3));
1017 svc.ready()
1018 .await
1019 .unwrap()
1020 .call(make_exchange("orderId", "A", "a1"))
1021 .await
1022 .unwrap();
1023 svc.ready()
1024 .await
1025 .unwrap()
1026 .call(make_exchange("orderId", "B", "b1"))
1027 .await
1028 .unwrap();
1029 svc.ready()
1030 .await
1031 .unwrap()
1032 .call(make_exchange("orderId", "A", "a2"))
1033 .await
1034 .unwrap();
1035 let ra = svc
1037 .ready()
1038 .await
1039 .unwrap()
1040 .call(make_exchange("orderId", "A", "a3"))
1041 .await
1042 .unwrap();
1043 assert!(matches!(ra.input.body, Body::Json(_)));
1045 let rb = svc
1047 .ready()
1048 .await
1049 .unwrap()
1050 .call(make_exchange("orderId", "B", "b_check"))
1051 .await
1052 .unwrap();
1053 assert!(matches!(rb.input.body, Body::Empty));
1054 }
1055
1056 #[tokio::test]
1057 async fn test_bucket_resets_after_completion() {
1058 let mut svc = new_test_svc(config_size(2));
1059 svc.ready()
1060 .await
1061 .unwrap()
1062 .call(make_exchange("orderId", "A", "x"))
1063 .await
1064 .unwrap();
1065 svc.ready()
1066 .await
1067 .unwrap()
1068 .call(make_exchange("orderId", "A", "x"))
1069 .await
1070 .unwrap(); let r = svc
1073 .ready()
1074 .await
1075 .unwrap()
1076 .call(make_exchange("orderId", "A", "new"))
1077 .await
1078 .unwrap();
1079 assert!(matches!(r.input.body, Body::Empty)); }
1081
1082 #[tokio::test]
1083 async fn test_completion_size_1_emits_immediately() {
1084 let mut svc = new_test_svc(config_size(1));
1085 let ex = make_exchange("orderId", "A", "solo");
1086 let result = svc.ready().await.unwrap().call(ex).await.unwrap();
1087 assert!(result.property(CAMEL_AGGREGATOR_PENDING).is_none());
1088 }
1089
1090 #[tokio::test]
1091 async fn test_custom_aggregation_strategy() {
1092 use camel_api::aggregator::AggregationFn;
1093 use std::sync::Arc;
1094
1095 let f: AggregationFn = Arc::new(|mut acc: Exchange, next: Exchange| {
1096 let combined = format!(
1097 "{}+{}",
1098 acc.input.body.as_text().unwrap_or(""),
1099 next.input.body.as_text().unwrap_or("")
1100 );
1101 acc.input.body = Body::Text(combined);
1102 acc
1103 });
1104 let config = AggregatorConfig::correlate_by("key")
1105 .complete_when_size(2)
1106 .strategy(AggregationStrategy::Custom(f))
1107 .build()
1108 .unwrap();
1109 let mut svc = new_test_svc(config);
1110 svc.ready()
1111 .await
1112 .unwrap()
1113 .call(make_exchange("key", "X", "hello"))
1114 .await
1115 .unwrap();
1116 let result = svc
1117 .ready()
1118 .await
1119 .unwrap()
1120 .call(make_exchange("key", "X", "world"))
1121 .await
1122 .unwrap();
1123 assert_eq!(result.input.body.as_text(), Some("hello+world"));
1124 }
1125
1126 #[tokio::test]
1127 async fn test_completion_predicate() {
1128 let config = AggregatorConfig::correlate_by("key")
1129 .complete_when(|bucket| {
1130 bucket
1131 .iter()
1132 .any(|e| e.input.body.as_text() == Some("DONE"))
1133 })
1134 .build()
1135 .unwrap();
1136 let mut svc = new_test_svc(config);
1137 svc.ready()
1138 .await
1139 .unwrap()
1140 .call(make_exchange("key", "K", "first"))
1141 .await
1142 .unwrap();
1143 svc.ready()
1144 .await
1145 .unwrap()
1146 .call(make_exchange("key", "K", "second"))
1147 .await
1148 .unwrap();
1149 let result = svc
1150 .ready()
1151 .await
1152 .unwrap()
1153 .call(make_exchange("key", "K", "DONE"))
1154 .await
1155 .unwrap();
1156 assert!(result.property(CAMEL_AGGREGATOR_PENDING).is_none());
1157 }
1158
1159 #[tokio::test]
1160 async fn test_missing_header_returns_error() {
1161 let mut svc = new_test_svc(config_size(2));
1162 let msg = Message {
1163 headers: Default::default(),
1164 body: Body::Text("no key".into()),
1165 };
1166 let ex = Exchange::new(msg);
1167 let result = svc.ready().await.unwrap().call(ex).await;
1168 assert!(result.is_err());
1169 assert!(matches!(
1170 result.unwrap_err(),
1171 camel_api::CamelError::ProcessorError(_)
1172 ));
1173 }
1174
1175 #[tokio::test]
1176 async fn test_cloned_service_shares_state() {
1177 let svc1 = new_test_svc(config_size(2));
1178 let mut svc2 = svc1.clone();
1179 svc1.clone()
1181 .ready()
1182 .await
1183 .unwrap()
1184 .call(make_exchange("orderId", "A", "from-svc1"))
1185 .await
1186 .unwrap();
1187 let result = svc2
1189 .ready()
1190 .await
1191 .unwrap()
1192 .call(make_exchange("orderId", "A", "from-svc2"))
1193 .await
1194 .unwrap();
1195 assert!(result.property(CAMEL_AGGREGATOR_PENDING).is_none());
1196 }
1197
1198 #[tokio::test]
1199 async fn test_camel_aggregated_key_property_set() {
1200 let mut svc = new_test_svc(config_size(1));
1201 let ex = make_exchange("orderId", "ORDER-42", "body");
1202 let result = svc.ready().await.unwrap().call(ex).await.unwrap();
1203 assert_eq!(
1204 result.property(CAMEL_AGGREGATED_KEY),
1205 Some(&serde_json::json!("ORDER-42"))
1206 );
1207 }
1208
1209 #[tokio::test]
1210 async fn test_aggregator_enforces_max_buckets() {
1211 let config = AggregatorConfig::correlate_by("orderId")
1212 .complete_when_size(2)
1213 .max_buckets(3)
1214 .build()
1215 .unwrap();
1216
1217 let mut svc = new_test_svc(config);
1218
1219 for i in 0..3 {
1221 let ex = make_exchange("orderId", &format!("key-{}", i), "body");
1222 let _ = svc.ready().await.unwrap().call(ex).await.unwrap();
1223 }
1224
1225 let ex = make_exchange("orderId", "key-4", "body");
1227 let result = svc.ready().await.unwrap().call(ex).await;
1228
1229 assert!(result.is_err(), "Should reject when max buckets reached");
1230 let err = result.unwrap_err().to_string();
1231 assert!(
1232 err.contains("maximum"),
1233 "Error message should contain 'maximum': {}",
1234 err
1235 );
1236 }
1237
1238 #[tokio::test]
1239 async fn test_max_buckets_allows_existing_key() {
1240 let config = AggregatorConfig::correlate_by("orderId")
1241 .complete_when_size(5) .max_buckets(2)
1243 .build()
1244 .unwrap();
1245
1246 let mut svc = new_test_svc(config);
1247
1248 let ex1 = make_exchange("orderId", "key-A", "body1");
1250 let _ = svc.ready().await.unwrap().call(ex1).await.unwrap();
1251 let ex2 = make_exchange("orderId", "key-B", "body2");
1252 let _ = svc.ready().await.unwrap().call(ex2).await.unwrap();
1253
1254 let ex3 = make_exchange("orderId", "key-A", "body3");
1256 let result = svc.ready().await.unwrap().call(ex3).await;
1257 assert!(
1258 result.is_ok(),
1259 "Should allow adding to existing bucket even at max limit"
1260 );
1261 }
1262
1263 #[tokio::test]
1267 async fn test_aggregator_enforces_max_bucket_size() {
1268 let config = AggregatorConfig::correlate_by("orderId")
1269 .complete_when(|_| false)
1271 .max_bucket_size(3)
1272 .build()
1273 .unwrap();
1274
1275 let mut svc = new_test_svc(config);
1276
1277 for _ in 0..3 {
1278 let ex = make_exchange("orderId", "hot-key", "body");
1279 let r = svc.ready().await.unwrap().call(ex).await;
1280 assert!(r.is_ok(), "first 3 exchanges accepted: {r:?}");
1281 }
1282
1283 let ex = make_exchange("orderId", "hot-key", "body");
1284 let result = svc.ready().await.unwrap().call(ex).await;
1285 assert!(result.is_err(), "4th exchange on hot key must be rejected");
1286 let err = result.unwrap_err().to_string();
1287 assert!(
1288 err.contains("maximum"),
1289 "error should mention the per-bucket maximum: {err}"
1290 );
1291
1292 let ex = make_exchange("orderId", "other-key", "body");
1294 let result = svc.ready().await.unwrap().call(ex).await;
1295 assert!(result.is_ok(), "other keys still accepted: {result:?}");
1296 }
1297
1298 #[tokio::test]
1299 async fn test_bucket_ttl_eviction() {
1300 let config = AggregatorConfig::correlate_by("orderId")
1301 .complete_when_size(10) .bucket_ttl(Duration::from_millis(50))
1303 .build()
1304 .unwrap();
1305
1306 let mut svc = new_test_svc(config);
1307
1308 let ex1 = make_exchange("orderId", "key-A", "body1");
1310 let _ = svc.ready().await.unwrap().call(ex1).await.unwrap();
1311
1312 tokio::time::sleep(Duration::from_millis(100)).await;
1314
1315 let ex2 = make_exchange("orderId", "key-B", "body2");
1317 let _ = svc.ready().await.unwrap().call(ex2).await.unwrap();
1318
1319 let ex3 = make_exchange("orderId", "key-A", "body3");
1322 let result = svc.ready().await.unwrap().call(ex3).await;
1323 assert!(result.is_ok(), "Should be able to recreate evicted bucket");
1324 }
1325
1326 #[tokio::test(start_paused = true)]
1327 async fn test_timeout_completes_bucket() {
1328 let config = AggregatorConfig::correlate_by("key")
1329 .complete_on_timeout(Duration::from_millis(100))
1330 .build()
1331 .unwrap();
1332 let mut svc = new_test_svc(config);
1333 let ex = make_exchange("key", "A", "data");
1334 let result = svc.ready().await.unwrap().call(ex).await.unwrap();
1335 assert!(result.property(CAMEL_AGGREGATOR_PENDING).is_some());
1336
1337 tokio::time::sleep(Duration::from_millis(200)).await;
1338
1339 assert_eq!(
1340 svc.buckets.lock().unwrap().len(),
1341 0,
1342 "bucket should be removed after timeout"
1343 );
1344 }
1345
1346 #[tokio::test(start_paused = true)]
1347 async fn test_timeout_resets_on_new_exchange() {
1348 let config = AggregatorConfig::correlate_by("key")
1349 .complete_on_timeout(Duration::from_millis(150))
1350 .build()
1351 .unwrap();
1352 let mut svc = new_test_svc(config);
1353
1354 let ex1 = make_exchange("key", "A", "first");
1355 let _ = svc.ready().await.unwrap().call(ex1).await.unwrap();
1356
1357 tokio::time::sleep(Duration::from_millis(100)).await;
1358
1359 let ex2 = make_exchange("key", "A", "second");
1360 let _ = svc.ready().await.unwrap().call(ex2).await.unwrap();
1361
1362 tokio::time::sleep(Duration::from_millis(100)).await;
1363
1364 assert_eq!(
1365 svc.buckets.lock().unwrap().len(),
1366 1,
1367 "bucket should still exist — timeout was reset"
1368 );
1369
1370 tokio::time::sleep(Duration::from_millis(100)).await;
1371
1372 assert_eq!(
1373 svc.buckets.lock().unwrap().len(),
1374 0,
1375 "bucket should be gone after timeout fires"
1376 );
1377 }
1378
1379 #[tokio::test]
1380 async fn test_composable_size_and_timeout() {
1381 let config = AggregatorConfig::correlate_by("key")
1382 .complete_on_size_or_timeout(2, Duration::from_millis(200))
1383 .build()
1384 .unwrap();
1385 let mut svc = new_test_svc(config);
1386
1387 let ex1 = make_exchange("key", "A", "first");
1388 let _ = svc.ready().await.unwrap().call(ex1).await.unwrap();
1389 assert!(svc.buckets.lock().unwrap().contains_key("\"A\""));
1390
1391 let ex2 = make_exchange("key", "A", "second");
1392 let result = svc.ready().await.unwrap().call(ex2).await.unwrap();
1393 assert!(result.property(CAMEL_AGGREGATOR_PENDING).is_none());
1394 assert_eq!(
1395 result.property(CAMEL_AGGREGATED_COMPLETION_REASON),
1396 Some(&serde_json::json!("size"))
1397 );
1398 }
1399
1400 #[tokio::test(start_paused = true)]
1401 async fn test_discard_on_timeout() {
1402 let config = AggregatorConfig::correlate_by("key")
1403 .complete_on_timeout(Duration::from_millis(50))
1404 .discard_on_timeout(true)
1405 .build()
1406 .unwrap();
1407 let (tx, mut rx) = mpsc::channel(256);
1408 let registry: SharedLanguageRegistry = Arc::new(std::sync::Mutex::new(HashMap::new()));
1409 let cancel = CancellationToken::new();
1410 let mut svc = AggregatorService::new(config, tx, registry, cancel);
1411
1412 let ex = make_exchange("key", "A", "data");
1413 let _ = svc.ready().await.unwrap().call(ex).await.unwrap();
1414
1415 tokio::time::sleep(Duration::from_millis(100)).await;
1416
1417 assert!(
1418 rx.try_recv().is_err(),
1419 "no emit expected with discard_on_timeout"
1420 );
1421 assert_eq!(svc.buckets.lock().unwrap().len(), 0);
1422 assert!(
1423 svc.timeout_tasks.lock().unwrap().is_empty(),
1424 "timeout task should be cleaned up"
1425 );
1426 }
1427
1428 #[tokio::test]
1429 async fn test_force_completion_on_stop() {
1430 let config = AggregatorConfig::correlate_by("key")
1431 .complete_when_size(10)
1432 .force_completion_on_stop(true)
1433 .build()
1434 .unwrap();
1435 let (tx, mut rx) = mpsc::channel(256);
1436 let registry: SharedLanguageRegistry = Arc::new(std::sync::Mutex::new(HashMap::new()));
1437 let cancel = CancellationToken::new();
1438 let svc = AggregatorService::new(config, tx, registry, cancel);
1439
1440 let mut call_svc = svc.clone();
1441 let ex = make_exchange("key", "A", "data");
1442 let _ = call_svc.ready().await.unwrap().call(ex).await.unwrap();
1443
1444 svc.force_complete_all();
1445
1446 let result = rx.try_recv().expect("should emit on force-complete");
1447 assert!(
1448 result.input.body.as_text().is_some() || matches!(result.input.body, Body::Json(_))
1449 );
1450 assert_eq!(
1451 result.property(CAMEL_AGGREGATED_COMPLETION_REASON),
1452 Some(&serde_json::json!("stop"))
1453 );
1454 }
1455
1456 #[tokio::test]
1457 async fn test_completion_reason_property_size() {
1458 let config = AggregatorConfig::correlate_by("key")
1459 .complete_when_size(1)
1460 .build()
1461 .unwrap();
1462 let mut svc = new_test_svc(config);
1463 let ex = make_exchange("key", "X", "body");
1464 let result = svc.ready().await.unwrap().call(ex).await.unwrap();
1465 assert_eq!(
1466 result.property(CAMEL_AGGREGATED_COMPLETION_REASON),
1467 Some(&serde_json::json!("size"))
1468 );
1469 }
1470
1471 #[tokio::test]
1472 async fn test_completion_reason_property_predicate() {
1473 let config = AggregatorConfig::correlate_by("key")
1474 .complete_when(|_| true)
1475 .build()
1476 .unwrap();
1477 let mut svc = new_test_svc(config);
1478 let ex = make_exchange("key", "X", "body");
1479 let result = svc.ready().await.unwrap().call(ex).await.unwrap();
1480 assert_eq!(
1481 result.property(CAMEL_AGGREGATED_COMPLETION_REASON),
1482 Some(&serde_json::json!("predicate"))
1483 );
1484 }
1485
1486 #[tokio::test(start_paused = true)]
1487 async fn test_size_completes_before_timeout() {
1488 let config = AggregatorConfig::correlate_by("key")
1489 .complete_on_size_or_timeout(2, Duration::from_millis(200))
1490 .build()
1491 .unwrap();
1492 let mut svc = new_test_svc(config);
1493
1494 let ex1 = make_exchange("key", "A", "first");
1495 let _ = svc.ready().await.unwrap().call(ex1).await.unwrap();
1496
1497 let ex2 = make_exchange("key", "A", "second");
1498 let result = svc.ready().await.unwrap().call(ex2).await.unwrap();
1499
1500 assert!(result.property(CAMEL_AGGREGATOR_PENDING).is_none());
1501 assert_eq!(
1502 result.property(CAMEL_AGGREGATED_COMPLETION_REASON),
1503 Some(&serde_json::json!("size"))
1504 );
1505 assert_eq!(svc.buckets.lock().unwrap().len(), 0);
1506
1507 tokio::time::sleep(Duration::from_millis(300)).await;
1508 assert_eq!(
1509 svc.buckets.lock().unwrap().len(),
1510 0,
1511 "no re-fire after timeout"
1512 );
1513 }
1514
1515 #[tokio::test(start_paused = true)]
1516 async fn test_concurrent_timeout_fire_and_new_exchange() {
1517 let config = AggregatorConfig::correlate_by("key")
1518 .complete_on_size_or_timeout(2, Duration::from_millis(100))
1519 .build()
1520 .unwrap();
1521 let (tx, mut rx) = mpsc::channel(256);
1522 let registry: SharedLanguageRegistry = Arc::new(std::sync::Mutex::new(HashMap::new()));
1523 let cancel = CancellationToken::new();
1524 let mut svc = AggregatorService::new(config, tx, registry, cancel);
1525
1526 let ex = make_exchange("key", "A", "data");
1527 let _ = svc.ready().await.unwrap().call(ex).await.unwrap();
1528
1529 tokio::time::sleep(Duration::from_millis(150)).await;
1531
1532 let ex2 = make_exchange("key", "A", "data2");
1534 let result = svc.ready().await.unwrap().call(ex2).await.unwrap();
1535 assert!(
1536 result.property(CAMEL_AGGREGATOR_PENDING).is_some(),
1537 "should be pending in new bucket"
1538 );
1539
1540 let mut late_count = 0;
1542 while rx.try_recv().is_ok() {
1543 late_count += 1;
1544 }
1545 assert_eq!(
1546 late_count, 1,
1547 "exactly 1 late emit from the timed-out bucket"
1548 );
1549 }
1550
1551 #[tokio::test(start_paused = true)]
1552 async fn test_late_channel_full_drops_with_warning() {
1553 let config = AggregatorConfig::correlate_by("key")
1554 .complete_on_timeout(Duration::from_millis(50))
1555 .build()
1556 .unwrap();
1557 let (tx, mut rx) = mpsc::channel(1);
1558 rx.close();
1559 let registry: SharedLanguageRegistry = Arc::new(std::sync::Mutex::new(HashMap::new()));
1560 let cancel = CancellationToken::new();
1561 let mut svc = AggregatorService::new(config, tx, registry, cancel);
1562
1563 let ex = make_exchange("key", "A", "data");
1564 let _ = svc.ready().await.unwrap().call(ex).await.unwrap();
1565
1566 tokio::time::sleep(Duration::from_millis(100)).await;
1567 assert_eq!(
1568 svc.buckets.lock().unwrap().len(),
1569 0,
1570 "bucket removed despite channel closed"
1571 );
1572 }
1573
1574 #[tokio::test]
1580 async fn test_da3_force_complete_all_drops_on_saturated_channel() {
1581 let config = AggregatorConfig::correlate_by("k")
1582 .complete_when_size(10)
1583 .force_completion_on_stop(true)
1584 .build()
1585 .unwrap();
1586 let (late_tx, mut late_rx) = mpsc::channel::<Exchange>(1);
1588 late_tx
1592 .try_send(make_exchange("k", "99", "dummy"))
1593 .expect("pre-fill succeeds");
1594
1595 let registry: SharedLanguageRegistry = Arc::new(std::sync::Mutex::new(HashMap::new()));
1596 let cancel = CancellationToken::new();
1597 let mut svc = AggregatorService::new(config, late_tx, registry, cancel);
1598
1599 for v in ["1", "2", "3"] {
1602 let ex = make_exchange("k", v, "body");
1603 let _ = svc.ready().await.unwrap().call(ex).await.unwrap();
1604 }
1605 assert_eq!(svc.buckets.lock().unwrap().len(), 3);
1606
1607 svc.force_complete_all();
1609
1610 let pre_fill = late_rx
1612 .try_recv()
1613 .expect("pre-fill should still be in channel");
1614 assert_eq!(
1615 pre_fill.input.headers.get("k"),
1616 Some(&serde_json::json!("99"))
1617 );
1618
1619 assert!(matches!(
1622 late_rx.try_recv(),
1623 Err(mpsc::error::TryRecvError::Empty)
1624 ));
1625
1626 assert!(svc.buckets.lock().unwrap().is_empty());
1628 }
1629
1630 #[tokio::test]
1631 async fn test_aggregate_stream_bodies_creates_valid_json() {
1632 use bytes::Bytes;
1633 use camel_api::{Body, StreamBody, StreamMetadata};
1634 use futures::stream;
1635 use tokio::sync::Mutex;
1636
1637 let chunks = vec![Ok(Bytes::from("test"))];
1638 let stream_body = StreamBody {
1639 stream: Arc::new(Mutex::new(Some(Box::pin(stream::iter(chunks))))),
1640 metadata: StreamMetadata {
1641 origin: Some("file:///test.txt".to_string()),
1642 ..Default::default()
1643 },
1644 };
1645
1646 let ex1 = Exchange::new(Message {
1647 headers: Default::default(),
1648 body: Body::Stream(stream_body),
1649 });
1650
1651 let exchanges = vec![ex1];
1652 let result = aggregate(exchanges, &AggregationStrategy::CollectAll);
1653
1654 let exchange = result.expect("Expected Ok result");
1655 assert!(
1656 matches!(exchange.input.body, Body::Json(_)),
1657 "Expected Json body"
1658 );
1659
1660 if let Body::Json(value) = exchange.input.body {
1661 let json_str = serde_json::to_string(&value).unwrap();
1662 let parsed: serde_json::Value = serde_json::from_str(&json_str).unwrap();
1663
1664 assert!(parsed.is_array(), "Result should be an array");
1665 let arr = parsed.as_array().unwrap();
1666 assert!(arr[0].is_object(), "First element should be an object");
1667 assert!(
1668 arr[0]["_stream"].is_object(),
1669 "Should contain _stream object"
1670 );
1671 assert_eq!(arr[0]["_stream"]["origin"], "file:///test.txt");
1672 assert_eq!(
1673 arr[0]["_stream"]["placeholder"], true,
1674 "placeholder flag should be true"
1675 );
1676 }
1677 }
1678
1679 #[tokio::test]
1680 async fn test_aggregate_stream_bodies_with_none_origin() {
1681 use bytes::Bytes;
1682 use camel_api::{Body, StreamBody, StreamMetadata};
1683 use futures::stream;
1684 use tokio::sync::Mutex;
1685
1686 let chunks = vec![Ok(Bytes::from("test"))];
1687 let stream_body = StreamBody {
1688 stream: Arc::new(Mutex::new(Some(Box::pin(stream::iter(chunks))))),
1689 metadata: StreamMetadata {
1690 origin: None,
1691 ..Default::default()
1692 },
1693 };
1694
1695 let ex1 = Exchange::new(Message {
1696 headers: Default::default(),
1697 body: Body::Stream(stream_body),
1698 });
1699
1700 let exchanges = vec![ex1];
1701 let result = aggregate(exchanges, &AggregationStrategy::CollectAll);
1702
1703 let exchange = result.expect("Expected Ok result");
1704 assert!(
1705 matches!(exchange.input.body, Body::Json(_)),
1706 "Expected Json body"
1707 );
1708
1709 if let Body::Json(value) = exchange.input.body {
1710 let json_str = serde_json::to_string(&value).unwrap();
1711 let parsed: serde_json::Value = serde_json::from_str(&json_str).unwrap();
1712
1713 assert!(parsed.is_array(), "Result should be an array");
1714 let arr = parsed.as_array().unwrap();
1715 assert!(arr[0].is_object(), "First element should be an object");
1716 assert!(
1717 arr[0]["_stream"].is_object(),
1718 "Should contain _stream object"
1719 );
1720 assert_eq!(
1721 arr[0]["_stream"]["origin"],
1722 serde_json::Value::Null,
1723 "origin should be null when None"
1724 );
1725 assert_eq!(
1726 arr[0]["_stream"]["placeholder"], true,
1727 "placeholder flag should be true"
1728 );
1729 }
1730 }
1731
1732 #[tokio::test]
1733 async fn timeout_completion_clears_handle_from_map() {
1734 let config = AggregatorConfig::correlate_by("key")
1739 .complete_on_timeout(Duration::from_millis(50))
1740 .build()
1741 .unwrap();
1742 let (tx, _rx) = mpsc::channel(256);
1743 let registry: SharedLanguageRegistry = Arc::new(std::sync::Mutex::new(HashMap::new()));
1744 let cancel = CancellationToken::new();
1745 let svc = AggregatorService::new(config, tx, registry, cancel);
1746
1747 let mut call_svc = svc.clone();
1749 let ex = make_exchange("key", "A", "data");
1750 let _ = call_svc.ready().await.unwrap().call(ex).await.unwrap();
1751 assert!(
1752 !svc.timeout_handles.lock().unwrap().is_empty(),
1753 "handle should exist while timeout pending"
1754 );
1755
1756 tokio::time::sleep(Duration::from_millis(200)).await;
1758
1759 assert!(
1760 svc.timeout_handles.lock().unwrap().is_empty(),
1761 "handle should be cleared from map after natural timeout completion (was leak)"
1762 );
1763 }
1764
1765 #[tokio::test]
1766 async fn aggregator_shutdown_via_trait_dispatch() {
1767 let config = AggregatorConfig::correlate_by("key")
1770 .complete_when_size(10)
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 step: Arc<dyn StepLifecycle> = Arc::new(svc);
1779 step.shutdown(StepShutdownReason::RouteStop)
1780 .await
1781 .expect("first shutdown should succeed");
1782 step.shutdown(StepShutdownReason::RouteStop)
1783 .await
1784 .expect("second shutdown (idempotent) should succeed");
1785 }
1786
1787 #[tokio::test(start_paused = true)]
1788 async fn test_shutdown_awaits_timeout_handles() {
1789 let config = AggregatorConfig::correlate_by("key")
1790 .complete_on_timeout(Duration::from_millis(100))
1791 .build()
1792 .unwrap();
1793 let (tx, _rx) = mpsc::channel(256);
1794 let registry: SharedLanguageRegistry = Arc::new(std::sync::Mutex::new(HashMap::new()));
1795 let cancel = CancellationToken::new();
1796 let svc = AggregatorService::new(config, tx, registry, cancel);
1797
1798 let mut call_svc = svc.clone();
1800 let ex = make_exchange("key", "A", "data");
1801 let _ = call_svc.ready().await.unwrap().call(ex).await.unwrap();
1802
1803 assert!(
1805 !svc.timeout_handles.lock().unwrap().is_empty(),
1806 "should have a timeout handle"
1807 );
1808
1809 svc.shutdown_inner().await;
1812
1813 assert!(
1814 svc.timeout_handles.lock().unwrap().is_empty(),
1815 "all handles should be cleaned up after shutdown"
1816 );
1817 }
1818
1819 #[tokio::test]
1826 async fn test_unique_key_flood_stays_bounded_by_default() {
1827 let config = AggregatorConfig::correlate_by("orderId")
1829 .complete_when_size(1_000_000) .build()
1831 .unwrap();
1832 let mut svc = new_test_svc(config);
1833
1834 for i in 0..10_000usize {
1837 let ex = make_exchange("orderId", &format!("key-{i}"), "body");
1838 let result = svc.ready().await.unwrap().call(ex).await;
1839 assert!(result.is_ok(), "key {i} should be accepted under the cap");
1840 }
1841 let ex = make_exchange("orderId", "key-10001", "body");
1842 let result = svc.ready().await.unwrap().call(ex).await;
1843 assert!(
1844 result.is_err(),
1845 "10_001st unique key must be rejected by the max_buckets cap"
1846 );
1847 let err = result.unwrap_err().to_string();
1848 assert!(
1849 err.contains("maximum") || err.contains("max"),
1850 "error should mention cap: {err}"
1851 );
1852 }
1853
1854 #[tokio::test]
1860 async fn test_background_sweep_spawns_on_first_poll_not_construction() {
1861 let config = AggregatorConfig::correlate_by("key")
1862 .complete_when_size(10_000)
1863 .bucket_ttl(Duration::from_millis(50))
1864 .build()
1865 .unwrap();
1866 let cancel = CancellationToken::new();
1867 let (tx, _rx) = mpsc::channel(8);
1868 let registry: SharedLanguageRegistry = Arc::new(std::sync::Mutex::new(HashMap::new()));
1869 let mut svc = AggregatorService::new(config, tx, registry, cancel.clone());
1870
1871 assert!(
1873 svc.sweep_handle
1874 .lock()
1875 .unwrap_or_else(|e| e.into_inner())
1876 .is_none(),
1877 "sweep must NOT be spawned at construction (runtime-free new)"
1878 );
1879
1880 let _ = svc.ready().await.unwrap();
1883 let sweep_present = svc
1884 .sweep_handle
1885 .lock()
1886 .unwrap_or_else(|e| e.into_inner())
1887 .is_some();
1888 assert!(
1889 sweep_present,
1890 "sweep handle should be Some after first poll when bucket_ttl is set"
1891 );
1892
1893 cancel.cancel();
1895 tokio::time::sleep(Duration::from_millis(50)).await;
1897 }
1898
1899 struct QueueDepthRecorder(Mutex<Vec<(String, usize)>>);
1902 impl MetricsCollector for QueueDepthRecorder {
1903 fn record_exchange_duration(&self, _: &str, _: std::time::Duration) {}
1904 fn increment_errors(&self, _: &str, _: &str) {}
1905 fn increment_exchanges(&self, _: &str) {}
1906 fn set_queue_depth(&self, queue: &str, depth: usize) {
1907 self.0
1908 .lock()
1909 .unwrap_or_else(|e| e.into_inner())
1910 .push((queue.to_string(), depth));
1911 }
1912 fn record_circuit_breaker_change(&self, _: &str, _: &str, _: &str) {}
1913 }
1914
1915 async fn await_depth_sample(
1918 recorder: &QueueDepthRecorder,
1919 label: &str,
1920 pred: impl Fn(usize) -> bool,
1921 ) {
1922 let deadline = std::time::Instant::now() + Duration::from_secs(2);
1923 loop {
1924 let matched = recorder
1925 .0
1926 .lock()
1927 .unwrap_or_else(|e| e.into_inner())
1928 .iter()
1929 .any(|(q, d)| q == label && pred(*d));
1930 if matched {
1931 return;
1932 }
1933 assert!(
1934 std::time::Instant::now() < deadline,
1935 "no queue-depth sample for '{label}' matched within 2s"
1936 );
1937 tokio::time::sleep(Duration::from_millis(20)).await;
1938 }
1939 }
1940
1941 #[tokio::test]
1946 async fn test_sweep_reports_queue_depth() {
1947 let config = AggregatorConfig::correlate_by("orderId")
1948 .complete_when_size(3)
1949 .bucket_ttl(Duration::from_millis(100))
1953 .build()
1954 .unwrap();
1955 let cancel = CancellationToken::new();
1956 let (tx, _rx) = mpsc::channel(8);
1957 let registry: SharedLanguageRegistry = Arc::new(std::sync::Mutex::new(HashMap::new()));
1958 let recorder = Arc::new(QueueDepthRecorder(Mutex::new(Vec::new())));
1959 let mut svc = AggregatorService::new(config, tx, registry, cancel).with_queue_metrics(
1960 Arc::clone(&recorder) as Arc<dyn MetricsCollector>,
1961 "aggregator:t",
1962 );
1963
1964 let _ = svc.ready().await.unwrap();
1966
1967 let ex = make_exchange("orderId", "g1", "partial");
1969 let _ = svc.ready().await.unwrap().call(ex).await;
1970
1971 await_depth_sample(&recorder, "aggregator:t", |d| d > 0).await;
1972
1973 let ex = make_exchange("orderId", "g1", "b");
1975 let _ = svc.ready().await.unwrap().call(ex).await;
1976 let ex = make_exchange("orderId", "g1", "c");
1977 let _ = svc.ready().await.unwrap().call(ex).await;
1978
1979 await_depth_sample(&recorder, "aggregator:t", |d| d == 0).await;
1980
1981 svc.shutdown(StepShutdownReason::RouteStop).await.unwrap();
1982 }
1983
1984 #[tokio::test]
1989 async fn test_no_ttl_aggregator_reports_queue_depth() {
1990 use camel_api::aggregator::CorrelationStrategy;
1991
1992 let config = AggregatorConfig {
1993 header_name: "orderId".into(),
1994 completion: CompletionMode::Single(CompletionCondition::Size(3)),
1995 correlation: CorrelationStrategy::HeaderName("orderId".into()),
1996 strategy: AggregationStrategy::CollectAll,
1997 max_buckets: Some(100),
1998 max_bucket_size: Some(100),
1999 bucket_ttl: None,
2000 force_completion_on_stop: false,
2001 discard_on_timeout: false,
2002 max_timeout_tasks: 64,
2003 };
2004 let cancel = CancellationToken::new();
2005 let (tx, _rx) = mpsc::channel(8);
2006 let registry: SharedLanguageRegistry = Arc::new(std::sync::Mutex::new(HashMap::new()));
2007 let recorder = Arc::new(QueueDepthRecorder(Mutex::new(Vec::new())));
2008 let mut svc = AggregatorService::new(config, tx, registry, cancel).with_queue_metrics(
2009 Arc::clone(&recorder) as Arc<dyn MetricsCollector>,
2010 "aggregator:nottl",
2011 );
2012
2013 let _ = svc.ready().await.unwrap();
2014 assert!(
2015 svc.sweep_handle
2016 .lock()
2017 .unwrap_or_else(|e| e.into_inner())
2018 .is_some(),
2019 "metrics-only config (bucket_ttl = None) must still spawn the sweep"
2020 );
2021
2022 let ex = make_exchange("orderId", "g1", "partial");
2024 let _ = svc.ready().await.unwrap().call(ex).await;
2025 await_depth_sample(&recorder, "aggregator:nottl", |d| d > 0).await;
2026
2027 let ex = make_exchange("orderId", "g1", "b");
2029 let _ = svc.ready().await.unwrap().call(ex).await;
2030 let ex = make_exchange("orderId", "g1", "c");
2031 let _ = svc.ready().await.unwrap().call(ex).await;
2032 await_depth_sample(&recorder, "aggregator:nottl", |d| d == 0).await;
2033
2034 svc.shutdown(StepShutdownReason::RouteStop).await.unwrap();
2035 }
2036
2037 #[tokio::test]
2042 async fn test_transient_clone_drop_keeps_sweep_sampling() {
2043 let config = AggregatorConfig::correlate_by("orderId")
2044 .complete_when_size(3)
2045 .bucket_ttl(Duration::from_millis(100))
2046 .build()
2047 .unwrap();
2048 let cancel = CancellationToken::new();
2049 let (tx, _rx) = mpsc::channel(8);
2050 let registry: SharedLanguageRegistry = Arc::new(std::sync::Mutex::new(HashMap::new()));
2051 let recorder = Arc::new(QueueDepthRecorder(Mutex::new(Vec::new())));
2052 let mut svc = AggregatorService::new(config, tx, registry, cancel).with_queue_metrics(
2053 Arc::clone(&recorder) as Arc<dyn MetricsCollector>,
2054 "aggregator:clone",
2055 );
2056
2057 let _ = svc.ready().await.unwrap();
2058
2059 drop(svc.clone());
2061 assert!(
2062 svc.sweep_handle
2063 .lock()
2064 .unwrap_or_else(|e| e.into_inner())
2065 .is_some(),
2066 "transient clone drop must not abort the shared sweep"
2067 );
2068
2069 let ex = make_exchange("orderId", "g1", "partial");
2071 let _ = svc.ready().await.unwrap().call(ex).await;
2072 await_depth_sample(&recorder, "aggregator:clone", |d| d > 0).await;
2073
2074 svc.shutdown(StepShutdownReason::RouteStop).await.unwrap();
2075 }
2076
2077 #[tokio::test]
2080 async fn test_aggregator_timeout_task_cap_no_panic_under_flood() {
2081 use camel_api::aggregator::CorrelationStrategy;
2084
2085 let config = AggregatorConfig {
2086 header_name: "k".into(),
2087 completion: CompletionMode::Any(vec![
2088 CompletionCondition::Size(999),
2089 CompletionCondition::Timeout(Duration::from_secs(30)),
2090 ]),
2091 correlation: CorrelationStrategy::HeaderName("k".into()),
2092 strategy: AggregationStrategy::CollectAll,
2093 max_buckets: Some(50),
2094 max_bucket_size: Some(50),
2095 bucket_ttl: Some(Duration::from_secs(30)),
2096 force_completion_on_stop: false,
2097 discard_on_timeout: false,
2098 max_timeout_tasks: 2,
2099 };
2100 let (late_tx, mut late_rx) = mpsc::channel(64);
2101 let registry: SharedLanguageRegistry = Arc::new(std::sync::Mutex::new(HashMap::new()));
2102 let cancel = CancellationToken::new();
2103 let svc = AggregatorService::new(config, late_tx, registry, cancel);
2104
2105 for i in 0..20u64 {
2107 let mut ex = Exchange::new(Message {
2108 headers: HashMap::from([("k".to_string(), serde_json::json!(i))]),
2109 body: Body::Text(i.to_string()),
2110 });
2111 ex.input
2112 .headers
2113 .insert("k".to_string(), serde_json::json!(i));
2114 let outcome = tokio::time::timeout(Duration::from_secs(2), async {
2115 let mut s = svc.clone();
2116 use tower::ServiceExt;
2117 s.ready().await.unwrap().call(ex).await
2118 })
2119 .await;
2120 assert!(outcome.is_ok(), "call {} hung/panicked under task cap", i);
2121 let res = outcome.unwrap().unwrap();
2123 assert_eq!(
2124 res.properties
2125 .get(CAMEL_AGGREGATOR_PENDING)
2126 .and_then(|v| v.as_bool()),
2127 Some(true),
2128 "exchange {} should be pending",
2129 i
2130 );
2131 }
2132 let _ = late_rx.try_recv();
2134 }
2135
2136 #[tokio::test]
2137 async fn evaluate_completion_predicate_or_combines_any() {
2138 use camel_api::aggregator::{CompletionCondition, CompletionMode};
2139 use camel_language_api::Language;
2140 use std::collections::HashMap;
2141
2142 let registry: SharedLanguageRegistry = Arc::new(std::sync::Mutex::new(HashMap::new()));
2144 registry.lock().unwrap().insert(
2145 "simple".to_string(),
2146 Arc::new(camel_language_simple::SimpleLanguage::new()) as Arc<dyn Language>,
2147 );
2148
2149 let incoming = make_exchange("k", "X", "hello");
2150
2151 let mode = CompletionMode::Any(vec![
2153 CompletionCondition::PredicateExpr {
2154 expr: "${body} == 'NOPE'".to_string(),
2155 language: "simple".to_string(),
2156 },
2157 CompletionCondition::PredicateExpr {
2158 expr: "${body} == 'hello'".to_string(),
2159 language: "simple".to_string(),
2160 },
2161 ]);
2162
2163 let satisfied = evaluate_completion_predicate(&mode, &incoming, ®istry)
2164 .await
2165 .expect("eval must succeed");
2166 assert!(satisfied, "second predicate matches → OR true");
2167 }
2168
2169 #[tokio::test]
2170 async fn evaluate_completion_predicate_no_predicate_skips_registry() {
2171 use camel_api::aggregator::{CompletionCondition, CompletionMode};
2172 use std::collections::HashMap;
2173
2174 let mode = CompletionMode::Single(CompletionCondition::Size(3));
2178 let registry: SharedLanguageRegistry = Arc::new(std::sync::Mutex::new(HashMap::new()));
2179 let incoming = make_exchange("k", "X", "hello");
2180 let result = evaluate_completion_predicate(&mode, &incoming, ®istry).await;
2181 assert!(result.is_ok(), "fast path must not error: {:?}", result);
2182 assert!(!result.unwrap(), "no predicate → not satisfied");
2183 }
2184
2185 #[tokio::test]
2186 async fn evaluate_completion_predicate_unregistered_language_errors() {
2187 use camel_api::aggregator::{CompletionCondition, CompletionMode};
2188
2189 let mode = CompletionMode::Single(CompletionCondition::PredicateExpr {
2190 expr: "${body} == 'DONE'".to_string(),
2191 language: "nonexistent-lang".to_string(),
2192 });
2193 let registry: SharedLanguageRegistry = Arc::new(std::sync::Mutex::new(HashMap::new()));
2194 let incoming = make_exchange("k", "X", "hello");
2195 let result = evaluate_completion_predicate(&mode, &incoming, ®istry).await;
2196 assert!(result.is_err(), "unregistered language must error");
2197 }
2198
2199 #[tokio::test]
2200 async fn evaluate_completion_predicate_all_miss_returns_false() {
2201 use camel_api::aggregator::{CompletionCondition, CompletionMode};
2202 use camel_language_api::Language;
2203 use std::collections::HashMap;
2204
2205 let registry: SharedLanguageRegistry = Arc::new(std::sync::Mutex::new(HashMap::new()));
2206 registry.lock().unwrap().insert(
2207 "simple".to_string(),
2208 Arc::new(camel_language_simple::SimpleLanguage::new()) as Arc<dyn Language>,
2209 );
2210
2211 let mode = CompletionMode::Single(CompletionCondition::PredicateExpr {
2212 expr: "${body} == 'NOPE'".to_string(),
2213 language: "simple".to_string(),
2214 });
2215 let incoming = make_exchange("k", "X", "hello");
2216 let result = evaluate_completion_predicate(&mode, &incoming, ®istry).await;
2217 assert!(result.is_ok(), "eval must succeed: {:?}", result);
2218 assert!(!result.unwrap(), "predicate does not match");
2219 }
2220
2221 #[tokio::test]
2222 async fn completion_predicate_expr_completes_on_match() {
2223 use camel_api::aggregator::{CompletionCondition, CompletionMode};
2224 use camel_language_api::Language;
2225 use std::collections::HashMap;
2226
2227 let registry: SharedLanguageRegistry = Arc::new(std::sync::Mutex::new(HashMap::new()));
2229 registry.lock().unwrap().insert(
2230 "simple".to_string(),
2231 Arc::new(camel_language_simple::SimpleLanguage::new()) as Arc<dyn Language>,
2232 );
2233
2234 let mut config = AggregatorConfig::correlate_by("key")
2239 .complete_when_size(999) .build()
2241 .expect("config builds");
2242 config.completion = CompletionMode::Single(CompletionCondition::PredicateExpr {
2243 expr: "${body} == 'DONE'".to_string(),
2244 language: "simple".to_string(),
2245 });
2246
2247 let mut svc = new_test_svc_with_registry(config, registry);
2248
2249 let r = svc
2251 .ready()
2252 .await
2253 .unwrap()
2254 .call(make_exchange("key", "K", "first"))
2255 .await
2256 .unwrap();
2257 assert!(r.property(CAMEL_AGGREGATOR_PENDING).is_some());
2258
2259 let r = svc
2261 .ready()
2262 .await
2263 .unwrap()
2264 .call(make_exchange("key", "K", "DONE"))
2265 .await
2266 .unwrap();
2267 assert!(r.property(CAMEL_AGGREGATOR_PENDING).is_none());
2268 assert_eq!(
2269 r.property(CAMEL_AGGREGATED_COMPLETION_REASON),
2270 Some(&serde_json::json!("predicate"))
2271 );
2272 }
2273
2274 #[tokio::test]
2286 async fn test_da1_strategy_receives_two_exchanges_first_message_preserved() {
2287 use camel_api::aggregator::{AggregationFn, AggregationStrategy};
2288 use std::sync::Arc;
2289
2290 let recorded: Arc<std::sync::Mutex<Vec<(String, String)>>> =
2291 Arc::new(std::sync::Mutex::new(Vec::new()));
2292 let recorded_for_closure = Arc::clone(&recorded);
2293
2294 let f: AggregationFn = Arc::new(move |old: Exchange, new: Exchange| {
2295 let old_body = old.input.body.as_text().unwrap_or("").to_string();
2296 let new_body = new.input.body.as_text().unwrap_or("").to_string();
2297 recorded_for_closure
2298 .lock()
2299 .expect("recorded mutex poisoned")
2300 .push((old_body, new_body));
2301 new
2302 });
2303
2304 let config = AggregatorConfig::correlate_by("k")
2305 .complete_when_size(2)
2306 .strategy(AggregationStrategy::Custom(f))
2307 .build()
2308 .unwrap();
2309 let mut svc = new_test_svc(config);
2310
2311 let first = svc
2313 .ready()
2314 .await
2315 .unwrap()
2316 .call(make_exchange("k", "1", "A"))
2317 .await
2318 .unwrap();
2319 assert!(
2320 first.property(CAMEL_AGGREGATOR_PENDING).is_some(),
2321 "first message must leave the bucket pending (size < 2)"
2322 );
2323 assert!(
2324 recorded.lock().expect("recorded mutex poisoned").is_empty(),
2325 "strategy must NOT be invoked on the first message of a bucket"
2326 );
2327
2328 let _completed = svc
2331 .ready()
2332 .await
2333 .unwrap()
2334 .call(make_exchange("k", "1", "B"))
2335 .await
2336 .unwrap();
2337
2338 let recorded = recorded.lock().expect("recorded mutex poisoned");
2339 assert_eq!(
2340 recorded.len(),
2341 1,
2342 "strategy must be invoked exactly once across the two-message bucket, got {recorded:?}"
2343 );
2344 assert_eq!(
2345 recorded[0],
2346 ("A".to_string(), "B".to_string()),
2347 "strategy must observe the first message as `old` and the second as `new`, \
2348 with both bodies preserved unchanged"
2349 );
2350 }
2351
2352 #[tokio::test]
2355 async fn sweep_shutdown_cancels_task() {
2356 let config = AggregatorConfig::correlate_by("key")
2357 .complete_when_size(10)
2358 .bucket_ttl(Duration::from_millis(100))
2359 .build()
2360 .unwrap();
2361 let (tx, _rx) = mpsc::channel(256);
2362 let registry: SharedLanguageRegistry = Arc::new(std::sync::Mutex::new(HashMap::new()));
2363 let cancel = CancellationToken::new();
2364 let mut svc = AggregatorService::new(config, tx, registry, cancel);
2365
2366 let _ = svc.ready().await.unwrap();
2367
2368 assert!(
2369 svc.sweep_handle
2370 .lock()
2371 .unwrap_or_else(|e| e.into_inner())
2372 .is_some(),
2373 "sweep handle should be Some after poll_ready"
2374 );
2375
2376 svc.shutdown(StepShutdownReason::RouteStop).await.unwrap();
2377
2378 assert!(
2379 svc.sweep_handle
2380 .lock()
2381 .unwrap_or_else(|e| e.into_inner())
2382 .is_none(),
2383 "sweep handle should be None after shutdown (taken + aborted)"
2384 );
2385 assert!(
2386 svc.sweep_cancel
2387 .lock()
2388 .unwrap_or_else(|e| e.into_inner())
2389 .is_cancelled(),
2390 "sweep_cancel token should be cancelled after shutdown"
2391 );
2392
2393 tokio::time::sleep(Duration::from_millis(50)).await;
2394 }
2395
2396 #[tokio::test]
2397 async fn sweep_start_respawns_after_shutdown() {
2398 let config = AggregatorConfig::correlate_by("key")
2399 .complete_when_size(10)
2400 .bucket_ttl(Duration::from_millis(100))
2401 .build()
2402 .unwrap();
2403 let (tx, _rx) = mpsc::channel(256);
2404 let registry: SharedLanguageRegistry = Arc::new(std::sync::Mutex::new(HashMap::new()));
2405 let cancel = CancellationToken::new();
2406 let mut svc = AggregatorService::new(config, tx, registry, cancel);
2407
2408 let _ = svc.ready().await.unwrap();
2409 svc.shutdown(StepShutdownReason::RouteStop).await.unwrap();
2410
2411 svc.start().await.unwrap();
2412 let _ = svc.ready().await.unwrap();
2413
2414 assert!(
2415 svc.sweep_handle
2416 .lock()
2417 .unwrap_or_else(|e| e.into_inner())
2418 .is_some(),
2419 "sweep handle should be Some after start + poll_ready"
2420 );
2421 assert!(
2422 !svc.sweep_cancel
2423 .lock()
2424 .unwrap_or_else(|e| e.into_inner())
2425 .is_cancelled(),
2426 "sweep_cancel should be a fresh uncancelled token after start"
2427 );
2428 }
2429
2430 #[tokio::test]
2431 async fn sweep_shutdown_hotswap_cancels_task() {
2432 let config = AggregatorConfig::correlate_by("key")
2433 .complete_when_size(10)
2434 .bucket_ttl(Duration::from_millis(100))
2435 .build()
2436 .unwrap();
2437 let (tx, _rx) = mpsc::channel(256);
2438 let registry: SharedLanguageRegistry = Arc::new(std::sync::Mutex::new(HashMap::new()));
2439 let cancel = CancellationToken::new();
2440 let mut svc = AggregatorService::new(config, tx, registry, cancel);
2441
2442 let _ = svc.ready().await.unwrap();
2443
2444 assert!(
2445 svc.sweep_handle
2446 .lock()
2447 .unwrap_or_else(|e| e.into_inner())
2448 .is_some(),
2449 "sweep handle should be Some after poll_ready"
2450 );
2451
2452 svc.shutdown(StepShutdownReason::HotSwap).await.unwrap();
2453
2454 assert!(
2455 svc.sweep_handle
2456 .lock()
2457 .unwrap_or_else(|e| e.into_inner())
2458 .is_none(),
2459 "sweep handle should be None after HotSwap shutdown"
2460 );
2461 assert!(
2462 svc.sweep_cancel
2463 .lock()
2464 .unwrap_or_else(|e| e.into_inner())
2465 .is_cancelled(),
2466 "sweep_cancel token should be cancelled after HotSwap shutdown"
2467 );
2468
2469 tokio::time::sleep(Duration::from_millis(50)).await;
2470 }
2471
2472 #[tokio::test]
2480 async fn constant_key_skips_reserialization() {
2481 let mut svc = new_test_svc(config_size(3));
2482 let mut result = None;
2483 for body in ["first", "second", "third"] {
2484 result = Some(
2485 svc.ready()
2486 .await
2487 .unwrap()
2488 .call(make_exchange("orderId", "A", body))
2489 .await
2490 .unwrap(),
2491 );
2492 }
2493 let result = result.unwrap();
2494 assert_eq!(
2495 result.property(CAMEL_AGGREGATED_SIZE),
2496 Some(&serde_json::json!(3u64)),
2497 "all 3 fragments must aggregate out of a single bucket"
2498 );
2499 assert!(svc.buckets.lock().unwrap().is_empty());
2500 assert_eq!(
2501 svc.key_serializations
2502 .load(std::sync::atomic::Ordering::Relaxed),
2503 1,
2504 "constant scalar key must serialize exactly once"
2505 );
2506 }
2507
2508 #[tokio::test]
2512 async fn divergent_keys_keep_serde_semantics() {
2513 let mut svc = new_test_svc(config_size(10)); for key in ["k1", "k2", "k1"] {
2515 svc.ready()
2516 .await
2517 .unwrap()
2518 .call(make_exchange("orderId", key, "body"))
2519 .await
2520 .unwrap();
2521 }
2522 let guard = svc.buckets.lock().unwrap();
2523 assert_eq!(guard.len(), 2, "k1/k2/k1 → exactly two buckets");
2524 for key in ["k1", "k2"] {
2525 let expected = serde_json::to_string(&serde_json::json!(key)).unwrap();
2526 assert!(
2527 guard.contains_key(expected.as_str()),
2528 "bucket name must be byte-identical to serde_json::to_string: \
2529 expected {expected}, have {:?}",
2530 guard.keys().collect::<Vec<_>>()
2531 );
2532 }
2533 }
2534
2535 #[tokio::test]
2543 async fn object_keys_bypass_cache() {
2544 let mut svc = new_test_svc(config_size(10)); let obj_a = serde_json::json!({"a": 1, "b": 2});
2546 let obj_b = serde_json::json!({"b": 2, "a": 1});
2547 for obj in [&obj_a, &obj_b, &obj_a] {
2548 let mut msg = Message {
2549 headers: Default::default(),
2550 body: Body::Text("body".into()),
2551 };
2552 msg.headers.insert("orderId".to_string(), obj.clone());
2553 svc.ready()
2554 .await
2555 .unwrap()
2556 .call(Exchange::new(msg))
2557 .await
2558 .unwrap();
2559 }
2560 let expected_keys: std::collections::HashSet<String> = [&obj_a, &obj_b]
2561 .into_iter()
2562 .map(|o| serde_json::to_string(o).unwrap())
2563 .collect();
2564 let guard = svc.buckets.lock().unwrap();
2565 let actual_keys: std::collections::HashSet<String> = guard.keys().cloned().collect();
2566 assert_eq!(
2567 actual_keys, expected_keys,
2568 "object keys must bucket exactly per serde_json::to_string (cache bypassed)"
2569 );
2570 assert_eq!(
2571 svc.key_serializations
2572 .load(std::sync::atomic::Ordering::Relaxed),
2573 3,
2574 "each object fragment must serialize (memo never consulted for objects)"
2575 );
2576 }
2577
2578 #[tokio::test]
2584 async fn float_zero_sign_keys_stay_distinct() {
2585 let mut svc = new_test_svc(config_size(10)); for key in [0.0_f64, -0.0, 0.0] {
2587 let mut msg = Message {
2588 headers: Default::default(),
2589 body: Body::Text("body".into()),
2590 };
2591 msg.headers
2592 .insert("orderId".to_string(), serde_json::json!(key));
2593 svc.ready()
2594 .await
2595 .unwrap()
2596 .call(Exchange::new(msg))
2597 .await
2598 .unwrap();
2599 }
2600 let guard = svc.buckets.lock().unwrap();
2601 assert_eq!(guard.len(), 2, "±0.0 must stay in distinct buckets");
2602 for key in [0.0_f64, -0.0] {
2603 let expected = serde_json::to_string(&serde_json::json!(key)).unwrap();
2604 assert!(
2605 guard.contains_key(expected.as_str()),
2606 "bucket name must be byte-identical to serde_json::to_string: \
2607 expected {expected}, have {:?}",
2608 guard.keys().collect::<Vec<_>>()
2609 );
2610 }
2611 assert_eq!(
2612 svc.key_serializations
2613 .load(std::sync::atomic::Ordering::Relaxed),
2614 3,
2615 "number keys serialize per fragment (never memoized)"
2616 );
2617 }
2618}