Skip to content

Commit 3389251

Browse files
martintmkCopilotCopilot
authored
feat(seatbelt): introduce hedging resilience middleware (#298)
## Hedging Middleware Adds a **hedging** resilience middleware to `seatbelt` that reduces tail latency by launching additional concurrent requests when the original is slow. The first acceptable result wins; remaining in-flight requests are cancelled. ### How it works 1. The original request is sent immediately. 2. After a configurable delay, additional hedging attempts launch concurrently. 3. A user-provided recovery callback classifies each result — non-recoverable results are returned immediately. ### Hedging delay | Method | Behavior | |--------|----------| | **`hedging_delay(duration)`** | Waits a fixed duration before each hedging attempt *(default: 500ms)* | | **`hedging_delay(Duration::ZERO)`** | Launches all hedging attempts at once | | **`hedging_delay_with(fn)`** | Computes delay per attempt via callback | ### Configuration Uses a **type-state builder** pattern — `clone_input` and `recovery` are enforced at compile time before the layer can be constructed. ```rust let stack = ( Hedging::layer("my_hedging", &context) .clone_input() .recovery_with(|result, _| match result { Ok(_) => RecoveryInfo::never(), Err(_) => RecoveryInfo::retry(), }) .hedging_delay(Duration::from_secs(1)) .max_hedged_attempts(2), Execute::new(my_operation), ); Runtime configuration is also supported via HedgingConfig (serde-compatible): .config(&HedgingConfig { enabled: true, hedging_delay: Duration::from_millis(500), max_hedged_attempts: 1, }) ``` --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com>
1 parent b2fb322 commit 3389251

27 files changed

+2818
-80
lines changed

.github/workflows/main.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ jobs:
7474
run: cargo sort --check --grouped --workspace
7575
- name: Readmes
7676
if: success() || failure()
77-
run: cargo workspaces exec --ignore-private cargo doc2readme --expand-macros --all-features --check --lib --template ../README.j2
77+
run: cargo workspaces exec --ignore-private cargo doc2readme --check --lib --template ../README.j2
7878
env:
7979
# Disable sccache: macro expansion (rustc -Zunpretty=expanded) does not
8080
# produce the artifact files sccache expects to cache, causing it to fail.

.spelling

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
306
1+
307
22
33
0.X.Y
44
100k
@@ -125,6 +125,7 @@ grey
125125
gRPC
126126
Hardcoded
127127
hashers
128+
hedges
128129
hotfix
129130
hotfixes
130131
How-tos

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/seatbelt/Cargo.toml

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,13 +43,15 @@ timeout = []
4343
retry = ["dep:fastrand"]
4444
breaker = ["dep:fastrand"]
4545
fallback = []
46+
hedging = ["dep:futures-util"]
4647
serde = ["dep:serde", "dep:jiff"]
4748
metrics = ["dep:opentelemetry", "opentelemetry/metrics"]
4849
logs = ["dep:tracing"]
4950
tower-service = ["dep:tower-service"]
5051

5152
[dependencies]
5253
fastrand = { workspace = true, optional = true }
54+
futures-util = { workspace = true, optional = true, features = ["alloc"] }
5355
jiff = { workspace = true, features = ["std", "serde"], optional = true }
5456
layered = { workspace = true }
5557
opentelemetry = { workspace = true, optional = true }
@@ -65,10 +67,11 @@ alloc_tracker.workspace = true
6567
criterion.workspace = true
6668
fastrand.workspace = true
6769
futures = { workspace = true, features = ["executor"] }
70+
futures-util = { workspace = true, features = ["alloc"] }
6871
http.workspace = true
6972
insta = { workspace = true, features = ["json"] }
7073
jiff = { workspace = true, default-features = true, features = ["serde"] }
71-
layered = { workspace = true, features = ["tower-service"] }
74+
layered = { workspace = true, features = ["tower-service", "dynamic-service"] }
7275
mutants.workspace = true
7376
ohno = { workspace = true, features = ["app-err"] }
7477
opentelemetry = { workspace = true, default-features = false, features = ["metrics"] }
@@ -113,6 +116,10 @@ required-features = ["retry", "timeout", "metrics"]
113116
name = "tower"
114117
required-features = ["retry", "timeout", "tower-service"]
115118

119+
[[example]]
120+
name = "hedging"
121+
required-features = ["hedging"]
122+
116123
[[example]]
117124
name = "breaker"
118125
required-features = ["breaker", "metrics"]
@@ -145,5 +152,10 @@ name = "breaker"
145152
harness = false
146153
required-features = ["breaker"]
147154

155+
[[bench]]
156+
name = "hedging"
157+
harness = false
158+
required-features = ["hedging"]
159+
148160
[lints]
149161
workspace = true

crates/seatbelt/README.md

Lines changed: 44 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -89,8 +89,9 @@ for each module for details on how to use them.
8989

9090
* [`timeout`][__link8] - Middleware that cancels long-running operations.
9191
* [`retry`][__link9] - Middleware that automatically retries failed operations.
92-
* [`breaker`][__link10] - Middleware that prevents cascading failures.
93-
* [`fallback`][__link11] - Middleware that replaces invalid output with a user-defined alternative.
92+
* [`hedging`][__link10] - Middleware that reduces tail latency via additional concurrent execution.
93+
* [`breaker`][__link11] - Middleware that prevents cascading failures.
94+
* [`fallback`][__link12] - Middleware that replaces invalid output with a user-defined alternative.
9495

9596
## Middleware Ordering
9697

@@ -142,34 +143,37 @@ let service = ServiceBuilder::new()
142143

143144
## Examples
144145

145-
Runnable examples covering each middleware and common composition patterns:
146+
Examples covering each middleware and common composition patterns:
146147

147-
* [`timeout`][__link12]: Basic timeout that cancels long-running operations.
148-
* [`timeout_advanced`][__link13]: Dynamic timeout durations and timeout callbacks.
149-
* [`retry`][__link14]: Automatic retry with input cloning and recovery classification.
150-
* [`retry_advanced`][__link15]: Custom input cloning with attempt metadata injection.
151-
* [`retry_outage`][__link16]: Input restoration from errors when cloning is not possible.
152-
* [`breaker`][__link17]: Circuit breaker that monitors failure rates.
153-
* [`fallback`][__link18]: Substitutes default values for invalid outputs.
154-
* [`resilience_pipeline`][__link19]: Composing retry and timeout with metrics.
155-
* [`tower`][__link20]: Tower `ServiceBuilder` integration.
156-
* [`config`][__link21]: Loading settings from a [JSON file][__link22].
148+
* [`timeout`][__link13]: Basic timeout that cancels long-running operations.
149+
* [`timeout_advanced`][__link14]: Dynamic timeout duration and timeout callbacks.
150+
* [`retry`][__link15]: Automatic retry with input cloning and recovery classification.
151+
* [`retry_advanced`][__link16]: Custom input cloning with attempt metadata injection.
152+
* [`retry_outage`][__link17]: Input restoration from errors when cloning is not possible.
153+
* [`breaker`][__link18]: Circuit breaker that monitors failure rates.
154+
* [`hedging`][__link19]: Hedging slow requests with parallel attempts to reduce tail latency.
155+
* [`fallback`][__link20]: Substitutes default values for invalid outputs.
156+
* [`resilience_pipeline`][__link21]: Composing retry and timeout with metrics.
157+
* [`tower`][__link22]: Tower `ServiceBuilder` integration.
158+
* [`config`][__link23]: Loading settings from a [JSON file][__link24].
157159

158160
## Features
159161

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

162-
* **`timeout`** - Enables the [`timeout`][__link23] middleware for canceling long-running operations.
163-
* **`retry`** - Enables the [`retry`][__link24] middleware for automatically retrying failed operations with
164+
* **`timeout`** - Enables the [`timeout`][__link25] middleware for canceling long-running operations.
165+
* **`retry`** - Enables the [`retry`][__link26] middleware for automatically retrying failed operations with
164166
configurable backoff strategies, jitter, and recovery classification.
165-
* **`breaker`** - Enables the [`breaker`][__link25] middleware for preventing cascading failures.
166-
* **`fallback`** - Enables the [`fallback`][__link26] middleware for replacing invalid output with a
167+
* **`hedging`** - Enables the [`hedging`][__link27] middleware for reducing tail latency via additional
168+
concurrent requests with configurable delay modes.
169+
* **`breaker`** - Enables the [`breaker`][__link28] middleware for preventing cascading failures.
170+
* **`fallback`** - Enables the [`fallback`][__link29] middleware for replacing invalid output with a
167171
user-defined alternative.
168172
* **`metrics`** - Exposes the OpenTelemetry metrics API for collecting and reporting metrics.
169173
* **`logs`** - Enables structured logging for resilience middleware using the `tracing` crate.
170174
* **`serde`** - Enables `serde::Serialize` and `serde::Deserialize` implementations for
171175
configuration types.
172-
* **`tower-service`** - Enables [`tower_service::Service`][__link27] trait implementations for all
176+
* **`tower-service`** - Enables [`tower_service::Service`][__link30] trait implementations for all
173177
resilience middleware.
174178

175179

@@ -178,29 +182,32 @@ This crate provides several optional features that can be enabled in your `Cargo
178182
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>.
179183
</sub>
180184

181-
[__cargo_doc2readme_dependencies_info]: ggGkYW0CYXSEGy4k8ldDFPOhG2VNeXtD5nnKG6EPY6OfW5wBG8g18NOFNdxpYXKEG-h5X_wYKkOdGzmzySp-X6wWGyOeAMrS0ICyG1NcDWN9F0QhYWSFgmdsYXllcmVkZTAuMy4wgmtyZWNvdmVyYWJsZWUwLjEuMYJoc2VhdGJlbHRlMC4zLjGCZHRpY2tlMC4yLjGCbXRvd2VyX3NlcnZpY2VlMC4zLjM
185+
[__cargo_doc2readme_dependencies_info]: ggGkYW0CYXSEGy4k8ldDFPOhG2VNeXtD5nnKG6EPY6OfW5wBG8g18NOFNdxpYXKEG4STOoP2_1kjG_YFfNmBbFbQG7vz1CXQ1d10GxetSJWWkufEYWSFgmdsYXllcmVkZTAuMy4wgmtyZWNvdmVyYWJsZWUwLjEuMYJoc2VhdGJlbHRlMC4zLjGCZHRpY2tlMC4yLjGCbXRvd2VyX3NlcnZpY2VlMC4zLjM
182186
[__link0]: https://crates.io/crates/layered/0.3.0
183187
[__link1]: https://docs.rs/layered/0.3.0/layered/?search=Stack
184-
[__link10]: https://docs.rs/seatbelt/0.3.1/seatbelt/breaker/index.html
185-
[__link11]: https://docs.rs/seatbelt/0.3.1/seatbelt/fallback/index.html
186-
[__link12]: https://github.com/microsoft/oxidizer/blob/main/crates/seatbelt/examples/timeout.rs
187-
[__link13]: https://github.com/microsoft/oxidizer/blob/main/crates/seatbelt/examples/timeout_advanced.rs
188-
[__link14]: https://github.com/microsoft/oxidizer/blob/main/crates/seatbelt/examples/retry.rs
189-
[__link15]: https://github.com/microsoft/oxidizer/blob/main/crates/seatbelt/examples/retry_advanced.rs
190-
[__link16]: https://github.com/microsoft/oxidizer/blob/main/crates/seatbelt/examples/retry_outage.rs
191-
[__link17]: https://github.com/microsoft/oxidizer/blob/main/crates/seatbelt/examples/breaker.rs
192-
[__link18]: https://github.com/microsoft/oxidizer/blob/main/crates/seatbelt/examples/fallback.rs
193-
[__link19]: https://github.com/microsoft/oxidizer/blob/main/crates/seatbelt/examples/resilience_pipeline.rs
188+
[__link10]: https://docs.rs/seatbelt/0.3.1/seatbelt/hedging/index.html
189+
[__link11]: https://docs.rs/seatbelt/0.3.1/seatbelt/breaker/index.html
190+
[__link12]: https://docs.rs/seatbelt/0.3.1/seatbelt/fallback/index.html
191+
[__link13]: https://github.com/microsoft/oxidizer/blob/main/crates/seatbelt/examples/timeout.rs
192+
[__link14]: https://github.com/microsoft/oxidizer/blob/main/crates/seatbelt/examples/timeout_advanced.rs
193+
[__link15]: https://github.com/microsoft/oxidizer/blob/main/crates/seatbelt/examples/retry.rs
194+
[__link16]: https://github.com/microsoft/oxidizer/blob/main/crates/seatbelt/examples/retry_advanced.rs
195+
[__link17]: https://github.com/microsoft/oxidizer/blob/main/crates/seatbelt/examples/retry_outage.rs
196+
[__link18]: https://github.com/microsoft/oxidizer/blob/main/crates/seatbelt/examples/breaker.rs
197+
[__link19]: https://github.com/microsoft/oxidizer/blob/main/crates/seatbelt/examples/hedging.rs
194198
[__link2]: https://docs.rs/tick/0.2.1/tick/?search=Clock
195-
[__link20]: https://github.com/microsoft/oxidizer/blob/main/crates/seatbelt/examples/tower.rs
196-
[__link21]: https://github.com/microsoft/oxidizer/blob/main/crates/seatbelt/examples/config.rs
197-
[__link22]: https://github.com/microsoft/oxidizer/blob/main/crates/seatbelt/examples/config.json
198-
[__link23]: https://docs.rs/seatbelt/0.3.1/seatbelt/timeout/index.html
199-
[__link24]: https://docs.rs/seatbelt/0.3.1/seatbelt/retry/index.html
200-
[__link25]: https://docs.rs/seatbelt/0.3.1/seatbelt/breaker/index.html
201-
[__link26]: https://docs.rs/seatbelt/0.3.1/seatbelt/fallback/index.html
202-
[__link27]: https://docs.rs/tower_service/0.3.3/tower_service/?search=Service
199+
[__link20]: https://github.com/microsoft/oxidizer/blob/main/crates/seatbelt/examples/fallback.rs
200+
[__link21]: https://github.com/microsoft/oxidizer/blob/main/crates/seatbelt/examples/resilience_pipeline.rs
201+
[__link22]: https://github.com/microsoft/oxidizer/blob/main/crates/seatbelt/examples/tower.rs
202+
[__link23]: https://github.com/microsoft/oxidizer/blob/main/crates/seatbelt/examples/config.rs
203+
[__link24]: https://github.com/microsoft/oxidizer/blob/main/crates/seatbelt/examples/config.json
204+
[__link25]: https://docs.rs/seatbelt/0.3.1/seatbelt/timeout/index.html
205+
[__link26]: https://docs.rs/seatbelt/0.3.1/seatbelt/retry/index.html
206+
[__link27]: https://docs.rs/seatbelt/0.3.1/seatbelt/hedging/index.html
207+
[__link28]: https://docs.rs/seatbelt/0.3.1/seatbelt/breaker/index.html
208+
[__link29]: https://docs.rs/seatbelt/0.3.1/seatbelt/fallback/index.html
203209
[__link3]: https://crates.io/crates/tick/0.2.1
210+
[__link30]: https://docs.rs/tower_service/0.3.3/tower_service/?search=Service
204211
[__link4]: https://docs.rs/seatbelt/0.3.1/seatbelt/?search=ResilienceContext
205212
[__link5]: https://docs.rs/seatbelt/0.3.1/seatbelt/?search=ResilienceContext
206213
[__link6]: https://docs.rs/recoverable/0.1.1/recoverable/?search=RecoveryInfo

crates/seatbelt/benches/hedging.rs

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
#![expect(missing_docs, reason = "benchmark code")]
4+
5+
use alloc_tracker::{Allocator, Session};
6+
use criterion::{Criterion, criterion_group, criterion_main};
7+
use futures::executor::block_on;
8+
use layered::{Execute, Service, Stack};
9+
use seatbelt::hedging::Hedging;
10+
use seatbelt::{RecoveryInfo, ResilienceContext};
11+
use tick::Clock;
12+
13+
#[global_allocator]
14+
static ALLOCATOR: Allocator<std::alloc::System> = Allocator::system();
15+
16+
fn entry(c: &mut Criterion) {
17+
let mut group = c.benchmark_group("hedging");
18+
let session = Session::new();
19+
20+
// No hedging (baseline)
21+
let service = Execute::new(|v: Input| async move { Output::from(v) });
22+
let operation = session.operation("no-hedging");
23+
group.bench_function("no-hedging", |b| {
24+
b.iter(|| {
25+
let _span = operation.measure_thread();
26+
_ = block_on(service.execute(Input));
27+
});
28+
});
29+
30+
// With hedging (delay, no recovery needed)
31+
let context = ResilienceContext::new(Clock::new_frozen());
32+
33+
let service = (
34+
Hedging::layer("bench", &context)
35+
.clone_input()
36+
.recovery_with(|_, _| RecoveryInfo::never()),
37+
Execute::new(|v: Input| async move { Output::from(v) }),
38+
)
39+
.into_service();
40+
41+
let operation = session.operation("with-hedging-delay");
42+
group.bench_function("with-hedging-delay", |b| {
43+
b.iter(|| {
44+
let _span = operation.measure_thread();
45+
_ = block_on(service.execute(Input));
46+
});
47+
});
48+
49+
// With hedging disabled (max_hedged_attempts = 0)
50+
let context = ResilienceContext::new(Clock::new_frozen());
51+
52+
let service = (
53+
Hedging::layer("bench", &context)
54+
.clone_input()
55+
.recovery_with(|_, _| RecoveryInfo::never())
56+
.max_hedged_attempts(0),
57+
Execute::new(|v: Input| async move { Output::from(v) }),
58+
)
59+
.into_service();
60+
61+
let operation = session.operation("with-hedging-passthrough");
62+
group.bench_function("with-hedging-passthrough", |b| {
63+
b.iter(|| {
64+
let _span = operation.measure_thread();
65+
_ = block_on(service.execute(Input));
66+
});
67+
});
68+
69+
group.finish();
70+
session.print_to_stdout();
71+
}
72+
73+
criterion_group!(benches, entry);
74+
criterion_main!(benches);
75+
76+
#[derive(Debug, Clone)]
77+
struct Input;
78+
79+
#[derive(Debug, Clone)]
80+
struct Output;
81+
82+
impl From<Input> for Output {
83+
fn from(_input: Input) -> Self {
84+
Self
85+
}
86+
}

crates/seatbelt/examples/AGENTS.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,6 @@
22

33
When adding a new example, update [`README.md`](README.md) with a link to the new file and a
44
brief one-line description. Keep the list in the same order: timeout, retry, breaker, fallback,
5-
combined pipelines, then configuration.
5+
combined pipelines, then configuration.
6+
7+
Also synchronize the `Examples` section in the [lib.rs](../lib.rs) documentation.

crates/seatbelt/examples/README.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
# Examples
22

3-
Runnable examples covering each middleware and common composition patterns:
3+
Examples covering each middleware and common composition patterns:
44

55
- [`timeout`](https://github.com/microsoft/oxidizer/blob/main/crates/seatbelt/examples/timeout.rs): Basic timeout that cancels long-running operations.
6-
- [`timeout_advanced`](https://github.com/microsoft/oxidizer/blob/main/crates/seatbelt/examples/timeout_advanced.rs): Dynamic timeout durations and timeout callbacks.
6+
- [`timeout_advanced`](https://github.com/microsoft/oxidizer/blob/main/crates/seatbelt/examples/timeout_advanced.rs): Dynamic timeout duration and timeout callbacks.
77
- [`retry`](https://github.com/microsoft/oxidizer/blob/main/crates/seatbelt/examples/retry.rs): Automatic retry with input cloning and recovery classification.
88
- [`retry_advanced`](https://github.com/microsoft/oxidizer/blob/main/crates/seatbelt/examples/retry_advanced.rs): Custom input cloning with attempt metadata injection.
99
- [`retry_outage`](https://github.com/microsoft/oxidizer/blob/main/crates/seatbelt/examples/retry_outage.rs): Input restoration from errors when cloning is not possible.
1010
- [`breaker`](https://github.com/microsoft/oxidizer/blob/main/crates/seatbelt/examples/breaker.rs): Circuit breaker that monitors failure rates.
11+
- [`hedging`](https://github.com/microsoft/oxidizer/blob/main/crates/seatbelt/examples/hedging.rs): Hedging slow requests with parallel attempts to reduce tail latency.
1112
- [`fallback`](https://github.com/microsoft/oxidizer/blob/main/crates/seatbelt/examples/fallback.rs): Substitutes default values for invalid outputs.
1213
- [`resilience_pipeline`](https://github.com/microsoft/oxidizer/blob/main/crates/seatbelt/examples/resilience_pipeline.rs): Composing retry and timeout with metrics.
1314
- [`tower`](https://github.com/microsoft/oxidizer/blob/main/crates/seatbelt/examples/tower.rs): Tower `ServiceBuilder` integration.
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
//! Hedging middleware example demonstrating how a slow primary request is hedged
5+
//! with a faster secondary request that completes first.
6+
7+
use std::sync::atomic::{AtomicU32, Ordering};
8+
use std::time::Duration;
9+
10+
use layered::{Execute, Service, Stack};
11+
use seatbelt::hedging::Hedging;
12+
use seatbelt::{RecoveryInfo, ResilienceContext};
13+
use tick::Clock;
14+
use tracing_subscriber::layer::SubscriberExt;
15+
use tracing_subscriber::util::SubscriberInitExt;
16+
17+
static CALL_COUNT: AtomicU32 = AtomicU32::new(0);
18+
19+
#[tokio::main]
20+
async fn main() {
21+
// Set up tracing subscriber for logs to console
22+
tracing_subscriber::registry().with(tracing_subscriber::fmt::layer()).init();
23+
24+
let clock = Clock::new_tokio();
25+
let context = ResilienceContext::new(&clock).use_logs();
26+
27+
let op_clock = clock.clone();
28+
29+
// Configure hedging: if the original request hasn't completed after 200ms,
30+
// launch a hedging request. The first successful response wins.
31+
let stack = (
32+
Hedging::layer("my_hedging", &context)
33+
.clone_input()
34+
.max_hedged_attempts(4)
35+
.recovery_with(|_output: &String, _args| RecoveryInfo::never())
36+
.hedging_delay(Duration::from_millis(200))
37+
.on_execute(|_input, args| {
38+
println!(
39+
"[execute] launching attempt {} (last: {})",
40+
args.attempt().index(),
41+
args.attempt().is_last()
42+
);
43+
}),
44+
Execute::new(move |input: String| {
45+
let clock = op_clock.clone();
46+
async move { slow_then_fast_operation(input, &clock).await }
47+
}),
48+
);
49+
50+
let service = stack.into_service();
51+
52+
println!("[main] sending request...");
53+
let start = std::time::Instant::now();
54+
55+
let output = service.execute("hello".to_string()).await;
56+
println!("[main] result: {output} (took {:?})", start.elapsed());
57+
}
58+
59+
/// Simulates a service where the first two calls are slow (500ms) and subsequent
60+
/// calls (the second hedging attempt onwards) are fast (50ms). The fast hedging
61+
/// attempt completes before the slow ones, demonstrating how hedging reduces tail latency.
62+
async fn slow_then_fast_operation(input: String, clock: &Clock) -> String {
63+
let call = CALL_COUNT.fetch_add(1, Ordering::Relaxed);
64+
65+
if call < 2 {
66+
// Original request and first hedge: simulate a slow response
67+
println!("[service] attempt {call}: slow path (500ms)");
68+
clock.delay(Duration::from_millis(500)).await;
69+
format!("{input} - slow response")
70+
} else {
71+
// Second hedging request: simulate a fast response
72+
println!("[service] attempt {call}: fast path (50ms)");
73+
clock.delay(Duration::from_millis(50)).await;
74+
format!("{input} - fast response")
75+
}
76+
}

0 commit comments

Comments
 (0)