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 set_timeout_tx(&self, tx: tokio::sync::mpsc::Sender<Exchange>) {
340 self.set_driver_tx(tx);
341 }
342}
343
344#[cfg(test)]
347mod tests {
348 use super::*;
349 use camel_api::exchange::ExchangePattern;
350 use camel_api::message::Message;
351
352 struct PropExpr(String);
354
355 #[async_trait::async_trait]
356 impl Expression for PropExpr {
357 async fn evaluate(
358 &self,
359 exchange: &Exchange,
360 ) -> Result<serde_json::Value, camel_language_api::LanguageError> {
361 Ok(exchange
362 .property(&self.0)
363 .cloned()
364 .unwrap_or(serde_json::Value::Null))
365 }
366 }
367
368 struct ConstExpr(String);
370
371 #[async_trait::async_trait]
372 impl Expression for ConstExpr {
373 async fn evaluate(
374 &self,
375 _exchange: &Exchange,
376 ) -> Result<serde_json::Value, camel_language_api::LanguageError> {
377 Ok(serde_json::Value::String(self.0.clone()))
378 }
379 }
380
381 struct FailingExpr;
383
384 #[async_trait::async_trait]
385 impl Expression for FailingExpr {
386 async fn evaluate(
387 &self,
388 _exchange: &Exchange,
389 ) -> Result<serde_json::Value, camel_language_api::LanguageError> {
390 Err(camel_language_api::LanguageError::EvalError(
391 "mock eval failure".into(),
392 ))
393 }
394 }
395
396 fn mk_exchange(seq: i64) -> Exchange {
397 let mut ex = Exchange::new(Message::new(camel_api::body::Body::Text(format!(
398 "msg-{seq}"
399 ))));
400 ex.set_property("seq", serde_json::json!(seq));
401 ex.pattern = ExchangePattern::InOnly;
402 ex
403 }
404
405 fn mk_exchange_with_key(seq: i64, key_prop: &str, key_val: &str) -> Exchange {
406 let mut ex = Exchange::new(Message::new(camel_api::body::Body::Text(format!(
407 "msg-{seq}"
408 ))));
409 ex.set_property("seq", serde_json::json!(seq));
410 ex.set_property(key_prop, serde_json::Value::String(key_val.to_string()));
411 ex.pattern = ExchangePattern::InOnly;
412 ex
413 }
414
415 #[tokio::test]
418 async fn batch_size_completion_emits_sorted_burst() {
419 let policy = BatchPolicy::new_cyclic(
420 Arc::new(ConstExpr("same".into())),
421 Arc::new(PropExpr("seq".into())),
422 BatchCompletion::Size(3),
423 );
424
425 assert!(policy.accept(mk_exchange(3)).await.is_empty());
426 assert!(policy.accept(mk_exchange(1)).await.is_empty());
427
428 let emitted = policy.accept(mk_exchange(2)).await;
429 assert_eq!(emitted.len(), 3, "should emit all 3 on completion");
430 let seqs: Vec<i64> = emitted
431 .iter()
432 .map(|ex| ex.property("seq").and_then(|v| v.as_i64()).unwrap_or(-1))
433 .collect();
434 assert_eq!(seqs, vec![1, 2, 3], "should be sorted ascending");
435 }
436
437 #[tokio::test]
440 async fn batch_timeout_completion_emits_after_timeout() {
441 let policy = BatchPolicy::new_cyclic(
442 Arc::new(ConstExpr("same".into())),
443 Arc::new(PropExpr("seq".into())),
444 BatchCompletion::Timeout(50),
445 );
446
447 let (tx, mut rx) = mpsc::channel::<Exchange>(16);
448 policy.set_driver_tx(tx);
449
450 assert!(policy.accept(mk_exchange(3)).await.is_empty());
451 assert!(policy.accept(mk_exchange(1)).await.is_empty());
452
453 let emitted: Vec<Exchange> = tokio::time::timeout(Duration::from_millis(500), async {
454 let mut out = Vec::new();
455 out.push(rx.recv().await.unwrap());
456 out.push(rx.recv().await.unwrap());
457 out
458 })
459 .await
460 .expect("timeout should fire within 500ms");
461
462 assert_eq!(emitted.len(), 2);
463 let seqs: Vec<i64> = emitted
464 .iter()
465 .map(|ex| ex.property("seq").and_then(|v| v.as_i64()).unwrap_or(-1))
466 .collect();
467 assert_eq!(seqs, vec![1, 3], "should be sorted ascending");
468 }
469
470 #[tokio::test]
472 async fn batch_size_or_timeout_size_wins() {
473 let policy = BatchPolicy::new_cyclic(
474 Arc::new(ConstExpr("same".into())),
475 Arc::new(PropExpr("seq".into())),
476 BatchCompletion::SizeOrTimeout(3, 5_000),
477 );
478
479 assert!(policy.accept(mk_exchange(2)).await.is_empty());
480 assert!(policy.accept(mk_exchange(1)).await.is_empty());
481
482 let emitted = policy.accept(mk_exchange(3)).await;
483 assert_eq!(emitted.len(), 3);
484 let seqs: Vec<i64> = emitted
485 .iter()
486 .map(|ex| ex.property("seq").and_then(|v| v.as_i64()).unwrap_or(-1))
487 .collect();
488 assert_eq!(seqs, vec![1, 2, 3]);
489 }
490
491 #[tokio::test]
493 async fn batch_multi_key_independence() {
494 let policy = BatchPolicy::new_cyclic(
495 Arc::new(PropExpr("region".into())),
496 Arc::new(PropExpr("seq".into())),
497 BatchCompletion::Size(2),
498 );
499
500 let _ = policy
501 .accept(mk_exchange_with_key(2, "region", "east"))
502 .await;
503 let east_emit = policy
504 .accept(mk_exchange_with_key(1, "region", "east"))
505 .await;
506 assert_eq!(east_emit.len(), 2, "east bucket should complete at size 2");
507
508 let west_result = policy
509 .accept(mk_exchange_with_key(3, "region", "west"))
510 .await;
511 assert!(
512 west_result.is_empty(),
513 "west bucket should NOT complete yet"
514 );
515 }
516
517 #[tokio::test]
520 async fn batch_flush_emits_remaining_sorted() {
521 let policy = BatchPolicy::new_cyclic(
522 Arc::new(ConstExpr("same".into())),
523 Arc::new(PropExpr("seq".into())),
524 BatchCompletion::Size(10),
525 );
526
527 assert!(policy.accept(mk_exchange(5)).await.is_empty());
528 assert!(policy.accept(mk_exchange(3)).await.is_empty());
529 assert!(policy.accept(mk_exchange(1)).await.is_empty());
530
531 let flushed = policy.flush().await;
532 assert_eq!(flushed.len(), 3);
533 let seqs: Vec<i64> = flushed
534 .iter()
535 .map(|ex| ex.property("seq").and_then(|v| v.as_i64()).unwrap_or(-1))
536 .collect();
537 assert_eq!(seqs, vec![1, 3, 5]);
538 }
539
540 #[tokio::test]
543 async fn batch_correlation_eval_failure_returns_empty() {
544 let policy = BatchPolicy::new_cyclic(
545 Arc::new(FailingExpr),
546 Arc::new(PropExpr("seq".into())),
547 BatchCompletion::Size(2),
548 );
549
550 let result = policy.accept(mk_exchange(1)).await;
551 assert!(
552 result.is_empty(),
553 "failed correlation should return empty vec, not crash"
554 );
555 }
556
557 #[tokio::test]
559 async fn batch_pure_size_no_timeout_needed() {
560 let policy = BatchPolicy::new_cyclic(
561 Arc::new(ConstExpr("same".into())),
562 Arc::new(PropExpr("seq".into())),
563 BatchCompletion::Size(2),
564 );
565
566 assert!(!policy.needs_timeout());
567 }
568}