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}