camel_processor/resequencer/
batch.rs1use std::collections::HashMap;
5use std::sync::atomic::{AtomicBool, Ordering};
6use std::sync::{Arc, Mutex, Weak};
7use std::time::Duration;
8
9use async_trait::async_trait;
10use camel_api::exchange::Exchange;
11use camel_api::resequencer::BatchCompletion;
12use camel_api::value::cmp_values;
13use camel_language_api::Expression;
14use tokio::sync::mpsc;
15use tokio::task::JoinHandle;
16use tokio_util::sync::CancellationToken;
17
18use super::ResequencePolicy;
19
20#[derive(Default)]
22struct Bucket {
23 exchanges: Vec<Exchange>,
24}
25
26pub struct BatchPolicy {
33 correlation_expr: Arc<dyn Expression>,
34 sort_expr: Arc<dyn Expression>,
35 completion: BatchCompletion,
36
37 weak_self: Weak<Self>,
39
40 buckets: Mutex<HashMap<String, Bucket>>,
42
43 timeout_tokens: Mutex<HashMap<String, CancellationToken>>,
45
46 timeout_handles: Mutex<HashMap<String, JoinHandle<()>>>,
48
49 driver_tx: Mutex<Option<mpsc::Sender<Exchange>>>,
52
53 shutdown_started: AtomicBool,
56}
57
58impl BatchPolicy {
59 pub fn new_cyclic(
62 correlation_expr: Arc<dyn Expression>,
63 sort_expr: Arc<dyn Expression>,
64 completion: BatchCompletion,
65 ) -> Arc<Self> {
66 Arc::new_cyclic(|weak| Self {
67 correlation_expr,
68 sort_expr,
69 completion,
70 weak_self: weak.clone(),
71 buckets: Mutex::new(HashMap::new()),
72 timeout_tokens: Mutex::new(HashMap::new()),
73 timeout_handles: Mutex::new(HashMap::new()),
74 driver_tx: Mutex::new(None),
75 shutdown_started: AtomicBool::new(false),
76 })
77 }
78
79 fn set_driver_tx(&self, tx: mpsc::Sender<Exchange>) {
82 let mut guard = self.driver_tx.lock().unwrap_or_else(|e| e.into_inner());
83 *guard = Some(tx);
84 }
85
86 async fn eval_key(&self, exchange: &Exchange) -> Result<String, String> {
88 self.correlation_expr
89 .evaluate(exchange)
90 .await
91 .map(|v| match v {
94 serde_json::Value::String(s) => s,
95 other => other.to_string(),
96 })
97 .map_err(|e| format!("correlation expression evaluation failed: {e}"))
98 }
99
100 async fn drain_and_sort(&self, mut bucket: Bucket) -> Vec<Exchange> {
102 let mut indexed: Vec<(serde_json::Value, Exchange)> = Vec::new();
103 for ex in bucket.exchanges.drain(..) {
104 let val = self
105 .sort_expr
106 .evaluate(&ex)
107 .await
108 .unwrap_or(serde_json::Value::Null);
109 indexed.push((val, ex));
110 }
111 indexed.sort_by(|a, b| cmp_values(&a.0, &b.0));
112 indexed.into_iter().map(|(_, ex)| ex).collect()
113 }
114
115 fn is_complete_by_size(&self, count: usize) -> bool {
117 match self.completion {
118 BatchCompletion::Size(s) => count >= s,
119 BatchCompletion::SizeOrTimeout(s, _) => count >= s,
120 _ => false,
122 }
123 }
124
125 fn needs_timeout(&self) -> bool {
127 matches!(
128 self.completion,
129 BatchCompletion::Timeout(_) | BatchCompletion::SizeOrTimeout(..)
130 )
131 }
132
133 fn take_bucket(&self, key: &str) -> Option<Bucket> {
135 let mut buckets = self.buckets.lock().unwrap_or_else(|e| e.into_inner());
136 buckets.remove(key)
137 }
138
139 fn cancel_timeout(&self, key: &str) {
141 {
142 let mut tokens = self
143 .timeout_tokens
144 .lock()
145 .unwrap_or_else(|e| e.into_inner());
146 if let Some(token) = tokens.remove(key) {
147 token.cancel();
148 }
149 }
150 {
151 let mut handles = self
152 .timeout_handles
153 .lock()
154 .unwrap_or_else(|e| e.into_inner());
155 handles.remove(key);
156 }
157 }
158
159 fn spawn_timeout_task(&self, key: String, timeout_ms: u64) {
162 let cancel = CancellationToken::new();
163 let cancel_clone = cancel.clone();
164
165 {
167 let mut tokens = self
168 .timeout_tokens
169 .lock()
170 .unwrap_or_else(|e| e.into_inner());
171 tokens.insert(key.clone(), cancel);
172 }
173
174 let weak = self.weak_self.clone();
175 let key_clone = key.clone();
176 let driver_tx_opt = {
177 let guard = self.driver_tx.lock().unwrap_or_else(|e| e.into_inner());
178 guard.clone()
179 };
180
181 let handle = tokio::spawn(async move {
182 let timeout = Duration::from_millis(timeout_ms);
183
184 tokio::select! {
185 _ = tokio::time::sleep(timeout) => {
186 if cancel_clone.is_cancelled() {
187 return;
188 }
189 }
190 _ = cancel_clone.cancelled() => {
191 return;
192 }
193 }
194
195 let Some(policy) = weak.upgrade() else {
197 return;
198 };
199
200 if policy.shutdown_started.load(Ordering::SeqCst) {
202 return;
203 }
204
205 let bucket = policy.take_bucket(&key_clone);
207 let Some(bucket) = bucket else {
208 return; };
210
211 let sorted = policy.drain_and_sort(bucket).await;
212
213 if let Some(tx) = driver_tx_opt {
215 for ex in sorted {
216 if tx.send(ex).await.is_err() {
217 tracing::debug!(
218 key = %key_clone,
219 "BatchPolicy timeout: driver channel closed during emission"
220 );
221 break;
222 }
223 }
224 }
225
226 {
228 let mut handles = policy
229 .timeout_handles
230 .lock()
231 .unwrap_or_else(|e| e.into_inner());
232 handles.remove(&key_clone);
233 }
234 });
235
236 {
237 let mut handles = self
238 .timeout_handles
239 .lock()
240 .unwrap_or_else(|e| e.into_inner());
241 handles.insert(key, handle);
242 }
243 }
244}
245
246#[async_trait]
247impl ResequencePolicy for BatchPolicy {
248 async fn accept(&self, input: Exchange) -> Vec<Exchange> {
249 let correlation_id = input.correlation_id().to_owned();
250 let key = match self.eval_key(&input).await {
251 Ok(k) => k,
252 Err(e) => {
253 tracing::warn!(
255 error = %e,
256 correlation_id = %correlation_id,
257 "BatchPolicy: correlation expression failed, dropping exchange"
258 );
259 return vec![];
260 }
261 };
262
263 let bucket_count = {
264 let mut buckets = self.buckets.lock().unwrap_or_else(|e| e.into_inner());
265 let bucket = buckets.entry(key.clone()).or_default();
266 bucket.exchanges.push(input);
267 bucket.exchanges.len()
268 };
269
270 if bucket_count == 1 && self.needs_timeout() {
272 let timeout_ms = match self.completion {
273 BatchCompletion::Timeout(t) | BatchCompletion::SizeOrTimeout(_, t) => t,
274 _ => unreachable!(),
275 };
276 self.spawn_timeout_task(key.clone(), timeout_ms);
277 }
278
279 if self.is_complete_by_size(bucket_count) {
281 self.cancel_timeout(&key);
282 if let Some(bucket) = self.take_bucket(&key) {
283 return self.drain_and_sort(bucket).await;
284 }
285 }
286
287 vec![]
288 }
289
290 async fn flush(&self) -> Vec<Exchange> {
291 self.shutdown_started.store(true, Ordering::SeqCst);
293
294 let all_keys: Vec<String> = {
295 let buckets = self.buckets.lock().unwrap_or_else(|e| e.into_inner());
296 buckets.keys().cloned().collect()
297 };
298
299 let mut all_sorted = Vec::new();
300 for key in &all_keys {
301 self.cancel_timeout(key);
302 if let Some(bucket) = self.take_bucket(key) {
303 let sorted = self.drain_and_sort(bucket).await;
304 all_sorted.extend(sorted);
305 }
306 }
307
308 {
310 let tokens: HashMap<String, CancellationToken> = {
311 let mut guard = self
312 .timeout_tokens
313 .lock()
314 .unwrap_or_else(|e| e.into_inner());
315 std::mem::take(&mut *guard)
316 };
317 for (_, token) in tokens {
318 token.cancel();
319 }
320 }
321 {
323 let _handles = {
324 let mut guard = self
325 .timeout_handles
326 .lock()
327 .unwrap_or_else(|e| e.into_inner());
328 std::mem::take(&mut *guard)
329 };
330 }
331
332 all_sorted
333 }
334
335 fn name(&self) -> &'static str {
336 "batch-resequencer"
337 }
338
339 fn buffered(&self) -> usize {
340 let buckets = self.buckets.lock().unwrap_or_else(|e| e.into_inner());
341 buckets.values().map(|b| b.exchanges.len()).sum()
342 }
343
344 fn set_timeout_tx(&self, tx: tokio::sync::mpsc::Sender<Exchange>) {
345 self.set_driver_tx(tx);
346 }
347}
348
349#[cfg(test)]
352mod tests {
353 use super::*;
354 use camel_api::exchange::ExchangePattern;
355 use camel_api::message::Message;
356
357 struct PropExpr(String);
359
360 #[async_trait::async_trait]
361 impl Expression for PropExpr {
362 async fn evaluate(
363 &self,
364 exchange: &Exchange,
365 ) -> Result<serde_json::Value, camel_language_api::LanguageError> {
366 Ok(exchange
367 .property(&self.0)
368 .cloned()
369 .unwrap_or(serde_json::Value::Null))
370 }
371 }
372
373 struct ConstExpr(String);
375
376 #[async_trait::async_trait]
377 impl Expression for ConstExpr {
378 async fn evaluate(
379 &self,
380 _exchange: &Exchange,
381 ) -> Result<serde_json::Value, camel_language_api::LanguageError> {
382 Ok(serde_json::Value::String(self.0.clone()))
383 }
384 }
385
386 struct FailingExpr;
388
389 #[async_trait::async_trait]
390 impl Expression for FailingExpr {
391 async fn evaluate(
392 &self,
393 _exchange: &Exchange,
394 ) -> Result<serde_json::Value, camel_language_api::LanguageError> {
395 Err(camel_language_api::LanguageError::EvalError(
396 "mock eval failure".into(),
397 ))
398 }
399 }
400
401 fn mk_exchange(seq: i64) -> Exchange {
402 let mut ex = Exchange::new(Message::new(camel_api::body::Body::Text(format!(
403 "msg-{seq}"
404 ))));
405 ex.set_property("seq", serde_json::json!(seq));
406 ex.pattern = ExchangePattern::InOnly;
407 ex
408 }
409
410 fn mk_exchange_with_key(seq: i64, key_prop: &str, key_val: &str) -> Exchange {
411 let mut ex = Exchange::new(Message::new(camel_api::body::Body::Text(format!(
412 "msg-{seq}"
413 ))));
414 ex.set_property("seq", serde_json::json!(seq));
415 ex.set_property(key_prop, serde_json::Value::String(key_val.to_string()));
416 ex.pattern = ExchangePattern::InOnly;
417 ex
418 }
419
420 #[tokio::test]
423 async fn batch_size_completion_emits_sorted_burst() {
424 let policy = BatchPolicy::new_cyclic(
425 Arc::new(ConstExpr("same".into())),
426 Arc::new(PropExpr("seq".into())),
427 BatchCompletion::Size(3),
428 );
429
430 assert!(policy.accept(mk_exchange(3)).await.is_empty());
431 assert!(policy.accept(mk_exchange(1)).await.is_empty());
432
433 let emitted = policy.accept(mk_exchange(2)).await;
434 assert_eq!(emitted.len(), 3, "should emit all 3 on completion");
435 let seqs: Vec<i64> = emitted
436 .iter()
437 .map(|ex| ex.property("seq").and_then(|v| v.as_i64()).unwrap_or(-1))
438 .collect();
439 assert_eq!(seqs, vec![1, 2, 3], "should be sorted ascending");
440 }
441
442 #[tokio::test]
445 async fn batch_timeout_completion_emits_after_timeout() {
446 let policy = BatchPolicy::new_cyclic(
447 Arc::new(ConstExpr("same".into())),
448 Arc::new(PropExpr("seq".into())),
449 BatchCompletion::Timeout(50),
450 );
451
452 let (tx, mut rx) = mpsc::channel::<Exchange>(16);
453 policy.set_driver_tx(tx);
454
455 assert!(policy.accept(mk_exchange(3)).await.is_empty());
456 assert!(policy.accept(mk_exchange(1)).await.is_empty());
457
458 let emitted: Vec<Exchange> = tokio::time::timeout(Duration::from_millis(500), async {
459 let mut out = Vec::new();
460 out.push(rx.recv().await.unwrap());
461 out.push(rx.recv().await.unwrap());
462 out
463 })
464 .await
465 .expect("timeout should fire within 500ms");
466
467 assert_eq!(emitted.len(), 2);
468 let seqs: Vec<i64> = emitted
469 .iter()
470 .map(|ex| ex.property("seq").and_then(|v| v.as_i64()).unwrap_or(-1))
471 .collect();
472 assert_eq!(seqs, vec![1, 3], "should be sorted ascending");
473 }
474
475 #[tokio::test]
477 async fn batch_size_or_timeout_size_wins() {
478 let policy = BatchPolicy::new_cyclic(
479 Arc::new(ConstExpr("same".into())),
480 Arc::new(PropExpr("seq".into())),
481 BatchCompletion::SizeOrTimeout(3, 5_000),
482 );
483
484 assert!(policy.accept(mk_exchange(2)).await.is_empty());
485 assert!(policy.accept(mk_exchange(1)).await.is_empty());
486
487 let emitted = policy.accept(mk_exchange(3)).await;
488 assert_eq!(emitted.len(), 3);
489 let seqs: Vec<i64> = emitted
490 .iter()
491 .map(|ex| ex.property("seq").and_then(|v| v.as_i64()).unwrap_or(-1))
492 .collect();
493 assert_eq!(seqs, vec![1, 2, 3]);
494 }
495
496 #[tokio::test]
498 async fn batch_multi_key_independence() {
499 let policy = BatchPolicy::new_cyclic(
500 Arc::new(PropExpr("region".into())),
501 Arc::new(PropExpr("seq".into())),
502 BatchCompletion::Size(2),
503 );
504
505 let _ = policy
506 .accept(mk_exchange_with_key(2, "region", "east"))
507 .await;
508 let east_emit = policy
509 .accept(mk_exchange_with_key(1, "region", "east"))
510 .await;
511 assert_eq!(east_emit.len(), 2, "east bucket should complete at size 2");
512
513 let west_result = policy
514 .accept(mk_exchange_with_key(3, "region", "west"))
515 .await;
516 assert!(
517 west_result.is_empty(),
518 "west bucket should NOT complete yet"
519 );
520 }
521
522 #[tokio::test]
525 async fn batch_flush_emits_remaining_sorted() {
526 let policy = BatchPolicy::new_cyclic(
527 Arc::new(ConstExpr("same".into())),
528 Arc::new(PropExpr("seq".into())),
529 BatchCompletion::Size(10),
530 );
531
532 assert!(policy.accept(mk_exchange(5)).await.is_empty());
533 assert!(policy.accept(mk_exchange(3)).await.is_empty());
534 assert!(policy.accept(mk_exchange(1)).await.is_empty());
535
536 let flushed = policy.flush().await;
537 assert_eq!(flushed.len(), 3);
538 let seqs: Vec<i64> = flushed
539 .iter()
540 .map(|ex| ex.property("seq").and_then(|v| v.as_i64()).unwrap_or(-1))
541 .collect();
542 assert_eq!(seqs, vec![1, 3, 5]);
543 }
544
545 #[tokio::test]
548 async fn batch_correlation_eval_failure_returns_empty() {
549 let policy = BatchPolicy::new_cyclic(
550 Arc::new(FailingExpr),
551 Arc::new(PropExpr("seq".into())),
552 BatchCompletion::Size(2),
553 );
554
555 let result = policy.accept(mk_exchange(1)).await;
556 assert!(
557 result.is_empty(),
558 "failed correlation should return empty vec, not crash"
559 );
560 }
561
562 #[tokio::test]
564 async fn batch_pure_size_no_timeout_needed() {
565 let policy = BatchPolicy::new_cyclic(
566 Arc::new(ConstExpr("same".into())),
567 Arc::new(PropExpr("seq".into())),
568 BatchCompletion::Size(2),
569 );
570
571 assert!(!policy.needs_timeout());
572 }
573}