Skip to main content

camel_api/
backoff.rs

1//! Shared backoff configuration for transient retry loops.
2//!
3//! Used by networked components (kafka, redis, ws, jms, xj, xslt) to
4//! throttle reconnect/polling attempts with capped exponential backoff.
5//!
6//! **This is NOT route supervision.** Component backoff throttles transient
7//! polling/reconnect loops. [`SupervisionConfig`](super::supervision::SupervisionConfig)
8//! governs route restart policy. They share math but are separate concepts.
9
10use std::time::Duration;
11
12/// Configuration for capped exponential backoff.
13///
14/// Defaults: 100ms initial, 2x multiplier, 30s cap. Multiplier MUST be >= 1.0;
15/// values below 1.0 silently produce zero-delay backoff (no throttling).
16#[derive(Debug, Clone, PartialEq)]
17pub struct BackoffConfig {
18    pub initial_delay: Duration,
19    pub multiplier: f64,
20    pub max_delay: Duration,
21}
22
23impl Default for BackoffConfig {
24    fn default() -> Self {
25        Self {
26            initial_delay: Duration::from_millis(100),
27            multiplier: 2.0,
28            max_delay: Duration::from_secs(30),
29        }
30    }
31}
32
33impl BackoffConfig {
34    pub fn delay_for_attempt(&self, attempt: u32) -> Duration {
35        let exp = (attempt as i32).min(63);
36        let exp_value = self.multiplier.powi(exp);
37        let millis = (self.initial_delay.as_millis() as f64 * exp_value) as u128;
38        let capped = millis.min(self.max_delay.as_millis());
39        Duration::from_millis(capped as u64)
40    }
41}
42
43/// Stateful backoff tracker. Call [`next_delay()`](Self::next_delay) after each
44/// failed attempt and [`reset()`](Self::reset) after success.
45#[derive(Debug, Clone)]
46pub struct BackoffState {
47    config: BackoffConfig,
48    current_delay: Duration,
49}
50
51impl BackoffState {
52    pub fn new(config: BackoffConfig) -> Self {
53        let current_delay = config.initial_delay;
54        Self {
55            config,
56            current_delay,
57        }
58    }
59
60    pub fn next_delay(&mut self) -> Duration {
61        let delay = self.current_delay;
62        let exp_value = self.config.multiplier;
63        let millis = (delay.as_millis() as f64 * exp_value) as u128;
64        let capped = millis.min(self.config.max_delay.as_millis());
65        self.current_delay = Duration::from_millis(capped as u64);
66        delay
67    }
68
69    pub fn reset(&mut self) {
70        self.current_delay = self.config.initial_delay;
71    }
72
73    pub fn current_delay(&self) -> Duration {
74        self.current_delay
75    }
76}
77
78#[cfg(test)]
79mod tests {
80    use super::*;
81
82    #[test]
83    fn backoff_default_config_values() {
84        let cfg = BackoffConfig::default();
85        assert_eq!(cfg.initial_delay, Duration::from_millis(100));
86        assert_eq!(cfg.multiplier, 2.0);
87        assert_eq!(cfg.max_delay, Duration::from_secs(30));
88    }
89
90    #[test]
91    fn backoff_state_exponential_growth() {
92        let mut state = BackoffState::new(BackoffConfig::default());
93        assert_eq!(state.next_delay(), Duration::from_millis(100));
94        assert_eq!(state.next_delay(), Duration::from_millis(200));
95        assert_eq!(state.next_delay(), Duration::from_millis(400));
96        assert_eq!(state.next_delay(), Duration::from_millis(800));
97    }
98
99    #[test]
100    fn backoff_state_caps_at_max() {
101        let cfg = BackoffConfig {
102            max_delay: Duration::from_millis(500),
103            ..Default::default()
104        };
105        let mut state = BackoffState::new(cfg);
106        assert_eq!(state.next_delay(), Duration::from_millis(100));
107        assert_eq!(state.next_delay(), Duration::from_millis(200));
108        assert_eq!(state.next_delay(), Duration::from_millis(400));
109        assert_eq!(state.next_delay(), Duration::from_millis(500));
110        assert_eq!(state.next_delay(), Duration::from_millis(500));
111    }
112
113    #[test]
114    fn backoff_state_reset() {
115        let mut state = BackoffState::new(BackoffConfig::default());
116        state.next_delay();
117        state.next_delay();
118        assert_eq!(state.current_delay(), Duration::from_millis(400));
119        state.reset();
120        assert_eq!(state.current_delay(), Duration::from_millis(100));
121        assert_eq!(state.next_delay(), Duration::from_millis(100));
122    }
123
124    #[test]
125    fn backoff_delay_for_attempt_stateless() {
126        let cfg = BackoffConfig::default();
127        assert_eq!(cfg.delay_for_attempt(0), Duration::from_millis(100));
128        assert_eq!(cfg.delay_for_attempt(1), Duration::from_millis(200));
129        assert_eq!(cfg.delay_for_attempt(2), Duration::from_millis(400));
130    }
131
132    #[test]
133    fn backoff_delay_for_attempt_caps() {
134        let cfg = BackoffConfig {
135            max_delay: Duration::from_secs(30),
136            ..Default::default()
137        };
138        let d = cfg.delay_for_attempt(100);
139        assert_eq!(d, Duration::from_secs(30));
140    }
141
142    #[test]
143    fn backoff_custom_multiplier() {
144        let cfg = BackoffConfig {
145            multiplier: 1.5,
146            initial_delay: Duration::from_millis(200),
147            max_delay: Duration::from_secs(60),
148        };
149        let mut state = BackoffState::new(cfg);
150        assert_eq!(state.next_delay(), Duration::from_millis(200));
151        assert_eq!(state.next_delay(), Duration::from_millis(300));
152        assert_eq!(state.next_delay(), Duration::from_millis(450));
153    }
154}