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 set_timeout_tx(&self, _tx: tokio::sync::mpsc::Sender<Exchange>) {}
83}
84
85#[derive(Clone)]
93pub struct ResequencerService {
94 policy: Arc<dyn ResequencePolicy>,
95 config: ResequencerConfig,
96 input_tx: Arc<Mutex<Option<mpsc::Sender<Exchange>>>>,
99 driver_tx: Arc<Mutex<Option<mpsc::Sender<Exchange>>>>,
103 actor_handle: Arc<Mutex<Option<JoinHandle<()>>>>,
104 driver_handle: Arc<Mutex<Option<JoinHandle<()>>>>,
105 shutdown_started: Arc<Mutex<bool>>,
106 post_lifecycles: Arc<Mutex<Vec<Arc<dyn StepLifecycle>>>>,
109 inout_counter: Arc<AtomicU64>,
111 last_inout_warn: Arc<TokioMutex<Option<Instant>>>,
113 metrics: Option<Arc<dyn MetricsCollector>>,
115 route_id: Option<String>,
117}
118
119impl std::fmt::Debug for ResequencerService {
120 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
121 f.debug_struct("ResequencerService")
122 .field("policy", &self.policy.name())
123 .finish_non_exhaustive()
124 }
125}
126
127impl ResequencerService {
128 pub fn new(
140 policy: Arc<dyn ResequencePolicy>,
141 post_continuation: BoxProcessor,
142 input_capacity: usize,
143 post_lifecycles: Vec<Arc<dyn StepLifecycle>>,
144 ) -> Self {
145 Self::with_config(
146 policy,
147 post_continuation,
148 input_capacity,
149 post_lifecycles,
150 ResequencerConfig::default(),
151 )
152 }
153
154 pub fn with_config(
160 policy: Arc<dyn ResequencePolicy>,
161 post_continuation: BoxProcessor,
162 input_capacity: usize,
163 post_lifecycles: Vec<Arc<dyn StepLifecycle>>,
164 config: ResequencerConfig,
165 ) -> Self {
166 let (input_tx, mut input_rx) = mpsc::channel::<Exchange>(input_capacity);
168
169 let (driver_tx, mut driver_rx) = mpsc::channel::<Exchange>(input_capacity);
171
172 let input_tx_shared: Arc<Mutex<Option<mpsc::Sender<Exchange>>>> =
174 Arc::new(Mutex::new(Some(input_tx)));
175 let driver_tx_shared: Arc<Mutex<Option<mpsc::Sender<Exchange>>>> =
176 Arc::new(Mutex::new(Some(driver_tx.clone()))); let actor_handle: Arc<Mutex<Option<JoinHandle<()>>>> = Arc::new(Mutex::new(None));
178 let driver_handle: Arc<Mutex<Option<JoinHandle<()>>>> = Arc::new(Mutex::new(None));
179 let shutdown_started = Arc::new(Mutex::new(false));
180
181 policy.set_timeout_tx(driver_tx.clone());
183
184 let sync_post = SyncBoxProcessor::new(post_continuation);
186
187 {
189 let policy = Arc::clone(&policy);
190 let actor_h = Arc::clone(&actor_handle);
191 let actor_driver_tx = driver_tx; let handle = tokio::spawn(async move {
193 while let Some(input) = input_rx.recv().await {
194 let ready = policy.accept(input).await;
195 for ex in ready {
196 if actor_driver_tx.send(ex).await.is_err() {
198 return;
200 }
201 }
202 }
203 });
205 *actor_h.lock().expect("actor_handle lock poisoned") = Some(handle); }
207
208 {
210 let post = sync_post.clone();
211 let driver_h = Arc::clone(&driver_handle);
212 let metrics = config.metrics.clone();
213 let route_id = config.route_id.clone();
214 let handle = tokio::spawn(async move {
215 while let Some(ex) = driver_rx.recv().await {
216 if camel_api::is_camel_stop(&ex) {
218 tracing::debug!(
219 "resequencer post-driver: skipping continuation for CamelStop exchange"
220 );
221 continue;
222 }
223 let mut proc = post.clone_inner();
225 match proc.call(ex).await {
226 Ok(_) => {}
227 Err(e) => {
228 tracing::warn!(
230 error = %e,
231 "resequencer post-driver: continuation call failed after ack (best-effort)"
232 );
233 if let Some(ref m) = metrics {
234 m.increment_errors(
235 route_id.as_deref().unwrap_or("unknown"),
236 "resequencer:post_ack_failure",
237 );
238 }
239 }
240 }
241 }
242 });
243 *driver_h.lock().expect("driver_handle lock poisoned") = Some(handle); }
245
246 let metrics = config.metrics.clone();
247 let route_id = config.route_id.clone();
248 Self {
249 policy,
250 config,
251 input_tx: input_tx_shared,
252 driver_tx: driver_tx_shared,
253 actor_handle,
254 driver_handle,
255 shutdown_started,
256 post_lifecycles: Arc::new(Mutex::new(post_lifecycles)),
257 inout_counter: Arc::new(AtomicU64::new(0)),
258 last_inout_warn: Arc::new(TokioMutex::new(None)),
259 metrics,
260 route_id,
261 }
262 }
263}
264
265impl Service<Exchange> for ResequencerService {
268 type Response = Exchange;
269 type Error = CamelError;
270 type Future =
271 std::pin::Pin<Box<dyn std::future::Future<Output = Result<Exchange, CamelError>> + Send>>;
272
273 fn poll_ready(
274 &mut self,
275 _cx: &mut std::task::Context<'_>,
276 ) -> std::task::Poll<Result<(), CamelError>> {
277 std::task::Poll::Ready(Ok(()))
279 }
280
281 fn call(&mut self, input: Exchange) -> Self::Future {
282 let config = self.config.clone();
283 let inout_counter = Arc::clone(&self.inout_counter);
284 let last_inout_warn = Arc::clone(&self.last_inout_warn);
285 let tx_opt = Arc::clone(&self.input_tx);
286 let metrics = self.metrics.clone();
287 let route_id = self.route_id.clone();
288
289 Box::pin(async move {
290 let mut ack = Exchange::new(Message::default());
292
293 if input.pattern == camel_api::exchange::ExchangePattern::InOut && !config.allow_inout {
295 inout_counter.fetch_add(1, Ordering::Relaxed);
296 ack.set_property(CAMEL_RESEQUENCER_INOUT_WARN, true);
297 if let Some(ref m) = metrics {
298 m.increment_errors(
299 route_id.as_deref().unwrap_or("unknown"),
300 "resequencer:inout_warning",
301 );
302 }
303 let now = Instant::now();
304 let mut last_guard = last_inout_warn.lock().await;
305 let should_warn = last_guard
306 .map(|t| now.duration_since(t) >= INOUT_WARN_INTERVAL)
307 .unwrap_or(true);
308 if should_warn {
309 let count = inout_counter.load(Ordering::Relaxed);
310 tracing::warn!(
311 inout_count = count,
312 "InOut exchange reached resequencer ({count} total); \
313 consider using InOnly pattern. \
314 Set allow_inout=true to suppress this warning."
315 );
316 *last_guard = Some(now);
317 }
318 }
319
320 let tx = {
323 let guard = tx_opt.lock().unwrap_or_else(|e| e.into_inner());
324 guard.clone()
325 };
326 if let Some(tx) = tx {
327 match tx.send(input).await {
329 Ok(()) => {
330 ack.set_property(CAMEL_RESEQUENCER_ACCEPTED, true);
331 }
332 Err(tokio::sync::mpsc::error::SendError(input)) => {
333 tracing::warn!(
334 correlation_id = %input.correlation_id,
335 "resequencer input dropped during shutdown"
336 );
337 ack.set_property(CAMEL_RESEQUENCER_ACCEPTED, false);
338 ack.set_property(CAMEL_RESEQUENCER_DROPPED, true);
339 }
340 }
341 }
342
343 Ok(ack)
344 })
345 }
346}
347
348#[async_trait]
351impl StepLifecycle for ResequencerService {
352 fn name(&self) -> &'static str {
353 self.policy.name()
354 }
355
356 async fn shutdown(&self, reason: StepShutdownReason) -> Result<(), CamelError> {
364 tracing::debug!(
368 reason = ?reason,
369 policy = self.policy.name(),
370 "ResequencerService shutdown via StepLifecycle"
371 );
372
373 {
375 let mut started = self
376 .shutdown_started
377 .lock()
378 .unwrap_or_else(|e| e.into_inner());
379 if *started {
380 tracing::debug!(
381 "ResequencerService shutdown already started (idempotent); skipping"
382 );
383 return Ok(());
384 }
385 *started = true;
386 }
387
388 {
392 let mut guard = self.input_tx.lock().unwrap_or_else(|e| e.into_inner());
393 *guard = None; }
395
396 let actor_handle_to_await = {
399 let mut guard = self.actor_handle.lock().unwrap_or_else(|e| e.into_inner());
400 guard.take()
401 };
402 if let Some(handle) = actor_handle_to_await {
403 let _ = tokio::time::timeout(Duration::from_secs(5), handle).await;
405 }
406
407 let flushed = self.policy.flush().await;
409 if !flushed.is_empty() {
410 let dt = {
411 let guard = self.driver_tx.lock().unwrap_or_else(|e| e.into_inner());
412 guard.clone()
413 };
414 if let Some(driver_tx) = dt {
415 for ex in flushed {
416 if driver_tx.send(ex).await.is_err() {
417 tracing::warn!(
418 "resequencer shutdown flush: post-driver channel closed early"
419 );
420 break;
421 }
422 }
423 }
424 }
425
426 {
428 let mut guard = self.driver_tx.lock().unwrap_or_else(|e| e.into_inner());
429 *guard = None; }
431
432 let driver_handle_to_await = {
434 let mut guard = self.driver_handle.lock().unwrap_or_else(|e| e.into_inner());
435 guard.take()
436 };
437 if let Some(handle) = driver_handle_to_await {
438 let result = tokio::time::timeout(Duration::from_secs(5), handle).await;
439 if result.is_err() {
440 tracing::warn!(
441 "resequencer post-driver task did not finish within 5s deadline; \
442 leaking handle (best-effort)"
443 );
444 }
445 }
446
447 {
450 let post_lcs: Vec<Arc<dyn StepLifecycle>> = {
451 let mut guard = self
452 .post_lifecycles
453 .lock()
454 .unwrap_or_else(|e| e.into_inner());
455 std::mem::take(&mut *guard)
456 };
457 for lc in &post_lcs {
458 if let Err(e) = lc.shutdown(reason).await {
459 tracing::warn!(
460 step = lc.name(),
461 error = %e,
462 "resequencer post-step lifecycle shutdown failed (best-effort)"
463 );
464 }
465 }
466 }
467
468 Ok(())
469 }
470}
471
472#[derive(Debug)]
477pub struct PassthroughPolicy;
478
479#[async_trait]
480impl ResequencePolicy for PassthroughPolicy {
481 async fn accept(&self, input: Exchange) -> Vec<Exchange> {
482 vec![input]
483 }
484
485 async fn flush(&self) -> Vec<Exchange> {
486 vec![]
487 }
488
489 fn name(&self) -> &'static str {
490 "passthrough"
491 }
492}
493
494#[cfg(test)]
497mod tests {
498 use super::*;
499 use tower::ServiceExt;
500
501 #[derive(Clone)]
503 struct CapturePost {
504 tx: mpsc::UnboundedSender<Exchange>,
505 }
506
507 impl Service<Exchange> for CapturePost {
508 type Response = Exchange;
509 type Error = CamelError;
510 type Future = std::pin::Pin<
511 Box<dyn std::future::Future<Output = Result<Exchange, CamelError>> + Send>,
512 >;
513
514 fn poll_ready(
515 &mut self,
516 _cx: &mut std::task::Context<'_>,
517 ) -> std::task::Poll<Result<(), CamelError>> {
518 std::task::Poll::Ready(Ok(()))
519 }
520
521 fn call(&mut self, exchange: Exchange) -> Self::Future {
522 let tx = self.tx.clone();
523 Box::pin(async move {
524 let _ = tx.send(exchange.clone());
526 Ok(exchange)
527 })
528 }
529 }
530
531 #[tokio::test]
532 async fn resequencer_boundary_passthrough_ack_and_continuation() {
533 use camel_api::body::Body;
534
535 let policy: Arc<dyn ResequencePolicy> = Arc::new(PassthroughPolicy);
537 let (capture_tx, mut capture_rx) = mpsc::unbounded_channel::<Exchange>();
538 let capture = CapturePost { tx: capture_tx };
539 let post_continuation: BoxProcessor = BoxProcessor::new(capture);
540
541 let service = ResequencerService::new(policy, post_continuation, 1024, vec![]);
542
543 let mut input = Exchange::new(Message::new(Body::Text("hello".into())));
545 input.set_property("seq", 1);
546
547 let ack = service.clone().oneshot(input).await.unwrap();
548
549 assert!(
551 matches!(ack.input.body, Body::Empty),
552 "ack body should be Empty, got {:?}",
553 ack.input.body
554 );
555 assert_eq!(
556 ack.property(CAMEL_RESEQUENCER_ACCEPTED)
557 .and_then(|v| v.as_bool()),
558 Some(true),
559 "CAMEL_RESEQUENCER_ACCEPTED should be true"
560 );
561
562 let captured = tokio::time::timeout(Duration::from_millis(500), capture_rx.recv())
564 .await
565 .expect("post-continuation did not receive exchange within 500ms timeout")
566 .expect("capture channel closed without receiving exchange");
567 let body = captured.input.body.as_text();
568 assert_eq!(
569 body,
570 Some("hello"),
571 "post-continuation received body should match"
572 );
573
574 service
576 .shutdown(StepShutdownReason::RouteStop)
577 .await
578 .expect("first shutdown should succeed");
579
580 service
581 .shutdown(StepShutdownReason::RouteStop)
582 .await
583 .expect("second shutdown should succeed (idempotent)");
584 }
585
586 #[tokio::test]
587 async fn resequencer_boundary_camel_stop_skipped() {
588 use camel_api::body::Body;
589
590 let policy: Arc<dyn ResequencePolicy> = Arc::new(PassthroughPolicy);
591 let (capture_tx, mut capture_rx) = mpsc::unbounded_channel::<Exchange>();
592 let capture = CapturePost { tx: capture_tx };
593 let post_continuation: BoxProcessor = BoxProcessor::new(capture);
594
595 let service = ResequencerService::new(policy, post_continuation, 1024, vec![]);
596
597 let mut input = Exchange::new(Message::new(Body::Text(
599 "should-not-reach-continuation".into(),
600 )));
601 input.set_property(camel_api::exchange::CAMEL_STOP, true);
602
603 let ack = service.clone().oneshot(input).await.unwrap();
604
605 assert_eq!(
607 ack.property(CAMEL_RESEQUENCER_ACCEPTED)
608 .and_then(|v| v.as_bool()),
609 Some(true),
610 "CamelStop exchange should still be accepted by resequencer actor"
611 );
612
613 let did_receive = tokio::time::timeout(Duration::from_millis(500), capture_rx.recv()).await;
615 match did_receive {
616 Ok(Some(_)) => panic!("CamelStop exchange should NOT reach post-continuation"),
617 Ok(None) => {} Err(_elapsed) => {} }
620
621 service
623 .shutdown(StepShutdownReason::RouteStop)
624 .await
625 .expect("shutdown should succeed");
626 }
627
628 #[tokio::test]
629 async fn inout_guard_increments_counter() {
630 let policy: Arc<dyn ResequencePolicy> = Arc::new(PassthroughPolicy);
631 let (tx, _rx) = mpsc::unbounded_channel::<Exchange>();
632 let post: BoxProcessor = BoxProcessor::new(CapturePost { tx });
633 let config = ResequencerConfig::default();
634 let service = ResequencerService::with_config(policy, post, 16, vec![], config);
635
636 let ex_inonly = Exchange::new(Message::new("inonly"));
638 let _ = service.clone().oneshot(ex_inonly).await.unwrap();
639
640 let ex_inout = Exchange::new_in_out(Message::new("inout"));
642 let _ = service.clone().oneshot(ex_inout).await.unwrap();
643 assert!(
644 service.inout_counter.load(Ordering::Relaxed) > 0,
645 "InOut counter should be > 0 after InOut exchange"
646 );
647
648 service
649 .shutdown(StepShutdownReason::RouteStop)
650 .await
651 .expect("shutdown");
652 }
653
654 #[tokio::test]
655 async fn inout_guard_allow_inout_suppresses() {
656 let policy: Arc<dyn ResequencePolicy> = Arc::new(PassthroughPolicy);
657 let (tx, _rx) = mpsc::unbounded_channel::<Exchange>();
658 let post: BoxProcessor = BoxProcessor::new(CapturePost { tx });
659 let config = ResequencerConfig {
660 allow_inout: true,
661 ..Default::default()
662 };
663 let service = ResequencerService::with_config(policy, post, 16, vec![], config);
664
665 let ex_inout = Exchange::new_in_out(Message::new("inout-allowed"));
666 let _ = service.clone().oneshot(ex_inout).await.unwrap();
667 assert_eq!(
668 service.inout_counter.load(Ordering::Relaxed),
669 0,
670 "InOut counter should be 0 when allow_inout=true"
671 );
672
673 service
674 .shutdown(StepShutdownReason::RouteStop)
675 .await
676 .expect("shutdown");
677 }
678}