Skip to main content

error_forge/recovery/
backoff.rs

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