retry_fn/
lib.rs

1#![doc(html_root_url = "https://docs.rs/retry_fn/0.2.0")]
2//! # retry
3//!
4//! Function for executing retry either as a closure with a std-based sleep
5//! (`thread::sleep`) or using either of the most popular async runtimes. See
6//! `tokio` or `async-std` module for futures-aware versions.
7//!
8//! ## Sync Example
9//!
10//! ```rust,no_run
11//! # use std::{io, time::Duration};
12//! use retry_fn::{retry, RetryResult, strategy::ExponentialBackoff};
13//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
14//! let mut count = 0;
15//! let res = retry(ExponentialBackoff::new(Duration::from_secs(2)), |op| {
16//!    if op.retries >= 3 {
17//!        RetryResult::<&str, _>::Err(io::Error::new(
18//!            io::ErrorKind::TimedOut,
19//!            "timed out",
20//!        ))
21//!    } else {
22//!        count += 1;
23//!        RetryResult::Retry()
24//!    }
25//! });
26//! assert_eq!(count, 3);
27//! assert!(res.is_err());
28//! Ok(())
29//! # }
30//! ```
31#![warn(
32    missing_debug_implementations,
33    missing_docs,
34    missing_copy_implementations,
35    rust_2018_idioms,
36    unreachable_pub,
37    non_snake_case,
38    non_upper_case_globals
39)]
40#![allow(clippy::cognitive_complexity)]
41#![deny(broken_intra_doc_links)]
42#![doc(test(
43    no_crate_inject,
44    attr(deny(warnings, rust_2018_idioms), allow(dead_code, unused_variables))
45))]
46
47use crate::strategy::Immediate;
48
49#[cfg(any(feature = "tokio-runtime", feature = "async-runtime"))]
50#[macro_use]
51mod macros;
52pub mod strategy;
53
54#[cfg(feature = "tokio-runtime")]
55pub mod tokio;
56
57#[cfg(feature = "async-runtime")]
58pub mod async_std;
59
60use std::{error::Error, fmt, thread, time::Duration};
61
62/// `RetryOp` gives some inspection into the current state of retries
63#[derive(Debug, Copy, Clone, Eq, PartialEq)]
64pub struct RetryOp {
65    /// number of retries
66    pub retries: usize,
67    /// total duration we've delayed
68    pub total_delay: Duration,
69}
70
71/// What to do with the current result of the function
72///
73/// `Retry` will execute the function again, `Err(E)` will return an error with
74/// E, `Ok(T)` will return success with T
75#[derive(Debug, Clone)]
76pub enum RetryResult<T, E> {
77    /// try again
78    Retry(),
79    /// return with an error
80    Err(E),
81    /// return with success
82    Ok(T),
83}
84
85/// Error type for retry
86#[derive(Debug, Clone)]
87pub enum RetryErr<E> {
88    /// Attempt failed with an error
89    FailedAttempt {
90        /// number of attempts
91        tries: usize,
92        /// total delay
93        total_delay: Duration,
94        /// the error
95        err: E,
96    },
97    /// Attempt failed by reaching the end of the iterator
98    IteratorEnded {
99        /// number of attempts
100        tries: usize,
101        /// total delay
102        total_delay: Duration,
103    },
104}
105
106impl<E> Error for RetryErr<E> where E: fmt::Display + fmt::Debug {}
107
108impl<E> fmt::Display for RetryErr<E>
109where
110    E: fmt::Display + fmt::Debug,
111{
112    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
113        match self {
114            RetryErr::FailedAttempt {
115                tries,
116                total_delay,
117                err,
118            } => write!(
119                f,
120                "failed with {}, tries {} total delay {:#?}",
121                err, tries, total_delay
122            ),
123            RetryErr::IteratorEnded { tries, total_delay } => write!(
124                f,
125                "iterator ended, retries {}, total delay {:#?}",
126                tries, total_delay
127            ),
128        }
129    }
130}
131
132/// retry with the 'immediate' strategy, i.e. no wait in between attempts
133///
134/// ```rust,no_run
135/// # use std::io;
136/// use retry_fn::{retry_immediate, RetryResult};
137/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
138/// let mut count = 0;
139/// let res = retry_immediate(|op| {
140///    if op.retries >= 3 {
141///        RetryResult::<&str, _>::Err(io::Error::new(
142///            io::ErrorKind::TimedOut,
143///            "timed out",
144///        ))
145///    } else {
146///        count += 1;
147///        RetryResult::Retry()
148///    }
149/// });
150/// assert_eq!(count, 3);
151/// assert!(res.is_err());
152/// Ok(())
153/// # }
154/// ```
155///
156/// # Returns
157/// If successful, return `Ok`, otherwise return `Retry` to try again or `Err`
158/// to exit with an error
159pub fn retry_immediate<F, T, E>(f: F) -> Result<T, RetryErr<E>>
160where
161    F: FnMut(RetryOp) -> RetryResult<T, E>,
162{
163    retry(Immediate, f)
164}
165
166/// Retry a function on some time interval
167///
168/// ```rust,no_run
169/// # use std::{io, time::Duration};
170/// use retry_fn::{retry, RetryResult, strategy::ExponentialBackoff};
171/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
172/// let mut count = 0;
173/// let res = retry(ExponentialBackoff::new(Duration::from_secs(2)), |op| {
174///    if op.retries >= 3 {
175///        RetryResult::<&str, _>::Err(io::Error::new(
176///            io::ErrorKind::TimedOut,
177///            "timed out",
178///        ))
179///    } else {
180///        count += 1;
181///        RetryResult::Retry()
182///    }
183/// });
184/// assert_eq!(count, 3);
185/// assert!(res.is_err());
186/// Ok(())
187/// # }
188/// ```
189///
190/// # Returns
191/// If successful, return `Ok`, otherwise return `Retry` to try again or `Err`
192/// to exit with an error
193pub fn retry<I, F, T, E>(iter: I, mut f: F) -> Result<T, RetryErr<E>>
194where
195    I: IntoIterator<Item = Duration>,
196    F: FnMut(RetryOp) -> RetryResult<T, E>,
197{
198    let mut count = 0;
199    let mut total_delay = Duration::from_millis(0);
200    for dur in iter.into_iter() {
201        match f(RetryOp {
202            retries: count,
203            total_delay,
204        }) {
205            RetryResult::Retry() => {
206                thread::sleep(dur);
207                total_delay += dur;
208                count += 1;
209            }
210            RetryResult::Err(err) => {
211                return Err(RetryErr::FailedAttempt {
212                    tries: count,
213                    total_delay,
214                    err,
215                });
216            }
217            RetryResult::Ok(val) => {
218                return Ok(val);
219            }
220        }
221    }
222    Err(RetryErr::IteratorEnded {
223        tries: count,
224        total_delay,
225    })
226}
227
228#[cfg(test)]
229mod test {
230    use crate::RetryResult;
231
232    use super::*;
233    use crate::strategy::*;
234
235    use std::io;
236
237    #[test]
238    fn fail_on_three() -> io::Result<()> {
239        let mut count = 0;
240        let res = retry(Constant::from_millis(100), |op| {
241            if op.retries >= 3 {
242                RetryResult::<&str, _>::Err(io::Error::new(io::ErrorKind::TimedOut, "timed out"))
243            } else {
244                count += 1;
245                RetryResult::Retry()
246            }
247        });
248        assert_eq!(count, 3);
249        assert!(res.is_err());
250        Ok(())
251    }
252
253    #[test]
254    fn pass_eventually() -> io::Result<()> {
255        let mut count = 0;
256        let res = retry(Constant::from_millis(100), |op| {
257            if op.retries >= 3 {
258                RetryResult::<usize, &str>::Ok(5)
259            } else {
260                count += 1;
261                RetryResult::Retry()
262            }
263        });
264        assert_eq!(count, 3);
265        assert!(res.is_ok());
266        assert_eq!(res.unwrap(), 5);
267
268        Ok(())
269    }
270}