backon/backoff/
fibonacci.rs

1use core::time::Duration;
2
3use crate::backoff::BackoffBuilder;
4
5/// FibonacciBuilder is used to build a [`FibonacciBackoff`] which offers a delay with Fibonacci-based retries.
6///
7/// # Default
8///
9/// - jitter: false
10/// - min_delay: 1s
11/// - max_delay: 60s
12/// - max_times: 3
13///
14/// # Examples
15///
16/// ```no_run
17/// use anyhow::Result;
18/// use backon::FibonacciBuilder;
19/// use backon::Retryable;
20///
21/// async fn fetch() -> Result<String> {
22///     Ok(reqwest::get("https://www.rust-lang.org")
23///         .await?
24///         .text()
25///         .await?)
26/// }
27///
28/// #[tokio::main(flavor = "current_thread")]
29/// async fn main() -> Result<()> {
30///     let content = fetch.retry(FibonacciBuilder::default()).await?;
31///     println!("fetch succeeded: {}", content);
32///
33///     Ok(())
34/// }
35/// ```
36#[derive(Debug, Clone, Copy)]
37pub struct FibonacciBuilder {
38    jitter: bool,
39    seed: Option<u64>,
40    min_delay: Duration,
41    max_delay: Option<Duration>,
42    max_times: Option<usize>,
43}
44
45impl Default for FibonacciBuilder {
46    fn default() -> Self {
47        Self::new()
48    }
49}
50
51impl FibonacciBuilder {
52    /// Create a new `FibonacciBuilder` with default values.
53    pub const fn new() -> Self {
54        Self {
55            jitter: false,
56            seed: None,
57            min_delay: Duration::from_secs(1),
58            max_delay: Some(Duration::from_secs(60)),
59            max_times: Some(3),
60        }
61    }
62
63    /// Set the jitter for the backoff.
64    ///
65    /// When jitter is enabled, FibonacciBackoff will add a random jitter between `(0, current_delay)` to the delay.
66    pub const fn with_jitter(mut self) -> Self {
67        self.jitter = true;
68        self
69    }
70
71    /// Set the seed value for the jitter random number generator. If no seed is given, a random seed is used in std and default seed is used in no_std.
72    pub fn with_jitter_seed(mut self, seed: u64) -> Self {
73        self.seed = Some(seed);
74        self
75    }
76
77    /// Set the minimum delay for the backoff.
78    pub const fn with_min_delay(mut self, min_delay: Duration) -> Self {
79        self.min_delay = min_delay;
80        self
81    }
82
83    /// Set the maximum delay for the current backoff.
84    ///
85    /// The delay will not increase if the current delay exceeds the maximum delay.
86    pub const fn with_max_delay(mut self, max_delay: Duration) -> Self {
87        self.max_delay = Some(max_delay);
88        self
89    }
90
91    /// Set no maximum delay for the backoff.
92    ///
93    /// The delay will keep increasing.
94    ///
95    /// _The delay will saturate at `Duration::MAX` which is an **unrealistic** delay._
96    pub const fn without_max_delay(mut self) -> Self {
97        self.max_delay = None;
98        self
99    }
100
101    /// Set the maximum number of attempts for the current backoff.
102    ///
103    /// The backoff will stop if the maximum number of attempts is reached.
104    pub const fn with_max_times(mut self, max_times: usize) -> Self {
105        self.max_times = Some(max_times);
106        self
107    }
108
109    /// Set no maximum number of attempts for the current backoff.
110    ///
111    /// The backoff will not stop by itself.
112    ///
113    /// _The backoff could stop reaching `usize::MAX` attempts but this is **unrealistic**._
114    pub const fn without_max_times(mut self) -> Self {
115        self.max_times = None;
116        self
117    }
118}
119
120impl BackoffBuilder for FibonacciBuilder {
121    type Backoff = FibonacciBackoff;
122
123    fn build(self) -> Self::Backoff {
124        FibonacciBackoff {
125            jitter: self.jitter,
126            rng: if let Some(seed) = self.seed {
127                fastrand::Rng::with_seed(seed)
128            } else {
129                #[cfg(feature = "std")]
130                let rng = fastrand::Rng::new();
131
132                #[cfg(not(feature = "std"))]
133                let rng = fastrand::Rng::with_seed(super::RANDOM_SEED);
134
135                rng
136            },
137            min_delay: self.min_delay,
138            max_delay: self.max_delay,
139            max_times: self.max_times,
140
141            previous_delay: None,
142            current_delay: None,
143            attempts: 0,
144        }
145    }
146}
147
148impl BackoffBuilder for &FibonacciBuilder {
149    type Backoff = FibonacciBackoff;
150
151    fn build(self) -> Self::Backoff {
152        (*self).build()
153    }
154}
155
156/// FibonacciBackoff offers a delay with Fibonacci-based retries.
157///
158/// This backoff strategy is constructed by [`FibonacciBuilder`].
159#[doc(hidden)]
160#[derive(Debug)]
161pub struct FibonacciBackoff {
162    jitter: bool,
163    rng: fastrand::Rng,
164    min_delay: Duration,
165    max_delay: Option<Duration>,
166    max_times: Option<usize>,
167
168    previous_delay: Option<Duration>,
169    current_delay: Option<Duration>,
170    attempts: usize,
171}
172
173impl Iterator for FibonacciBackoff {
174    type Item = Duration;
175
176    fn next(&mut self) -> Option<Self::Item> {
177        if self.attempts >= self.max_times.unwrap_or(usize::MAX) {
178            return None;
179        }
180        self.attempts += 1;
181
182        match self.current_delay {
183            None => {
184                // If current_delay is None, it's must be the first time to retry.
185                let mut next = self.min_delay;
186                self.current_delay = Some(next);
187
188                // If jitter is enabled, add random jitter based on min delay.
189                if self.jitter {
190                    next += next.mul_f32(self.rng.f32());
191                }
192
193                Some(next)
194            }
195            Some(cur) => {
196                let mut next = cur;
197
198                // If current delay larger than max delay, we should stop increment anymore.
199                if next < self.max_delay.unwrap_or(Duration::MAX) {
200                    if let Some(prev) = self.previous_delay {
201                        next = next.saturating_add(prev);
202                        self.current_delay = Some(next);
203                    }
204                    self.previous_delay = Some(cur);
205                }
206
207                // If jitter is enabled, add random jitter based on min delay.
208                if self.jitter {
209                    next += self.min_delay.mul_f32(self.rng.f32());
210                }
211
212                Some(next)
213            }
214        }
215    }
216}
217
218#[cfg(test)]
219mod tests {
220    use core::time::Duration;
221
222    #[cfg(target_arch = "wasm32")]
223    use wasm_bindgen_test::wasm_bindgen_test as test;
224
225    use super::*;
226
227    const TEST_BUILDER: FibonacciBuilder = FibonacciBuilder::new()
228        .with_jitter()
229        .with_min_delay(Duration::from_secs(2))
230        .with_max_delay(Duration::from_secs(30))
231        .with_max_times(5);
232
233    #[test]
234    fn test_fibonacci_default() {
235        let mut fib = FibonacciBuilder::default().build();
236
237        assert_eq!(Some(Duration::from_secs(1)), fib.next());
238        assert_eq!(Some(Duration::from_secs(1)), fib.next());
239        assert_eq!(Some(Duration::from_secs(2)), fib.next());
240        assert_eq!(None, fib.next());
241    }
242
243    #[test]
244    fn test_fibonacci_jitter() {
245        let mut fib = FibonacciBuilder::default().with_jitter().build();
246
247        let v = fib.next().expect("value must valid");
248        assert!(v >= Duration::from_secs(1), "current: {v:?}");
249        assert!(v < Duration::from_secs(2), "current: {v:?}");
250
251        let v = fib.next().expect("value must valid");
252        assert!(v >= Duration::from_secs(1), "current: {v:?}");
253        assert!(v < Duration::from_secs(2), "current: {v:?}");
254
255        let v = fib.next().expect("value must valid");
256        assert!(v >= Duration::from_secs(2), "current: {v:?}");
257        assert!(v < Duration::from_secs(3), "current: {v:?}");
258
259        assert_eq!(None, fib.next());
260    }
261
262    #[test]
263    fn test_fibonacci_min_delay() {
264        let mut fib = FibonacciBuilder::default()
265            .with_min_delay(Duration::from_millis(500))
266            .build();
267
268        assert_eq!(Some(Duration::from_millis(500)), fib.next());
269        assert_eq!(Some(Duration::from_millis(500)), fib.next());
270        assert_eq!(Some(Duration::from_secs(1)), fib.next());
271        assert_eq!(None, fib.next());
272    }
273
274    #[test]
275    fn test_fibonacci_max_delay() {
276        let mut fib = FibonacciBuilder::default()
277            .with_max_times(4)
278            .with_max_delay(Duration::from_secs(2))
279            .build();
280
281        assert_eq!(Some(Duration::from_secs(1)), fib.next());
282        assert_eq!(Some(Duration::from_secs(1)), fib.next());
283        assert_eq!(Some(Duration::from_secs(2)), fib.next());
284        assert_eq!(Some(Duration::from_secs(2)), fib.next());
285        assert_eq!(None, fib.next());
286    }
287
288    #[test]
289    fn test_fibonacci_no_max_delay() {
290        let mut fib = FibonacciBuilder::default()
291            .with_max_times(4)
292            .with_min_delay(Duration::from_secs(10_000_000_000_000_000_000))
293            .without_max_delay()
294            .build();
295
296        assert_eq!(
297            Some(Duration::from_secs(10_000_000_000_000_000_000)),
298            fib.next()
299        );
300        assert_eq!(
301            Some(Duration::from_secs(10_000_000_000_000_000_000)),
302            fib.next()
303        );
304        assert_eq!(Some(Duration::MAX), fib.next());
305        assert_eq!(Some(Duration::MAX), fib.next());
306        assert_eq!(None, fib.next());
307    }
308
309    #[test]
310    fn test_fibonacci_max_times() {
311        let mut fib = FibonacciBuilder::default().with_max_times(6).build();
312
313        assert_eq!(Some(Duration::from_secs(1)), fib.next());
314        assert_eq!(Some(Duration::from_secs(1)), fib.next());
315        assert_eq!(Some(Duration::from_secs(2)), fib.next());
316        assert_eq!(Some(Duration::from_secs(3)), fib.next());
317        assert_eq!(Some(Duration::from_secs(5)), fib.next());
318        assert_eq!(Some(Duration::from_secs(8)), fib.next());
319        assert_eq!(None, fib.next());
320    }
321
322    #[test]
323    fn test_fibonacci_no_max_times() {
324        let mut fib = FibonacciBuilder::default()
325            .with_min_delay(Duration::from_secs(0))
326            .without_max_times()
327            .build();
328
329        // to fully test we would need to call this `usize::MAX`
330        // which seems unreasonable for a test as it would take too long...
331        for _ in 0..10_000 {
332            assert_eq!(Some(Duration::from_secs(0)), fib.next());
333        }
334    }
335
336    // allow assertions on constants because they are not optimized out by unit tests
337    #[allow(clippy::assertions_on_constants)]
338    #[test]
339    fn test_fibonacci_const_builder() {
340        assert!(TEST_BUILDER.jitter);
341        assert_eq!(TEST_BUILDER.min_delay, Duration::from_secs(2));
342        assert_eq!(TEST_BUILDER.max_delay, Some(Duration::from_secs(30)));
343        assert_eq!(TEST_BUILDER.max_times, Some(5));
344    }
345}