Skip to content
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ layered = { path = "crates/layered", default-features = false, version = "0.3.0"
ohno = { path = "crates/ohno", default-features = false, version = "0.3.1" }
ohno_macros = { path = "crates/ohno_macros", default-features = false, version = "0.3.0" }
recoverable = { path = "crates/recoverable", default-features = false, version = "0.1.1" }
seatbelt = { path = "crates/seatbelt", default-features = false, version = "0.4.2" }
seatbelt = { path = "crates/seatbelt", default-features = false, version = "0.4.3" }
templated_uri = { path = "crates/templated_uri", default-features = false, version = "0.1.0" }
templated_uri_macros = { path = "crates/templated_uri_macros", default-features = false, version = "0.1.0" }
templated_uri_macros_impl = { path = "crates/templated_uri_macros_impl", default-features = false, version = "0.1.0" }
Expand Down
6 changes: 6 additions & 0 deletions crates/seatbelt/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Changelog

## [0.4.3] - 2026-03-24

- ✨ Features

- introduce chaos injection middleware ([#335](https://github.com/microsoft/oxidizer/pull/335))

## [0.4.2] - 2026-03-10

- ✨ Features
Expand Down
7 changes: 6 additions & 1 deletion crates/seatbelt/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
[package]
name = "seatbelt"
description = "Resilience and recovery mechanisms for fallible operations."
version = "0.4.2"
version = "0.4.3"
readme = "README.md"
keywords = ["oxidizer", "resilience", "layered", "recovery", "retry"]
categories = ["data-structures"]
Expand Down Expand Up @@ -44,6 +44,7 @@ retry = ["dep:fastrand"]
breaker = ["dep:fastrand"]
fallback = []
hedging = ["dep:futures-util"]
chaos-injection = ["dep:fastrand"]
serde = ["dep:serde", "dep:jiff"]
metrics = ["dep:opentelemetry", "opentelemetry/metrics"]
logs = ["dep:tracing"]
Expand Down Expand Up @@ -132,6 +133,10 @@ required-features = ["retry", "breaker", "timeout", "serde"]
name = "fallback"
required-features = ["fallback"]

[[example]]
name = "chaos_injection"
required-features = ["chaos-injection"]

[[bench]]
name = "observability"
harness = false
Expand Down
101 changes: 58 additions & 43 deletions crates/seatbelt/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,14 @@ for each module for details on how to use them.
* [`breaker`][__link11] - Middleware that prevents cascading failures.
* [`fallback`][__link12] - Middleware that replaces invalid output with a user-defined alternative.

### Chaos Testing

The [`chaos`][__link13] module provides middleware for deliberately injecting faults into a service
pipeline, enabling teams to verify that their systems handle failures gracefully.

* [`chaos::injection`][__link14] - Middleware that replaces service output with a user-provided value
at a configurable probability.

## Middleware Ordering

The order in which resilience middleware is composed **matters**. Layers apply outer to inner
Expand Down Expand Up @@ -145,35 +153,38 @@ let service = ServiceBuilder::new()

Examples covering each middleware and common composition patterns:

* [`timeout`][__link13]: Basic timeout that cancels long-running operations.
* [`timeout_advanced`][__link14]: Dynamic timeout duration and timeout callbacks.
* [`retry`][__link15]: Automatic retry with input cloning and recovery classification.
* [`retry_advanced`][__link16]: Custom input cloning with attempt metadata injection.
* [`retry_outage`][__link17]: Input restoration from errors when cloning is not possible.
* [`breaker`][__link18]: Circuit breaker that monitors failure rates.
* [`hedging`][__link19]: Hedging slow requests with parallel attempts to reduce tail latency.
* [`fallback`][__link20]: Substitutes default values for invalid outputs.
* [`resilience_pipeline`][__link21]: Composing retry and timeout with metrics.
* [`tower`][__link22]: Tower `ServiceBuilder` integration.
* [`config`][__link23]: Loading settings from a [JSON file][__link24].
* [`timeout`][__link15]: Basic timeout that cancels long-running operations.
* [`timeout_advanced`][__link16]: Dynamic timeout duration and timeout callbacks.
* [`retry`][__link17]: Automatic retry with input cloning and recovery classification.
* [`retry_advanced`][__link18]: Custom input cloning with attempt metadata injection.
* [`retry_outage`][__link19]: Input restoration from errors when cloning is not possible.
* [`breaker`][__link20]: Circuit breaker that monitors failure rates.
* [`hedging`][__link21]: Hedging slow requests with parallel attempts to reduce tail latency.
* [`fallback`][__link22]: Substitutes default values for invalid outputs.
* [`resilience_pipeline`][__link23]: Composing retry and timeout with metrics.
* [`tower`][__link24]: Tower `ServiceBuilder` integration.
* [`config`][__link25]: Loading settings from a [JSON file][__link26].
* [`chaos_injection`][__link27]: Fault injection with configurable probability.

## Features

This crate provides several optional features that can be enabled in your `Cargo.toml`:

* **`timeout`** - Enables the [`timeout`][__link25] middleware for canceling long-running operations.
* **`retry`** - Enables the [`retry`][__link26] middleware for automatically retrying failed operations with
* **`timeout`** - Enables the [`timeout`][__link28] middleware for canceling long-running operations.
* **`retry`** - Enables the [`retry`][__link29] middleware for automatically retrying failed operations with
configurable backoff strategies, jitter, and recovery classification.
* **`hedging`** - Enables the [`hedging`][__link27] middleware for reducing tail latency via additional
* **`hedging`** - Enables the [`hedging`][__link30] middleware for reducing tail latency via additional
concurrent requests with configurable delay modes.
* **`breaker`** - Enables the [`breaker`][__link28] middleware for preventing cascading failures.
* **`fallback`** - Enables the [`fallback`][__link29] middleware for replacing invalid output with a
* **`breaker`** - Enables the [`breaker`][__link31] middleware for preventing cascading failures.
* **`fallback`** - Enables the [`fallback`][__link32] middleware for replacing invalid output with a
user-defined alternative.
* **`chaos-injection`** - Enables the [`chaos::injection`][__link33] middleware for injecting faults
with a configurable probability.
* **`metrics`** - Exposes the OpenTelemetry metrics API for collecting and reporting metrics.
* **`logs`** - Enables structured logging for resilience middleware using the `tracing` crate.
* **`serde`** - Enables `serde::Serialize` and `serde::Deserialize` implementations for
configuration types.
* **`tower-service`** - Enables [`tower_service::Service`][__link30] trait implementations for all
* **`tower-service`** - Enables [`tower_service::Service`][__link34] trait implementations for all
resilience middleware.


Expand All @@ -182,35 +193,39 @@ This crate provides several optional features that can be enabled in your `Cargo
This crate was developed as part of <a href="../..">The Oxidizer Project</a>. Browse this crate's <a href="https://github.com/microsoft/oxidizer/tree/main/crates/seatbelt">source code</a>.
</sub>

[__cargo_doc2readme_dependencies_info]: ggGkYW0CYXSEGy4k8ldDFPOhG2VNeXtD5nnKG6EPY6OfW5wBG8g18NOFNdxpYXKEG4STOoP2_1kjG_YFfNmBbFbQG7vz1CXQ1d10GxetSJWWkufEYWSFgmdsYXllcmVkZTAuMy4wgmtyZWNvdmVyYWJsZWUwLjEuMYJoc2VhdGJlbHRlMC40LjKCZHRpY2tlMC4yLjGCbXRvd2VyX3NlcnZpY2VlMC4zLjM
[__cargo_doc2readme_dependencies_info]: ggGkYW0CYXSEGy4k8ldDFPOhG2VNeXtD5nnKG6EPY6OfW5wBG8g18NOFNdxpYXKEG6EJkGIuyI_QG4MIJOv2f7HEG5KAUP5RTsAAG-ts5sYIlPpgYWSFgmdsYXllcmVkZTAuMy4wgmtyZWNvdmVyYWJsZWUwLjEuMYJoc2VhdGJlbHRlMC40LjOCZHRpY2tlMC4yLjGCbXRvd2VyX3NlcnZpY2VlMC4zLjM
[__link0]: https://crates.io/crates/layered/0.3.0
[__link1]: https://docs.rs/layered/0.3.0/layered/?search=Stack
[__link10]: https://docs.rs/seatbelt/0.4.2/seatbelt/hedging/index.html
[__link11]: https://docs.rs/seatbelt/0.4.2/seatbelt/breaker/index.html
[__link12]: https://docs.rs/seatbelt/0.4.2/seatbelt/fallback/index.html
[__link13]: https://github.com/microsoft/oxidizer/blob/main/crates/seatbelt/examples/timeout.rs
[__link14]: https://github.com/microsoft/oxidizer/blob/main/crates/seatbelt/examples/timeout_advanced.rs
[__link15]: https://github.com/microsoft/oxidizer/blob/main/crates/seatbelt/examples/retry.rs
[__link16]: https://github.com/microsoft/oxidizer/blob/main/crates/seatbelt/examples/retry_advanced.rs
[__link17]: https://github.com/microsoft/oxidizer/blob/main/crates/seatbelt/examples/retry_outage.rs
[__link18]: https://github.com/microsoft/oxidizer/blob/main/crates/seatbelt/examples/breaker.rs
[__link19]: https://github.com/microsoft/oxidizer/blob/main/crates/seatbelt/examples/hedging.rs
[__link10]: https://docs.rs/seatbelt/0.4.3/seatbelt/hedging/index.html
[__link11]: https://docs.rs/seatbelt/0.4.3/seatbelt/breaker/index.html
[__link12]: https://docs.rs/seatbelt/0.4.3/seatbelt/fallback/index.html
[__link13]: https://docs.rs/seatbelt/0.4.3/seatbelt/chaos/index.html
[__link14]: https://docs.rs/seatbelt/0.4.3/seatbelt/?search=chaos::injection
[__link15]: https://github.com/microsoft/oxidizer/blob/main/crates/seatbelt/examples/timeout.rs
[__link16]: https://github.com/microsoft/oxidizer/blob/main/crates/seatbelt/examples/timeout_advanced.rs
[__link17]: https://github.com/microsoft/oxidizer/blob/main/crates/seatbelt/examples/retry.rs
[__link18]: https://github.com/microsoft/oxidizer/blob/main/crates/seatbelt/examples/retry_advanced.rs
[__link19]: https://github.com/microsoft/oxidizer/blob/main/crates/seatbelt/examples/retry_outage.rs
[__link2]: https://docs.rs/tick/0.2.1/tick/?search=Clock
[__link20]: https://github.com/microsoft/oxidizer/blob/main/crates/seatbelt/examples/fallback.rs
[__link21]: https://github.com/microsoft/oxidizer/blob/main/crates/seatbelt/examples/resilience_pipeline.rs
[__link22]: https://github.com/microsoft/oxidizer/blob/main/crates/seatbelt/examples/tower.rs
[__link23]: https://github.com/microsoft/oxidizer/blob/main/crates/seatbelt/examples/config.rs
[__link24]: https://github.com/microsoft/oxidizer/blob/main/crates/seatbelt/examples/config.json
[__link25]: https://docs.rs/seatbelt/0.4.2/seatbelt/timeout/index.html
[__link26]: https://docs.rs/seatbelt/0.4.2/seatbelt/retry/index.html
[__link27]: https://docs.rs/seatbelt/0.4.2/seatbelt/hedging/index.html
[__link28]: https://docs.rs/seatbelt/0.4.2/seatbelt/breaker/index.html
[__link29]: https://docs.rs/seatbelt/0.4.2/seatbelt/fallback/index.html
[__link20]: https://github.com/microsoft/oxidizer/blob/main/crates/seatbelt/examples/breaker.rs
[__link21]: https://github.com/microsoft/oxidizer/blob/main/crates/seatbelt/examples/hedging.rs
[__link22]: https://github.com/microsoft/oxidizer/blob/main/crates/seatbelt/examples/fallback.rs
[__link23]: https://github.com/microsoft/oxidizer/blob/main/crates/seatbelt/examples/resilience_pipeline.rs
[__link24]: https://github.com/microsoft/oxidizer/blob/main/crates/seatbelt/examples/tower.rs
[__link25]: https://github.com/microsoft/oxidizer/blob/main/crates/seatbelt/examples/config.rs
[__link26]: https://github.com/microsoft/oxidizer/blob/main/crates/seatbelt/examples/config.json
[__link27]: https://github.com/microsoft/oxidizer/blob/main/crates/seatbelt/examples/chaos_injection.rs
[__link28]: https://docs.rs/seatbelt/0.4.3/seatbelt/timeout/index.html
[__link29]: https://docs.rs/seatbelt/0.4.3/seatbelt/retry/index.html
[__link3]: https://crates.io/crates/tick/0.2.1
[__link30]: https://docs.rs/tower_service/0.3.3/tower_service/?search=Service
[__link4]: https://docs.rs/seatbelt/0.4.2/seatbelt/?search=ResilienceContext
[__link5]: https://docs.rs/seatbelt/0.4.2/seatbelt/?search=ResilienceContext
[__link30]: https://docs.rs/seatbelt/0.4.3/seatbelt/hedging/index.html
[__link31]: https://docs.rs/seatbelt/0.4.3/seatbelt/breaker/index.html
[__link32]: https://docs.rs/seatbelt/0.4.3/seatbelt/fallback/index.html
[__link33]: https://docs.rs/seatbelt/0.4.3/seatbelt/?search=chaos::injection
[__link34]: https://docs.rs/tower_service/0.3.3/tower_service/?search=Service
[__link4]: https://docs.rs/seatbelt/0.4.3/seatbelt/?search=ResilienceContext
[__link5]: https://docs.rs/seatbelt/0.4.3/seatbelt/?search=ResilienceContext
[__link6]: https://docs.rs/recoverable/0.1.1/recoverable/?search=RecoveryInfo
[__link7]: https://docs.rs/recoverable/0.1.1/recoverable/?search=Recovery
[__link8]: https://docs.rs/seatbelt/0.4.2/seatbelt/timeout/index.html
[__link9]: https://docs.rs/seatbelt/0.4.2/seatbelt/retry/index.html
[__link8]: https://docs.rs/seatbelt/0.4.3/seatbelt/timeout/index.html
[__link9]: https://docs.rs/seatbelt/0.4.3/seatbelt/retry/index.html
1 change: 1 addition & 0 deletions crates/seatbelt/examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,5 @@ Examples covering each middleware and common composition patterns:
- [`fallback`](https://github.com/microsoft/oxidizer/blob/main/crates/seatbelt/examples/fallback.rs): Substitutes default values for invalid outputs.
- [`resilience_pipeline`](https://github.com/microsoft/oxidizer/blob/main/crates/seatbelt/examples/resilience_pipeline.rs): Composing retry and timeout with metrics.
- [`tower`](https://github.com/microsoft/oxidizer/blob/main/crates/seatbelt/examples/tower.rs): Tower `ServiceBuilder` integration.
- [`chaos_injection`](https://github.com/microsoft/oxidizer/blob/main/crates/seatbelt/examples/chaos_injection.rs): Injecting faults with a configurable probability to test resilience.
- [`config`](https://github.com/microsoft/oxidizer/blob/main/crates/seatbelt/examples/config.rs): Loading settings from a [JSON file](https://github.com/microsoft/oxidizer/blob/main/crates/seatbelt/examples/config.json).
41 changes: 41 additions & 0 deletions crates/seatbelt/examples/chaos_injection.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

//! Chaos injection middleware example that injects faults with a configurable probability.
//!
//! This example simulates a service where 30% of requests are intercepted and
//! replaced with an injected error output, demonstrating how chaos injection can
//! be used to test resilience under failure conditions.

use layered::{Execute, Service, Stack};
use seatbelt::ResilienceContext;
use seatbelt::chaos::injection::Injection;
use tick::Clock;

#[tokio::main]
async fn main() {
let clock = Clock::new_tokio();
let context = ResilienceContext::new(&clock);

// Define stack with injection layer
let stack = (
Injection::layer("my_injection", &context)
// Required: probability of injection (30%)
.rate(0.3)
// Required: the output to inject (receives the consumed input)
.output_with(|input: String, _args| format!("INJECTED_FAULT for '{input}'")),
Execute::new(execute_operation),
);

// Create the service from the stack
let service = stack.into_service();

for i in 0..10 {
let result = service.execute(format!("request-{i}")).await;
println!("{i}: result = '{result}'");
}
}

async fn execute_operation(input: String) -> String {
format!("processed:{input}")
}
14 changes: 14 additions & 0 deletions crates/seatbelt/src/chaos/injection/args.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

/// Arguments passed to the [`output_with`][super::InjectionLayer::output_with] callback.
///
/// This type is `#[non_exhaustive]` so that additional fields can be added in the
/// future without a breaking change.
#[derive(Debug)]
#[non_exhaustive]
#[expect(
clippy::empty_structs_with_brackets,
reason = "non_exhaustive requires braces for forward-compatibility"
)]
pub struct InjectionOutputArgs {}
6 changes: 6 additions & 0 deletions crates/seatbelt/src/chaos/injection/callbacks.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

use super::InjectionOutputArgs;

crate::utils::define_fn_wrapper!(InjectionOutput<In, Out>(Fn(In, InjectionOutputArgs) -> Out));
68 changes: 68 additions & 0 deletions crates/seatbelt/src/chaos/injection/config.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

/// Default injection rate (no injection).
const DEFAULT_RATE: f64 = 0.0;

/// Configuration for the [`Injection`][super::Injection] middleware.
///
/// This type can be deserialized from configuration files when the `serde`
/// feature is enabled.
///
/// # Example
///
/// ```rust
/// use seatbelt::chaos::injection::InjectionConfig;
///
/// let config = InjectionConfig::default();
/// assert!(config.enabled);
/// assert_eq!(config.rate, 0.0);
/// ```
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(any(feature = "serde", test), derive(serde::Serialize, serde::Deserialize))]
#[non_exhaustive]
pub struct InjectionConfig {
/// Whether the injection middleware is enabled. When `false`, the middleware
/// is bypassed and requests pass through directly to the inner service.
pub enabled: bool,

/// The probability of injecting the configured output instead of calling the
/// inner service. Must be in the range `[0.0, 1.0]` where `0.0` means never
/// inject and `1.0` means always inject.
pub rate: f64,
}

impl Default for InjectionConfig {
fn default() -> Self {
Self {
enabled: true,
rate: DEFAULT_RATE,
}
}
}

#[cfg_attr(coverage_nightly, coverage(off))]
#[cfg(test)]
mod tests {
use super::*;

#[test]
#[cfg_attr(miri, ignore)]
fn default_snapshot() {
let config = InjectionConfig::default();
insta::assert_json_snapshot!(config);
}

#[test]
fn serde_roundtrip() {
let config = InjectionConfig {
enabled: false,
rate: 0.42,
};

let json = serde_json::to_string(&config).unwrap();
let deserialized: InjectionConfig = serde_json::from_str(&json).unwrap();

assert_eq!(config, deserialized);
}
}
Loading
Loading