Expand description
Retry and polling for Rust — with composable strategies, policy reuse, and
first-class support for polling workflows where Ok(_) doesn’t always mean
“done.”
Most retry libraries handle the simple case well: call a function, retry on
error, back off. relentless handles that too, but it also handles the cases
those libraries make awkward:
- Polling, where
Ok("pending")means “keep going” and you need.until(predicate)rather than just retrying errors. - Policy reuse, where a single
RetryPolicycaptures your retry rules and gets shared across multiple call sites — no duplicated builder chains. - Strategy composition, where
wait::fixed(50ms) + wait::exponential(100ms)andstop::attempts(5) | stop::elapsed(2s)express complex behavior in one line. - Hooks and stats, where you observe the retry lifecycle (logging, metrics) without restructuring your retry logic.
All of this works in sync and async code, across std, no_std, and wasm
targets.
§Quick start
The RetryExt extension trait is the fastest way to add retries. Defaults:
3 attempts, exponential backoff from 100 ms, retry on any Err.
use relentless::RetryExt;
let result = (|| Ok::<_, &str>(42)).retry().sleep(|_| {}).call();
assert_eq!(result.unwrap(), 42);The retry and retry_async free functions are equivalent, with the
added ability to capture retry loop state via the RetryState argument:
use relentless::retry;
let result = retry(|state| {
if state.attempt >= 2 { Ok(state.attempt) } else { Err("not yet") }
})
.sleep(|_| {})
.call();
assert_eq!(result.unwrap(), 2);§Customizing retry behavior
Builder methods control when to retry, how long to wait, and when to stop:
use core::time::Duration;
use relentless::{Wait, retry, predicate, stop, wait};
let result = retry(|_| Err::<(), &str>("boom"))
.when(predicate::error(|e: &&str| *e == "boom"))
.wait(
wait::exponential(Duration::from_millis(100))
.full_jitter()
.cap(Duration::from_secs(5)),
)
.stop(stop::attempts(3))
.sleep(|_| {})
.call();
assert!(result.is_err());§Policy reuse
RetryPolicy captures retry rules once. Compose wait strategies with +
and stop strategies with | or &.
use core::time::Duration;
use relentless::{RetryPolicy, stop, wait};
let policy = RetryPolicy::new()
.wait(
wait::fixed(Duration::from_millis(50))
+ wait::exponential(Duration::from_millis(100)),
)
.stop(stop::attempts(5) | stop::elapsed(Duration::from_secs(30)));
// Same policy, different operations.
let a = policy.retry(|_| Ok::<_, &str>("a")).sleep(|_| {}).call();
let b = policy.retry(|_| Ok::<_, &str>("b")).sleep(|_| {}).call();
assert_eq!(a.unwrap(), "a");
assert_eq!(b.unwrap(), "b");§Polling for a condition
Use .until(predicate) to keep retrying until a
success condition is met. Unlike .when(), which
retries on matching outcomes, .until() retries on everything except the
matching outcome.
use relentless::{RetryPolicy, predicate};
#[derive(Debug, PartialEq)]
enum Status { Pending, Done }
let mut count = 0;
let result = RetryPolicy::new()
.until(predicate::ok(|s: &Status| *s == Status::Done))
.retry(|_| {
count += 1;
Ok::<_, &str>(if count >= 2 { Status::Done } else { Status::Pending })
})
.sleep(|_| {})
.call();
assert_eq!(result.unwrap(), Status::Done);§Hooks and stats
use relentless::retry;
let (result, stats) = retry(|_| Ok::<_, &str>("done"))
.before_attempt(|state| {
if state.attempt > 1 {
println!("retrying (attempt {})", state.attempt);
}
})
.after_attempt(|state| {
if let Err(e) = state.outcome {
eprintln!("attempt {} failed: {e}", state.attempt);
}
})
.sleep(|_| {})
.with_stats()
.call();
println!("attempts: {}, total wait: {:?}", stats.attempts, stats.total_wait);§Error handling
The retry loop returns Ok(T) on success. On failure it returns
RetryError, which distinguishes between exhaustion (stop strategy fired)
and rejection (predicate deemed the error non-retryable):
use relentless::{retry, RetryError};
match retry(|_| Err::<(), &str>("boom")).sleep(|_| {}).call() {
Ok(val) => println!("success: {val:?}"),
Err(RetryError::Exhausted { last }) => {
println!("gave up: {last:?}");
}
Err(RetryError::Rejected { last }) => {
println!("non-retryable: {last}");
}
}§Custom wait strategies
Implement Wait to build your own wait strategies. All combinators
(.cap(), .full_jitter(),
.chain(), +) work on any Wait implementor.
use core::time::Duration;
use relentless::{RetryState, Wait, wait};
struct CustomWait(Duration);
impl Wait for CustomWait {
fn next_wait(&self, _state: &RetryState) -> Duration {
self.0
}
}
let strategy = CustomWait(Duration::from_millis(20))
.cap(Duration::from_millis(15))
.chain(wait::fixed(Duration::from_millis(50)), 2);
let state = RetryState::new(3, None);
assert_eq!(strategy.next_wait(&state), Duration::from_millis(50));§Feature flags
| Flag | Purpose |
|---|---|
std (default) | std::thread::sleep fallback, Instant elapsed clock, std::error::Error on RetryError |
alloc | Boxed policies, closure elapsed clocks, multiple hooks per point |
tokio-sleep | sleep::tokio() async sleep adapter |
embassy-sleep | sleep::embassy() async sleep adapter |
gloo-timers-sleep | sleep::gloo() async sleep adapter (wasm32) |
futures-timer-sleep | sleep::futures_timer() async sleep adapter |
Async retry does not require alloc. Sync std builds automatically fall
back to std::thread::sleep, so .sleep(...) is optional.
Re-exports§
Modules§
- predicate
- Predicate trait and built-in retry predicate factories.
- sleep
- Async sleep abstractions used by the retry engine between attempts.
- stop
- Stop trait and built-in stop strategies.
- wait
- Wait trait and built-in wait strategies.
Structs§
- Async
Retry - Async retry execution object.
- Async
Retry Builder - Owned async retry builder created from
AsyncRetryExt::retry_async. - Async
Retry Builder With Stats - Owned async retry builder wrapper that returns statistics.
- Async
Retry With Stats - Async retry execution wrapper that returns statistics.
- Attempt
State - Read-only context passed to the
after_attempthook. - Exit
State - Final read-only context passed to the
on_exithook. - Retry
Policy - Reusable retry configuration.
- Retry
State - Non-generic retry context passed to
Stop::should_stop,Wait::next_wait, the operation, and thebefore_attempthook. - Retry
Stats - Aggregate statistics for a completed retry execution.
- Sync
Retry - Sync retry execution object.
- Sync
Retry Builder - Owned sync retry builder created from
RetryExt::retry. - Sync
Retry Builder With Stats - Owned sync retry builder wrapper that returns statistics.
- Sync
Retry With Stats - Sync retry execution wrapper that returns statistics.
Enums§
- Retry
Error - Error returned when a retry loop terminates without producing an accepted result.
- Stop
Reason - Why a retry loop terminated.
Traits§
- Async
Retry Ext - Extension trait to start async retries directly from a closure/function.
- Retry
Ext - Extension trait to start sync retries directly from a closure/function.
Functions§
- retry
- Returns a
SyncRetryBuilderwith default policy:attempts(3),exponential(100ms),any_error(). - retry_
async - Returns an
AsyncRetryBuilderwith default policy:attempts(3),exponential(100ms),any_error().
Type Aliases§
- Default
Async Retry Builder - Alias for the default owned async retry builder returned by
AsyncRetryExt::retry_async. - Default
Async Retry Builder With Stats - Alias for the default owned async retry builder-with-stats returned by
calling
.with_stats()onAsyncRetryExt::retry_async. - Default
Sync Retry Builder - Alias for the default owned sync retry builder returned by
RetryExt::retry. - Default
Sync Retry Builder With Stats - Alias for the default owned sync retry builder-with-stats returned by
calling
.with_stats()onRetryExt::retry. - Retry
Result - Convenience alias:
Result<T, RetryError<T, E>>.