altair-retry
Async retry with exponential backoff, jitter, cancellation, and per-attempt tracing.
Part of the altair-rs workspace.
Add to your project
CancellationToken is re-exported — no need to add tokio-util separately.
Quick start
use *;
use Duration;
# async
# async
All the knobs
use *;
use Duration;
#
Sensible defaults if you skip any: 5 retries, 100ms initial, 30s max, ×1.5 multiplier, jitter on.
Permanent (non-retryable) errors
Wrap an error in PermanentError to short-circuit retry — e.g. a 4xx response that won't get better with another attempt:
use *;
# async
#
# async
The retry returns Error::Permanent { name, source } — no further attempts are made.
Cancellation
For graceful shutdown — pass a CancellationToken and any cancel signal aborts the retry loop:
use *;
use Duration;
# async
# async
Tracing output
Each attempt runs inside a tracing::span!("retry.attempt", retry.attempt = N) span, nested under a top-level retry span:
INFO retry{retry.name=db.connect retry.max_attempts=4}:retry.attempt{retry.attempt=1}: retrying after backoff retry.delay_ms=100
INFO retry{retry.name=db.connect retry.max_attempts=4}:retry.attempt{retry.attempt=2}: retrying after backoff retry.delay_ms=150
INFO retry{retry.name=db.connect retry.max_attempts=4}:retry.attempt{retry.attempt=3}: retry succeeded retry.outcome=success retry.elapsed_ms=347 retry.attempts=3
Final outcome (success, permanent, exhausted, cancelled) emits as an event with retry.elapsed_ms and retry.attempts attributes.
If altair-otel is initialized in the same process, these spans flow to OTLP automatically — zero extra setup.
Error reference
| Variant | When |
|---|---|
Error::Exhausted |
All retry attempts used up; final underlying error preserved as source |
Error::Permanent |
Operation returned PermanentError::wrap(...); no more attempts made |
Error::Cancelled |
Provided CancellationToken fired |