Skip to main content

atomr_core/pattern/
backoff.rs

1//! Backoff supervisor — restart child with exponential backoff.
2
3use std::time::Duration;
4
5#[derive(Debug, Clone)]
6pub struct BackoffOptions {
7    pub min_backoff: Duration,
8    pub max_backoff: Duration,
9    pub random_factor: f64,
10    pub max_restarts: Option<u32>,
11}
12
13impl Default for BackoffOptions {
14    fn default() -> Self {
15        Self {
16            min_backoff: Duration::from_millis(200),
17            max_backoff: Duration::from_secs(30),
18            random_factor: 0.2,
19            max_restarts: Some(10),
20        }
21    }
22}
23
24impl BackoffOptions {
25    pub fn next_delay(&self, attempt: u32) -> Duration {
26        let base = self.min_backoff.as_secs_f64() * 2f64.powi(attempt as i32);
27        let capped = base.min(self.max_backoff.as_secs_f64());
28        let jitter = 1.0 + (pseudo_random_01(attempt) * self.random_factor);
29        Duration::from_secs_f64(capped * jitter)
30    }
31}
32
33fn pseudo_random_01(seed: u32) -> f64 {
34    // Deterministic stand-in — tests don't depend on true randomness.
35
36    ((seed.wrapping_mul(2654435761)) % 10_000) as f64 / 10_000.0
37}
38
39/// Wrapper around the supervisor logic — held by tests and demonstrations.
40pub struct BackoffSupervisor {
41    pub options: BackoffOptions,
42}
43
44impl BackoffSupervisor {
45    pub fn new(options: BackoffOptions) -> Self {
46        Self { options }
47    }
48}
49
50#[cfg(test)]
51mod tests {
52    use super::*;
53
54    #[test]
55    fn backoff_grows_but_capped() {
56        let o = BackoffOptions::default();
57        let d0 = o.next_delay(0);
58        let d1 = o.next_delay(1);
59        let huge = o.next_delay(30);
60        assert!(d0 < d1);
61        assert!(huge <= o.max_backoff.mul_f64(1.0 + o.random_factor));
62    }
63}