Skip to main content

reliakit_backoff/
lib.rs

1//! Clock-agnostic retry backoff policies.
2//!
3//! `reliakit-backoff` computes *how long to wait* between retries. It does not
4//! sleep, spawn tasks, or touch the clock — you decide when to call it and how
5//! to wait. That makes it usable from sync code, any async runtime, and
6//! `no_std` / embedded contexts, with deterministic, exact-byte-style tests.
7//!
8//! The core type is [`Backoff`]: a small, `Copy` policy describing a base delay,
9//! a growth strategy (constant, linear, or exponential), an optional maximum
10//! delay, and an optional retry limit. [`Backoff::delay`] maps a zero-based
11//! attempt number to the delay to wait before that retry, or `None` once the
12//! retry limit is reached.
13//!
14//! Randomized jitter is provided as explicit, pure functions
15//! ([`full_jitter`], [`equal_jitter`], [`decorrelated_jitter`]) that take a
16//! caller-supplied random value, so the crate stays dependency-free and its
17//! output stays deterministic in tests.
18//!
19//! # Example
20//!
21//! ```
22//! use core::time::Duration;
23//! use reliakit_backoff::Backoff;
24//!
25//! // 100ms base, double each attempt, capped at 2s, give up after 8 retries.
26//! let policy = Backoff::exponential(Duration::from_millis(100), 2)
27//!     .with_max_delay(Duration::from_secs(2))
28//!     .with_max_retries(8);
29//!
30//! assert_eq!(policy.delay(0), Some(Duration::from_millis(100)));
31//! assert_eq!(policy.delay(1), Some(Duration::from_millis(200)));
32//! assert_eq!(policy.delay(4), Some(Duration::from_millis(1600)));
33//! assert_eq!(policy.delay(5), Some(Duration::from_secs(2))); // 3200ms capped to 2s
34//! assert_eq!(policy.delay(8), None); // retry limit reached
35//!
36//! // Drive your own retry loop (attempts 0..8 here):
37//! for delay in policy.delays() {
38//!     // sleep(delay); try_again()?; ...
39//!     let _ = delay;
40//!     # break;
41//! }
42//! ```
43
44#![no_std]
45#![forbid(unsafe_code)]
46#![warn(missing_docs)]
47
48mod jitter;
49
50pub use jitter::{decorrelated_jitter, equal_jitter, full_jitter};
51
52use core::time::Duration;
53
54/// Growth strategy for the delay between retries.
55#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
56enum Kind {
57    /// Same delay every attempt.
58    Constant,
59    /// `base + step * attempt`.
60    Linear { step: Duration },
61    /// `base * factor^attempt`.
62    Exponential { factor: u32 },
63}
64
65/// A retry backoff policy.
66///
67/// `Backoff` is a small, `Copy` value. Construct one with [`Backoff::constant`],
68/// [`Backoff::linear`], or [`Backoff::exponential`], then optionally cap it with
69/// [`Backoff::with_max_delay`] and [`Backoff::with_max_retries`].
70///
71/// [`Backoff::delay`] takes a zero-based attempt number (attempt `0` is the wait
72/// before the first retry) and returns the delay, or `None` when the retry limit
73/// has been reached.
74#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
75pub struct Backoff {
76    base: Duration,
77    kind: Kind,
78    max_delay: Duration,
79    max_retries: Option<u32>,
80}
81
82impl Backoff {
83    /// A constant backoff that always waits `base`.
84    pub const fn constant(base: Duration) -> Self {
85        Self {
86            base,
87            kind: Kind::Constant,
88            max_delay: Duration::MAX,
89            max_retries: None,
90        }
91    }
92
93    /// A linear backoff: attempt `n` waits `base + step * n`.
94    pub const fn linear(base: Duration, step: Duration) -> Self {
95        Self {
96            base,
97            kind: Kind::Linear { step },
98            max_delay: Duration::MAX,
99            max_retries: None,
100        }
101    }
102
103    /// An exponential backoff: attempt `n` waits `base * factor^n`.
104    ///
105    /// `factor` is the integer multiplier (e.g. `2` doubles each attempt). A
106    /// `factor` below `2` makes the policy behave like [`Backoff::constant`].
107    pub const fn exponential(base: Duration, factor: u32) -> Self {
108        Self {
109            base,
110            kind: Kind::Exponential { factor },
111            max_delay: Duration::MAX,
112            max_retries: None,
113        }
114    }
115
116    /// Caps every computed delay at `max_delay`.
117    pub const fn with_max_delay(mut self, max_delay: Duration) -> Self {
118        self.max_delay = max_delay;
119        self
120    }
121
122    /// Limits the policy to at most `max_retries` retries.
123    ///
124    /// After this, [`delay`](Self::delay) returns `None` for attempt numbers
125    /// `>= max_retries`.
126    pub const fn with_max_retries(mut self, max_retries: u32) -> Self {
127        self.max_retries = Some(max_retries);
128        self
129    }
130
131    /// Returns the base delay.
132    pub const fn base(&self) -> Duration {
133        self.base
134    }
135
136    /// Returns the maximum delay cap.
137    pub const fn max_delay(&self) -> Duration {
138        self.max_delay
139    }
140
141    /// Returns the retry limit, if any.
142    pub const fn max_retries(&self) -> Option<u32> {
143        self.max_retries
144    }
145
146    /// Returns the delay to wait before retry `attempt` (zero-based), or `None`
147    /// if the retry limit has been reached.
148    ///
149    /// The result is always clamped to [`max_delay`](Self::max_delay). All
150    /// arithmetic saturates and the computation runs in bounded time, so large
151    /// attempt numbers never overflow, panic, or hang.
152    pub fn delay(&self, attempt: u32) -> Option<Duration> {
153        if let Some(max) = self.max_retries {
154            if attempt >= max {
155                return None;
156            }
157        }
158
159        let raw = match self.kind {
160            Kind::Constant => self.base,
161            Kind::Linear { step } => self.base.saturating_add(step.saturating_mul(attempt)),
162            Kind::Exponential { factor } => {
163                if factor <= 1 {
164                    self.base
165                } else {
166                    let mut delay = self.base;
167                    let mut i = 0;
168                    while i < attempt {
169                        if delay >= self.max_delay {
170                            delay = self.max_delay;
171                            break;
172                        }
173                        let next = delay.saturating_mul(factor);
174                        if next == delay {
175                            // No further growth possible (zero base or the
176                            // delay has saturated): stop instead of looping the
177                            // full attempt count.
178                            break;
179                        }
180                        delay = next;
181                        i += 1;
182                    }
183                    delay
184                }
185            }
186        };
187
188        Some(if raw > self.max_delay {
189            self.max_delay
190        } else {
191            raw
192        })
193    }
194
195    /// Returns an iterator over the delays for attempts `0, 1, 2, ...`.
196    ///
197    /// The iterator ends when the retry limit is reached. With no retry limit it
198    /// is infinite.
199    pub const fn delays(&self) -> Delays {
200        Delays {
201            backoff: *self,
202            attempt: 0,
203        }
204    }
205}
206
207/// Iterator over the successive delays of a [`Backoff`] policy.
208///
209/// Created by [`Backoff::delays`].
210#[derive(Debug, Clone)]
211pub struct Delays {
212    backoff: Backoff,
213    attempt: u32,
214}
215
216impl Iterator for Delays {
217    type Item = Duration;
218
219    fn next(&mut self) -> Option<Self::Item> {
220        let delay = self.backoff.delay(self.attempt)?;
221        self.attempt = self.attempt.saturating_add(1);
222        Some(delay)
223    }
224}
225
226#[cfg(test)]
227mod tests {
228    use super::*;
229
230    const MS: fn(u64) -> Duration = Duration::from_millis;
231
232    #[test]
233    fn constant_is_flat() {
234        let b = Backoff::constant(MS(50));
235        assert_eq!(b.delay(0), Some(MS(50)));
236        assert_eq!(b.delay(1000), Some(MS(50)));
237    }
238
239    #[test]
240    fn linear_grows_by_step() {
241        let b = Backoff::linear(MS(100), MS(25));
242        assert_eq!(b.delay(0), Some(MS(100)));
243        assert_eq!(b.delay(1), Some(MS(125)));
244        assert_eq!(b.delay(4), Some(MS(200)));
245    }
246
247    #[test]
248    fn exponential_doubles() {
249        let b = Backoff::exponential(MS(10), 2);
250        assert_eq!(b.delay(0), Some(MS(10)));
251        assert_eq!(b.delay(1), Some(MS(20)));
252        assert_eq!(b.delay(2), Some(MS(40)));
253        assert_eq!(b.delay(3), Some(MS(80)));
254    }
255
256    #[test]
257    fn exponential_factor_three() {
258        let b = Backoff::exponential(MS(1), 3);
259        assert_eq!(b.delay(0), Some(MS(1)));
260        assert_eq!(b.delay(1), Some(MS(3)));
261        assert_eq!(b.delay(2), Some(MS(9)));
262    }
263
264    #[test]
265    fn max_delay_caps() {
266        let b = Backoff::exponential(MS(100), 2).with_max_delay(MS(500));
267        assert_eq!(b.delay(0), Some(MS(100)));
268        assert_eq!(b.delay(2), Some(MS(400)));
269        assert_eq!(b.delay(3), Some(MS(500))); // 800 capped to 500
270        assert_eq!(b.delay(50), Some(MS(500)));
271    }
272
273    #[test]
274    fn max_retries_stops() {
275        let b = Backoff::constant(MS(10)).with_max_retries(3);
276        assert_eq!(b.delay(0), Some(MS(10)));
277        assert_eq!(b.delay(2), Some(MS(10)));
278        assert_eq!(b.delay(3), None);
279        assert_eq!(b.delay(100), None);
280    }
281
282    #[test]
283    fn exponential_factor_below_two_is_constant() {
284        let b = Backoff::exponential(MS(7), 1);
285        assert_eq!(b.delay(0), Some(MS(7)));
286        assert_eq!(b.delay(9), Some(MS(7)));
287    }
288
289    #[test]
290    fn huge_attempt_saturates_without_hanging() {
291        // No max_delay: must saturate quickly, not loop billions of times.
292        let b = Backoff::exponential(Duration::from_secs(1), 2);
293        assert_eq!(b.delay(u32::MAX), Some(Duration::MAX));
294    }
295
296    #[test]
297    fn zero_base_exponential_does_not_hang() {
298        // base == ZERO never grows; delay(huge) must return instantly, not loop
299        // the full attempt count.
300        let b = Backoff::exponential(Duration::ZERO, 2);
301        assert_eq!(b.delay(u32::MAX), Some(Duration::ZERO));
302        assert_eq!(b.delay(0), Some(Duration::ZERO));
303
304        // Same guard with a max_delay set.
305        let capped = Backoff::exponential(Duration::ZERO, 4).with_max_delay(MS(500));
306        assert_eq!(capped.delay(u32::MAX), Some(Duration::ZERO));
307    }
308
309    #[test]
310    fn linear_saturates() {
311        let b = Backoff::linear(Duration::MAX, Duration::from_secs(1));
312        assert_eq!(b.delay(10), Some(Duration::MAX));
313    }
314
315    #[test]
316    fn delays_iterator_respects_limit() {
317        let b = Backoff::exponential(MS(10), 2).with_max_retries(4);
318        let collected: heapless_vec::Vec = b.delays().collect();
319        assert_eq!(collected.as_slice(), &[MS(10), MS(20), MS(40), MS(80)]);
320    }
321
322    #[test]
323    fn getters() {
324        let b = Backoff::exponential(MS(5), 2)
325            .with_max_delay(MS(100))
326            .with_max_retries(7);
327        assert_eq!(b.base(), MS(5));
328        assert_eq!(b.max_delay(), MS(100));
329        assert_eq!(b.max_retries(), Some(7));
330    }
331
332    // Tiny fixed-capacity collector so tests stay `no_std` without `alloc`.
333    mod heapless_vec {
334        use core::time::Duration;
335
336        pub struct Vec {
337            data: [Duration; 16],
338            len: usize,
339        }
340
341        impl Vec {
342            pub fn as_slice(&self) -> &[Duration] {
343                &self.data[..self.len]
344            }
345        }
346
347        impl FromIterator<Duration> for Vec {
348            fn from_iter<I: IntoIterator<Item = Duration>>(iter: I) -> Self {
349                let mut data = [Duration::ZERO; 16];
350                let mut len = 0;
351                for d in iter {
352                    data[len] = d;
353                    len += 1;
354                }
355                Self { data, len }
356            }
357        }
358    }
359}