camel_processor/resequencer/
mod.rs1use std::sync::atomic::{AtomicU64, Ordering};
11use std::sync::{Arc, Mutex};
12use std::time::{Duration, Instant};
13
14use async_trait::async_trait;
15use tokio::sync::{Mutex as TokioMutex, mpsc};
16use tokio::task::JoinHandle;
17use tower::Service;
18
19pub mod batch;
20pub mod stream;
21
22use camel_api::{
23 BoxProcessor, CamelError, MetricsCollector, StepLifecycle, StepShutdownReason,
24 exchange::Exchange, message::Message, processor::SyncBoxProcessor,
25};
26
27const INOUT_WARN_INTERVAL: Duration = Duration::from_secs(30);
29
30#[derive(Clone, Default)]
32pub struct ResequencerConfig {
33 pub allow_inout: bool,
36 pub metrics: Option<Arc<dyn MetricsCollector>>,
38 pub route_id: Option<String>,
40}
41
42impl std::fmt::Debug for ResequencerConfig {
43 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
44 f.debug_struct("ResequencerConfig")
45 .field("allow_inout", &self.allow_inout)
46 .field("metrics", &self.metrics.as_ref().map(|_| "<metrics>"))
47 .field("route_id", &self.route_id)
48 .finish()
49 }
50}
51
52pub const CAMEL_RESEQUENCER_ACCEPTED: &str = "CamelResequencerAccepted";
56
57pub const CAMEL_RESEQUENCER_DROPPED: &str = "CamelResequencerDropped";
59
60pub const CAMEL_RESEQUENCER_INOUT_WARN: &str = "CamelResequencerInoutWarn";
62
63#[async_trait]
69pub trait ResequencePolicy: Send + Sync + 'static {
70 async fn accept(&self, input: Exchange) -> Vec<Exchange>;
72
73 async fn flush(&self) -> Vec<Exchange>;
75
76 fn name(&self) -> &'static str;
78
79 fn buffered(&self) -> usize {
84 0
85 }
86
87 fn set_timeout_tx(&self, _tx: tokio::sync::mpsc::Sender<Exchange>) {}
91}
92
93#[derive(Clone)]
101pub struct ResequencerService {
102 policy: Arc<dyn ResequencePolicy>,
103 config: ResequencerConfig,
104 input_tx: Arc<Mutex<Option<mpsc::Sender<Exchange>>>>,
107 driver_tx: Arc<Mutex<Option<mpsc::Sender<Exchange>>>>,
111 actor_handle: Arc<Mutex<Option<JoinHandle<()>>>>,
112 driver_handle: Arc<Mutex<Option<JoinHandle<()>>>>,
113 shutdown_started: Arc<Mutex<bool>>,
114 post_lifecycles: Arc<Mutex<Vec<Arc<dyn StepLifecycle>>>>,
117 inout_counter: Arc<AtomicU64>,
119 last_inout_warn: Arc<TokioMutex<Option<Instant>>>,
121 metrics: Option<Arc<dyn MetricsCollector>>,
123 route_id: Option<String>,
125}
126
127impl std::fmt::Debug for ResequencerService {
128 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
129 f.debug_struct("ResequencerService")
130 .field("policy", &self.policy.name())
131 .finish_non_exhaustive()
132 }
133}
134
135impl ResequencerService {
136 pub fn new(
148 policy: Arc<dyn ResequencePolicy>,
149 post_continuation: BoxProcessor,
150 input_capacity: usize,
151 post_lifecycles: Vec<Arc<dyn StepLifecycle>>,
152 ) -> Self {
153 Self::with_config(
154 policy,
155 post_continuation,
156 input_capacity,
157 post_lifecycles,
158 ResequencerConfig::default(),
159 )
160 }
161
162 pub fn with_config(
168 policy: Arc<dyn ResequencePolicy>,
169 post_continuation: BoxProcessor,
170 input_capacity: usize,
171 post_lifecycles: Vec<Arc<dyn StepLifecycle>>,
172 config: ResequencerConfig,
173 ) -> Self {
174 let (input_tx, mut input_rx) = mpsc::channel::<Exchange>(input_capacity);
176
177 let (driver_tx, mut driver_rx) = mpsc::channel::<Exchange>(input_capacity);
179
180 let input_tx_shared: Arc<Mutex<Option<mpsc::Sender<Exchange>>>> =
182 Arc::new(Mutex::new(Some(input_tx)));
183 let driver_tx_shared: Arc<Mutex<Option<mpsc::Sender<Exchange>>>> =
184 Arc::new(Mutex::new(Some(driver_tx.clone()))); let actor_handle: Arc<Mutex<Option<JoinHandle<()>>>> = Arc::new(Mutex::new(None));
186 let driver_handle: Arc<Mutex<Option<JoinHandle<()>>>> = Arc::new(Mutex::new(None));
187 let shutdown_started = Arc::new(Mutex::new(false));
188
189 policy.set_timeout_tx(driver_tx.clone());
191
192 let sync_post = SyncBoxProcessor::new(post_continuation);
194
195 {
197 let policy = Arc::clone(&policy);
198 let actor_h = Arc::clone(&actor_handle);
199 let actor_driver_tx = driver_tx; let queue_metrics = config.metrics.clone();
205 let queue_label = config
206 .route_id
207 .as_ref()
208 .map(|route| format!("resequencer:{route}"));
209 let handle = tokio::spawn(async move {
210 while let Some(input) = input_rx.recv().await {
211 let ready = policy.accept(input).await;
212 for ex in ready {
213 if actor_driver_tx.send(ex).await.is_err() {
215 return;
217 }
218 }
219 if let (Some(metrics), Some(label)) =
220 (queue_metrics.as_ref(), queue_label.as_ref())
221 {
222 metrics.set_queue_depth(label, policy.buffered());
223 }
224 }
225 });
227 *actor_h.lock().expect("actor_handle lock poisoned") = Some(handle); }
229
230 {
232 let post = sync_post.clone();
233 let driver_h = Arc::clone(&driver_handle);
234 let metrics = config.metrics.clone();
235 let route_id = config.route_id.clone();
236 let handle = tokio::spawn(async move {
237 while let Some(ex) = driver_rx.recv().await {
238 if camel_api::is_camel_stop(&ex) {
240 tracing::debug!(
241 "resequencer post-driver: skipping continuation for CamelStop exchange"
242 );
243 continue;
244 }
245 let mut proc = post.clone_inner();
247 match proc.call(ex).await {
248 Ok(_) => {}
249 Err(e) => {
250 tracing::warn!(
252 error = %e,
253 "resequencer post-driver: continuation call failed after ack (best-effort)"
254 );
255 if let Some(ref m) = metrics {
256 m.increment_errors(
257 route_id.as_deref().unwrap_or("unknown"),
258 "resequencer:post_ack_failure",
259 );
260 }
261 }
262 }
263 }
264 });
265 *driver_h.lock().expect("driver_handle lock poisoned") = Some(handle); }
267
268 let metrics = config.metrics.clone();
269 let route_id = config.route_id.clone();
270 Self {
271 policy,
272 config,
273 input_tx: input_tx_shared,
274 driver_tx: driver_tx_shared,
275 actor_handle,
276 driver_handle,
277 shutdown_started,
278 post_lifecycles: Arc::new(Mutex::new(post_lifecycles)),
279 inout_counter: Arc::new(AtomicU64::new(0)),
280 last_inout_warn: Arc::new(TokioMutex::new(None)),
281 metrics,
282 route_id,
283 }
284 }
285}
286
287impl Service<Exchange> for ResequencerService {
290 type Response = Exchange;
291 type Error = CamelError;
292 type Future =
293 std::pin::Pin<Box<dyn std::future::Future<Output = Result<Exchange, CamelError>> + Send>>;
294
295 fn poll_ready(
296 &mut self,
297 _cx: &mut std::task::Context<'_>,
298 ) -> std::task::Poll<Result<(), CamelError>> {
299 std::task::Poll::Ready(Ok(()))
301 }
302
303 fn call(&mut self, input: Exchange) -> Self::Future {
304 let config = self.config.clone();
305 let inout_counter = Arc::clone(&self.inout_counter);
306 let last_inout_warn = Arc::clone(&self.last_inout_warn);
307 let tx_opt = Arc::clone(&self.input_tx);
308 let metrics = self.metrics.clone();
309 let route_id = self.route_id.clone();
310
311 Box::pin(async move {
312 let mut ack = Exchange::new(Message::default());
314
315 if input.pattern == camel_api::exchange::ExchangePattern::InOut && !config.allow_inout {
317 inout_counter.fetch_add(1, Ordering::Relaxed);
318 ack.set_property(CAMEL_RESEQUENCER_INOUT_WARN, true);
319 if let Some(ref m) = metrics {
320 m.increment_errors(
321 route_id.as_deref().unwrap_or("unknown"),
322 "resequencer:inout_warning",
323 );
324 }
325 let now = Instant::now();
326 let mut last_guard = last_inout_warn.lock().await;
327 let should_warn = last_guard
328 .map(|t| now.duration_since(t) >= INOUT_WARN_INTERVAL)
329 .unwrap_or(true);
330 if should_warn {
331 let count = inout_counter.load(Ordering::Relaxed);
332 tracing::warn!(
333 inout_count = count,
334 "InOut exchange reached resequencer ({count} total); \
335 consider using InOnly pattern. \
336 Set allow_inout=true to suppress this warning."
337 );
338 *last_guard = Some(now);
339 }
340 }
341
342 let tx = {
345 let guard = tx_opt.lock().unwrap_or_else(|e| e.into_inner());
346 guard.clone()
347 };
348 if let Some(tx) = tx {
349 match tx.send(input).await {
351 Ok(()) => {
352 ack.set_property(CAMEL_RESEQUENCER_ACCEPTED, true);
353 }
354 Err(tokio::sync::mpsc::error::SendError(input)) => {
355 tracing::warn!(
356 correlation_id = %input.correlation_id,
357 "resequencer input dropped during shutdown"
358 );
359 ack.set_property(CAMEL_RESEQUENCER_ACCEPTED, false);
360 ack.set_property(CAMEL_RESEQUENCER_DROPPED, true);
361 }
362 }
363 }
364
365 Ok(ack)
366 })
367 }
368}
369
370#[async_trait]
373impl StepLifecycle for ResequencerService {
374 fn name(&self) -> &'static str {
375 self.policy.name()
376 }
377
378 async fn shutdown(&self, reason: StepShutdownReason) -> Result<(), CamelError> {
386 tracing::debug!(
390 reason = ?reason,
391 policy = self.policy.name(),
392 "ResequencerService shutdown via StepLifecycle"
393 );
394
395 {
397 let mut started = self
398 .shutdown_started
399 .lock()
400 .unwrap_or_else(|e| e.into_inner());
401 if *started {
402 tracing::debug!(
403 "ResequencerService shutdown already started (idempotent); skipping"
404 );
405 return Ok(());
406 }
407 *started = true;
408 }
409
410 {
414 let mut guard = self.input_tx.lock().unwrap_or_else(|e| e.into_inner());
415 *guard = None; }
417
418 let actor_handle_to_await = {
421 let mut guard = self.actor_handle.lock().unwrap_or_else(|e| e.into_inner());
422 guard.take()
423 };
424 if let Some(handle) = actor_handle_to_await {
425 let _ = tokio::time::timeout(Duration::from_secs(5), handle).await;
427 }
428
429 let flushed = self.policy.flush().await;
431 if !flushed.is_empty() {
432 let dt = {
433 let guard = self.driver_tx.lock().unwrap_or_else(|e| e.into_inner());
434 guard.clone()
435 };
436 if let Some(driver_tx) = dt {
437 for ex in flushed {
438 if driver_tx.send(ex).await.is_err() {
439 tracing::warn!(
440 "resequencer shutdown flush: post-driver channel closed early"
441 );
442 break;
443 }
444 }
445 }
446 }
447
448 {
450 let mut guard = self.driver_tx.lock().unwrap_or_else(|e| e.into_inner());
451 *guard = None; }
453
454 let driver_handle_to_await = {
456 let mut guard = self.driver_handle.lock().unwrap_or_else(|e| e.into_inner());
457 guard.take()
458 };
459 if let Some(handle) = driver_handle_to_await {
460 let result = tokio::time::timeout(Duration::from_secs(5), handle).await;
461 if result.is_err() {
462 tracing::warn!(
463 "resequencer post-driver task did not finish within 5s deadline; \
464 leaking handle (best-effort)"
465 );
466 }
467 }
468
469 {
472 let post_lcs: Vec<Arc<dyn StepLifecycle>> = {
473 let mut guard = self
474 .post_lifecycles
475 .lock()
476 .unwrap_or_else(|e| e.into_inner());
477 std::mem::take(&mut *guard)
478 };
479 for lc in &post_lcs {
480 if let Err(e) = lc.shutdown(reason).await {
481 tracing::warn!(
482 step = lc.name(),
483 error = %e,
484 "resequencer post-step lifecycle shutdown failed (best-effort)"
485 );
486 }
487 }
488 }
489
490 Ok(())
491 }
492}
493
494#[derive(Debug)]
499pub struct PassthroughPolicy;
500
501#[async_trait]
502impl ResequencePolicy for PassthroughPolicy {
503 async fn accept(&self, input: Exchange) -> Vec<Exchange> {
504 vec![input]
505 }
506
507 async fn flush(&self) -> Vec<Exchange> {
508 vec![]
509 }
510
511 fn name(&self) -> &'static str {
512 "passthrough"
513 }
514}
515
516#[cfg(test)]
519mod tests {
520 use super::*;
521 use tower::ServiceExt;
522
523 #[derive(Clone)]
525 struct CapturePost {
526 tx: mpsc::UnboundedSender<Exchange>,
527 }
528
529 impl Service<Exchange> for CapturePost {
530 type Response = Exchange;
531 type Error = CamelError;
532 type Future = std::pin::Pin<
533 Box<dyn std::future::Future<Output = Result<Exchange, CamelError>> + Send>,
534 >;
535
536 fn poll_ready(
537 &mut self,
538 _cx: &mut std::task::Context<'_>,
539 ) -> std::task::Poll<Result<(), CamelError>> {
540 std::task::Poll::Ready(Ok(()))
541 }
542
543 fn call(&mut self, exchange: Exchange) -> Self::Future {
544 let tx = self.tx.clone();
545 Box::pin(async move {
546 let _ = tx.send(exchange.clone());
548 Ok(exchange)
549 })
550 }
551 }
552
553 #[tokio::test]
554 async fn resequencer_boundary_passthrough_ack_and_continuation() {
555 use camel_api::body::Body;
556
557 let policy: Arc<dyn ResequencePolicy> = Arc::new(PassthroughPolicy);
559 let (capture_tx, mut capture_rx) = mpsc::unbounded_channel::<Exchange>();
560 let capture = CapturePost { tx: capture_tx };
561 let post_continuation: BoxProcessor = BoxProcessor::new(capture);
562
563 let service = ResequencerService::new(policy, post_continuation, 1024, vec![]);
564
565 let mut input = Exchange::new(Message::new(Body::Text("hello".into())));
567 input.set_property("seq", 1);
568
569 let ack = service.clone().oneshot(input).await.unwrap();
570
571 assert!(
573 matches!(ack.input.body, Body::Empty),
574 "ack body should be Empty, got {:?}",
575 ack.input.body
576 );
577 assert_eq!(
578 ack.property(CAMEL_RESEQUENCER_ACCEPTED)
579 .and_then(|v| v.as_bool()),
580 Some(true),
581 "CAMEL_RESEQUENCER_ACCEPTED should be true"
582 );
583
584 let captured = tokio::time::timeout(Duration::from_millis(500), capture_rx.recv())
586 .await
587 .expect("post-continuation did not receive exchange within 500ms timeout")
588 .expect("capture channel closed without receiving exchange");
589 let body = captured.input.body.as_text();
590 assert_eq!(
591 body,
592 Some("hello"),
593 "post-continuation received body should match"
594 );
595
596 service
598 .shutdown(StepShutdownReason::RouteStop)
599 .await
600 .expect("first shutdown should succeed");
601
602 service
603 .shutdown(StepShutdownReason::RouteStop)
604 .await
605 .expect("second shutdown should succeed (idempotent)");
606 }
607
608 #[tokio::test]
609 async fn resequencer_boundary_camel_stop_skipped() {
610 use camel_api::body::Body;
611
612 let policy: Arc<dyn ResequencePolicy> = Arc::new(PassthroughPolicy);
613 let (capture_tx, mut capture_rx) = mpsc::unbounded_channel::<Exchange>();
614 let capture = CapturePost { tx: capture_tx };
615 let post_continuation: BoxProcessor = BoxProcessor::new(capture);
616
617 let service = ResequencerService::new(policy, post_continuation, 1024, vec![]);
618
619 let mut input = Exchange::new(Message::new(Body::Text(
621 "should-not-reach-continuation".into(),
622 )));
623 input.set_property(camel_api::exchange::CAMEL_STOP, true);
624
625 let ack = service.clone().oneshot(input).await.unwrap();
626
627 assert_eq!(
629 ack.property(CAMEL_RESEQUENCER_ACCEPTED)
630 .and_then(|v| v.as_bool()),
631 Some(true),
632 "CamelStop exchange should still be accepted by resequencer actor"
633 );
634
635 let did_receive = tokio::time::timeout(Duration::from_millis(500), capture_rx.recv()).await;
637 match did_receive {
638 Ok(Some(_)) => panic!("CamelStop exchange should NOT reach post-continuation"),
639 Ok(None) => {} Err(_elapsed) => {} }
642
643 service
645 .shutdown(StepShutdownReason::RouteStop)
646 .await
647 .expect("shutdown should succeed");
648 }
649
650 #[tokio::test]
651 async fn inout_guard_increments_counter() {
652 let policy: Arc<dyn ResequencePolicy> = Arc::new(PassthroughPolicy);
653 let (tx, _rx) = mpsc::unbounded_channel::<Exchange>();
654 let post: BoxProcessor = BoxProcessor::new(CapturePost { tx });
655 let config = ResequencerConfig::default();
656 let service = ResequencerService::with_config(policy, post, 16, vec![], config);
657
658 let ex_inonly = Exchange::new(Message::new("inonly"));
660 let _ = service.clone().oneshot(ex_inonly).await.unwrap();
661
662 let ex_inout = Exchange::new_in_out(Message::new("inout"));
664 let _ = service.clone().oneshot(ex_inout).await.unwrap();
665 assert!(
666 service.inout_counter.load(Ordering::Relaxed) > 0,
667 "InOut counter should be > 0 after InOut exchange"
668 );
669
670 service
671 .shutdown(StepShutdownReason::RouteStop)
672 .await
673 .expect("shutdown");
674 }
675
676 #[tokio::test]
677 async fn inout_guard_allow_inout_suppresses() {
678 let policy: Arc<dyn ResequencePolicy> = Arc::new(PassthroughPolicy);
679 let (tx, _rx) = mpsc::unbounded_channel::<Exchange>();
680 let post: BoxProcessor = BoxProcessor::new(CapturePost { tx });
681 let config = ResequencerConfig {
682 allow_inout: true,
683 ..Default::default()
684 };
685 let service = ResequencerService::with_config(policy, post, 16, vec![], config);
686
687 let ex_inout = Exchange::new_in_out(Message::new("inout-allowed"));
688 let _ = service.clone().oneshot(ex_inout).await.unwrap();
689 assert_eq!(
690 service.inout_counter.load(Ordering::Relaxed),
691 0,
692 "InOut counter should be 0 when allow_inout=true"
693 );
694
695 service
696 .shutdown(StepShutdownReason::RouteStop)
697 .await
698 .expect("shutdown");
699 }
700}