futures_retrying/
backoff.rs

1use rand::{thread_rng, Rng};
2use std::time::{Duration, Instant};
3
4/// Make a zero delay backoff
5pub fn instant() -> impl Backoff + Sized {
6    Duration::from_secs(0)
7}
8
9/// Make a constant duration backoff
10pub fn constant(duration: Duration) -> impl Backoff + Sized {
11    duration
12}
13
14pub trait Backoff: Send {
15    /// Get the duration to wait for before attempting again
16    fn next_retry(&mut self) -> Option<Duration>;
17
18    /// Grow the backoff duration exponentially
19    fn exponential(self) -> Exponential<Self>
20    where
21        Self: Sized,
22    {
23        Exponential {
24            factor: 1,
25            inner: self,
26        }
27    }
28
29    /// Set the maximum backoff duration
30    fn max_backoff(self, max: Duration) -> Max<Self>
31    where
32        Self: Sized,
33    {
34        Max { max, inner: self }
35    }
36
37    /// Set the minimum backoff duration
38    fn min_backoff(self, min: Duration) -> Min<Self>
39    where
40        Self: Sized,
41    {
42        Min { min, inner: self }
43    }
44
45    /// Randomize the backoff duration.
46    ///
47    /// The returned duration will never be larger than the base duration and will
48    /// never be smaller than `base * (1.0 - scale)`.
49    fn jitter(self, scale: f64) -> Jitter<Self>
50    where
51        Self: Sized,
52    {
53        assert!(scale > 0.0, "scale must be larger than zero");
54        assert!(scale <= 1.0, "scale must be smaller or equal to one");
55        Jitter { scale, inner: self }
56    }
57
58    fn num_attempts(self, num: u32) -> MaxAttempts<Self>
59    where
60        Self: Sized,
61    {
62        assert!(num > 0, "num must be larger than zero");
63        let num_attempts_left = num - 1;
64        MaxAttempts {
65            num_attempts_left,
66            inner: self,
67        }
68    }
69
70    fn deadline(self, deadline: Instant) -> Deadline<Self>
71    where
72        Self: Sized,
73    {
74        Deadline {
75            deadline,
76            inner: self,
77        }
78    }
79}
80
81impl Backoff for Duration {
82    fn next_retry(&mut self) -> Option<Duration> {
83        Some(*self)
84    }
85}
86
87pub struct Exponential<S>
88where
89    S: Backoff,
90{
91    inner: S,
92    factor: u32,
93}
94
95impl<S> Backoff for Exponential<S>
96where
97    S: Backoff,
98{
99    fn next_retry(&mut self) -> Option<Duration> {
100        let dur = self.inner.next_retry().map(|dur| dur * (self.factor as _));
101        self.factor *= 2;
102        dur
103    }
104}
105
106pub struct Max<S>
107where
108    S: Backoff,
109{
110    inner: S,
111    max: Duration,
112}
113
114impl<S> Backoff for Max<S>
115where
116    S: Backoff,
117{
118    fn next_retry(&mut self) -> Option<Duration> {
119        self.inner
120            .next_retry()
121            .map(|dur| std::cmp::min(self.max, dur))
122    }
123}
124
125pub struct Min<S>
126where
127    S: Backoff,
128{
129    inner: S,
130    min: Duration,
131}
132
133impl<S> Backoff for Min<S>
134where
135    S: Backoff,
136{
137    fn next_retry(&mut self) -> Option<Duration> {
138        self.inner
139            .next_retry()
140            .map(|dur| std::cmp::max(self.min, dur))
141    }
142}
143
144pub struct Jitter<S>
145where
146    S: Backoff,
147{
148    inner: S,
149    scale: f64,
150}
151
152impl<S> Backoff for Jitter<S>
153where
154    S: Backoff,
155{
156    fn next_retry(&mut self) -> Option<Duration> {
157        self.inner.next_retry().map(|dur| {
158            let margin = Duration::from_secs_f64(dur.as_secs_f64() * self.scale);
159            thread_rng().gen_range(dur - margin, dur)
160        })
161    }
162}
163
164pub struct MaxAttempts<S>
165where
166    S: Backoff,
167{
168    inner: S,
169    num_attempts_left: u32,
170}
171
172impl<S> Backoff for MaxAttempts<S>
173where
174    S: Backoff,
175{
176    fn next_retry(&mut self) -> Option<Duration> {
177        if self.num_attempts_left > 0 {
178            self.num_attempts_left -= 1;
179            self.inner.next_retry()
180        } else {
181            None
182        }
183    }
184}
185
186pub struct Deadline<S>
187where
188    S: Backoff,
189{
190    inner: S,
191    deadline: Instant,
192}
193
194impl<S> Backoff for Deadline<S>
195where
196    S: Backoff,
197{
198    fn next_retry(&mut self) -> Option<Duration> {
199        if self.deadline < Instant::now() {
200            None
201        } else {
202            self.inner.next_retry()
203        }
204    }
205}
206
207#[cfg(test)]
208mod tests {
209    use super::*;
210
211    #[test]
212    fn test_instant() {
213        let mut bo = instant();
214        assert_eq!(bo.next_retry(), Some(Duration::from_secs(0)));
215        assert_eq!(bo.next_retry(), Some(Duration::from_secs(0)));
216    }
217
218    #[test]
219    fn test_constant() {
220        let mut bo = constant(Duration::from_secs(5));
221        assert_eq!(bo.next_retry(), Some(Duration::from_secs(5)));
222        assert_eq!(bo.next_retry(), Some(Duration::from_secs(5)));
223    }
224
225    #[test]
226    fn test_min_backoff() {
227        let mut bo = constant(Duration::from_secs(5)).min_backoff(Duration::from_secs(10));
228        assert_eq!(bo.next_retry(), Some(Duration::from_secs(10)));
229        assert_eq!(bo.next_retry(), Some(Duration::from_secs(10)));
230
231        let mut bo = constant(Duration::from_secs(5)).min_backoff(Duration::from_secs(3));
232        assert_eq!(bo.next_retry(), Some(Duration::from_secs(5)));
233        assert_eq!(bo.next_retry(), Some(Duration::from_secs(5)));
234    }
235
236    #[test]
237    fn test_max_backoff() {
238        let mut bo = constant(Duration::from_secs(5)).max_backoff(Duration::from_secs(10));
239        assert_eq!(bo.next_retry(), Some(Duration::from_secs(5)));
240        assert_eq!(bo.next_retry(), Some(Duration::from_secs(5)));
241
242        let mut bo = constant(Duration::from_secs(5)).max_backoff(Duration::from_secs(3));
243        assert_eq!(bo.next_retry(), Some(Duration::from_secs(3)));
244        assert_eq!(bo.next_retry(), Some(Duration::from_secs(3)));
245    }
246
247    #[test]
248    fn test_exponential() {
249        let mut bo = constant(Duration::from_secs(1)).exponential();
250        assert_eq!(bo.next_retry(), Some(Duration::from_secs(1)));
251        assert_eq!(bo.next_retry(), Some(Duration::from_secs(2)));
252        assert_eq!(bo.next_retry(), Some(Duration::from_secs(4)));
253        assert_eq!(bo.next_retry(), Some(Duration::from_secs(8)));
254    }
255
256    #[test]
257    fn test_jitter() {
258        let mut bo = constant(Duration::from_secs(1)).jitter(0.1);
259        let range = Duration::from_millis(900)..=Duration::from_secs(1);
260        for _i in 0..100_000 {
261            let dur = bo.next_retry().unwrap();
262            assert!(range.contains(&dur));
263        }
264    }
265
266    #[test]
267    fn test_num_attempts() {
268        let mut bo = constant(Duration::from_secs(1)).num_attempts(3);
269        assert_eq!(bo.next_retry(), Some(Duration::from_secs(1)));
270        assert_eq!(bo.next_retry(), Some(Duration::from_secs(1)));
271        assert_eq!(bo.next_retry(), None);
272        assert_eq!(bo.next_retry(), None);
273    }
274
275    #[test]
276    fn deadline() {
277        let mut bo =
278            constant(Duration::from_secs(1)).deadline(Instant::now() + Duration::from_millis(20));
279        assert_eq!(bo.next_retry(), Some(Duration::from_secs(1)));
280        assert_eq!(bo.next_retry(), Some(Duration::from_secs(1)));
281        std::thread::sleep(Duration::from_millis(21));
282        assert_eq!(bo.next_retry(), None);
283        assert_eq!(bo.next_retry(), None);
284    }
285}