retry/
lib.rs

1//! Crate `retry` provides utilities for retrying operations that can fail.
2//!
3//! # Usage
4//!
5//! Retry an operation using the [`retry`] function. [`retry`] accepts an iterator over
6//! [`Duration`]s and a closure that returns a [`Result`] (or [`OperationResult`]; see below). The
7//! iterator is used to determine how long to wait after each unsuccessful try and how many times to
8//! try before giving up and returning [`Result::Err`]. The closure determines either the final
9//! successful value, or an error value, which can either be returned immediately or used to
10//! indicate that the operation should be retried.
11//!
12//! Any type that implements [`Iterator`] with an associated `Item` type of [`Duration`] can be
13//! used to determine retry behavior, though a few useful implementations are provided in the
14//! [`delay`] module, including a fixed delay and exponential backoff.
15//!
16//! ```
17//! # use retry::retry;
18//! # use retry::delay::Fixed;
19//! let mut collection = vec![1, 2, 3].into_iter();
20//!
21//! let result = retry(Fixed::from_millis(100), || {
22//!     match collection.next() {
23//!         Some(n) if n == 3 => Ok("n is 3!"),
24//!         Some(_) => Err("n must be 3!"),
25//!         None => Err("n was never 3!"),
26//!     }
27//! });
28//!
29//! assert!(result.is_ok());
30//! ```
31//!
32//! The [`Iterator`] API can be used to limit or modify the delay strategy. For example, to limit
33//! the number of retries to 1:
34//!
35//! ```
36//! # use retry::retry;
37//! # use retry::delay::Fixed;
38//! let mut collection = vec![1, 2, 3].into_iter();
39//!
40//! let result = retry(Fixed::from_millis(100).take(1), || {
41//!     match collection.next() {
42//!         Some(n) if n == 3 => Ok("n is 3!"),
43//!         Some(_) => Err("n must be 3!"),
44//!         None => Err("n was never 3!"),
45//!     }
46//! });
47//!
48//! assert!(result.is_err());
49//! ```
50//!
51#![cfg_attr(
52    feature = "random",
53    doc = r##"
54To apply random jitter to any delay strategy, the [`delay::jitter`] function can be used in
55combination with the [`Iterator`] API:
56
57```
58# use retry::retry;
59# use retry::delay::{Exponential, jitter};
60let mut collection = vec![1, 2, 3].into_iter();
61
62let result = retry(Exponential::from_millis(10).map(jitter).take(3), || {
63    match collection.next() {
64        Some(n) if n == 3 => Ok("n is 3!"),
65        Some(_) => Err("n must be 3!"),
66        None => Err("n was never 3!"),
67    }
68});
69
70assert!(result.is_ok());
71```
72"##
73)]
74//!
75//! To deal with fatal errors, return [`OperationResult`], which is like [`Result`], but with a
76//! third case to distinguish between errors that should cause a retry and errors that should
77//! immediately return, halting retry behavior. (Internally, [`OperationResult`] is always used, and
78//! closures passed to [`retry`] that return plain [`Result`] are converted into
79//! [`OperationResult`].)
80//!
81//! ```
82//! # use retry::retry;
83//! # use retry::delay::Fixed;
84//! use retry::OperationResult;
85//! let mut collection = vec![1, 2].into_iter();
86//! let value = retry(Fixed::from_millis(1), || {
87//!     match collection.next() {
88//!         Some(n) if n == 2 => OperationResult::Ok(n),
89//!         Some(_) => OperationResult::Retry("not 2"),
90//!         None => OperationResult::Err("not found"),
91//!     }
92//! }).unwrap();
93//!
94//! assert_eq!(value, 2);
95//! ```
96//!
97//! If your operation needs to know how many times it's been tried, use the [`retry_with_index`]
98//! function. This works the same as [`retry`], but passes the number of the current try to the
99//! closure as an argument.
100//!
101//! ```
102//! # use retry::retry_with_index;
103//! # use retry::delay::Fixed;
104//! # use retry::OperationResult;
105//! let mut collection = vec![1, 2, 3, 4, 5].into_iter();
106//!
107//! let result = retry_with_index(Fixed::from_millis(100), |current_try| {
108//!     if current_try > 3 {
109//!         return OperationResult::Err("did not succeed within 3 tries");
110//!     }
111//!
112//!     match collection.next() {
113//!         Some(n) if n == 5 => OperationResult::Ok("n is 5!"),
114//!         Some(_) => OperationResult::Retry("n must be 5!"),
115//!         None => OperationResult::Retry("n was never 5!"),
116//!     }
117//! });
118//!
119//! assert!(result.is_err());
120//! ```
121//!
122//! # Features
123//!
124//! - `random`: offer some random delay utilities (on by default)
125
126#![deny(missing_debug_implementations, missing_docs, warnings)]
127
128use std::{
129    error::Error as StdError,
130    fmt::{Display, Error as FmtError, Formatter},
131    thread::sleep,
132    time::Duration,
133};
134
135pub mod delay;
136mod opresult;
137
138#[doc(inline)]
139pub use opresult::OperationResult;
140
141/// Retry the given operation synchronously until it succeeds, or until the given [`Duration`]
142/// iterator ends.
143pub fn retry<I, O, R, E, OR>(iterable: I, mut operation: O) -> Result<R, Error<E>>
144where
145    I: IntoIterator<Item = Duration>,
146    O: FnMut() -> OR,
147    OR: Into<OperationResult<R, E>>,
148{
149    retry_with_index(iterable, |_| operation())
150}
151
152/// Retry the given operation synchronously until it succeeds, or until the given [`Duration`]
153/// iterator ends, with each iteration of the operation receiving the number of the attempt as an
154/// argument.
155pub fn retry_with_index<I, O, R, E, OR>(iterable: I, mut operation: O) -> Result<R, Error<E>>
156where
157    I: IntoIterator<Item = Duration>,
158    O: FnMut(u64) -> OR,
159    OR: Into<OperationResult<R, E>>,
160{
161    let mut iterator = iterable.into_iter();
162    let mut current_try = 1;
163    let mut total_delay = Duration::default();
164
165    loop {
166        match operation(current_try).into() {
167            OperationResult::Ok(value) => return Ok(value),
168            OperationResult::Retry(error) => {
169                if let Some(delay) = iterator.next() {
170                    sleep(delay);
171                    current_try += 1;
172                    total_delay += delay;
173                } else {
174                    return Err(Error {
175                        error,
176                        total_delay,
177                        tries: current_try,
178                    });
179                }
180            }
181            OperationResult::Err(error) => {
182                return Err(Error {
183                    error,
184                    total_delay,
185                    tries: current_try,
186                });
187            }
188        }
189    }
190}
191
192/// An error with a retryable operation.
193#[derive(Debug, PartialEq, Eq)]
194pub struct Error<E> {
195    /// The error returned by the operation on the last try.
196    pub error: E,
197    /// The duration spent waiting between retries of the operation.
198    ///
199    /// Note that this does not include the time spent running the operation itself.
200    pub total_delay: Duration,
201    /// The total number of times the operation was tried.
202    pub tries: u64,
203}
204
205impl<E> Display for Error<E>
206where
207    E: Display,
208{
209    fn fmt(&self, formatter: &mut Formatter) -> Result<(), FmtError> {
210        Display::fmt(&self.error, formatter)
211    }
212}
213
214impl<E> StdError for Error<E>
215where
216    E: StdError,
217{
218    #[allow(deprecated)]
219    fn description(&self) -> &str {
220        self.error.description()
221    }
222
223    fn cause(&self) -> Option<&dyn StdError> {
224        Some(&self.error)
225    }
226}
227
228#[cfg(test)]
229mod tests {
230    use std::time::Duration;
231
232    use super::delay::{Exponential, Fixed, NoDelay};
233    use super::opresult::OperationResult;
234    use super::{retry, retry_with_index, Error};
235
236    #[test]
237    fn succeeds_with_infinite_retries() {
238        let mut collection = vec![1, 2, 3, 4, 5].into_iter();
239
240        let value = retry(NoDelay, || match collection.next() {
241            Some(n) if n == 5 => Ok(n),
242            Some(_) => Err("not 5"),
243            None => Err("not 5"),
244        })
245        .unwrap();
246
247        assert_eq!(value, 5);
248    }
249
250    #[test]
251    fn succeeds_with_maximum_retries() {
252        let mut collection = vec![1, 2].into_iter();
253
254        let value = retry(NoDelay.take(1), || match collection.next() {
255            Some(n) if n == 2 => Ok(n),
256            Some(_) => Err("not 2"),
257            None => Err("not 2"),
258        })
259        .unwrap();
260
261        assert_eq!(value, 2);
262    }
263
264    #[test]
265    fn fails_after_last_try() {
266        let mut collection = vec![1].into_iter();
267
268        let res = retry(NoDelay.take(1), || match collection.next() {
269            Some(n) if n == 2 => Ok(n),
270            Some(_) => Err("not 2"),
271            None => Err("not 2"),
272        });
273
274        assert_eq!(
275            res,
276            Err(Error {
277                error: "not 2",
278                tries: 2,
279                total_delay: Duration::from_millis(0)
280            })
281        );
282    }
283
284    #[test]
285    fn fatal_errors() {
286        let mut collection = vec![1].into_iter();
287
288        let res = retry(NoDelay.take(2), || match collection.next() {
289            Some(n) if n == 2 => OperationResult::Ok(n),
290            Some(_) => OperationResult::Err("no retry"),
291            None => OperationResult::Err("not 2"),
292        });
293
294        assert_eq!(
295            res,
296            Err(Error {
297                error: "no retry",
298                tries: 1,
299                total_delay: Duration::from_millis(0)
300            })
301        );
302    }
303
304    #[test]
305    fn succeeds_with_fixed_delay() {
306        let mut collection = vec![1, 2].into_iter();
307
308        let value = retry(Fixed::from_millis(1), || match collection.next() {
309            Some(n) if n == 2 => Ok(n),
310            Some(_) => Err("not 2"),
311            None => Err("not 2"),
312        })
313        .unwrap();
314
315        assert_eq!(value, 2);
316    }
317
318    #[test]
319    fn fixed_delay_from_duration() {
320        assert_eq!(
321            Fixed::from_millis(1_000).next(),
322            Fixed::from(Duration::from_secs(1)).next(),
323        );
324    }
325
326    #[test]
327    fn succeeds_with_exponential_delay() {
328        let mut collection = vec![1, 2].into_iter();
329
330        let value = retry(Exponential::from_millis(1), || match collection.next() {
331            Some(n) if n == 2 => Ok(n),
332            Some(_) => Err("not 2"),
333            None => Err("not 2"),
334        })
335        .unwrap();
336
337        assert_eq!(value, 2);
338    }
339
340    #[test]
341    fn succeeds_with_exponential_delay_with_factor() {
342        let mut collection = vec![1, 2].into_iter();
343
344        let value = retry(
345            Exponential::from_millis_with_factor(1000, 2.0),
346            || match collection.next() {
347                Some(n) if n == 2 => Ok(n),
348                Some(_) => Err("not 2"),
349                None => Err("not 2"),
350            },
351        )
352        .unwrap();
353
354        assert_eq!(value, 2);
355    }
356
357    #[test]
358    #[cfg(feature = "random")]
359    fn succeeds_with_ranged_delay() {
360        use super::delay::Range;
361
362        let mut collection = vec![1, 2].into_iter();
363
364        let value = retry(Range::from_millis_exclusive(1, 10), || {
365            match collection.next() {
366                Some(n) if n == 2 => Ok(n),
367                Some(_) => Err("not 2"),
368                None => Err("not 2"),
369            }
370        })
371        .unwrap();
372
373        assert_eq!(value, 2);
374    }
375
376    #[test]
377    fn succeeds_with_index() {
378        let mut collection = vec![1, 2, 3].into_iter();
379
380        let value = retry_with_index(NoDelay, |current_try| match collection.next() {
381            Some(n) if n == current_try => Ok(n),
382            Some(_) => Err("not current_try"),
383            None => Err("not current_try"),
384        })
385        .unwrap();
386
387        assert_eq!(value, 1);
388    }
389}