Skip to main content

ease_off/
lib.rs

1//! An easy, opinionated exponential backoff implementation.
2//!
3//! Benefits over other implementations:
4//! * More flexible control flow (you implement the loop yourself).
5//! * Opinionated but sane defaults
6//!     * Explicitly choose deadline, timeout or unlimited,
7//!       so you know exactly what's going to happen.
8//! * [`RetryableError`] trait allows for more reusable code.
9//! * Immutable [`core`] API for when stateful backoffs aren't appropriate.
10//!
11//! # Examples
12//!
13//! ## Blocking Operation
14//!
15//! (Source: `examples/blocking.rs`)
16#![doc = "```rust"]
17#![doc = include_str!("../examples/blocking.rs")]
18// If this were written using `//!`, RustRover would think this is the start of a new code block.
19#![doc = "```"]
20//!
21//! ## Async Operation (Tokio)
22//!
23//! (Source: `examples/tokio.rs`)
24#![cfg_attr(feature = "tokio", doc = "```rust")]
25#![cfg_attr(
26    not(feature = "tokio"),
27    doc = "```rust,ignore\n\
28           // Note: example not compiled if `tokio` feature is not enabled.\n"
29)]
30#![doc = include_str!("../examples/tokio.rs")]
31// If this were written using `//!`, RustRover would think this is the start of a new code block.
32#![doc = "```"]
33#![cfg_attr(docsrs, feature(doc_cfg))]
34#![warn(missing_docs)]
35
36use crate::core::EaseOffCore;
37use std::cmp;
38use std::num::Saturating;
39use std::ops::ControlFlow;
40use std::time::{Duration, Instant};
41
42#[cfg(feature = "futures")]
43#[cfg_attr(docsrs, doc(cfg(any(feature = "tokio", feature = "async-io-2"))))]
44pub mod futures;
45
46pub mod core;
47
48mod options;
49
50pub use options::Options;
51
52/// Exponential backoff controller.
53///
54/// The constructors of this type use [`Options::DEFAULT`].
55#[derive(Debug)]
56pub struct EaseOff<E> {
57    core: EaseOffCore,
58    started_at: Instant,
59    deadline: Option<Instant>,
60    num_attempts: Saturating<u32>,
61    last_error: Option<E>,
62    next_retry_at: Option<Instant>,
63}
64
65impl<E> EaseOff<E> {
66    /// Alias for [`Options::start_unlimited()`] using [`Options::DEFAULT`].
67    #[inline(always)]
68    pub fn start_unlimited() -> Self {
69        Options::DEFAULT.start_unlimited()
70    }
71
72    /// Alias for [`Options::start_timeout()`] using [`Options::DEFAULT`].
73    #[inline(always)]
74    pub fn start_timeout(timeout: Duration) -> Self {
75        Options::DEFAULT.start_timeout(timeout)
76    }
77
78    /// Alias for [`Options::start_timeout_opt()`] using [`Options::DEFAULT`].
79    #[inline(always)]
80    pub fn start_timeout_opt(timeout: Option<Duration>) -> Self {
81        Options::DEFAULT.start_timeout_opt(timeout)
82    }
83
84    /// Alias for [`Options::start_deadline()`] using [`Options::DEFAULT`].
85    #[inline(always)]
86    pub fn start_deadline(deadline: Instant) -> Self {
87        Options::DEFAULT.start_deadline(deadline)
88    }
89
90    /// Alias for [`Options::start_deadline_opt()`] using [`Options::DEFAULT`].
91    #[inline(always)]
92    pub fn start_deadline_opt(deadline: Option<Instant>) -> Self {
93        Options::DEFAULT.start_deadline_opt(deadline)
94    }
95
96    /// Returns the [`Instant`] when this instance was constructed.
97    #[inline(always)]
98    pub fn started_at(&self) -> Instant {
99        self.started_at
100    }
101
102    /// Returns the deadline, if provided.
103    ///
104    /// If constructed with a timeout, it is converted to a deadline on construction
105    /// by adding the timeout to [`Self::started_at()`].
106    #[inline(always)]
107    pub fn deadline(&self) -> Option<Instant> {
108        self.deadline
109    }
110
111    /// Returns the number of attempts that have been made.
112    ///
113    /// Saturates at [`u32::MAX`].
114    #[inline(always)]
115    pub fn num_attempts(&self) -> u32 {
116        self.num_attempts.0
117    }
118
119    fn next_retry_at(&mut self) -> Result<Option<Instant>, Error<E>> {
120        let now = Instant::now();
121
122        let mut rng = rand::thread_rng();
123
124        if self.last_error.is_none() {
125            self.num_attempts = Saturating(0);
126            return Ok(cmp::max(
127                self.core
128                    .nth_retry_at(0, now, None, &mut rng)
129                    .expect("passed `None` for deadline, should not be `Err`"),
130                self.next_retry_at.take(),
131            ));
132        }
133
134        let attempt_num = self.num_attempts.0;
135        // `num_attempts` is `Saturating<u32>` so we don't have to worry about overflow.
136        self.num_attempts += 1;
137
138        self.core
139            .nth_retry_at(attempt_num, now, self.deadline, &mut rng)
140            .map_err(|_e| {
141                Error::TimedOut(TimeoutError {
142                    last_error: self
143                        .last_error
144                        .take()
145                        .expect("BUG: `last_error` should not be `None` here"),
146                })
147            })
148            .map(|retry_at| cmp::max(retry_at, self.next_retry_at.take()))
149    }
150
151    fn wrap_result<T>(&mut self, result: Result<T, Error<E>>) -> ResultWrapper<'_, T, E> {
152        ResultWrapper {
153            result,
154            ease_off: self,
155        }
156    }
157}
158
159impl<E> EaseOff<E> {
160    /// Attempt a blocking operation.
161    ///
162    /// If the operation previously failed, sleeps for the prescribed backoff period
163    /// using [`std::thread::sleep()`].
164    ///
165    /// ### Note: Behavior at Deadline
166    /// Most blocking operations cannot be cancelled once begun, so the [deadline][Self::deadline],
167    /// if set, is only checked *before* attempting the operation.
168    ///
169    /// Generally, the only kinds of blocking operations that support cancellation
170    /// take an explicit timeout (such as setting a read timeout on a socket).
171    ///
172    /// If you want a blocking operation to be cancelled immediately once the deadline elapses,
173    /// consult the documentation for the API you are calling to see if timeouts are supported,
174    /// and if so, how to configure them.
175    pub fn try_blocking<T>(
176        &mut self,
177        op: impl FnOnce() -> Result<T, E>,
178    ) -> ResultWrapper<'_, T, E> {
179        match self.next_retry_at() {
180            Ok(Some(instant)) => {
181                blocking_sleep_until(instant);
182            }
183            Ok(None) => (),
184            Err(e) => return self.wrap_result(Err(e)),
185        }
186
187        self.wrap_result(op().map_err(Error::MaybeRetryable))
188    }
189}
190
191/// Wrapper for [`Result`] returned from methods on [`EaseOff`].
192///
193/// Retryable errors will be stored in the `EaseOff` to be returned on the next attempt
194/// if the [deadline][EaseOff::deadline] has passed.
195#[must_use = "`.or_retry()` or `.or_retry_if()` must be called"]
196pub struct ResultWrapper<'a, T, E: 'a> {
197    result: Result<T, Error<E>>,
198    ease_off: &'a mut EaseOff<E>,
199}
200
201impl<'a, T, E: 'a> ResultWrapper<'a, T, E> {
202    /// Convert a [`TimeoutError`], if applicable, to another [`Error`] variant.
203    ///
204    /// May be used to convert a timeout error into [`Error::MaybeRetryable`].
205    ///
206    /// If not otherwise handled, `.or_retry()` and `.or_retry_if()` will return the error.
207    pub fn on_timeout(
208        self,
209        on_timeout: impl FnOnce(TimeoutError<E>) -> Error<E>,
210    ) -> ResultWrapper<'a, T, E> {
211        Self {
212            result: self.result.map_err(|e| e.on_timeout(on_timeout)),
213            ease_off: self.ease_off,
214        }
215    }
216
217    /// Inspect the error if the operation failed.
218    ///
219    /// This could also be [`Error::TimedOut`] containing an error from a previous iteration.
220    pub fn inspect_err(self, inspect_err: impl FnOnce(&Error<E>)) -> Self {
221        Self {
222            result: self.result.inspect_err(inspect_err),
223            ease_off: self.ease_off,
224        }
225    }
226
227    /// Check the result, testing the error for retryability using [`RetryableError`] if applicable.
228    ///
229    /// If the operation was successful, `Ok(Some(_))` is returned.
230    ///
231    /// If the operation failed but [`RetryableError::can_retry()`] returned `true`,
232    /// `Ok(None)` is returned and the error is stored in the [`EaseOff`] instance for the next
233    /// iteration.
234    ///
235    /// If the error was determined to be fatal or the [deadline][EaseOff::deadline()] has elapsed,
236    /// `Err` is returned.
237    pub fn or_retry(self) -> Result<Option<T>, E>
238    where
239        E: RetryableError,
240    {
241        self.or_retry_if(RetryableError::can_retry)
242    }
243
244    /// Check the result, testing the error for retryability using the given closure if applicable.
245    ///
246    /// The closure will be invoked with either the error from the current attempt,
247    /// or a previous error if the [deadline][EaseOff::deadline()] has elapsed
248    /// (this is indicated by the [`Error::TimedOut`] variant).
249    ///
250    /// If the operation was successful, `Ok(Some(_))` is returned.
251    ///
252    /// If the operation failed but the given closure returns `true`,
253    /// `Ok(None)` is returned and the error is stored in the [`EaseOff`] instance for the next
254    /// iteration.
255    ///
256    /// If the error was determined to be fatal, `Err` is returned.
257    pub fn or_retry_if(self, can_retry: impl FnOnce(&Error<E>) -> bool) -> Result<Option<T>, E> {
258        self.or_retry_with(|e| {
259            if can_retry(e) {
260                ControlFlow::Continue(None)
261            } else {
262                ControlFlow::Break(())
263            }
264        })
265    }
266
267    /// Check the result, testing the error for retryability using the given closure if applicable.
268    ///
269    /// The closure will be invoked with either the error from the current attempt,
270    /// or a previous error if the [deadline][EaseOff::deadline()] has elapsed
271    /// (this is indicated by the [`Error::TimedOut`] variant).
272    ///
273    /// If the operation was successful, `Ok(Some(_))` is returned.
274    ///
275    /// If the operation failed but the given closure returns `ControlFlow::Continue`,
276    /// `Ok(None)` is returned and the error is stored in the [`EaseOff`] instance for the next
277    /// iteration.
278    ///
279    /// If the given error specifies a retry time,
280    /// the closure may return `Control::Continue(Some(retry_at))`
281    /// with the next attempt waiting until `retry_at` or the current backoff,
282    /// whichever is later.
283    ///
284    /// Otherwise, the closure should return `ControlFlow::Continue(None)` to use the
285    /// next backoff time.
286    ///
287    /// If the error is fatal, the closure should return `ControlFlow::Break(())`
288    /// and then `Err` is returned.
289    pub fn or_retry_with(
290        self,
291        should_retry: impl FnOnce(&Error<E>) -> ControlFlow<(), Option<Instant>>,
292    ) -> Result<Option<T>, E> {
293        match self.result {
294            Ok(success) => {
295                self.ease_off.last_error = None;
296                self.ease_off.next_retry_at = None;
297                Ok(Some(success))
298            }
299            Err(e) => match should_retry(&e) {
300                ControlFlow::Continue(next_retry_at) => {
301                    self.ease_off.last_error = Some(e.into_inner());
302                    self.ease_off.next_retry_at = next_retry_at;
303
304                    Ok(None)
305                }
306                ControlFlow::Break(()) => Err(e.into_inner()),
307            },
308        }
309    }
310}
311
312/// Trait which may be implemented for error types to enable code reuse with [`EaseOff`].
313pub trait RetryableError {
314    /// Returns `true` if the error is non-fatal, `false` otherwise.
315    fn can_retry(&self) -> bool;
316}
317
318/// Error type for [`EaseOff`] which includes the fatality level of the error.
319#[derive(Debug)]
320pub enum Error<E> {
321    /// The inner error has not been determined to be fatal yet.
322    ///
323    /// [`RetryableError::can_retry()`] passes through to the inner error.
324    MaybeRetryable(E),
325    /// The error was determined to be fatal.
326    ///
327    /// Always returns `false` from [`RetryableError::can_retry()`].
328    Fatal(E),
329    /// The [deadline][EaseOff::deadline()] has elapsed.
330    ///
331    /// Contained is the error from the most recent attempt.
332    ///
333    /// Always returns `false` from [`RetryableError::can_retry()`].
334    TimedOut(TimeoutError<E>),
335}
336
337/// Error wrapper type indicating a failure due to a [deadline][EaseOff::deadline()] elapsing.
338#[derive(Debug)]
339#[non_exhaustive]
340pub struct TimeoutError<E> {
341    /// The error from the most recent failed attempt.
342    pub last_error: E,
343}
344
345impl<E: RetryableError> RetryableError for Error<E> {
346    fn can_retry(&self) -> bool {
347        match self {
348            Self::MaybeRetryable(e) => e.can_retry(),
349            Self::Fatal(_) => false,
350            Self::TimedOut(_) => false,
351        }
352    }
353}
354
355impl<E> Error<E> {
356    /// Convert [`Error::TimedOut`], if applicable, to another variant.
357    pub fn on_timeout(self, on_timeout: impl FnOnce(TimeoutError<E>) -> Self) -> Self {
358        match self {
359            Self::TimedOut(e) => on_timeout(e),
360            other => other,
361        }
362    }
363
364    /// Map the inner error type, retaining its retryability status.
365    pub fn map<E2>(self, map: impl FnOnce(E) -> E2) -> Error<E2> {
366        match self {
367            Self::TimedOut(e) => Error::TimedOut(TimeoutError {
368                last_error: map(e.last_error),
369            }),
370            Self::MaybeRetryable(e) => Error::MaybeRetryable(map(e)),
371            Self::Fatal(e) => Error::Fatal(map(e)),
372        }
373    }
374
375    /// Get the inner error.
376    pub fn inner(&self) -> &E {
377        match self {
378            Self::TimedOut(e) => &e.last_error,
379            Self::MaybeRetryable(e) => e,
380            Self::Fatal(e) => e,
381        }
382    }
383
384    /// Unwrap the inner error.
385    pub fn into_inner(self) -> E {
386        match self {
387            Self::TimedOut(e) => e.last_error,
388            Self::MaybeRetryable(e) => e,
389            Self::Fatal(e) => e,
390        }
391    }
392}
393
394fn blocking_sleep_until(instant: Instant) {
395    let now = Instant::now();
396
397    if let Some(sleep_duration) = instant.checked_duration_since(now) {
398        std::thread::sleep(sleep_duration);
399    }
400}