throttle_net/backoff.rs
1//! Backoff strategies and their jitter variants.
2//!
3//! A [`Backoff`] turns an attempt number into a delay. The base curve is
4//! constant, linear, or exponential; on top of it sits a [`Jitter`] mode that
5//! spreads retries so a fleet of clients that failed together does not retry in
6//! lockstep (the thundering herd). [`Jitter::Decorrelated`] is the recommended
7//! default and the one the [`Backoff::default`] uses.
8//!
9//! A backoff is a *policy*; call [`iter`](Backoff::iter) to get a
10//! [`BackoffIter`] that yields one delay per attempt. The iterator carries the
11//! state the jittered curves need (the previous delay, the random generator), so
12//! a single policy can drive many independent retry loops.
13
14use core::time::Duration;
15
16/// The default ceiling a backoff delay is clamped to: 30 seconds.
17const DEFAULT_MAX: Duration = Duration::from_secs(30);
18/// The default exponential base delay: 100 milliseconds.
19const DEFAULT_BASE: Duration = Duration::from_millis(100);
20/// The default exponential growth factor.
21const DEFAULT_FACTOR: f64 = 2.0;
22
23/// How retry delays are randomized to avoid synchronized retries.
24///
25/// The non-`None` modes follow the taxonomy from the AWS Architecture Blog's
26/// "Exponential Backoff And Jitter": full, equal, and decorrelated.
27///
28/// `#[non_exhaustive]`: more modes may be added, so a `match` needs a wildcard.
29#[non_exhaustive]
30#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
31pub enum Jitter {
32 /// No randomization: the delay is exactly the (capped) base curve.
33 None,
34 /// Uniform in `[0, delay]`. Maximum spread; a retry can fire almost
35 /// immediately.
36 Full,
37 /// Half the delay plus a uniform half: `delay/2 + rand(0, delay/2)`. Keeps a
38 /// floor under each wait while still spreading.
39 Equal,
40 /// `min(max, rand(base, previous * 3))`. Self-correlated growth that adapts to
41 /// observed waits; the strongest at breaking up a thundering herd, and the
42 /// default.
43 #[default]
44 Decorrelated,
45}
46
47/// The base delay curve.
48#[derive(Debug, Clone, Copy, PartialEq, Eq)]
49enum Kind {
50 Constant,
51 Linear,
52 Exponential,
53}
54
55/// A backoff policy: a base curve plus a jitter mode and a delay ceiling.
56///
57/// Construct with [`constant`](Self::constant), [`linear`](Self::linear), or
58/// [`exponential`](Self::exponential), then tune with
59/// [`with_max`](Self::with_max) and [`with_jitter`](Self::with_jitter). It is
60/// usable on its own — pair it with [`Retry`](crate::Retry), or call
61/// [`iter`](Self::iter) and drive your own loop.
62///
63/// # Examples
64///
65/// ```
66/// use std::time::Duration;
67/// use throttle_net::{Backoff, Jitter};
68///
69/// // Exponential from 50ms, doubling, capped at 5s, with full jitter.
70/// let backoff = Backoff::exponential(Duration::from_millis(50), 2.0)
71/// .with_max(Duration::from_secs(5))
72/// .with_jitter(Jitter::Full);
73///
74/// let mut delays = backoff.iter();
75/// let first = delays.next_delay();
76/// assert!(first <= Duration::from_millis(50)); // full jitter: in [0, 50ms]
77/// ```
78#[derive(Debug, Clone, Copy, PartialEq)]
79pub struct Backoff {
80 kind: Kind,
81 base: Duration,
82 factor: f64,
83 increment: Duration,
84 max: Duration,
85 jitter: Jitter,
86}
87
88impl Backoff {
89 /// A fixed delay on every attempt, with no jitter.
90 ///
91 /// # Examples
92 ///
93 /// ```
94 /// use std::time::Duration;
95 /// use throttle_net::Backoff;
96 ///
97 /// let mut delays = Backoff::constant(Duration::from_millis(200)).iter();
98 /// assert_eq!(delays.next_delay(), Duration::from_millis(200));
99 /// assert_eq!(delays.next_delay(), Duration::from_millis(200));
100 /// ```
101 #[must_use]
102 pub fn constant(delay: Duration) -> Self {
103 Self {
104 kind: Kind::Constant,
105 base: delay,
106 factor: DEFAULT_FACTOR,
107 increment: Duration::ZERO,
108 max: delay,
109 jitter: Jitter::None,
110 }
111 }
112
113 /// A delay that grows by `increment` each attempt: `initial`, `initial +
114 /// increment`, `initial + 2*increment`, … capped at the maximum.
115 ///
116 /// # Examples
117 ///
118 /// ```
119 /// use std::time::Duration;
120 /// use throttle_net::Backoff;
121 ///
122 /// let mut delays = Backoff::linear(Duration::from_millis(100), Duration::from_millis(100)).iter();
123 /// assert_eq!(delays.next_delay(), Duration::from_millis(100));
124 /// assert_eq!(delays.next_delay(), Duration::from_millis(200));
125 /// assert_eq!(delays.next_delay(), Duration::from_millis(300));
126 /// ```
127 #[must_use]
128 pub fn linear(initial: Duration, increment: Duration) -> Self {
129 Self {
130 kind: Kind::Linear,
131 base: initial,
132 factor: DEFAULT_FACTOR,
133 increment,
134 max: DEFAULT_MAX,
135 jitter: Jitter::None,
136 }
137 }
138
139 /// A delay that multiplies by `factor` each attempt: `initial`, `initial *
140 /// factor`, `initial * factor^2`, … capped at the maximum.
141 ///
142 /// A `factor` of `2.0` doubles each time. Non-finite or sub-one factors are
143 /// accepted but make the curve flat or shrinking; pair with jitter for the
144 /// usual behavior.
145 ///
146 /// # Examples
147 ///
148 /// ```
149 /// use std::time::Duration;
150 /// use throttle_net::Backoff;
151 ///
152 /// let mut delays = Backoff::exponential(Duration::from_millis(100), 2.0).iter();
153 /// assert_eq!(delays.next_delay(), Duration::from_millis(100));
154 /// assert_eq!(delays.next_delay(), Duration::from_millis(200));
155 /// assert_eq!(delays.next_delay(), Duration::from_millis(400));
156 /// ```
157 #[must_use]
158 pub fn exponential(initial: Duration, factor: f64) -> Self {
159 Self {
160 kind: Kind::Exponential,
161 base: initial,
162 factor,
163 increment: Duration::ZERO,
164 max: DEFAULT_MAX,
165 jitter: Jitter::None,
166 }
167 }
168
169 /// Returns a copy with the delay ceiling set to `max`. No delay this backoff
170 /// produces will exceed it.
171 #[must_use]
172 pub fn with_max(mut self, max: Duration) -> Self {
173 self.max = max;
174 self
175 }
176
177 /// Returns a copy with the jitter mode set.
178 #[must_use]
179 pub fn with_jitter(mut self, jitter: Jitter) -> Self {
180 self.jitter = jitter;
181 self
182 }
183
184 /// The configured delay ceiling.
185 #[must_use]
186 pub const fn max(&self) -> Duration {
187 self.max
188 }
189
190 /// The configured jitter mode.
191 #[must_use]
192 pub const fn jitter(&self) -> Jitter {
193 self.jitter
194 }
195
196 /// Starts a delay sequence, seeded from system entropy.
197 ///
198 /// Each call returns an independent [`BackoffIter`] with its own random
199 /// state, so two retry loops sharing one policy still jitter independently.
200 #[must_use]
201 pub fn iter(&self) -> BackoffIter {
202 self.iter_seeded(entropy_seed())
203 }
204
205 /// Starts a delay sequence with an explicit seed, for deterministic,
206 /// reproducible tests.
207 ///
208 /// # Examples
209 ///
210 /// ```
211 /// use std::time::Duration;
212 /// use throttle_net::{Backoff, Jitter};
213 ///
214 /// let backoff = Backoff::exponential(Duration::from_millis(10), 2.0)
215 /// .with_jitter(Jitter::Full);
216 /// // The same seed always yields the same sequence.
217 /// let a: Vec<_> = (0..5).scan(backoff.iter_seeded(7), |it, _| Some(it.next_delay())).collect();
218 /// let b: Vec<_> = (0..5).scan(backoff.iter_seeded(7), |it, _| Some(it.next_delay())).collect();
219 /// assert_eq!(a, b);
220 /// ```
221 #[must_use]
222 pub fn iter_seeded(&self, seed: u64) -> BackoffIter {
223 BackoffIter {
224 policy: *self,
225 attempt: 0,
226 previous: self.base,
227 rng: Rng::new(seed),
228 }
229 }
230}
231
232impl Default for Backoff {
233 /// Exponential from 100ms, doubling, capped at 30s, with decorrelated jitter
234 /// — a sane production default that resists thundering herds.
235 fn default() -> Self {
236 Self::exponential(DEFAULT_BASE, DEFAULT_FACTOR)
237 .with_max(DEFAULT_MAX)
238 .with_jitter(Jitter::Decorrelated)
239 }
240}
241
242/// A live delay sequence produced by a [`Backoff`].
243///
244/// Each [`next_delay`](Self::next_delay) returns the wait for the next attempt.
245/// The sequence is infinite — bounding the attempt count is the retry policy's
246/// job (see [`Retry`](crate::Retry)) — so it also implements [`Iterator`] for
247/// convenience, always yielding `Some`.
248#[derive(Debug, Clone)]
249pub struct BackoffIter {
250 policy: Backoff,
251 attempt: u32,
252 previous: Duration,
253 rng: Rng,
254}
255
256impl BackoffIter {
257 /// Returns the delay for the next attempt and advances the sequence.
258 pub fn next_delay(&mut self) -> Duration {
259 let max = dur_to_nanos(self.policy.max);
260 let nanos = if let Jitter::Decorrelated = self.policy.jitter {
261 self.decorrelated(max)
262 } else {
263 let capped = self.raw_nanos(self.attempt).min(max);
264 match self.policy.jitter {
265 Jitter::Full => self.rng.range(0, capped),
266 Jitter::Equal => {
267 let half = capped / 2;
268 half + self.rng.range(0, capped - half)
269 }
270 // `None`, and the unreachable `Decorrelated` (handled above):
271 // both fall through to the deterministic capped value.
272 _ => capped,
273 }
274 };
275 self.attempt = self.attempt.saturating_add(1);
276 Duration::from_nanos(nanos)
277 }
278
279 /// The base-curve delay in nanoseconds for `attempt`, before jitter or cap.
280 fn raw_nanos(&self, attempt: u32) -> u64 {
281 let base = dur_to_nanos(self.policy.base);
282 match self.policy.kind {
283 Kind::Constant => base,
284 Kind::Linear => {
285 let inc = dur_to_nanos(self.policy.increment);
286 base.saturating_add(inc.saturating_mul(u64::from(attempt)))
287 }
288 Kind::Exponential => {
289 // Clamp the exponent so `powi` cannot overflow f64 to infinity for
290 // pathological attempt counts; the result is clamped to the cap
291 // by the caller anyway.
292 let exp = i32::try_from(attempt.min(64)).unwrap_or(64);
293 let scaled = (base as f64) * self.policy.factor.powi(exp);
294 if scaled.is_finite() && scaled >= 0.0 && scaled < (u64::MAX as f64) {
295 scaled as u64
296 } else {
297 u64::MAX
298 }
299 }
300 }
301 }
302
303 /// One step of the decorrelated-jitter recurrence:
304 /// `min(max, rand(base, previous * 3))`, remembering the result.
305 fn decorrelated(&mut self, max: u64) -> u64 {
306 let base = dur_to_nanos(self.policy.base);
307 let prev = dur_to_nanos(self.previous);
308 let hi = prev.saturating_mul(3).max(base).min(max);
309 let lo = base.min(hi);
310 let chosen = self.rng.range(lo, hi);
311 self.previous = Duration::from_nanos(chosen.max(base));
312 chosen
313 }
314}
315
316impl Iterator for BackoffIter {
317 type Item = Duration;
318
319 fn next(&mut self) -> Option<Duration> {
320 Some(self.next_delay())
321 }
322}
323
324/// Converts a duration to nanoseconds, saturating at `u64::MAX` (~584 years), so
325/// all delay math can stay in integer nanoseconds.
326#[inline]
327fn dur_to_nanos(d: Duration) -> u64 {
328 u64::try_from(d.as_nanos()).unwrap_or(u64::MAX)
329}
330
331/// A small, fast, non-cryptographic generator (SplitMix64) for jitter.
332///
333/// Jitter needs uniform spread, not cryptographic randomness, so a tiny
334/// well-distributed generator is the right tool — no dependency, no syscall on
335/// the hot path. It is seedable for deterministic tests.
336#[derive(Debug, Clone)]
337struct Rng(u64);
338
339impl Rng {
340 #[inline]
341 const fn new(seed: u64) -> Self {
342 Self(seed)
343 }
344
345 /// SplitMix64: advance the state and return a well-mixed 64-bit value.
346 #[inline]
347 fn next_u64(&mut self) -> u64 {
348 self.0 = self.0.wrapping_add(0x9E37_79B9_7F4A_7C15);
349 let mut z = self.0;
350 z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
351 z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
352 z ^ (z >> 31)
353 }
354
355 /// A uniform value in the inclusive range `[lo, hi]`.
356 #[inline]
357 fn range(&mut self, lo: u64, hi: u64) -> u64 {
358 if hi <= lo {
359 return lo;
360 }
361 let span = hi - lo + 1;
362 // Modulo bias is immaterial for jitter spread over nanosecond spans.
363 lo + (self.next_u64() % span)
364 }
365}
366
367/// Derives a seed from a monotonically-increasing counter mixed with the wall
368/// clock, so distinct `BackoffIter`s — even within one process and one instant —
369/// start from different states.
370fn entropy_seed() -> u64 {
371 use std::sync::atomic::{AtomicU64, Ordering};
372 static COUNTER: AtomicU64 = AtomicU64::new(0);
373
374 let counter = COUNTER.fetch_add(1, Ordering::Relaxed);
375 let nanos = std::time::SystemTime::now()
376 .duration_since(std::time::UNIX_EPOCH)
377 .map_or(0, |d| d.as_nanos() as u64);
378
379 // Mix the two sources through the SplitMix64 finalizer for avalanche.
380 let mut z = nanos ^ counter.wrapping_mul(0x9E37_79B9_7F4A_7C15);
381 z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
382 z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
383 z ^ (z >> 31)
384}
385
386#[cfg(test)]
387mod tests {
388 use super::{Backoff, Jitter, Rng};
389 use core::time::Duration;
390
391 #[test]
392 fn test_constant_is_flat() {
393 let mut it = Backoff::constant(Duration::from_millis(50)).iter();
394 for _ in 0..5 {
395 assert_eq!(it.next_delay(), Duration::from_millis(50));
396 }
397 }
398
399 #[test]
400 fn test_linear_grows_by_increment_and_caps() {
401 let backoff = Backoff::linear(Duration::from_millis(100), Duration::from_millis(100))
402 .with_max(Duration::from_millis(250));
403 let mut it = backoff.iter();
404 assert_eq!(it.next_delay(), Duration::from_millis(100));
405 assert_eq!(it.next_delay(), Duration::from_millis(200));
406 assert_eq!(it.next_delay(), Duration::from_millis(250)); // capped
407 assert_eq!(it.next_delay(), Duration::from_millis(250));
408 }
409
410 #[test]
411 fn test_exponential_doubles_and_caps() {
412 let backoff = Backoff::exponential(Duration::from_millis(100), 2.0)
413 .with_max(Duration::from_millis(500));
414 let mut it = backoff.iter();
415 assert_eq!(it.next_delay(), Duration::from_millis(100));
416 assert_eq!(it.next_delay(), Duration::from_millis(200));
417 assert_eq!(it.next_delay(), Duration::from_millis(400));
418 assert_eq!(it.next_delay(), Duration::from_millis(500)); // capped
419 }
420
421 #[test]
422 fn test_full_jitter_stays_within_zero_and_cap() {
423 let backoff = Backoff::exponential(Duration::from_millis(100), 2.0)
424 .with_max(Duration::from_secs(10))
425 .with_jitter(Jitter::Full);
426 let mut it = backoff.iter_seeded(1);
427 for attempt in 0..6u32 {
428 let ceiling =
429 Duration::from_millis(100 * 2u64.pow(attempt)).min(Duration::from_secs(10));
430 let d = it.next_delay();
431 assert!(d <= ceiling, "{d:?} exceeded {ceiling:?}");
432 }
433 }
434
435 #[test]
436 fn test_equal_jitter_keeps_a_floor() {
437 let backoff = Backoff::constant(Duration::from_millis(1000)).with_jitter(Jitter::Equal);
438 let mut it = backoff.iter_seeded(42);
439 for _ in 0..20 {
440 let d = it.next_delay();
441 // Equal jitter: at least half the base, at most the base.
442 assert!(d >= Duration::from_millis(500), "{d:?} below the floor");
443 assert!(d <= Duration::from_millis(1000), "{d:?} above the cap");
444 }
445 }
446
447 #[test]
448 fn test_decorrelated_respects_base_and_cap() {
449 let backoff = Backoff::exponential(Duration::from_millis(100), 2.0)
450 .with_max(Duration::from_secs(2))
451 .with_jitter(Jitter::Decorrelated);
452 let mut it = backoff.iter_seeded(99);
453 for _ in 0..50 {
454 let d = it.next_delay();
455 assert!(d >= Duration::from_millis(100), "{d:?} below base");
456 assert!(d <= Duration::from_secs(2), "{d:?} above cap");
457 }
458 }
459
460 #[test]
461 fn test_seeded_sequences_are_reproducible() {
462 let backoff = Backoff::default();
463 let a: Vec<_> = {
464 let mut it = backoff.iter_seeded(123);
465 (0..8).map(|_| it.next_delay()).collect()
466 };
467 let b: Vec<_> = {
468 let mut it = backoff.iter_seeded(123);
469 (0..8).map(|_| it.next_delay()).collect()
470 };
471 assert_eq!(a, b);
472 }
473
474 #[test]
475 fn test_default_is_decorrelated_exponential() {
476 let backoff = Backoff::default();
477 assert_eq!(backoff.jitter(), Jitter::Decorrelated);
478 assert_eq!(backoff.max(), Duration::from_secs(30));
479 }
480
481 #[test]
482 fn test_rng_range_is_within_bounds_and_handles_degenerate() {
483 let mut rng = Rng::new(7);
484 for _ in 0..1000 {
485 let v = rng.range(10, 20);
486 assert!((10..=20).contains(&v));
487 }
488 assert_eq!(rng.range(5, 5), 5);
489 assert_eq!(rng.range(9, 4), 9); // hi <= lo returns lo
490 }
491}