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}