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}