Skip to main content

camel_api/
step_lifecycle.rs

1use crate::CamelError;
2use async_trait::async_trait;
3
4/// Why a stateful pipeline step is being shut down.
5#[derive(Debug, Clone, Copy, PartialEq, Eq)]
6pub enum StepShutdownReason {
7    /// The route is stopping (`stop_route`).
8    RouteStop,
9    /// The pipeline is being replaced via hot reload (Restart path).
10    HotSwap,
11}
12
13/// Lifecycle hook for **stateful** pipeline steps that own background work
14/// (timers, buckets, gap-detectors, queues) beyond a single `process()` call.
15///
16/// Stateless processors do NOT implement this trait. The runtime collects
17/// `Arc<dyn StepLifecycle>` at compile time and drains them in route order
18/// during `stop_route` and hot-swap. See ADR-0022.
19///
20/// **Why `&self`, not `&mut self`?** `Lifecycle` uses `&mut self` for exclusive
21/// start/stop of services. `StepLifecycle` is dispatched through
22/// `Arc<dyn StepLifecycle>` carried inside `ArcSwap` pipeline snapshots, so it
23/// MUST be `&self` (shared-reference, interior-mutability) for `Arc` cloning and
24/// concurrent snapshots to work. See ADR-0022.
25///
26/// `shutdown` MUST be idempotent. By the time it is called, intake is cancelled
27/// and the pipeline task has been joined, so no `process()` is in flight.
28/// `Err` is best-effort: the runtime logs and continues (it does NOT fail
29/// `stop_route`), mirroring `CamelContext::stop` service handling.
30#[async_trait]
31pub trait StepLifecycle: std::fmt::Debug + Send + Sync + 'static {
32    /// Stable name for logging/diagnostics.
33    fn name(&self) -> &'static str;
34
35    async fn shutdown(&self, reason: StepShutdownReason) -> Result<(), CamelError>;
36
37    /// Called when the route starts.
38    ///
39    /// Default is a no-op so existing implementors remain source-compatible.
40    /// Stateful steps that own background work (timers, buckets, gap-detectors)
41    /// should override this to start that work after construction. Mirrors
42    /// `Lifecycle::start` semantics, but with `&self` (shared-reference,
43    /// interior-mutability) — see ADR-0022.
44    // TODO(reload-start): `start()` is currently invoked only from `start_route`.
45    // The hot-reload path (swap_pipeline / swap_pipeline_raw) stores lifecycle
46    // handles but does not yet call `start()` on rebuilt handles; whether they
47    // need re-start is a future decision (ADR-0022).
48    async fn start(&self) -> Result<(), CamelError> {
49        Ok(())
50    }
51}
52
53#[cfg(test)]
54mod tests {
55    use super::*;
56    use std::sync::{Arc, Mutex};
57
58    #[derive(Debug)]
59    struct FakeStep {
60        shutdowns: Mutex<Vec<StepShutdownReason>>,
61    }
62
63    #[async_trait]
64    impl StepLifecycle for FakeStep {
65        fn name(&self) -> &'static str {
66            "fake"
67        }
68        async fn shutdown(&self, reason: StepShutdownReason) -> Result<(), CamelError> {
69            self.shutdowns.lock().unwrap().push(reason);
70            Ok(())
71        }
72    }
73
74    #[tokio::test]
75    async fn dyn_dispatch_works() {
76        // Keep concrete handle for assertion, dispatch through Arc<dyn StepLifecycle>
77        // (the ArcSwap-snapshot shape).
78        let inner = Arc::new(FakeStep {
79            shutdowns: Mutex::new(vec![]),
80        });
81        let step: Arc<dyn StepLifecycle> = inner.clone();
82        step.shutdown(StepShutdownReason::RouteStop).await.unwrap();
83        step.shutdown(StepShutdownReason::HotSwap).await.unwrap();
84        assert_eq!(
85            *inner.shutdowns.lock().unwrap(),
86            vec![StepShutdownReason::RouteStop, StepShutdownReason::HotSwap,]
87        );
88    }
89
90    #[tokio::test]
91    async fn start_default_is_noop() {
92        // FakeStep does not override `start`; the default impl on the trait
93        // must return `Ok(())` so existing implementors (ResequencerService,
94        // AggregatorService, test fakes) are unaffected.
95        let step: Arc<dyn StepLifecycle> = Arc::new(FakeStep {
96            shutdowns: Mutex::new(vec![]),
97        });
98        let result = step.start().await;
99        assert!(result.is_ok());
100    }
101
102    #[tokio::test]
103    async fn shutdown_err_is_not_fatal() {
104        #[derive(Debug)]
105        struct FailingStep;
106        #[async_trait]
107        impl StepLifecycle for FailingStep {
108            fn name(&self) -> &'static str {
109                "fail"
110            }
111            async fn shutdown(&self, _: StepShutdownReason) -> Result<(), CamelError> {
112                Err(CamelError::ProcessorError("boom".into()))
113            }
114        }
115        let step: Arc<dyn StepLifecycle> = Arc::new(FailingStep);
116        let result = step.shutdown(StepShutdownReason::RouteStop).await;
117        assert!(result.is_err());
118        // But typical drain loop continues (log + skip), see Task 5.
119    }
120}