sisyphus
A generic, execution-agnostic and time-agnostic backoff & retry utility — a pure state machine for
no_std, zero-allocation environments.
Sisyphus was condemned by the gods to roll a boulder up a hill for eternity, only to watch it roll back down every time — the original retry loop. This crate is the part of that punishment worth keeping: it computes when to push again, and nothing else.
Why?
Most retry crates bake in std::time::Instant and std::thread::sleep, welding
policy (how long to wait) to execution (how to wait). That breaks down in:
- WebAssembly engines executing Wasm directly, with no host threads;
- Deterministic blockchain / consensus state machines, where reading the wall clock or panicking is a fault;
no_stdembedded targets with no allocator and nostd::time.
sisyphus is a pure state machine instead:
#![no_std], zero allocation, nodyn(unless you opt in viaalloc).- Time is just
core::time::Duration; instants come from yourClock. - Randomness is injected via
Jitter(deterministic by default) — great for reproducible P2P tests and avoiding thundering herds. - Execution is driven by your
sleep— a sync closure or an async future, with no runtime lock-in (no Tokio dependency). #![forbid(unsafe_code)].
Quick start
use ControlFlow;
use Duration;
use ;
// Compose pure state-machine policies.
let policy = new
.with_max_delay
.max_attempts;
// Drive it with your own operation + sleep.
let mut tries = 0u32;
let result: = retry_sync;
assert_eq!;
The ControlFlow contract
Your operation returns core::ops::ControlFlow<B, C>:
Break(value)— terminal. The loop stops and returnsOk(value). Encode both success and fatal errors here (e.g.Break(Result<T, E>)).Continue(state)— transient. Retry after a backoff delay. If the policy gives up, the laststateis returned inRetryError::Exhausted.
Async, runtime-free
retry_async.await
retry_async is a plain async fn over core::future::Future; it never names a
specific executor.
Building blocks
| Item | Role |
|---|---|
BackoffPolicy |
trait: next_delay() -> Option<Duration> + reset() |
ExponentialBackoff<J> |
exponential growth, saturating (never panics) |
Constant |
fixed-interval delay |
MaxAttempts<P> |
caps the number of retries |
WithMaxDelay<P> |
clamps the maximum delay |
MaxElapsedTime<P, C> |
gives up after a time budget (uses a Clock) |
PolicyExt |
fluent combinators: .max_attempts(..), .with_max_delay(..), .max_elapsed_time(..) |
Clock |
host-provided time source |
Jitter / NoJitter / SplitMix64 |
injected randomness |
retry_sync / retry_async |
execution drivers |
Feature flags
| feature | default | adds |
|---|---|---|
alloc |
off | BoxedPolicy + impl BackoffPolicy for Box<dyn …> |
std |
off | SystemClock backed by std::time::Instant |
The default build pulls in neither: pure core, allocation-free.
Examples
Runnable examples live in examples/:
Minimum supported Rust version (MSRV)
sisyphus supports Rust 1.66 and later. Bumping the MSRV is considered a
minor, not patch, change.
License
Licensed under either of MIT or Apache-2.0 at your option.