Skip to main content

camel_processor/
delayer.rs

1use std::future::Future;
2use std::pin::Pin;
3use std::task::{Context, Poll};
4use std::time::Duration;
5
6use tower::Service;
7
8use camel_api::{CamelError, DelayConfig, Exchange, Value};
9
10#[derive(Clone)]
11pub struct DelayerService {
12    config: DelayConfig,
13}
14
15impl DelayerService {
16    pub fn new(config: DelayConfig) -> Self {
17        Self { config }
18    }
19
20    fn effective_delay_ms(&self, exchange: &Exchange) -> u64 {
21        if let Some(ref header) = self.config.dynamic_header
22            && let Some(Value::Number(n)) = exchange.input.header(header)
23            && let Some(n) = n.as_f64()
24            && n >= 0.0
25        {
26            // H12 Batch 1: clamp the raw f64 to [0, max_delay_ms] BEFORE the
27            // cast. Without this, `1e20 as u64` saturates to u64::MAX, then
28            // `Duration::from_millis(u64::MAX)` + `Instant::now()` overflows
29            // and panics. NaN / negative inputs are already filtered above.
30            let cap = self.config.max_delay_ms as f64;
31            let clamped = n.min(cap);
32            return clamped as u64;
33        }
34        self.config.delay_ms
35    }
36}
37
38impl Service<Exchange> for DelayerService {
39    type Response = Exchange;
40    type Error = CamelError;
41    type Future = Pin<Box<dyn Future<Output = Result<Exchange, CamelError>> + Send>>;
42
43    fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
44        Poll::Ready(Ok(()))
45    }
46
47    fn call(&mut self, exchange: Exchange) -> Self::Future {
48        let delay_ms = self.effective_delay_ms(&exchange);
49        Box::pin(async move {
50            if delay_ms > 0 {
51                tokio::time::sleep(Duration::from_millis(delay_ms)).await;
52            }
53            Ok(exchange)
54        })
55    }
56}
57
58#[cfg(test)]
59mod tests {
60    use std::time::{Duration, Instant};
61
62    use camel_api::DelayConfig;
63    use camel_api::{Exchange, Message, Value};
64    use tower::{Service, ServiceExt};
65
66    use super::DelayerService;
67
68    #[tokio::test]
69    async fn test_delayer_fixed_delay() {
70        let config = DelayConfig::new(100);
71        let mut svc = DelayerService::new(config);
72
73        let start = Instant::now();
74        let ex = Exchange::new(Message::new("test"));
75        let result = svc.ready().await.unwrap().call(ex).await;
76        let elapsed = start.elapsed();
77
78        assert!(result.is_ok());
79        assert!(elapsed >= Duration::from_millis(90));
80    }
81
82    #[tokio::test]
83    async fn test_delayer_dynamic_from_header() {
84        let config = DelayConfig::new(5000).with_dynamic_header("CamelDelayMs");
85        let mut svc = DelayerService::new(config);
86
87        let mut ex = Exchange::new(Message::new("test"));
88        ex.input
89            .set_header("CamelDelayMs", Value::Number(50.into()));
90
91        let start = Instant::now();
92        let result = svc.ready().await.unwrap().call(ex).await;
93        let elapsed = start.elapsed();
94
95        assert!(result.is_ok());
96        assert!(elapsed >= Duration::from_millis(40));
97        assert!(elapsed < Duration::from_millis(200));
98    }
99
100    #[tokio::test]
101    async fn test_delayer_zero_delay() {
102        let config = DelayConfig::new(0);
103        let mut svc = DelayerService::new(config);
104
105        let start = Instant::now();
106        let ex = Exchange::new(Message::new("test"));
107        let result = svc.ready().await.unwrap().call(ex).await;
108        let elapsed = start.elapsed();
109
110        assert!(result.is_ok());
111        assert!(elapsed < Duration::from_millis(50));
112    }
113
114    #[tokio::test]
115    async fn test_delayer_zero_from_header() {
116        let config = DelayConfig::new(5000).with_dynamic_header("CamelDelayMs");
117        let mut svc = DelayerService::new(config);
118
119        let mut ex = Exchange::new(Message::new("test"));
120        ex.input.set_header("CamelDelayMs", Value::Number(0.into()));
121
122        let start = Instant::now();
123        let result = svc.ready().await.unwrap().call(ex).await;
124        let elapsed = start.elapsed();
125
126        assert!(result.is_ok());
127        assert!(elapsed < Duration::from_millis(50));
128    }
129
130    #[tokio::test]
131    async fn test_delayer_fallback_on_invalid_header() {
132        let config = DelayConfig::new(80).with_dynamic_header("CamelDelayMs");
133        let mut svc = DelayerService::new(config);
134
135        let mut ex = Exchange::new(Message::new("test"));
136        ex.input
137            .set_header("CamelDelayMs", Value::String("not a number".into()));
138
139        let start = Instant::now();
140        let result = svc.ready().await.unwrap().call(ex).await;
141        let elapsed = start.elapsed();
142
143        assert!(result.is_ok());
144        assert!(elapsed >= Duration::from_millis(70));
145    }
146
147    #[tokio::test]
148    async fn test_delayer_negative_header() {
149        let config = DelayConfig::new(80).with_dynamic_header("CamelDelayMs");
150        let mut svc = DelayerService::new(config);
151
152        let mut ex = Exchange::new(Message::new("test"));
153        ex.input
154            .set_header("CamelDelayMs", Value::Number((-100).into()));
155
156        let start = Instant::now();
157        let result = svc.ready().await.unwrap().call(ex).await;
158        let elapsed = start.elapsed();
159
160        assert!(result.is_ok());
161        assert!(elapsed >= Duration::from_millis(70));
162    }
163
164    #[tokio::test]
165    async fn test_delayer_missing_header() {
166        let config = DelayConfig::new(80).with_dynamic_header("CamelDelayMs");
167        let mut svc = DelayerService::new(config);
168
169        let start = Instant::now();
170        let ex = Exchange::new(Message::new("test"));
171        let result = svc.ready().await.unwrap().call(ex).await;
172        let elapsed = start.elapsed();
173
174        assert!(result.is_ok());
175        assert!(elapsed >= Duration::from_millis(70));
176    }
177
178    // ── H12 Batch 1: clamp header-derived delay to max_delay_ms ──────
179
180    /// H12: a header value of `1e20` (which saturates to `u64::MAX` when
181    /// cast) MUST be clamped to `max_delay_ms` before the sleep. The test
182    /// uses a tiny cap so the test runs in milliseconds, not hours.
183    /// Without the clamp, the call would panic with `Instant + Duration`
184    /// overflow (or sleep for ~584M years).
185    #[tokio::test]
186    async fn test_huge_header_value_clamps_to_max_delay_ms() {
187        let config = DelayConfig::new(80)
188            .with_dynamic_header("CamelDelayMs")
189            .with_max_delay_ms(50); // tiny cap so the test is fast
190        let mut svc = DelayerService::new(config);
191
192        let mut ex = Exchange::new(Message::new("test"));
193        // 1e20 saturates to u64::MAX without the clamp.
194        ex.input.set_header(
195            "CamelDelayMs",
196            Value::Number(serde_json::Number::from_f64(1e20).unwrap()),
197        );
198
199        let start = Instant::now();
200        let result = svc.ready().await.unwrap().call(ex).await;
201        let elapsed = start.elapsed();
202
203        assert!(result.is_ok(), "huge header value must clamp, not panic");
204        assert!(
205            elapsed < Duration::from_millis(500),
206            "should clamp to ~50ms, got {elapsed:?}"
207        );
208        assert!(
209            elapsed >= Duration::from_millis(40),
210            "should sleep at least ~50ms (the cap), got {elapsed:?}"
211        );
212    }
213
214    /// When the header value is under the cap, the clamp is a no-op.
215    #[tokio::test]
216    async fn test_small_header_value_unaffected_by_clamp() {
217        let config = DelayConfig::new(80)
218            .with_dynamic_header("CamelDelayMs")
219            .with_max_delay_ms(3_600_000); // 1 hour, default
220        let mut svc = DelayerService::new(config);
221
222        let mut ex = Exchange::new(Message::new("test"));
223        ex.input
224            .set_header("CamelDelayMs", Value::Number(20.into()));
225
226        let start = Instant::now();
227        let result = svc.ready().await.unwrap().call(ex).await;
228        let elapsed = start.elapsed();
229
230        assert!(result.is_ok());
231        assert!(
232            elapsed < Duration::from_millis(200),
233            "small value should be unaffected, got {elapsed:?}"
234        );
235    }
236}