Skip to main content

relentless/
lib.rs

1//! Retry and polling for Rust — with composable strategies, policy reuse, and
2//! first-class support for polling workflows where `Ok(_)` doesn't always mean
3//! "done."
4//!
5//! Most retry libraries handle the simple case well: call a function, retry on
6//! error, back off. `relentless` handles that too, but it also handles the cases
7//! those libraries make awkward:
8//!
9//! - **Polling**, where `Ok("pending")` means "keep going" and you need
10//!   [`.until(predicate)`](SyncRetryBuilder::until) rather than just retrying errors.
11//! - **Policy reuse**, where a single [`RetryPolicy`] captures your retry rules and
12//!   gets shared across multiple call sites — no duplicated builder chains.
13//! - **Strategy composition**, where
14//!   `wait::fixed(50ms) + wait::exponential(100ms)` and
15//!   `stop::attempts(5) | stop::elapsed(2s)` express complex behavior in one line.
16//! - **Hooks and stats**, where you observe the retry lifecycle (logging, metrics)
17//!   without restructuring your retry logic.
18//!
19//! All of this works in sync and async code, across `std`, `no_std`, and `wasm`
20//! targets.
21//!
22//! # Quick start
23//!
24//! The [`RetryExt`] extension trait is the fastest way to add retries. Defaults:
25//! 3 attempts, exponential backoff from 100 ms, retry on any `Err`.
26//!
27//! ```
28//! use relentless::RetryExt;
29//!
30//! let result = (|| Ok::<_, &str>(42)).retry().sleep(|_| {}).call();
31//! assert_eq!(result.unwrap(), 42);
32//! ```
33//!
34//! The [`retry`] and [`retry_async`] free functions are equivalent, with the
35//! added ability to capture retry loop state via the [`RetryState`] argument:
36//!
37//! ```
38//! use relentless::retry;
39//!
40//! let result = retry(|state| {
41//!     if state.attempt >= 2 { Ok(state.attempt) } else { Err("not yet") }
42//! })
43//! .sleep(|_| {})
44//! .call();
45//!
46//! assert_eq!(result.unwrap(), 2);
47//! ```
48//!
49//! # Customizing retry behavior
50//!
51//! Builder methods control **when** to retry, **how long** to wait, and **when**
52//! to stop:
53//!
54//! ```
55//! use core::time::Duration;
56//! use relentless::{Wait, retry, predicate, stop, wait};
57//!
58//! let result = retry(|_| Err::<(), &str>("boom"))
59//!     .when(predicate::error(|e: &&str| *e == "boom"))
60//!     .wait(
61//!         wait::exponential(Duration::from_millis(100))
62//!             .full_jitter()
63//!             .cap(Duration::from_secs(5)),
64//!     )
65//!     .stop(stop::attempts(3))
66//!     .sleep(|_| {})
67//!     .call();
68//!
69//! assert!(result.is_err());
70//! ```
71//!
72//! # Policy reuse
73//!
74//! [`RetryPolicy`] captures retry rules once. Compose wait strategies with `+`
75//! and stop strategies with `|` or `&`.
76//!
77//! ```
78//! use core::time::Duration;
79//! use relentless::{RetryPolicy, stop, wait};
80//!
81//! let policy = RetryPolicy::new()
82//!     .wait(
83//!         wait::fixed(Duration::from_millis(50))
84//!             + wait::exponential(Duration::from_millis(100)),
85//!     )
86//!     .stop(stop::attempts(5) | stop::elapsed(Duration::from_secs(30)));
87//!
88//! // Same policy, different operations.
89//! let a = policy.retry(|_| Ok::<_, &str>("a")).sleep(|_| {}).call();
90//! let b = policy.retry(|_| Ok::<_, &str>("b")).sleep(|_| {}).call();
91//!
92//! assert_eq!(a.unwrap(), "a");
93//! assert_eq!(b.unwrap(), "b");
94//! ```
95//!
96//! # Polling for a condition
97//!
98//! Use [`.until(predicate)`](SyncRetryBuilder::until) to keep retrying until a
99//! success condition is met. Unlike [`.when()`](SyncRetryBuilder::when), which
100//! retries on matching outcomes, `.until()` retries on everything *except* the
101//! matching outcome.
102//!
103//! ```
104//! use relentless::{RetryPolicy, predicate};
105//!
106//! #[derive(Debug, PartialEq)]
107//! enum Status { Pending, Done }
108//!
109//! let mut count = 0;
110//! let result = RetryPolicy::new()
111//!     .until(predicate::ok(|s: &Status| *s == Status::Done))
112//!     .retry(|_| {
113//!         count += 1;
114//!         Ok::<_, &str>(if count >= 2 { Status::Done } else { Status::Pending })
115//!     })
116//!     .sleep(|_| {})
117//!     .call();
118//!
119//! assert_eq!(result.unwrap(), Status::Done);
120//! ```
121//!
122//! # Hooks and stats
123//!
124//! ```
125//! use relentless::retry;
126//!
127//! let (result, stats) = retry(|_| Ok::<_, &str>("done"))
128//!     .before_attempt(|state| {
129//!         if state.attempt > 1 {
130//!             println!("retrying (attempt {})", state.attempt);
131//!         }
132//!     })
133//!     .after_attempt(|state| {
134//!         if let Err(e) = state.outcome {
135//!             eprintln!("attempt {} failed: {e}", state.attempt);
136//!         }
137//!     })
138//!     .sleep(|_| {})
139//!     .with_stats()
140//!     .call();
141//!
142//! println!("attempts: {}, total wait: {:?}", stats.attempts, stats.total_wait);
143//! ```
144//!
145//! # Error handling
146//!
147//! The retry loop returns `Ok(T)` on success. On failure it returns
148//! [`RetryError`], which distinguishes between exhaustion (stop strategy fired)
149//! and rejection (predicate deemed the error non-retryable):
150//!
151//! ```
152//! use relentless::{retry, RetryError};
153//!
154//! match retry(|_| Err::<(), &str>("boom")).sleep(|_| {}).call() {
155//!     Ok(val) => println!("success: {val:?}"),
156//!     Err(RetryError::Exhausted { last }) => {
157//!         println!("gave up: {last:?}");
158//!     }
159//!     Err(RetryError::Rejected { last }) => {
160//!         println!("non-retryable: {last}");
161//!     }
162//! }
163//! ```
164//!
165//! # Custom wait strategies
166//!
167//! Implement [`Wait`] to build your own wait strategies. All combinators
168//! ([`.cap()`](Wait::cap), [`.full_jitter()`](Wait::full_jitter),
169//! [`.chain()`](Wait::chain), `+`) work on any `Wait` implementor.
170//!
171//! ```
172//! use core::time::Duration;
173//! use relentless::{RetryState, Wait, wait};
174//!
175//! struct CustomWait(Duration);
176//!
177//! impl Wait for CustomWait {
178//!     fn next_wait(&self, _state: &RetryState) -> Duration {
179//!         self.0
180//!     }
181//! }
182//!
183//! let strategy = CustomWait(Duration::from_millis(20))
184//!     .cap(Duration::from_millis(15))
185//!     .chain(wait::fixed(Duration::from_millis(50)), 2);
186//!
187//! let state = RetryState::new(3, None);
188//! assert_eq!(strategy.next_wait(&state), Duration::from_millis(50));
189//! ```
190//!
191//! # Feature flags
192//!
193//! | Flag | Purpose |
194//! |------|---------|
195//! | `std` (default) | `std::thread::sleep` fallback, `Instant` elapsed clock, `std::error::Error` on `RetryError` |
196//! | `alloc` | Boxed policies, closure elapsed clocks, multiple hooks per point |
197//! | `tokio-sleep` | `sleep::tokio()` async sleep adapter |
198//! | `embassy-sleep` | `sleep::embassy()` async sleep adapter |
199//! | `gloo-timers-sleep` | `sleep::gloo()` async sleep adapter (wasm32) |
200//! | `futures-timer-sleep` | `sleep::futures_timer()` async sleep adapter |
201//!
202//! Async retry does not require `alloc`. Sync `std` builds automatically fall
203//! back to `std::thread::sleep`, so `.sleep(...)` is optional.
204
205#![no_std]
206#![forbid(unsafe_code)]
207#![warn(missing_docs)]
208
209// Compile-test README code examples as doctests.
210// Gated on `tokio-sleep` because the async example uses `sleep::tokio()`.
211#[cfg(all(doctest, feature = "tokio-sleep"))]
212#[doc = include_str!("../README.md")]
213mod readme_doctests {}
214
215#[cfg(feature = "alloc")]
216extern crate alloc;
217
218#[cfg(feature = "std")]
219extern crate std;
220
221mod compat;
222
223mod error;
224mod policy;
225pub mod predicate;
226/// Async sleep abstractions used by the retry engine between attempts.
227pub mod sleep;
228mod state;
229mod stats;
230pub mod stop;
231pub mod wait;
232
233pub use error::{RetryError, RetryResult};
234pub use policy::RetryPolicy;
235pub use policy::{AsyncRetry, AsyncRetryExt, AsyncRetryWithStats};
236pub use policy::{
237    AsyncRetryBuilder, AsyncRetryBuilderWithStats, DefaultAsyncRetryBuilder,
238    DefaultAsyncRetryBuilderWithStats, DefaultSyncRetryBuilder, DefaultSyncRetryBuilderWithStats,
239    SyncRetryBuilder, SyncRetryBuilderWithStats,
240};
241pub use policy::{RetryExt, SyncRetry, SyncRetryWithStats};
242pub use predicate::Predicate;
243pub use sleep::Sleeper;
244pub use state::{AttemptState, ExitState, RetryState};
245pub use stats::{RetryStats, StopReason};
246pub use stop::Stop;
247pub use wait::Wait;
248
249/// Returns a [`SyncRetryBuilder`] with default policy: `attempts(3)`,
250/// `exponential(100ms)`, `any_error()`.
251///
252/// # Examples
253///
254/// ```
255/// use relentless::{retry, stop};
256///
257/// let result = retry(|_| Ok::<u32, &str>(42))
258///     .stop(stop::attempts(1))
259///     .sleep(|_| {})
260///     .call();
261/// assert_eq!(result.unwrap(), 42);
262/// ```
263pub fn retry<F, T, E>(
264    op: F,
265) -> SyncRetryBuilder<
266    stop::StopAfterAttempts,
267    wait::WaitExponential,
268    predicate::PredicateAnyError,
269    (),
270    (),
271    (),
272    F,
273    policy::NoSyncSleep,
274    T,
275    E,
276>
277where
278    F: FnMut(RetryState) -> Result<T, E>,
279{
280    SyncRetryBuilder::from_policy(RetryPolicy::new(), op)
281}
282
283/// Returns an [`AsyncRetryBuilder`] with default policy: `attempts(3)`,
284/// `exponential(100ms)`, `any_error()`.
285///
286/// # Examples
287///
288/// ```
289/// use core::time::Duration;
290/// use relentless::retry_async;
291///
292/// let retry = retry_async(|_| async { Ok::<u32, &str>(42) })
293///     .sleep(|_dur: Duration| async {});
294/// let _ = retry;
295/// ```
296pub fn retry_async<F, T, E, Fut>(
297    op: F,
298) -> AsyncRetryBuilder<
299    stop::StopAfterAttempts,
300    wait::WaitExponential,
301    predicate::PredicateAnyError,
302    (),
303    (),
304    (),
305    F,
306    Fut,
307    policy::NoAsyncSleep,
308    T,
309    E,
310>
311where
312    F: FnMut(RetryState) -> Fut,
313    Fut: core::future::Future<Output = Result<T, E>>,
314{
315    AsyncRetryBuilder::from_policy(RetryPolicy::new(), op)
316}