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 process 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 /// The seed mixes a per-call counter with the wall clock under `std`; under
201 /// `no_std` it is counter-only (still distinct per iterator within a run). For
202 /// a fully reproducible sequence, use [`iter_seeded`](Self::iter_seeded).
203 #[must_use]
204 pub fn iter(&self) -> BackoffIter {
205 self.iter_seeded(entropy_seed())
206 }
207
208 /// Starts a delay sequence with an explicit seed, for deterministic,
209 /// reproducible tests.
210 ///
211 /// # Examples
212 ///
213 /// ```
214 /// use std::time::Duration;
215 /// use throttle_net::{Backoff, Jitter};
216 ///
217 /// let backoff = Backoff::exponential(Duration::from_millis(10), 2.0)
218 /// .with_jitter(Jitter::Full);
219 /// // The same seed always yields the same sequence.
220 /// let a: Vec<_> = (0..5).scan(backoff.iter_seeded(7), |it, _| Some(it.next_delay())).collect();
221 /// let b: Vec<_> = (0..5).scan(backoff.iter_seeded(7), |it, _| Some(it.next_delay())).collect();
222 /// assert_eq!(a, b);
223 /// ```
224 #[must_use]
225 pub fn iter_seeded(&self, seed: u64) -> BackoffIter {
226 BackoffIter {
227 policy: *self,
228 attempt: 0,
229 previous: self.base,
230 rng: Rng::new(seed),
231 }
232 }
233}
234
235impl Default for Backoff {
236 /// Exponential from 100ms, doubling, capped at 30s, with decorrelated jitter
237 /// — a sane production default that resists thundering herds.
238 fn default() -> Self {
239 Self::exponential(DEFAULT_BASE, DEFAULT_FACTOR)
240 .with_max(DEFAULT_MAX)
241 .with_jitter(Jitter::Decorrelated)
242 }
243}
244
245/// A live delay sequence produced by a [`Backoff`].
246///
247/// Each [`next_delay`](Self::next_delay) returns the wait for the next attempt.
248/// The sequence is infinite — bounding the attempt count is the retry policy's
249/// job (see [`Retry`](crate::Retry)) — so it also implements [`Iterator`] for
250/// convenience, always yielding `Some`.
251#[derive(Debug, Clone)]
252pub struct BackoffIter {
253 policy: Backoff,
254 attempt: u32,
255 previous: Duration,
256 rng: Rng,
257}
258
259impl BackoffIter {
260 /// Returns the delay for the next attempt and advances the sequence.
261 pub fn next_delay(&mut self) -> Duration {
262 let max = dur_to_nanos(self.policy.max);
263 let nanos = if let Jitter::Decorrelated = self.policy.jitter {
264 self.decorrelated(max)
265 } else {
266 let capped = self.raw_nanos(self.attempt).min(max);
267 match self.policy.jitter {
268 Jitter::Full => self.rng.range(0, capped),
269 Jitter::Equal => {
270 let half = capped / 2;
271 half + self.rng.range(0, capped - half)
272 }
273 // `None`, and the unreachable `Decorrelated` (handled above):
274 // both fall through to the deterministic capped value.
275 _ => capped,
276 }
277 };
278 self.attempt = self.attempt.saturating_add(1);
279 Duration::from_nanos(nanos)
280 }
281
282 /// The base-curve delay in nanoseconds for `attempt`, before jitter or cap.
283 fn raw_nanos(&self, attempt: u32) -> u64 {
284 let base = dur_to_nanos(self.policy.base);
285 match self.policy.kind {
286 Kind::Constant => base,
287 Kind::Linear => {
288 let inc = dur_to_nanos(self.policy.increment);
289 base.saturating_add(inc.saturating_mul(u64::from(attempt)))
290 }
291 Kind::Exponential => {
292 // `base * factor^attempt`, by repeated multiplication so it works
293 // in `no_std` (where `f64::powi` is unavailable). The exponent is
294 // clamped so the loop is bounded and the value cannot run away; the
295 // result is clamped to the cap by the caller anyway.
296 let mut scaled = base as f64;
297 for _ in 0..attempt.min(64) {
298 scaled *= self.policy.factor;
299 }
300 if scaled.is_finite() && scaled >= 0.0 && scaled < (u64::MAX as f64) {
301 scaled as u64
302 } else {
303 u64::MAX
304 }
305 }
306 }
307 }
308
309 /// One step of the decorrelated-jitter recurrence:
310 /// `min(max, rand(base, previous * 3))`, remembering the result.
311 fn decorrelated(&mut self, max: u64) -> u64 {
312 let base = dur_to_nanos(self.policy.base);
313 let prev = dur_to_nanos(self.previous);
314 let hi = prev.saturating_mul(3).max(base).min(max);
315 let lo = base.min(hi);
316 let chosen = self.rng.range(lo, hi);
317 self.previous = Duration::from_nanos(chosen.max(base));
318 chosen
319 }
320}
321
322impl Iterator for BackoffIter {
323 type Item = Duration;
324
325 fn next(&mut self) -> Option<Duration> {
326 Some(self.next_delay())
327 }
328}
329
330/// Converts a duration to nanoseconds, saturating at `u64::MAX` (~584 years), so
331/// all delay math can stay in integer nanoseconds.
332#[inline]
333fn dur_to_nanos(d: Duration) -> u64 {
334 u64::try_from(d.as_nanos()).unwrap_or(u64::MAX)
335}
336
337/// A small, fast, non-cryptographic generator (SplitMix64) for jitter.
338///
339/// Jitter needs uniform spread, not cryptographic randomness, so a tiny
340/// well-distributed generator is the right tool — no dependency, no syscall on
341/// the hot path. It is seedable for deterministic tests.
342#[derive(Debug, Clone)]
343struct Rng(u64);
344
345impl Rng {
346 #[inline]
347 const fn new(seed: u64) -> Self {
348 Self(seed)
349 }
350
351 /// SplitMix64: advance the state and return a well-mixed 64-bit value.
352 #[inline]
353 fn next_u64(&mut self) -> u64 {
354 self.0 = self.0.wrapping_add(0x9E37_79B9_7F4A_7C15);
355 let mut z = self.0;
356 z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
357 z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
358 z ^ (z >> 31)
359 }
360
361 /// A uniform value in the inclusive range `[lo, hi]`.
362 #[inline]
363 fn range(&mut self, lo: u64, hi: u64) -> u64 {
364 if hi <= lo {
365 return lo;
366 }
367 let span = hi - lo + 1;
368 // Modulo bias is immaterial for jitter spread over nanosecond spans.
369 lo + (self.next_u64() % span)
370 }
371}
372
373/// Derives a seed from a monotonically-increasing counter, mixed with the wall
374/// clock under `std`, so distinct `BackoffIter`s start from different states.
375/// `no_std` builds have no clock, so the seed is counter-only — still distinct per
376/// iterator within a run, though it repeats across process restarts.
377fn entropy_seed() -> u64 {
378 use core::sync::atomic::{AtomicU64, Ordering};
379 static COUNTER: AtomicU64 = AtomicU64::new(0);
380
381 let counter = COUNTER.fetch_add(1, Ordering::Relaxed);
382 #[cfg(feature = "std")]
383 let nanos = std::time::SystemTime::now()
384 .duration_since(std::time::UNIX_EPOCH)
385 .map_or(0, |d| d.as_nanos() as u64);
386 #[cfg(not(feature = "std"))]
387 let nanos = 0u64;
388
389 // Mix the sources through the SplitMix64 finalizer for avalanche.
390 let mut z = nanos ^ counter.wrapping_mul(0x9E37_79B9_7F4A_7C15);
391 z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
392 z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
393 z ^ (z >> 31)
394}
395
396#[cfg(test)]
397mod tests {
398 use super::{Backoff, Jitter, Rng};
399 use core::time::Duration;
400
401 #[test]
402 fn test_constant_is_flat() {
403 let mut it = Backoff::constant(Duration::from_millis(50)).iter();
404 for _ in 0..5 {
405 assert_eq!(it.next_delay(), Duration::from_millis(50));
406 }
407 }
408
409 #[test]
410 fn test_linear_grows_by_increment_and_caps() {
411 let backoff = Backoff::linear(Duration::from_millis(100), Duration::from_millis(100))
412 .with_max(Duration::from_millis(250));
413 let mut it = backoff.iter();
414 assert_eq!(it.next_delay(), Duration::from_millis(100));
415 assert_eq!(it.next_delay(), Duration::from_millis(200));
416 assert_eq!(it.next_delay(), Duration::from_millis(250)); // capped
417 assert_eq!(it.next_delay(), Duration::from_millis(250));
418 }
419
420 #[test]
421 fn test_exponential_doubles_and_caps() {
422 let backoff = Backoff::exponential(Duration::from_millis(100), 2.0)
423 .with_max(Duration::from_millis(500));
424 let mut it = backoff.iter();
425 assert_eq!(it.next_delay(), Duration::from_millis(100));
426 assert_eq!(it.next_delay(), Duration::from_millis(200));
427 assert_eq!(it.next_delay(), Duration::from_millis(400));
428 assert_eq!(it.next_delay(), Duration::from_millis(500)); // capped
429 }
430
431 #[test]
432 fn test_full_jitter_stays_within_zero_and_cap() {
433 let backoff = Backoff::exponential(Duration::from_millis(100), 2.0)
434 .with_max(Duration::from_secs(10))
435 .with_jitter(Jitter::Full);
436 let mut it = backoff.iter_seeded(1);
437 for attempt in 0..6u32 {
438 let ceiling =
439 Duration::from_millis(100 * 2u64.pow(attempt)).min(Duration::from_secs(10));
440 let d = it.next_delay();
441 assert!(d <= ceiling, "{d:?} exceeded {ceiling:?}");
442 }
443 }
444
445 #[test]
446 fn test_equal_jitter_keeps_a_floor() {
447 let backoff = Backoff::constant(Duration::from_millis(1000)).with_jitter(Jitter::Equal);
448 let mut it = backoff.iter_seeded(42);
449 for _ in 0..20 {
450 let d = it.next_delay();
451 // Equal jitter: at least half the base, at most the base.
452 assert!(d >= Duration::from_millis(500), "{d:?} below the floor");
453 assert!(d <= Duration::from_millis(1000), "{d:?} above the cap");
454 }
455 }
456
457 #[test]
458 fn test_decorrelated_respects_base_and_cap() {
459 let backoff = Backoff::exponential(Duration::from_millis(100), 2.0)
460 .with_max(Duration::from_secs(2))
461 .with_jitter(Jitter::Decorrelated);
462 let mut it = backoff.iter_seeded(99);
463 for _ in 0..50 {
464 let d = it.next_delay();
465 assert!(d >= Duration::from_millis(100), "{d:?} below base");
466 assert!(d <= Duration::from_secs(2), "{d:?} above cap");
467 }
468 }
469
470 #[test]
471 fn test_seeded_sequences_are_reproducible() {
472 let backoff = Backoff::default();
473 let a: Vec<_> = {
474 let mut it = backoff.iter_seeded(123);
475 (0..8).map(|_| it.next_delay()).collect()
476 };
477 let b: Vec<_> = {
478 let mut it = backoff.iter_seeded(123);
479 (0..8).map(|_| it.next_delay()).collect()
480 };
481 assert_eq!(a, b);
482 }
483
484 #[test]
485 fn test_default_is_decorrelated_exponential() {
486 let backoff = Backoff::default();
487 assert_eq!(backoff.jitter(), Jitter::Decorrelated);
488 assert_eq!(backoff.max(), Duration::from_secs(30));
489 }
490
491 #[test]
492 fn test_rng_range_is_within_bounds_and_handles_degenerate() {
493 let mut rng = Rng::new(7);
494 for _ in 0..1000 {
495 let v = rng.range(10, 20);
496 assert!((10..=20).contains(&v));
497 }
498 assert_eq!(rng.range(5, 5), 5);
499 assert_eq!(rng.range(9, 4), 9); // hi <= lo returns lo
500 }
501}