Async retry with exponential backoff and circuit breaker for Rust
cargo add philiprehberger-retry-kitAsync retry with exponential backoff and circuit breaker for Rust
[dependencies]
philiprehberger-retry-kit = "0.8.0"
The crate ships with the async feature enabled by default, which pulls in tokio and exposes retry_async() and CircuitBreaker::call_async(). To opt out, disable default features. To add the crate with the async feature explicitly:
cargo add philiprehberger-retry-kit --features async
| Feature | Default | Description |
|---|---|---|
async | yes | Enables async retry (retry_async) and async circuit breaker (call_async) via tokio. |
use philiprehberger_retry_kit::{retry, RetryOptions, Backoff};
use std::time::Duration;
let result = retry(RetryOptions::default(), || {
fetch_data()
});
let opts = RetryOptions::default()
.max_attempts(5)
.backoff(Backoff::Exponential)
.initial_delay(Duration::from_secs(1))
.max_delay(Duration::from_secs(30))
.jitter(true);
let result = retry(opts, || fetch_data());
use philiprehberger_retry_kit::{retry_async, RetryOptions};
let result = retry_async(RetryOptions::default(), || async {
fetch_data().await
}).await;
use philiprehberger_retry_kit::{retry_async_if, RetryOptions};
let result = retry_async_if(
RetryOptions::default(),
|| async { might_fail().await },
|err| err.is_transient(), // only retry transient errors
).await;
use philiprehberger_retry_kit::{retry_async_with_fallback, RetryOptions};
let result = retry_async_with_fallback(
RetryOptions::default(),
|| async { primary_db_query().await },
|| async { replica_db_query().await }, // fallback if primary exhausts retries
).await;
use philiprehberger_retry_kit::presets;
let result = retry(presets::network_request(), || fetch_data());
let result = retry(presets::database_query(), || query_db());
let result = retry(presets::aggressive(), || critical_op());
use philiprehberger_retry_kit::CircuitBreaker;
use std::time::Duration;
let mut cb = CircuitBreaker::new(5, Duration::from_secs(30))
.half_open_max_attempts(2); // allow 2 trial requests in half-open state
match cb.call(|| fetch_data()) {
Ok(data) => println!("Got: {:?}", data),
Err(e) => eprintln!("Failed: {}", e),
}
// Manually reset the circuit breaker
cb.reset();
use philiprehberger_retry_kit::retry_if;
let result = retry_if(
RetryOptions::default(),
|| might_fail(),
|err| err.is_transient(), // only retry transient errors
);
let opts = RetryOptions::default()
.on_retry(|attempt, delay| {
println!("Retry #{}, waiting {:?}", attempt, delay);
});
let result = retry(opts, || fetch_data());
Stop retrying after an absolute deadline or a relative timeout, regardless of remaining attempts:
use std::time::{Duration, Instant};
// Absolute deadline
let opts = RetryOptions::default()
.max_attempts(10)
.with_deadline(Instant::now() + Duration::from_secs(30));
let result = retry(opts, || fetch_data());
// Relative timeout (converted to a deadline when the retry loop starts)
let opts = RetryOptions::default()
.max_attempts(10)
.with_total_timeout(Duration::from_secs(30));
let result = retry(opts, || fetch_data());
Both options can be combined; the earlier of the two takes effect. Deadline support works with retry(), retry_if(), and retry_async().
Inspect cumulative statistics and timing information from a CircuitBreaker:
use philiprehberger_retry_kit::CircuitBreaker;
use std::time::Duration;
let mut cb = CircuitBreaker::new(5, Duration::from_secs(30));
let _ = cb.call(|| ok_or_fail());
// Snapshot of cumulative metrics
let m = cb.metrics();
println!("calls={} ok={} err={}", m.total_calls, m.successes, m.failures);
println!("consecutive_failures={} state={}", m.consecutive_failures, m.state);
// Individual accessors
println!("consecutive: {}", cb.consecutive_failures());
if let Some(t) = cb.last_failure_time() {
println!("last failure was {:?} ago", t.elapsed());
}
Metrics are cumulative and survive reset(); only the consecutive failure counter and state are cleared.
use philiprehberger_retry_kit::CircuitBreaker;
use std::time::Duration;
let mut cb = CircuitBreaker::new(5, Duration::from_secs(30));
let result = cb.call_async(|| async {
fetch_data().await
}).await;
use philiprehberger_retry_kit::{CircuitBreaker, CircuitState};
use std::time::Duration;
let mut cb = CircuitBreaker::new(3, Duration::from_secs(30))
.on_state_change(|from, to| {
println!("Circuit: {:?} -> {:?}", from, to);
});
let _ = cb.call(|| fetch_data());
Try a primary function with retries; if exhausted, try a fallback once:
use philiprehberger_retry_kit::{retry_with_fallback, RetryOptions};
let result = retry_with_fallback(
RetryOptions::default(),
|| primary_db_query(),
|| replica_db_query(), // fallback if primary exhausts retries
);
| Function / Type | Description |
|---|---|
retry(opts, f) | Retry a synchronous function with the given options |
retry_if(opts, f, predicate) | Retry a synchronous function only when the predicate returns true for the error |
retry_async(opts, f) | Retry an async function (requires async feature) |
retry_async_if(opts, f, predicate) | Retry an async function only when the predicate returns true for the error (requires async feature) |
retry_with_fallback(opts, f, fallback) | Retry primary function, then try fallback once on exhaustion |
retry_async_with_fallback(opts, f, fallback) | Retry primary async function, then try async fallback once on exhaustion (requires async feature) |
RetryOptions | Configuration for retry behavior (max attempts, backoff, delays, jitter, deadline) |
RetryOptions::default() | Create default options (3 attempts, exponential backoff, 1s initial, 30s max, jitter on) |
Backoff | Backoff strategy enum: Exponential, Linear, Fixed |
RetryError | Error returned when all retry attempts are exhausted |
CircuitBreaker::new(threshold, timeout) | Create a circuit breaker with failure threshold and reset timeout |
cb.call(f) | Execute a function through the circuit breaker |
cb.call_async(f) | Execute an async function through the circuit breaker |
cb.on_state_change(callback) | Register a callback for circuit state transitions |
cb.reset() | Manually reset the circuit breaker to closed state |
cb.half_open_max_attempts(n) | Set max trial attempts allowed in half-open state |
cb.state() | Get current circuit state |
cb.metrics() | Get a snapshot of cumulative metrics |
cb.consecutive_failures() | Get current consecutive failure count |
cb.last_failure_time() | Get the time of the last recorded failure |
CircuitState | Circuit state enum: Closed, Open, HalfOpen |
CircuitBreakerMetrics | Snapshot of circuit breaker metrics (total calls, successes, failures, state) |
CircuitOpenError | Error returned when the circuit breaker is open |
presets::aggressive() | Preset: 5 attempts, 500ms initial, 5s max |
presets::gentle() | Preset: 3 attempts, 2s initial, 30s max |
presets::network_request() | Preset: 3 attempts, 1s initial, 10s max |
presets::database_query() | Preset: 3 attempts, linear backoff, 500ms initial, no jitter |
cargo test
cargo clippy -- -D warnings
If you find this project useful: