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 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}