Skip to main content

ease_off/
options.rs

1use crate::core::EaseOffCore;
2use crate::EaseOff;
3use std::num::Saturating;
4use std::time::{Duration, Instant};
5
6/// Configuration options for [`EaseOff`] and [`EaseOffCore`].
7///
8/// Designed to be stored in a `const` or `static`:
9///
10/// ```rust
11/// use std::time::Duration;
12///
13/// const BACKOFF_OPTS: ease_off::Options = ease_off::Options::new()
14///     .initial_jitter(0.25)
15///     .initial_delay(Duration::from_secs(1))
16///     .max_delay(Duration::from_secs(5 * 60)); // 5 minutes
17/// ```
18#[derive(Debug, Clone)]
19pub struct Options {
20    pub(crate) multiplier: f32,
21    pub(crate) jitter: f32,
22    pub(crate) initial_jitter: f32,
23    pub(crate) initial_delay: Duration,
24    pub(crate) max_delay: Duration,
25}
26
27impl Options {
28    /// Default ease-off options which should be suitable for most applications.
29    ///
30    /// See source for current values.
31    pub const DEFAULT: Options = Options {
32        multiplier: 2.0,
33        jitter: 0.25,
34        initial_jitter: 0.0,
35        initial_delay: Duration::from_millis(150),
36        max_delay: Duration::from_secs(60), // one minute
37    };
38
39    /// Returns [`Self::DEFAULT`].
40    #[inline(always)]
41    pub const fn new() -> Self {
42        Self::DEFAULT
43    }
44
45    /// Set the factor to multiply the next delay by.
46    ///
47    /// * If `> 1`, backoff is exponential.
48    /// * If `== 1`, backoff is constant before [jitter][Self::jitter].
49    /// * If `< 1`, backoff is logarithmic(?). In any case, not recommended.
50    ///
51    /// Any multiplication that results in an invalid value for [`Duration`] saturates
52    /// to [`Duration::MAX`] or [`max_delay`][Self::max_delay], whichever is lower.
53    #[inline(always)]
54    pub const fn multiplier(self, multiplier: f32) -> Self {
55        Self { multiplier, ..self }
56    }
57
58    /// Get the factor that the next delay will be multiplied by.
59    #[inline(always)]
60    pub const fn get_multiplier(&self) -> f32 {
61        self.multiplier
62    }
63
64    /// Set the maximum jitter factor.
65    ///
66    /// The next backoff delay will be multiplied by a random factor in the range `(1 - jitter, 1]`.
67    ///
68    /// This helps prevent a situation where attempts line up from multiple processes
69    /// following the same backoff algorithm, which would constitute a [thundering herd].
70    ///
71    /// This value is clamped to the interval `[0, 1]` when calculating the next delay.
72    ///
73    /// If `jitter` is `<= 0` or `NaN`, no random jitter is applied (not recommended for most cases).
74    ///
75    /// If `jitter >= 1`, the next delay can be anywhere between `[0, next_delay]`,
76    /// which means the next attempt _could_ happen immediately, without waiting.
77    ///
78    /// [thundering herd]: https://en.wikipedia.org/wiki/Thundering_herd_problem
79    #[inline(always)]
80    pub const fn jitter(self, jitter: f32) -> Self {
81        Self { jitter, ..self }
82    }
83
84    /// Get the maximum jitter factor.
85    ///
86    /// See [`Self::jitter()`] for details.
87    #[inline(always)]
88    pub const fn get_jitter(&self) -> f32 {
89        self.jitter
90    }
91
92    /// Set the jitter factor used to delay the first attempt.
93    ///
94    /// The initial wait before the first attempt will be [`initial_delay`][Self::initial_delay]
95    /// multiplied by a random factor in the range `(1 - initial_jitter, 1]`.
96    ///
97    /// This mitigates the [thundering herd problem] when multiple processes start up
98    /// at the same time and all try to access the same resource.
99    ///
100    /// This value is clamped to the interval `[0, 1]` when calculating the initial delay.
101    ///
102    /// If `initial_jitter` is `<= 0` or `NaN`, the first attempt occurs immediately.
103    ///
104    /// The delay after the first failure will be calculated as normal;
105    /// [`multiplier`][Self::multiplier] is _not_ applied until after the first retryable failure.
106    ///
107    /// [thundering herd problem]: https://en.wikipedia.org/wiki/Thundering_herd_problem
108    #[inline(always)]
109    pub const fn initial_jitter(self, initial_jitter: f32) -> Self {
110        Self {
111            initial_jitter,
112            ..self
113        }
114    }
115
116    /// Get the jitter factor used for the _first_ attempt.
117    ///
118    /// See [`Self::initial_jitter()`] for details.
119    #[inline(always)]
120    pub const fn get_initial_jitter(&self) -> f32 {
121        self.initial_jitter
122    }
123
124    /// Set the delay for the first backoff attempt.
125    #[inline(always)]
126    pub const fn initial_delay(self, initial_delay: Duration) -> Self {
127        Self {
128            initial_delay,
129            ..self
130        }
131    }
132
133    /// Get the delay for the first backoff attempt.
134    ///
135    /// See [`Self::initial_delay()`] for details.
136    #[inline(always)]
137    pub const fn get_initial_delay(&self) -> Duration {
138        self.initial_delay
139    }
140
141    /// Set the maximum delay to wait between backoff attempts.
142    #[inline(always)]
143    pub const fn max_delay(self, max_delay: Duration) -> Self {
144        Self { max_delay, ..self }
145    }
146
147    /// Get the maximum delay to wait between backoff attempts.
148    ///
149    /// See [`Self::max_delay()`] for details.
150    #[inline(always)]
151    pub const fn get_max_delay(&self) -> Duration {
152        self.max_delay
153    }
154
155    /// Convert this `Options` into an [`EaseOffCore`].
156    #[inline(always)]
157    pub const fn into_core(self) -> EaseOffCore {
158        EaseOffCore::new(self)
159    }
160}
161
162/// Methods to create an [`EaseOff`].
163impl Options {
164    /// Begin backing off with **indefinite** retries.
165    ///
166    /// The operation will be retried until it succeeds, or a non-retryable error occurs.
167    pub fn start_unlimited<E>(&self) -> EaseOff<E> {
168        self.start(Instant::now(), None)
169    }
170
171    /// Begin backing off, limited by the given timeout.
172    ///
173    /// Always makes one attempt, even if the timeout is zero or has elapsed
174    /// by the time the first attempt is made.
175    ///
176    /// See also:
177    /// * [`Self::start_timeout_opt()`] for a conditional timeout.
178    /// * [`Self::start_deadline()`] to specify an [`Instant`] as a deadline.
179    pub fn start_timeout<E>(&self, timeout: Duration) -> EaseOff<E> {
180        let started_at = Instant::now();
181        self.start(started_at, started_at.checked_add(timeout))
182    }
183
184    /// Begin backing off, limited by the given optional timeout.
185    ///
186    /// If `timeout` is `None`, this is equivalent to [`Self::start_unlimited()`].
187    ///
188    /// Always makes one attempt, even if the timeout is zero or has elapsed
189    /// by the time the first attempt is made.
190    ///
191    /// See also:
192    /// * [`Self::start_timeout()`] for a non-conditional timeout.
193    /// * [`Self::start_deadline_opt()`] to specify an optional [`Instant`] as a deadline.
194    pub fn start_timeout_opt<E>(&self, timeout: Option<Duration>) -> EaseOff<E> {
195        let started_at = Instant::now();
196        self.start(
197            started_at,
198            timeout.and_then(|timeout| started_at.checked_add(timeout)),
199        )
200    }
201
202    /// Begin backing off, halting attempts at the given deadline.
203    ///
204    /// Always makes one attempt, even if the deadline is `<= Instant::now()` or has elapsed
205    /// by the time the first attempt is made.
206    ///
207    /// See also:
208    /// * [`Self::start_deadline_opt()`] for a conditional deadline.
209    /// * [`Self::start_timeout()`] to specify a [`Duration`] as a timeout.
210    pub fn start_deadline<E>(&self, deadline: Instant) -> EaseOff<E> {
211        self.start(Instant::now(), Some(deadline))
212    }
213
214    /// Begin backing off, halting attempts at the given deadline.
215    ///
216    /// If `deadline` is `None`, this is equivalent to [`Self::start_unlimited()`].
217    ///
218    /// Always makes one attempt, even if the deadline is `<= Instant::now()` or has elapsed
219    /// by the time the first attempt is made.
220    ///
221    /// See also:
222    /// * [`Self::start_deadline()`] for a non-conditional deadline.
223    /// * [`Self::start_timeout_opt()`] to specify an optional [`Duration`] as a timeout.
224    pub fn start_deadline_opt<E>(&self, deadline: Option<Instant>) -> EaseOff<E> {
225        self.start(Instant::now(), deadline)
226    }
227
228    fn start<E>(&self, started_at: Instant, deadline: Option<Instant>) -> EaseOff<E> {
229        EaseOff {
230            core: EaseOffCore::new(self.clone()),
231            started_at,
232            deadline,
233            num_attempts: Saturating(0),
234            last_error: None,
235            next_retry_at: None,
236        }
237    }
238}
239
240impl Default for Options {
241    /// Returns [`Self::DEFAULT`].
242    #[inline(always)]
243    fn default() -> Self {
244        Self::DEFAULT
245    }
246}