error_forge/recovery/
backoff.rs

1use std::time::Duration;
2use std::cmp::min;
3use rand::Rng;
4
5/// Trait for backoff strategies used in retry mechanisms
6pub trait Backoff: Send + Sync + 'static {
7    /// Get the next delay duration based on the current attempt
8    fn next_delay(&self, attempt: usize) -> Duration;
9    
10    /// Reset the backoff state
11    fn reset(&mut self) {}
12    
13    /// Create a clone of this backoff strategy
14    fn box_clone(&self) -> Box<dyn Backoff>;
15}
16
17/// Exponential backoff strategy with optional jitter
18///
19/// This strategy increases the delay exponentially with each attempt,
20/// and can add random jitter to prevent multiple retries from synchronizing.
21#[derive(Clone)]
22pub struct ExponentialBackoff {
23    initial_delay_ms: u64,
24    max_delay_ms: u64,
25    factor: f64,
26    jitter: bool,
27}
28
29impl ExponentialBackoff {
30    /// Create a new exponential backoff with default settings
31    ///
32    /// Defaults:
33    /// - Initial delay: 100ms
34    /// - Max delay: 30000ms (30 seconds)
35    /// - Factor: 2.0 (doubles with each attempt)
36    /// - Jitter: false
37    pub fn new() -> Self {
38        Self {
39            initial_delay_ms: 100,
40            max_delay_ms: 30000,
41            factor: 2.0,
42            jitter: false,
43        }
44    }
45    
46    /// Set the initial delay in milliseconds
47    pub fn with_initial_delay(mut self, delay_ms: u64) -> Self {
48        self.initial_delay_ms = delay_ms;
49        self
50    }
51    
52    /// Set the maximum delay in milliseconds
53    pub fn with_max_delay(mut self, delay_ms: u64) -> Self {
54        self.max_delay_ms = delay_ms;
55        self
56    }
57    
58    /// Set the multiplication factor for each attempt
59    pub fn with_factor(mut self, factor: f64) -> Self {
60        self.factor = factor;
61        self
62    }
63    
64    /// Enable or disable jitter
65    pub fn with_jitter(mut self, jitter: bool) -> Self {
66        self.jitter = jitter;
67        self
68    }
69}
70
71impl Backoff for ExponentialBackoff {
72    fn next_delay(&self, attempt: usize) -> Duration {
73        if attempt == 0 {
74            return Duration::from_millis(self.initial_delay_ms);
75        }
76        
77        // Calculate exponential delay
78        let exp_factor = self.factor.powi(attempt as i32);
79        let calculated_delay = (self.initial_delay_ms as f64 * exp_factor) as u64;
80        let capped_delay = min(calculated_delay, self.max_delay_ms);
81        
82        if self.jitter {
83            // Apply jitter (±20%)
84            let mut rng = rand::thread_rng();
85            let jitter_factor = rng.gen_range(0.8..1.2);
86            let jittered_delay = (capped_delay as f64 * jitter_factor) as u64;
87            Duration::from_millis(jittered_delay)
88        } else {
89            Duration::from_millis(capped_delay)
90        }
91    }
92    
93    fn box_clone(&self) -> Box<dyn Backoff> {
94        Box::new(self.clone())
95    }
96}
97
98/// Linear backoff strategy
99///
100/// Increases delay linearly by adding a fixed increment with each attempt.
101#[derive(Clone)]
102pub struct LinearBackoff {
103    initial_delay_ms: u64,
104    increment_ms: u64,
105    max_delay_ms: u64,
106}
107
108impl LinearBackoff {
109    /// Create a new linear backoff with default settings
110    ///
111    /// Defaults:
112    /// - Initial delay: 100ms
113    /// - Increment: 100ms (adds 100ms per attempt)
114    /// - Max delay: 10000ms (10 seconds)
115    pub fn new() -> Self {
116        Self {
117            initial_delay_ms: 100,
118            increment_ms: 100,
119            max_delay_ms: 10000,
120        }
121    }
122    
123    /// Set the initial delay in milliseconds
124    pub fn with_initial_delay(mut self, delay_ms: u64) -> Self {
125        self.initial_delay_ms = delay_ms;
126        self
127    }
128    
129    /// Set the increment in milliseconds
130    pub fn with_increment(mut self, increment_ms: u64) -> Self {
131        self.increment_ms = increment_ms;
132        self
133    }
134    
135    /// Set the maximum delay in milliseconds
136    pub fn with_max_delay(mut self, delay_ms: u64) -> Self {
137        self.max_delay_ms = delay_ms;
138        self
139    }
140}
141
142impl Backoff for LinearBackoff {
143    fn next_delay(&self, attempt: usize) -> Duration {
144        let delay_ms = self.initial_delay_ms + (attempt as u64 * self.increment_ms);
145        let capped_delay = min(delay_ms, self.max_delay_ms);
146        Duration::from_millis(capped_delay)
147    }
148    
149    fn box_clone(&self) -> Box<dyn Backoff> {
150        Box::new(self.clone())
151    }
152}
153
154/// Fixed backoff strategy
155///
156/// Uses the same delay for all retry attempts.
157#[derive(Clone)]
158pub struct FixedBackoff {
159    delay_ms: u64,
160}
161
162impl FixedBackoff {
163    /// Create a new fixed backoff with the given delay
164    pub fn new(delay_ms: u64) -> Self {
165        Self { delay_ms }
166    }
167}
168
169impl Backoff for FixedBackoff {
170    fn next_delay(&self, _attempt: usize) -> Duration {
171        Duration::from_millis(self.delay_ms)
172    }
173    
174    fn box_clone(&self) -> Box<dyn Backoff> {
175        Box::new(self.clone())
176    }
177}
178
179impl Default for ExponentialBackoff {
180    fn default() -> Self {
181        Self::new()
182    }
183}
184
185impl Default for LinearBackoff {
186    fn default() -> Self {
187        Self::new()
188    }
189}
190
191// Implement Backoff for Box<dyn Backoff> to enable boxed trait objects
192impl Backoff for Box<dyn Backoff> {
193    fn next_delay(&self, attempt: usize) -> Duration {
194        (**self).next_delay(attempt)
195    }
196    
197    fn reset(&mut self) {
198        (**self).reset()
199    }
200    
201    fn box_clone(&self) -> Box<dyn Backoff> {
202        (**self).box_clone()
203    }
204}