1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
//! Hedged requests: cut tail latency by racing a second attempt.
//!
//! Start an operation. If it has not answered after `delay`, start a second
//! copy and take whichever answers first. The slower copy is dropped, which
//! cancels it. This is the "hedged request" from Dean and Barroso,
//! *The Tail at Scale*. Pick `delay` near the p95 of the call, and only about
//! 5 % of calls send a second copy while the slowest ones get a fast second
//! chance.
//!
//! Only hedge calls that are safe to run twice (reads, or writes that carry an
//! idempotency key). The `allow` hook runs before each extra copy, so a
//! retry budget or a load check can veto it
//! and keep hedging from adding load to an upstream that is already struggling.
//!
//! Time comes from `tokio::time`, so a paused test clock or a
//! [`Sim`](crate::sim::Sim) replays hedges exactly.
//!
//! ```
//! use std::time::Duration;
//! use autumn_web::hedge::{HedgePolicy, hedged};
//!
//! # async fn demo() {
//! let policy = HedgePolicy::new(Duration::from_millis(50));
//! let body = hedged(policy, || true, |_attempt| async { "ok" }).await;
//! assert_eq!(body, "ok");
//! # }
//! ```
use std::future::Future;
use std::time::Duration;
use futures::StreamExt as _;
use futures::stream::FuturesUnordered;
/// When and how often to send extra copies of a call.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct HedgePolicy {
delay: Duration,
max_hedges: u32,
}
impl HedgePolicy {
/// One extra copy, sent after `delay` of silence.
#[must_use]
pub const fn new(delay: Duration) -> Self {
Self {
delay,
max_hedges: 1,
}
}
/// Allow up to `max_hedges` extra copies. Each one waits a further `delay`
/// after the last. `0` turns hedging off.
#[must_use]
pub const fn with_max_hedges(mut self, max_hedges: u32) -> Self {
self.max_hedges = max_hedges;
self
}
/// The silence before an extra copy starts.
#[must_use]
pub const fn delay(&self) -> Duration {
self.delay
}
/// The most extra copies one call may start.
#[must_use]
pub const fn max_hedges(&self) -> u32 {
self.max_hedges
}
}
/// Run `make(0)`; while nothing has answered, start `make(1)`, `make(2)`, …
/// (one per `policy.delay()`, up to `policy.max_hedges()`), and return the
/// first output. Every copy still in flight is dropped.
///
/// `allow` is asked before each extra copy. When it says no, hedging stops for
/// this call and the copies already running carry on.
pub async fn hedged<T, Fut>(
policy: HedgePolicy,
mut allow: impl FnMut() -> bool,
mut make: impl FnMut(u32) -> Fut,
) -> T
where
Fut: Future<Output = T>,
{
let mut in_flight = FuturesUnordered::new();
in_flight.push(make(0));
let mut started = 0;
while started < policy.max_hedges {
tokio::select! {
out = in_flight.next() => {
// `in_flight` is never empty here: it holds the first copy.
if let Some(out) = out {
return out;
}
}
() = tokio::time::sleep(policy.delay) => {
if allow() {
started += 1;
in_flight.push(make(started));
} else {
break;
}
}
}
}
loop {
if let Some(out) = in_flight.next().await {
return out;
}
}
}
#[cfg(test)]
mod tests {
use std::sync::Arc;
use std::sync::atomic::{AtomicU32, Ordering};
use super::*;
const MS: fn(u64) -> Duration = Duration::from_millis;
#[tokio::test(start_paused = true)]
async fn fast_first_copy_never_hedges() {
let made = Arc::new(AtomicU32::new(0));
let m = made.clone();
let out = hedged(
HedgePolicy::new(MS(50)),
|| true,
move |n| {
m.fetch_add(1, Ordering::SeqCst);
async move {
tokio::time::sleep(MS(10)).await;
n
}
},
)
.await;
assert_eq!(out, 0);
assert_eq!(made.load(Ordering::SeqCst), 1);
}
#[tokio::test(start_paused = true)]
async fn slow_first_copy_loses_to_the_hedge() {
let start = tokio::time::Instant::now();
let out = hedged(
HedgePolicy::new(MS(50)),
|| true,
|n| async move {
// The first copy takes 1 s; the hedge takes 10 ms.
tokio::time::sleep(if n == 0 { MS(1_000) } else { MS(10) }).await;
n
},
)
.await;
assert_eq!(out, 1);
assert_eq!(start.elapsed(), MS(60));
}
#[tokio::test(start_paused = true)]
async fn denied_hedge_waits_for_the_first_copy() {
let made = Arc::new(AtomicU32::new(0));
let m = made.clone();
let out = hedged(
HedgePolicy::new(MS(50)),
|| false,
move |n| {
m.fetch_add(1, Ordering::SeqCst);
async move {
tokio::time::sleep(MS(500)).await;
n
}
},
)
.await;
assert_eq!(out, 0);
assert_eq!(made.load(Ordering::SeqCst), 1);
}
#[tokio::test(start_paused = true)]
async fn hedges_are_capped_and_spaced_by_delay() {
let made = Arc::new(AtomicU32::new(0));
let m = made.clone();
let start = tokio::time::Instant::now();
let out = hedged(
HedgePolicy::new(MS(10)).with_max_hedges(2),
|| true,
move |n| {
m.fetch_add(1, Ordering::SeqCst);
async move {
// Only the last copy is quick.
tokio::time::sleep(if n == 2 { MS(5) } else { MS(10_000) }).await;
n
}
},
)
.await;
assert_eq!(out, 2);
assert_eq!(made.load(Ordering::SeqCst), 3);
assert_eq!(start.elapsed(), MS(25));
}
#[tokio::test(start_paused = true)]
async fn zero_max_hedges_is_a_plain_call() {
let out = hedged(
HedgePolicy::new(MS(1)).with_max_hedges(0),
|| panic!("must not ask"),
|n| async move {
tokio::time::sleep(MS(100)).await;
n
},
)
.await;
assert_eq!(out, 0);
}
#[tokio::test(start_paused = true)]
async fn losing_copy_is_dropped() {
struct Flag(Arc<AtomicU32>);
impl Drop for Flag {
fn drop(&mut self) {
self.0.fetch_add(1, Ordering::SeqCst);
}
}
let dropped = Arc::new(AtomicU32::new(0));
let d = dropped.clone();
let out = hedged(
HedgePolicy::new(MS(5)),
|| true,
move |n| {
let guard = Flag(d.clone());
async move {
let _guard = guard;
tokio::time::sleep(if n == 0 { MS(1_000) } else { MS(1) }).await;
n
}
},
)
.await;
assert_eq!(out, 1);
assert_eq!(dropped.load(Ordering::SeqCst), 2);
}
}