Skip to main content

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}