Skip to main content

tool_retry_policy/
lib.rs

1//! # tool-retry-policy
2//!
3//! Declarative retry policy primitive. Returns "how long to wait, then
4//! retry" or "give up". You run the call; this crate decides whether
5//! to retry and for how long.
6//!
7//! - Exponential backoff: `base * 2^(attempt-1)`, capped at `max`.
8//! - Optional uniform jitter (default ±25%) using a tiny PRNG.
9//! - Per-attempt cap (`max_attempts`).
10//!
11//! ## Example
12//!
13//! ```
14//! use tool_retry_policy::{Policy, Decision};
15//! use std::time::Duration;
16//!
17//! let p = Policy {
18//!     max_attempts: 4,
19//!     base: Duration::from_millis(100),
20//!     max: Duration::from_secs(10),
21//!     jitter: false,
22//! };
23//!
24//! let mut attempt = 0;
25//! loop {
26//!     attempt += 1;
27//!     // run the call here; pretend it failed
28//!     match p.next(attempt) {
29//!         Decision::Retry(d) => { /* sleep d, continue */ break }
30//!         Decision::GiveUp => break,
31//!     }
32//! }
33//! ```
34
35#![deny(missing_docs)]
36
37use std::time::Duration;
38
39/// Retry policy.
40#[derive(Debug, Clone, Copy)]
41pub struct Policy {
42    /// Max attempts including the first try.
43    pub max_attempts: u32,
44    /// Backoff base.
45    pub base: Duration,
46    /// Backoff cap.
47    pub max: Duration,
48    /// Add ±25% uniform jitter to the chosen sleep.
49    pub jitter: bool,
50}
51
52impl Default for Policy {
53    fn default() -> Self {
54        Self {
55            max_attempts: 3,
56            base: Duration::from_millis(250),
57            max: Duration::from_secs(8),
58            jitter: true,
59        }
60    }
61}
62
63/// Decision returned by `next`.
64#[derive(Debug, Clone, Copy, PartialEq, Eq)]
65pub enum Decision {
66    /// Sleep this duration then retry.
67    Retry(Duration),
68    /// Out of attempts; surface the error.
69    GiveUp,
70}
71
72impl Policy {
73    /// `attempt` is the 1-based number of the call that just failed.
74    pub fn next(&self, attempt: u32) -> Decision {
75        if attempt >= self.max_attempts {
76            return Decision::GiveUp;
77        }
78        let exp_factor = 2u32.saturating_pow(attempt.saturating_sub(1));
79        let nanos = self
80            .base
81            .as_nanos()
82            .saturating_mul(exp_factor as u128);
83        let nanos = nanos.min(self.max.as_nanos());
84        let mut d = Duration::from_nanos(nanos.min(u128::from(u64::MAX)) as u64);
85        if self.jitter {
86            // Seed: mix attempt and the duration so it's deterministic per call
87            // but varies across attempts. Splitmix64.
88            let mut s = (attempt as u64).wrapping_mul(0x9E3779B97F4A7C15);
89            s ^= d.as_nanos() as u64;
90            s = (s ^ (s >> 30)).wrapping_mul(0xBF58476D1CE4E5B9);
91            s = (s ^ (s >> 27)).wrapping_mul(0x94D049BB133111EB);
92            s = s ^ (s >> 31);
93            let frac = (s % 1000) as f64 / 1000.0; // 0.0..1.0
94            let jitter_factor = 0.75 + frac * 0.5; // 0.75..1.25
95            let scaled = (d.as_nanos() as f64 * jitter_factor) as u64;
96            d = Duration::from_nanos(scaled);
97        }
98        Decision::Retry(d)
99    }
100}