Coverage Report

Created: 2025-10-31 06:57

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/rust/registry/src/index.crates.io-1949cf8c6b5b557f/backon-1.6.0/src/backoff/exponential.rs
Line
Count
Source
1
use core::time::Duration;
2
3
use crate::backoff::BackoffBuilder;
4
5
/// ExponentialBuilder is used to construct an [`ExponentialBackoff`] that offers delays with exponential retries.
6
///
7
/// # Default
8
///
9
/// - jitter: false
10
/// - factor: 2
11
/// - min_delay: 1s
12
/// - max_delay: 60s
13
/// - max_times: 3
14
///
15
/// # Examples
16
///
17
/// ```no_run
18
/// use anyhow::Result;
19
/// use backon::ExponentialBuilder;
20
/// use backon::Retryable;
21
///
22
/// async fn fetch() -> Result<String> {
23
///     Ok(reqwest::get("https://www.rust-lang.org")
24
///         .await?
25
///         .text()
26
///         .await?)
27
/// }
28
///
29
/// #[tokio::main(flavor = "current_thread")]
30
/// async fn main() -> Result<()> {
31
///     let content = fetch.retry(ExponentialBuilder::default()).await?;
32
///     println!("fetch succeeded: {}", content);
33
///
34
///     Ok(())
35
/// }
36
/// ```
37
#[derive(Debug, Clone, Copy)]
38
pub struct ExponentialBuilder {
39
    jitter: bool,
40
    factor: f32,
41
    min_delay: Duration,
42
    max_delay: Option<Duration>,
43
    max_times: Option<usize>,
44
    total_delay: Option<Duration>,
45
    seed: Option<u64>,
46
}
47
48
impl Default for ExponentialBuilder {
49
0
    fn default() -> Self {
50
0
        Self::new()
51
0
    }
52
}
53
54
impl ExponentialBuilder {
55
    /// Create a new `ExponentialBuilder` with default values.
56
0
    pub const fn new() -> Self {
57
0
        Self {
58
0
            jitter: false,
59
0
            factor: 2.0,
60
0
            min_delay: Duration::from_secs(1),
61
0
            max_delay: Some(Duration::from_secs(60)),
62
0
            max_times: Some(3),
63
0
            total_delay: None,
64
0
            seed: None,
65
0
        }
66
0
    }
67
68
    /// Enable jitter for the backoff.
69
    ///
70
    /// When jitter is enabled, [`ExponentialBackoff`] will add a random jitter within `(0, current_delay)`
71
    /// to the current delay.
72
0
    pub const fn with_jitter(mut self) -> Self {
73
0
        self.jitter = true;
74
0
        self
75
0
    }
76
77
    /// 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.
78
0
    pub fn with_jitter_seed(mut self, seed: u64) -> Self {
79
0
        self.seed = Some(seed);
80
0
        self
81
0
    }
82
83
    /// Set the factor for the backoff.
84
    ///
85
    /// Note: Having a factor less than `1.0` does not make any sense as it would create a
86
    /// smaller negative backoff.
87
0
    pub const fn with_factor(mut self, factor: f32) -> Self {
88
0
        self.factor = factor;
89
0
        self
90
0
    }
91
92
    /// Set the minimum delay for the backoff.
93
0
    pub const fn with_min_delay(mut self, min_delay: Duration) -> Self {
94
0
        self.min_delay = min_delay;
95
0
        self
96
0
    }
97
98
    /// Set the maximum delay for the backoff.
99
    ///
100
    /// The delay will not increase if the current delay exceeds the maximum delay.
101
0
    pub const fn with_max_delay(mut self, max_delay: Duration) -> Self {
102
0
        self.max_delay = Some(max_delay);
103
0
        self
104
0
    }
105
106
    /// Set no maximum delay for the backoff.
107
    ///
108
    /// The delay will keep increasing.
109
    ///
110
    /// _The delay will saturate at `Duration::MAX` which is an **unrealistic** delay._
111
0
    pub const fn without_max_delay(mut self) -> Self {
112
0
        self.max_delay = None;
113
0
        self
114
0
    }
115
116
    /// Set the maximum number of attempts for the current backoff.
117
    ///
118
    /// The backoff will stop if the maximum number of attempts is reached.
119
0
    pub const fn with_max_times(mut self, max_times: usize) -> Self {
120
0
        self.max_times = Some(max_times);
121
0
        self
122
0
    }
123
124
    /// Set no maximum number of attempts for the current backoff.
125
    ///
126
    /// The backoff will not stop by itself.
127
    ///
128
    /// _The backoff could stop reaching `usize::MAX` attempts but this is **unrealistic**._
129
0
    pub const fn without_max_times(mut self) -> Self {
130
0
        self.max_times = None;
131
0
        self
132
0
    }
133
134
    /// Set the total delay for the backoff.
135
    ///
136
    /// The backoff will stop yielding sleep durations once the cumulative sleep time
137
    /// plus the next sleep duration would exceed `total_delay`.
138
0
    pub const fn with_total_delay(mut self, total_delay: Option<Duration>) -> Self {
139
0
        self.total_delay = total_delay;
140
0
        self
141
0
    }
142
}
143
144
impl BackoffBuilder for ExponentialBuilder {
145
    type Backoff = ExponentialBackoff;
146
147
0
    fn build(self) -> Self::Backoff {
148
        ExponentialBackoff {
149
0
            jitter: self.jitter,
150
0
            rng: if let Some(seed) = self.seed {
151
0
                fastrand::Rng::with_seed(seed)
152
            } else {
153
                #[cfg(feature = "std")]
154
0
                let rng = fastrand::Rng::new();
155
156
                #[cfg(not(feature = "std"))]
157
                let rng = fastrand::Rng::with_seed(super::RANDOM_SEED);
158
159
0
                rng
160
            },
161
0
            factor: self.factor,
162
0
            min_delay: self.min_delay,
163
0
            max_delay: self.max_delay,
164
0
            max_times: self.max_times,
165
166
0
            current_delay: None,
167
            attempts: 0,
168
            cumulative_delay: Duration::ZERO,
169
0
            total_delay: self.total_delay,
170
        }
171
0
    }
172
}
173
174
impl BackoffBuilder for &ExponentialBuilder {
175
    type Backoff = ExponentialBackoff;
176
177
0
    fn build(self) -> Self::Backoff {
178
0
        (*self).build()
179
0
    }
180
}
181
182
/// ExponentialBackoff provides a delay with exponential retries.
183
///
184
/// This backoff strategy is constructed by [`ExponentialBuilder`].
185
#[doc(hidden)]
186
#[derive(Debug)]
187
pub struct ExponentialBackoff {
188
    jitter: bool,
189
    rng: fastrand::Rng,
190
    factor: f32,
191
    min_delay: Duration,
192
    max_delay: Option<Duration>,
193
    max_times: Option<usize>,
194
    total_delay: Option<Duration>,
195
196
    current_delay: Option<Duration>,
197
    cumulative_delay: Duration,
198
    attempts: usize,
199
}
200
201
impl Iterator for ExponentialBackoff {
202
    type Item = Duration;
203
204
0
    fn next(&mut self) -> Option<Self::Item> {
205
0
        if self.attempts >= self.max_times.unwrap_or(usize::MAX) {
206
0
            return None;
207
0
        }
208
0
        self.attempts += 1;
209
210
0
        let mut tmp_cur = match self.current_delay {
211
            None => {
212
                // If current_delay is None, it's must be the first time to retry.
213
0
                self.min_delay
214
            }
215
0
            Some(mut cur) => {
216
                // If current delay larger than max delay, we should stop increment anymore.
217
0
                if let Some(max_delay) = self.max_delay {
218
0
                    if cur < max_delay {
219
0
                        cur = saturating_mul(cur, self.factor);
220
0
                    }
221
0
                    if cur > max_delay {
222
0
                        cur = max_delay;
223
0
                    }
224
0
                } else {
225
0
                    cur = saturating_mul(cur, self.factor);
226
0
                }
227
0
                cur
228
            }
229
        };
230
231
0
        let current_delay = tmp_cur;
232
        // If jitter is enabled, add random jitter based on min delay.
233
0
        if self.jitter {
234
0
            tmp_cur = tmp_cur.saturating_add(tmp_cur.mul_f32(self.rng.f32()));
235
0
        }
236
237
        // Check if adding the current delay would exceed the total delay limit.
238
0
        let total_delay_check = self
239
0
            .total_delay
240
0
            .is_none_or(|total| self.cumulative_delay + tmp_cur <= total);
241
242
0
        if !total_delay_check {
243
0
            return None;
244
0
        }
245
246
0
        if self.total_delay.is_some() {
247
0
            self.cumulative_delay = self.cumulative_delay.saturating_add(tmp_cur);
248
0
        }
249
250
0
        self.current_delay = Some(current_delay);
251
252
0
        Some(tmp_cur)
253
0
    }
254
}
255
256
#[inline]
257
0
pub(crate) fn saturating_mul(d: Duration, rhs: f32) -> Duration {
258
0
    Duration::try_from_secs_f32(rhs * d.as_secs_f32()).unwrap_or(Duration::MAX)
259
0
}
260
261
#[cfg(test)]
262
mod tests {
263
    use core::time::Duration;
264
265
    #[cfg(target_arch = "wasm32")]
266
    use wasm_bindgen_test::wasm_bindgen_test as test;
267
268
    use crate::BackoffBuilder;
269
    use crate::ExponentialBuilder;
270
271
    const TEST_BUILDER: ExponentialBuilder = ExponentialBuilder::new()
272
        .with_jitter()
273
        .with_factor(1.5)
274
        .with_min_delay(Duration::from_secs(2))
275
        .with_max_delay(Duration::from_secs(30))
276
        .with_max_times(5);
277
278
    #[test]
279
    fn test_exponential_default() {
280
        let mut exp = ExponentialBuilder::default().build();
281
282
        assert_eq!(Some(Duration::from_secs(1)), exp.next());
283
        assert_eq!(Some(Duration::from_secs(2)), exp.next());
284
        assert_eq!(Some(Duration::from_secs(4)), exp.next());
285
        assert_eq!(None, exp.next());
286
    }
287
288
    #[test]
289
    fn test_exponential_factor() {
290
        let mut exp = ExponentialBuilder::default().with_factor(1.5).build();
291
292
        assert_eq!(Some(Duration::from_secs_f32(1.0)), exp.next());
293
        assert_eq!(Some(Duration::from_secs_f32(1.5)), exp.next());
294
        assert_eq!(Some(Duration::from_secs_f32(2.25)), exp.next());
295
        assert_eq!(None, exp.next());
296
    }
297
298
    #[test]
299
    fn test_exponential_jitter() {
300
        let mut exp = ExponentialBuilder::default().with_jitter().build();
301
302
        let v = exp.next().expect("value must valid");
303
        assert!(v >= Duration::from_secs(1), "current: {v:?}");
304
        assert!(v < Duration::from_secs(2), "current: {v:?}");
305
306
        let v = exp.next().expect("value must valid");
307
        assert!(v >= Duration::from_secs(2), "current: {v:?}");
308
        assert!(v < Duration::from_secs(4), "current: {v:?}");
309
310
        let v = exp.next().expect("value must valid");
311
        assert!(v >= Duration::from_secs(4), "current: {v:?}");
312
        assert!(v < Duration::from_secs(8), "current: {v:?}");
313
314
        assert_eq!(None, exp.next());
315
    }
316
317
    #[test]
318
    fn test_exponential_min_delay() {
319
        let mut exp = ExponentialBuilder::default()
320
            .with_min_delay(Duration::from_millis(500))
321
            .build();
322
323
        assert_eq!(Some(Duration::from_millis(500)), exp.next());
324
        assert_eq!(Some(Duration::from_secs(1)), exp.next());
325
        assert_eq!(Some(Duration::from_secs(2)), exp.next());
326
        assert_eq!(None, exp.next());
327
    }
328
329
    #[test]
330
    fn test_exponential_total_delay() {
331
        let mut exp = ExponentialBuilder::default()
332
            .with_min_delay(Duration::from_secs(1))
333
            .with_factor(1.0)
334
            .with_total_delay(Some(Duration::from_secs(3)))
335
            .with_max_times(5)
336
            .build();
337
338
        assert_eq!(Some(Duration::from_secs(1)), exp.next());
339
        assert_eq!(Some(Duration::from_secs(1)), exp.next());
340
        assert_eq!(Some(Duration::from_secs(1)), exp.next());
341
        assert_eq!(None, exp.next());
342
    }
343
344
    #[test]
345
    fn test_exponential_no_max_times_with_default() {
346
        let mut exp = ExponentialBuilder::default()
347
            .with_min_delay(Duration::from_secs(1))
348
            .with_factor(1_f32)
349
            .without_max_times()
350
            .build();
351
352
        // to fully test we would need to call this `usize::MAX`
353
        // which seems unreasonable for a test as it would take too long...
354
        for _ in 0..10_000 {
355
            assert_eq!(Some(Duration::from_secs(1)), exp.next());
356
        }
357
    }
358
359
    #[test]
360
    fn test_exponential_max_delay_with_default() {
361
        let mut exp = ExponentialBuilder::default()
362
            .with_max_delay(Duration::from_secs(2))
363
            .build();
364
365
        assert_eq!(Some(Duration::from_secs(1)), exp.next());
366
        assert_eq!(Some(Duration::from_secs(2)), exp.next());
367
        assert_eq!(Some(Duration::from_secs(2)), exp.next());
368
        assert_eq!(None, exp.next());
369
    }
370
371
    #[test]
372
    fn test_exponential_no_max_delay_with_default() {
373
        let mut exp = ExponentialBuilder::default()
374
            .with_min_delay(Duration::from_secs(1))
375
            .with_factor(10_000_000_000_f32)
376
            .without_max_delay()
377
            .with_max_times(4)
378
            .build();
379
380
        assert_eq!(Some(Duration::from_secs(1)), exp.next());
381
        assert_eq!(Some(Duration::from_secs(10_000_000_000)), exp.next());
382
        assert_eq!(Some(Duration::MAX), exp.next());
383
        assert_eq!(Some(Duration::MAX), exp.next());
384
        assert_eq!(None, exp.next());
385
    }
386
387
    #[test]
388
    fn test_exponential_max_delay_without_default_1() {
389
        let mut exp = ExponentialBuilder {
390
            jitter: false,
391
            seed: Some(0x2fdb0020ffc7722b),
392
            factor: 10_000_000_000_f32,
393
            min_delay: Duration::from_secs(1),
394
            max_delay: None,
395
            max_times: None,
396
            total_delay: None,
397
        }
398
        .build();
399
400
        assert_eq!(Some(Duration::from_secs(1)), exp.next());
401
        assert_eq!(Some(Duration::from_secs(10_000_000_000)), exp.next());
402
        assert_eq!(Some(Duration::MAX), exp.next());
403
        assert_eq!(Some(Duration::MAX), exp.next());
404
    }
405
406
    #[test]
407
    fn test_exponential_max_delay_without_default_2() {
408
        let mut exp = ExponentialBuilder {
409
            jitter: true,
410
            seed: Some(0x2fdb0020ffc7722b),
411
            factor: 10_000_000_000_f32,
412
            min_delay: Duration::from_secs(10_000_000_000),
413
            max_delay: None,
414
            max_times: Some(2),
415
            total_delay: None,
416
        }
417
        .build();
418
        let v = exp.next().expect("value must valid");
419
        assert!(v >= Duration::from_secs(10_000_000_000), "current: {v:?}");
420
        assert!(v < Duration::from_secs(20_000_000_000), "current: {v:?}");
421
        assert_eq!(Some(Duration::MAX), exp.next());
422
        assert_eq!(None, exp.next());
423
    }
424
425
    #[test]
426
    fn test_exponential_max_delay_without_default_3() {
427
        let mut exp = ExponentialBuilder {
428
            jitter: false,
429
            seed: Some(0x2fdb0020ffc7722b),
430
            factor: 10_000_000_000_f32,
431
            min_delay: Duration::from_secs(10_000_000_000),
432
            max_delay: Some(Duration::from_secs(60_000_000_000)),
433
            max_times: Some(3),
434
            total_delay: None,
435
        }
436
        .build();
437
        assert_eq!(Some(Duration::from_secs(10_000_000_000)), exp.next());
438
        assert_eq!(Some(Duration::from_secs(60_000_000_000)), exp.next());
439
        assert_eq!(Some(Duration::from_secs(60_000_000_000)), exp.next());
440
        assert_eq!(None, exp.next());
441
    }
442
443
    #[test]
444
    fn test_exponential_max_times() {
445
        let mut exp = ExponentialBuilder::default().with_max_times(1).build();
446
447
        assert_eq!(Some(Duration::from_secs(1)), exp.next());
448
        assert_eq!(None, exp.next());
449
    }
450
451
    // allow assertions on constants because they are not optimized out by unit tests
452
    #[allow(clippy::assertions_on_constants)]
453
    #[test]
454
    fn test_exponential_const_builder() {
455
        assert!(TEST_BUILDER.jitter);
456
        assert_eq!(TEST_BUILDER.factor, 1.5);
457
        assert_eq!(TEST_BUILDER.min_delay, Duration::from_secs(2));
458
        assert_eq!(TEST_BUILDER.max_delay, Some(Duration::from_secs(30)));
459
        assert_eq!(TEST_BUILDER.max_times, Some(5));
460
    }
461
}