Skip to main content

mx_core/resilience/
backoff.rs

1//! Exponential backoff calculation utilities.
2
3use std::time::Duration;
4
5/// Trait for backoff strategies.
6pub trait Backoff: Send + Sync {
7    /// Calculates the delay for the given attempt number (0-based).
8    fn delay(&self, attempt: u32) -> Duration;
9
10    /// Returns the maximum delay this backoff will produce.
11    fn max_delay(&self) -> Duration;
12}
13
14/// Exponential backoff: `min(base * 2^attempt, max_delay)`.
15#[derive(Debug, Clone)]
16pub struct ExponentialBackoff {
17    pub base_delay: Duration,
18    pub max_delay: Duration,
19    /// Maximum exponent to prevent overflow (default: 16).
20    pub max_exponent: u32,
21}
22
23impl ExponentialBackoff {
24    pub fn new(base_delay: Duration, max_delay: Duration) -> Self {
25        Self {
26            base_delay,
27            max_delay,
28            max_exponent: 16,
29        }
30    }
31
32    pub fn default_settings() -> Self {
33        Self::new(Duration::from_millis(100), Duration::from_secs(30))
34    }
35
36    pub fn with_max_exponent(mut self, max_exponent: u32) -> Self {
37        self.max_exponent = max_exponent;
38        self
39    }
40}
41
42impl Default for ExponentialBackoff {
43    fn default() -> Self {
44        Self::default_settings()
45    }
46}
47
48impl Backoff for ExponentialBackoff {
49    fn delay(&self, attempt: u32) -> Duration {
50        let exponent = attempt.min(self.max_exponent);
51        let multiplier = 1u64.checked_shl(exponent).unwrap_or(u64::MAX);
52        let delay_nanos = self
53            .base_delay
54            .as_nanos()
55            .saturating_mul(u128::from(multiplier));
56        let delay = Duration::from_nanos(delay_nanos.min(u128::from(u64::MAX)) as u64);
57        delay.min(self.max_delay)
58    }
59
60    fn max_delay(&self) -> Duration {
61        self.max_delay
62    }
63}
64
65#[cfg(test)]
66mod tests {
67    use super::*;
68
69    #[test]
70    fn test_exponential_sequence() {
71        let b = ExponentialBackoff::new(Duration::from_millis(100), Duration::from_secs(30));
72        assert_eq!(b.delay(0), Duration::from_millis(100));
73        assert_eq!(b.delay(1), Duration::from_millis(200));
74        assert_eq!(b.delay(2), Duration::from_millis(400));
75        assert_eq!(b.delay(3), Duration::from_millis(800));
76    }
77
78    #[test]
79    fn test_capped_at_max() {
80        let b = ExponentialBackoff::new(Duration::from_millis(100), Duration::from_secs(1));
81        assert_eq!(b.delay(10), Duration::from_secs(1));
82        assert_eq!(b.delay(20), Duration::from_secs(1));
83    }
84
85    #[test]
86    fn test_overflow_protection() {
87        let b = ExponentialBackoff::new(Duration::from_secs(1), Duration::from_secs(3600));
88        let delay = b.delay(100);
89        assert!(delay <= Duration::from_secs(3600));
90    }
91}