1use std::time::Duration;
10
11#[derive(Clone, Debug)]
18pub struct RetryConfig {
19 pub max_attempts: u32,
21 pub base_delay: Duration,
23 pub max_delay: Duration,
25 pub multiplier: f64,
27 pub jitter: f64,
30}
31
32impl Default for RetryConfig {
33 fn default() -> Self {
34 Self {
35 max_attempts: 1,
36 base_delay: Duration::from_millis(100),
37 max_delay: Duration::from_secs(10),
38 multiplier: 2.0,
39 jitter: 0.2,
40 }
41 }
42}
43
44impl RetryConfig {
45 pub fn disabled() -> Self {
47 Self {
48 max_attempts: 1,
49 ..Default::default()
50 }
51 }
52
53 pub fn delay_before(&self, attempt: u32) -> Duration {
65 self.delay_before_with_jitter(attempt, jitter_fraction())
66 }
67
68 pub fn delay_before_with_jitter(&self, attempt: u32, jitter_frac: f64) -> Duration {
72 if attempt == 0 {
73 return Duration::ZERO;
74 }
75 let exp = (attempt - 1) as i32;
76 let base = self.base_delay.as_secs_f64() * self.multiplier.powi(exp);
77 let mut d = base.min(self.max_delay.as_secs_f64());
78 if self.jitter > 0.0 {
79 let spread = d * self.jitter;
80 d += spread * (jitter_frac.clamp(0.0, 1.0) * 2.0 - 1.0);
81 }
82 if !d.is_finite() {
85 d = self.max_delay.as_secs_f64();
86 }
87 Duration::from_secs_f64(d.max(0.0))
88 }
89
90 pub fn schedule(&self) -> impl Iterator<Item = Duration> + '_ {
92 (0..self.max_attempts).map(move |n| self.delay_before(n))
93 }
94}
95
96fn jitter_fraction() -> f64 {
103 use std::cell::Cell;
104 use std::time::{SystemTime, UNIX_EPOCH};
105
106 thread_local! {
107 static SEQ: Cell<u64> = const { Cell::new(0) };
108 }
109 let seq = SEQ.with(|c| {
110 let v = c.get().wrapping_add(1);
111 c.set(v);
112 v
113 });
114 let nanos = SystemTime::now()
115 .duration_since(UNIX_EPOCH)
116 .map(|d| d.as_nanos() as u64)
117 .unwrap_or(0);
118
119 let mut x = nanos
121 .wrapping_mul(0x9E37_79B9_7F4A_7C15)
122 .wrapping_add(seq.wrapping_mul(0xD1B5_4A32_D192_ED03));
123 x ^= x >> 30;
124 x = x.wrapping_mul(0xBF58_476D_1CE4_E5B9);
125 x ^= x >> 27;
126 x = x.wrapping_mul(0x94D0_49BB_1331_11EB);
127 x ^= x >> 31;
128 (x as f64) / (u64::MAX as f64)
129}
130
131#[cfg(test)]
132mod tests {
133 use super::*;
134
135 #[test]
136 fn first_attempt_has_no_delay() {
137 let cfg = RetryConfig::default();
138 assert_eq!(cfg.delay_before(0), Duration::ZERO);
139 }
140
141 #[test]
142 fn exponential_growth_capped_at_max() {
143 let cfg = RetryConfig {
144 max_attempts: 6,
145 base_delay: Duration::from_millis(100),
146 max_delay: Duration::from_millis(400),
147 multiplier: 2.0,
148 jitter: 0.0,
149 };
150 let delays: Vec<Duration> = cfg.schedule().collect();
151 assert_eq!(delays[0], Duration::ZERO);
152 assert_eq!(delays[1], Duration::from_millis(100));
153 assert_eq!(delays[2], Duration::from_millis(200));
154 assert_eq!(delays[3], Duration::from_millis(400)); assert_eq!(delays[4], Duration::from_millis(400));
156 }
157
158 #[test]
159 fn disabled_yields_single_zero_delay() {
160 let cfg = RetryConfig::disabled();
161 let delays: Vec<Duration> = cfg.schedule().collect();
162 assert_eq!(delays, vec![Duration::ZERO]);
163 }
164
165 #[test]
166 fn retries_are_opt_in() {
167 assert_eq!(RetryConfig::default().max_attempts, 1);
168 }
169
170 #[test]
171 fn jitter_stays_non_negative() {
172 let cfg = RetryConfig {
173 max_attempts: 10,
174 base_delay: Duration::from_millis(1),
175 max_delay: Duration::from_secs(1),
176 multiplier: 2.0,
177 jitter: 0.9,
178 };
179 for d in cfg.schedule() {
180 assert!(d >= Duration::ZERO);
181 }
182 }
183
184 #[test]
185 fn jitter_is_not_deterministic_across_calls() {
186 let cfg = RetryConfig {
189 max_attempts: 2,
190 base_delay: Duration::from_millis(100),
191 max_delay: Duration::from_secs(10),
192 multiplier: 2.0,
193 jitter: 0.5,
194 };
195 let mut seen = std::collections::HashSet::new();
196 for _ in 0..50 {
197 seen.insert(cfg.delay_before(1).as_nanos());
198 }
199 assert!(
200 seen.len() > 1,
201 "jitter produced identical delays on every call"
202 );
203 }
204
205 #[test]
206 fn explicit_jitter_fraction_is_reproducible() {
207 let cfg = RetryConfig {
208 max_attempts: 2,
209 base_delay: Duration::from_millis(100),
210 max_delay: Duration::from_secs(10),
211 multiplier: 2.0,
212 jitter: 0.5,
213 };
214 let a = cfg.delay_before_with_jitter(1, 0.25);
215 let b = cfg.delay_before_with_jitter(1, 0.25);
216 assert_eq!(a, b);
217 assert_ne!(a, cfg.delay_before_with_jitter(1, 0.75));
219 }
220
221 #[test]
222 fn non_finite_multiplier_does_not_panic() {
223 let cfg = RetryConfig {
224 max_attempts: 3,
225 base_delay: Duration::from_millis(100),
226 max_delay: Duration::from_secs(10),
227 multiplier: f64::NAN,
228 jitter: 0.0,
229 };
230 let _ = cfg.delay_before_with_jitter(2, 0.0);
233 }
234}