Skip to main content

retry/
lib.rs

1//! Retry async operations with configurable backoff strategies.
2//!
3//! This crate provides a single [`retry`] function backed by a [`Config`]
4//! that controls how many attempts are made and the delay between them.
5//!
6//! # Example
7//!
8//! ```rust
9//! use std::time::Duration;
10//! use retry::{retry, Config};
11//! use retry::delay::{Exponential, Fixed};
12//!
13//! # async fn example() -> Result<(), retry::Error<&'static str>> {
14//! // Exponential backoff, max 5 attempts, with jitter
15//! let result = retry(
16//!     || async { Ok::<_, &'static str>(42) },
17//!     Config::new(
18//!         Exponential::new(Duration::from_millis(100))
19//!             .with_max(Duration::from_secs(5))
20//!             .with_jitter(),
21//!     )
22//!     .with_attempts(5),
23//! )
24//! .await?;
25//!
26//! // Fixed delay, 3 attempts, with on_retry callback
27//! let result = retry(
28//!     || async { Ok::<_, &'static str>(42) },
29//!     Config::new(Fixed::new(Duration::from_secs(1)))
30//!         .with_attempts(3)
31//!         .with_on_retry(|attempt, delay| {
32//!             eprintln!("retry #{attempt} in {delay:?}");
33//!         }),
34//! )
35//! .await?;
36//! # Ok(())
37//! # }
38//! ```
39
40use std::{future::Future, time::Duration};
41
42pub mod delay;
43pub mod error;
44
45use delay::Delay;
46pub use error::Error;
47
48/// Configuration for the [`retry`] function.
49///
50/// # Example
51///
52/// ```
53/// use std::time::Duration;
54/// use retry::Config;
55/// use retry::delay::Fixed;
56///
57/// let config: Config<_> = Config::new(Fixed::new(Duration::from_secs(1)))
58///     .with_attempts(5)
59///     .with_on_retry(|attempt, delay| {
60///         eprintln!("Attempt {attempt} failed, retrying in {delay:?}");
61///     });
62/// ```
63pub struct Config<D> {
64    /// Number of attempts. `0` means retry forever (until success).
65    /// Includes the initial call. E.g. `attempts: 5` = 1 initial + 4 retries.
66    pub attempts: usize,
67    /// The delay strategy used between retries.
68    pub delay: D,
69    /// Optional callback invoked before each retry.
70    /// Receives the attempt number (1-based) and the delay duration.
71    pub on_retry: Option<Box<dyn FnMut(usize, Duration) + 'static>>,
72}
73
74impl<D> Config<D> {
75    /// Creates a new config with infinite retries and the given delay strategy.
76    pub const fn new(delay: D) -> Self {
77        Self {
78            attempts: 0,
79            delay,
80            on_retry: None,
81        }
82    }
83
84    /// Sets the maximum number of attempts.
85    ///
86    /// `0` means infinite retries. An attempt count of `5` means
87    /// 1 initial call + up to 4 retries.
88    pub const fn with_attempts(mut self, attempts: usize) -> Self {
89        self.attempts = attempts;
90        self
91    }
92
93    /// Sets a callback that is invoked before each retry.
94    ///
95    /// The callback receives the attempt number (1-based) and
96    /// the delay that will be waited before retrying.
97    pub fn with_on_retry(mut self, f: impl FnMut(usize, Duration) + 'static) -> Self {
98        self.on_retry = Some(Box::new(f));
99        self
100    }
101}
102
103/// Retries a fallible async operation with the given configuration.
104///
105/// The operation `f` is called repeatedly. On success the value is returned.
106/// On failure the config's delay strategy determines how long to wait before
107/// retrying. Once the configured number of attempts is exhausted, the last
108/// error is returned wrapped in [`Error`].
109///
110/// If `attempts` is `0` (the default), the operation is retried forever
111/// until it succeeds.
112///
113/// # Example
114///
115/// ```rust
116/// use std::time::Duration;
117/// use retry::{retry, Config};
118/// use retry::delay::Fixed;
119///
120/// # async fn example() -> Result<(), retry::Error<&'static str>> {
121/// let result = retry(
122///     || async { Ok::<_, &'static str>("hello") },
123///     Config::new(Fixed::new(Duration::from_millis(10))).with_attempts(3),
124/// )
125/// .await?;
126/// assert_eq!(result, "hello");
127/// # Ok(())
128/// # }
129/// ```
130pub async fn retry<F, Fut, T, E, D>(mut f: F, config: Config<D>) -> Result<T, Error<E>>
131where
132    F: FnMut() -> Fut,
133    Fut: Future<Output = Result<T, E>>,
134    D: Delay,
135{
136    let Config {
137        attempts,
138        mut delay,
139        mut on_retry,
140    } = config;
141
142    let mut attempt = 0;
143
144    loop {
145        attempt += 1;
146
147        match f().await {
148            Ok(value) => return Ok(value),
149            Err(err) => {
150                if attempts > 0 && attempt >= attempts {
151                    return Err(Error::new(err, attempt));
152                }
153
154                let d = delay.next_delay();
155
156                if let Some(ref mut cb) = on_retry {
157                    cb(attempt, d);
158                }
159
160                tokio::time::sleep(d).await;
161            }
162        }
163    }
164}