Skip to main content

tower_resilience_retry/
backoff.rs

1use std::time::Duration;
2
3/// Abstraction for computing retry intervals.
4///
5/// This trait allows for flexible backoff strategies including fixed delays,
6/// exponential backoff, randomized backoff, and custom implementations.
7pub trait IntervalFunction: Send + Sync {
8    /// Computes the delay before the next retry attempt.
9    ///
10    /// # Arguments
11    /// * `attempt` - The retry attempt number (0-indexed, so first retry is 0)
12    fn next_interval(&self, attempt: usize) -> Duration;
13}
14
15/// Fixed interval backoff - returns the same duration for every retry.
16#[derive(Debug, Clone)]
17pub struct FixedInterval {
18    duration: Duration,
19}
20
21impl FixedInterval {
22    /// Creates a new fixed interval backoff.
23    pub fn new(duration: Duration) -> Self {
24        Self { duration }
25    }
26}
27
28impl IntervalFunction for FixedInterval {
29    fn next_interval(&self, _attempt: usize) -> Duration {
30        self.duration
31    }
32}
33
34/// Exponential backoff with configurable multiplier.
35#[derive(Debug, Clone)]
36pub struct ExponentialBackoff {
37    initial_interval: Duration,
38    multiplier: f64,
39    max_interval: Option<Duration>,
40}
41
42impl ExponentialBackoff {
43    /// Creates a new exponential backoff with default multiplier of 2.0.
44    pub fn new(initial_interval: Duration) -> Self {
45        Self {
46            initial_interval,
47            multiplier: 2.0,
48            max_interval: None,
49        }
50    }
51
52    /// Sets the multiplier for exponential growth.
53    pub fn multiplier(mut self, multiplier: f64) -> Self {
54        self.multiplier = multiplier;
55        self
56    }
57
58    /// Sets the maximum interval to cap exponential growth.
59    pub fn max_interval(mut self, max_interval: Duration) -> Self {
60        self.max_interval = Some(max_interval);
61        self
62    }
63}
64
65impl IntervalFunction for ExponentialBackoff {
66    fn next_interval(&self, attempt: usize) -> Duration {
67        let multiplier = self.multiplier.powi(attempt as i32);
68        let interval = self.initial_interval.mul_f64(multiplier);
69
70        if let Some(max) = self.max_interval {
71            interval.min(max)
72        } else {
73            interval
74        }
75    }
76}
77
78/// Exponential backoff with randomization to prevent thundering herd.
79#[derive(Debug, Clone)]
80pub struct ExponentialRandomBackoff {
81    initial_interval: Duration,
82    multiplier: f64,
83    randomization_factor: f64,
84    max_interval: Option<Duration>,
85}
86
87impl ExponentialRandomBackoff {
88    /// Creates a new exponential random backoff.
89    ///
90    /// # Arguments
91    /// * `initial_interval` - The base interval
92    /// * `randomization_factor` - Factor for randomization (0.0 to 1.0)
93    ///   A factor of 0.5 means the interval will be randomized between 50% and 150% of the calculated value.
94    pub fn new(initial_interval: Duration, randomization_factor: f64) -> Self {
95        Self {
96            initial_interval,
97            multiplier: 2.0,
98            randomization_factor: randomization_factor.clamp(0.0, 1.0),
99            max_interval: None,
100        }
101    }
102
103    /// Sets the multiplier for exponential growth.
104    pub fn multiplier(mut self, multiplier: f64) -> Self {
105        self.multiplier = multiplier;
106        self
107    }
108
109    /// Sets the maximum interval to cap exponential growth.
110    pub fn max_interval(mut self, max_interval: Duration) -> Self {
111        self.max_interval = Some(max_interval);
112        self
113    }
114
115    fn randomize(&self, duration: Duration) -> Duration {
116        use rand::Rng;
117        let mut rng = rand::rng();
118        let delta = duration.as_secs_f64() * self.randomization_factor;
119        let min = duration.as_secs_f64() - delta;
120        let max = duration.as_secs_f64() + delta;
121        let randomized = rng.random_range(min..=max);
122        Duration::from_secs_f64(randomized.max(0.0))
123    }
124}
125
126impl IntervalFunction for ExponentialRandomBackoff {
127    fn next_interval(&self, attempt: usize) -> Duration {
128        let multiplier = self.multiplier.powi(attempt as i32);
129        let interval = self.initial_interval.mul_f64(multiplier);
130
131        let capped = if let Some(max) = self.max_interval {
132            interval.min(max)
133        } else {
134            interval
135        };
136
137        self.randomize(capped)
138    }
139}
140
141/// Function-based interval implementation.
142pub struct FnInterval<F> {
143    f: F,
144}
145
146impl<F> FnInterval<F>
147where
148    F: Fn(usize) -> Duration + Send + Sync,
149{
150    /// Creates a new function-based interval.
151    pub fn new(f: F) -> Self {
152        Self { f }
153    }
154}
155
156impl<F> IntervalFunction for FnInterval<F>
157where
158    F: Fn(usize) -> Duration + Send + Sync,
159{
160    fn next_interval(&self, attempt: usize) -> Duration {
161        (self.f)(attempt)
162    }
163}
164
165#[cfg(test)]
166mod tests {
167    use super::*;
168
169    #[test]
170    fn fixed_interval_returns_same_duration() {
171        let backoff = FixedInterval::new(Duration::from_secs(1));
172        assert_eq!(backoff.next_interval(0), Duration::from_secs(1));
173        assert_eq!(backoff.next_interval(1), Duration::from_secs(1));
174        assert_eq!(backoff.next_interval(10), Duration::from_secs(1));
175    }
176
177    #[test]
178    fn exponential_backoff_grows() {
179        let backoff = ExponentialBackoff::new(Duration::from_millis(100));
180        assert_eq!(backoff.next_interval(0), Duration::from_millis(100));
181        assert_eq!(backoff.next_interval(1), Duration::from_millis(200));
182        assert_eq!(backoff.next_interval(2), Duration::from_millis(400));
183        assert_eq!(backoff.next_interval(3), Duration::from_millis(800));
184    }
185
186    #[test]
187    fn exponential_backoff_custom_multiplier() {
188        let backoff = ExponentialBackoff::new(Duration::from_millis(100)).multiplier(3.0);
189        assert_eq!(backoff.next_interval(0), Duration::from_millis(100));
190        assert_eq!(backoff.next_interval(1), Duration::from_millis(300));
191        assert_eq!(backoff.next_interval(2), Duration::from_millis(900));
192    }
193
194    #[test]
195    fn exponential_backoff_respects_max() {
196        let backoff = ExponentialBackoff::new(Duration::from_millis(100))
197            .max_interval(Duration::from_millis(500));
198        assert_eq!(backoff.next_interval(0), Duration::from_millis(100));
199        assert_eq!(backoff.next_interval(1), Duration::from_millis(200));
200        assert_eq!(backoff.next_interval(2), Duration::from_millis(400));
201        assert_eq!(backoff.next_interval(3), Duration::from_millis(500)); // capped
202        assert_eq!(backoff.next_interval(4), Duration::from_millis(500)); // capped
203    }
204
205    #[test]
206    fn exponential_random_backoff_has_variance() {
207        let backoff = ExponentialRandomBackoff::new(Duration::from_millis(100), 0.5);
208
209        // Run multiple times to check randomization
210        let mut intervals = Vec::new();
211        for _ in 0..10 {
212            intervals.push(backoff.next_interval(1));
213        }
214
215        // Should have some variance (not all the same)
216        let all_same = intervals.windows(2).all(|w| w[0] == w[1]);
217        assert!(!all_same, "Randomized intervals should vary");
218
219        // All should be within expected range (100ms to 300ms for attempt 1)
220        // Base: 200ms (100 * 2^1), with 0.5 factor: 100ms to 300ms
221        for interval in intervals {
222            assert!(
223                interval >= Duration::from_millis(100) && interval <= Duration::from_millis(300),
224                "Interval {:?} outside expected range",
225                interval
226            );
227        }
228    }
229
230    #[test]
231    fn fn_interval_uses_custom_function() {
232        let backoff = FnInterval::new(|attempt| Duration::from_secs((attempt + 1) as u64));
233        assert_eq!(backoff.next_interval(0), Duration::from_secs(1));
234        assert_eq!(backoff.next_interval(1), Duration::from_secs(2));
235        assert_eq!(backoff.next_interval(2), Duration::from_secs(3));
236    }
237}