# mettle
[](https://crates.io/crates/mettle)
[](https://docs.rs/mettle)
[](https://github.com/azeemshaik025/mettle/actions/workflows/ci.yml)
[](#license)
**A resilience toolkit for Rust.**
Composable, testable primitives for handling failure. [Documentation](https://docs.rs/mettle).
## Install
```sh
cargo add mettle
```
Blocking only, without an async runtime (no `tokio`):
```sh
cargo add mettle --no-default-features --features blocking
```
## Example
```rust
use mettle::retry;
use std::time::Duration;
// Retry with sensible defaults (exponential backoff, up to 3 retries),
// then override only what you need.
.max_elapsed(Duration::from_secs(30)) // give up after ~30s total
.await?;
```
No async runtime? The blocking twin is identical but ends in `.call()` instead of `.await`.
Retrying on a fixed schedule means every client that failed together retries together, so a service
that is coming back up gets a synchronized wave. Jitter spreads them out:
```rust
use mettle::{Backoff, DecorrelatedBackoff, DecorrelatedBackoffConfig, ExponentialBackoff};
// Randomize any strategy's delays into 0 ..= delay ("full jitter")...
let backoff = ExponentialBackoff::default().jittered();
// ...or use decorrelated jitter, where each delay is drawn from the previous one.
let backoff = DecorrelatedBackoff::new(DecorrelatedBackoffConfig::default())?;
```
Which one: `.jittered()` works on any strategy, including one you wrote, and spreads delays as
widely as possible. `DecorrelatedBackoff` is its own strategy and never draws below its `base`, so
reach for it when you want a floor under every wait. The trade is that a floor also means never
retrying sooner than `base`, so a dependency that frees up early isn't picked up until then.
Both seed from entropy by default and take a fixed seed (`with_seed`) when you want a test to
replay the same delays.
When a retry gives up you get a `RetryError`, which says what stopped it:
```rust
Err(e) => {
// "max_elapsed after 5 attempts in 29.4s: connection refused"
tracing::error!("{} after {} attempts in {:?}: {}",
e.stop_reason().as_str(), e.attempts(), e.elapsed(), e.error());
return Err(e.into()); // ?-able into Box<dyn Error> / anyhow
}
}
```
Only want the underlying error? `.map_err(RetryError::into_error)`.
## Tools
Each tool comes with a runnable example. Start there:
- **retry**: async [examples/retry.rs](https://github.com/azeemshaik025/mettle/blob/main/examples/retry.rs) ยท blocking [examples/blocking_retry.rs](https://github.com/azeemshaik025/mettle/blob/main/examples/blocking_retry.rs)
Retries emit `tracing` events out of the box (target `mettle::retry`). Install any subscriber
(e.g. `tracing_subscriber::fmt::init()`) to see them.
## Status
v0.x, with async (Tokio) and blocking APIs. Expect breaking changes before 1.0.
## License
Licensed under either of
- [Apache License, Version 2.0](https://github.com/azeemshaik025/mettle/blob/main/LICENSE-APACHE)
- [MIT license](https://github.com/azeemshaik025/mettle/blob/main/LICENSE-MIT)
at your option.
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in
the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any
additional terms or conditions.