execution-policy
Closure-first, runtime-light reliability policies for any async Rust operation: retry · backoff · jitter · attempt/total timeouts · circuit breaking · bounded concurrency · retry budgets.
A fluent, closure-first API with explicit Rust ownership, real deadlines, and
deterministic testing. It wraps any async function, job, DB call, or HTTP
client — you bring a closure, it brings the resilience. (Building a Tower
Service stack instead? Reach for tower-resilience; this crate is for
everything that isn't a Tower service.)
Quick start
Start tiny and add policy as you need it — every example below is the same builder, just with more layers.
1. Retry a flaky call. The closure is re-run on each attempt.
use ;
let policy = new
.retry
.build;
let value = policy.run.await?;
2. Only retry transient errors, and add jitter. Classification keeps you from
retrying a 404 forever.
use Duration;
use ;
let policy = new
.retry
.build;
let resp = policy.run.await?;
3. Bound how long it can take. A per-attempt cap and an overall deadline; use
execute when you want the attempt number (e.g. for a header or log).
let policy = new
.retry
.attempt_timeout
.total_timeout
.build;
let resp = policy
.execute
.await?;
4. The full picture. Add a circuit breaker and a concurrency limit, and inject your client as state so the closure borrows it cleanly across retries.
use ;
let policy = new
.retry
.attempt_timeout
.total_timeout
.circuit_breaker
.concurrency_limit
.build;
let body = policy
.execute_with
.await?;
Why it's different
- Operation factory, not a future. The closure is re-invoked per attempt, so
every retry builds a fresh request/future — no
Clonebound on your types. !Sendfriendly. The engine drives futures in place (neverspawns), so operations capturingRc/RefCellwork fine.- Classification separate from policy. Retry and circuit-breaker decisions are
independent; you can inspect
Ok-wrapped failures (e.g. an HTTP 503 insideOk(Response)). - Deterministic tests. Inject a
TestCore/ManualClock— no real sleeps, reproducible jitter and breaker windows. - Fast. ~65 ns success overhead; zero heap allocation on the success path (timers are armed lazily, only once an operation actually pends).
The four-method ergonomic gradient
policy.run.await?; // no state, no metadata
policy.run_with.await?; // state
policy.execute.await?; // attempt metadata
policy.execute_with.await?; // both
Composition order (fixed & documented)
total_timeout( concurrency( circuit_breaker( retry( attempt_timeout( operation ) ) ) ) )
The concurrency gate is acquired once per call; the breaker records one
vote per pipeline outcome; attempt_timeout bounds each try; total_timeout
bounds everything including backoff.
Errors
ExecutionError<E> implements std::error::Error (the operation error is its
source, so ? chains cleanly) and carries diagnostic context — attempts made,
elapsed time, last backoff delay, breaker state. Predicates avoid matching:
is_timeout(), is_circuit_open(), is_rejected(), is_exhausted();
into_inner() recovers the operation error.
Observability
Register a synchronous hook — zero cost when absent (events aren't even constructed without a hook):
let policy = new
.retry
.on_event
.build;
Enable the tracing feature and call .with_tracing() to bridge events to
tracing automatically.
Retry budgets
Bound retry storms across calls with a shared token bucket:
use RetryBudget;
let budget = standard; // 20% retry ratio, burst 10
let policy = new
.retry
.build;
Features
| feature | default | enables |
|---|---|---|
tokio |
✅ | TokioCore / DefaultCore (production timers) |
test-util |
✅ | TestCore / ManualClock (no extra deps) |
tracing |
.with_tracing() event bridge |
test-util is default-on so tests/benches/examples work out of the box — it pulls
in no dependencies. For a lean production build:
= { = "*", = false, = ["tokio"] }
The core even compiles with --no-default-features (no runtime); supply your own
Core to run anywhere.
Cancellation
A timeout drops the operation future — it does not abort remote or blocking
work the operation started. The engine is built around a select!-style seam, so
cooperative CancellationToken support can be added without breaking the API.
License
BSD-3-Clause.