Skip to main content

ease_off/
core.rs

1//! Immutable core backoff API, without error management or sleeps.
2//!
3//! Potentially useful for when stateful backoffs are not appropriate,
4//! e.g. retrying many similar operations concurrently,
5//! or when a convenient error type is not available.
6//!
7//! # Example: Schedule Many Operations with `tokio_util::time::DelayQueue`
8//!
9//! (Source: `examples/tokio-concurrent.rs`)
10#![cfg_attr(feature = "tokio", doc = "```rust")]
11#![cfg_attr(
12    not(feature = "tokio"),
13    doc = "```rust,ignore\n\
14           // Note: example not compiled if `tokio` feature is not enabled.\n"
15)]
16#![doc = include_str!("../examples/tokio-concurrent.rs")]
17// If this were written using `//!`, RustRover would think this is the start of a new code block.
18#![doc = "```"]
19
20use crate::options::Options;
21use rand::Rng;
22use std::cmp;
23use std::time::{Duration, Instant};
24
25/// Immutable core backoff API, without error management or sleeps.
26#[derive(Debug, Clone)]
27pub struct EaseOffCore {
28    options: Options,
29}
30
31/// Error returned by [`EaseOffCore::nth_retry_at()`].
32#[derive(Debug, Clone, thiserror::Error)]
33#[error("{n}th retry is {:?} after deadline", retry_at.duration_since(*deadline))]
34pub struct RetryAfterDeadline {
35    /// The `n` passed to `nth_retry_at()`.
36    pub n: u32,
37    /// The recommended time for the `n`th backoff attempt.
38    pub retry_at: Instant,
39    /// The deadline that elapsed.
40    pub deadline: Instant,
41}
42
43impl EaseOffCore {
44    /// Create an instance from a built [`Options`].
45    ///
46    /// May be more conveniently invoked as [`Options::into_core()`].
47    #[inline(always)]
48    pub const fn new(options: Options) -> Self {
49        Self { options }
50    }
51
52    /// Returns the recommended [`Instant`] at which to schedule the `n`th backoff attempt.
53    ///
54    /// Returns `Ok(None)` if `n == 0` and [`Options::initial_jitter`] is not greater than zero.
55    ///
56    /// Returns `Err` if the calculated [`Instant`] falls after `deadline`.
57    pub fn nth_retry_at(
58        &self,
59        n: u32,
60        now: Instant,
61        deadline: Option<Instant>,
62        rng: &mut (impl Rng + ?Sized),
63    ) -> Result<Option<Instant>, RetryAfterDeadline> {
64        let Options {
65            multiplier,
66            jitter,
67            initial_jitter,
68            initial_delay,
69            max_delay,
70        } = self.options;
71
72        let (delay, jitter) = if let Some(powi) = n.checked_sub(1) {
73            let delay = cmp::min(
74                duration_saturating_mul_f32(
75                    initial_delay,
76                    multiplier.powi(powi.try_into().unwrap_or(i32::MAX)),
77                ),
78                max_delay,
79            );
80
81            let jitter = get_jitter(delay, jitter, rng);
82
83            (delay, jitter)
84        } else {
85            // We actually _want_ this to evaluate to false if NaN.
86            #[allow(clippy::neg_cmp_op_on_partial_ord)]
87            if !(initial_jitter > 0f32) {
88                return Ok(None);
89            }
90
91            let jitter = get_jitter(initial_delay, initial_jitter, rng);
92            (initial_delay, jitter)
93        };
94
95        // We only subtract jitter so that `deadline` is a hard limit
96        let retry_at = now + delay - jitter;
97
98        match deadline {
99            Some(deadline) if retry_at > deadline => Err(RetryAfterDeadline {
100                n,
101                retry_at,
102                deadline,
103            }),
104            _ => Ok(Some(retry_at)),
105        }
106    }
107}
108
109// This does not exist in `std`
110#[inline(always)]
111fn duration_saturating_mul_f32(duration: Duration, mul: f32) -> Duration {
112    Duration::try_from_secs_f32(duration.as_secs_f32() * mul).unwrap_or(Duration::MAX)
113}
114
115fn get_jitter(
116    base_duration: Duration,
117    jitter_factor: f32,
118    rng: &mut (impl Rng + ?Sized),
119) -> Duration {
120    let jitter_factor = if jitter_factor > 0f32 && jitter_factor < 1f32 {
121        jitter_factor * rng.gen::<f32>()
122    } else if jitter_factor >= 1f32 {
123        // Act as if `jitter == 1`
124        rng.gen::<f32>()
125    } else {
126        // `jitter` is NaN or <= 0
127        0f32
128    };
129
130    duration_saturating_mul_f32(base_duration, jitter_factor)
131}