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}