1use std::future::Future;
21use std::time::Duration;
22
23use crate::error::{BrainError, Result};
24
25fn jittered(delay: Duration, floor: Duration) -> Duration {
35 let floor = floor.min(delay);
36 let span = delay.saturating_sub(floor);
37 let span_nanos = u64::try_from(span.as_nanos()).unwrap_or(u64::MAX);
38 if span_nanos == 0 {
39 return floor;
40 }
41 let seed = std::time::SystemTime::now()
42 .duration_since(std::time::UNIX_EPOCH)
43 .map_or(0x9E37_79B9_7F4A_7C15, |d| {
47 u64::try_from(d.as_nanos() & u128::from(u64::MAX)).unwrap_or(0)
48 });
49 let mut z = seed.wrapping_add(0x9E37_79B9_7F4A_7C15);
51 z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
52 z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
53 z ^= z >> 31;
54 let extra = z % span_nanos;
55 floor + Duration::from_nanos(extra)
56}
57
58#[derive(Clone, Copy, Debug, PartialEq, Eq)]
63pub enum RetryStep {
64 Retry(Option<Duration>),
66 GiveUp,
68}
69
70#[derive(Clone, Debug)]
72pub struct RetryPolicy {
73 pub max_attempts: u32,
75 pub base_delay: Duration,
77 pub max_delay: Duration,
79}
80
81impl RetryPolicy {
82 #[must_use]
84 pub fn none() -> Self {
85 Self {
86 max_attempts: 1,
87 base_delay: Duration::ZERO,
88 max_delay: Duration::ZERO,
89 }
90 }
91
92 #[must_use]
94 pub fn new(max_attempts: u32, base_delay: Duration, max_delay: Duration) -> Self {
95 Self {
96 max_attempts: max_attempts.max(1),
97 base_delay,
98 max_delay,
99 }
100 }
101
102 #[must_use]
106 pub fn backoff(&self, attempt: u32, server_retry_after: Option<Duration>) -> Option<Duration> {
107 if let Some(ra) = server_retry_after {
108 return Some(ra.min(self.max_delay));
109 }
110 if self.base_delay.is_zero() {
111 return None;
112 }
113 let shift = attempt.saturating_sub(1).min(31);
115 let mult = 1u32 << shift;
116 let delay = self
117 .base_delay
118 .checked_mul(mult)
119 .unwrap_or(self.max_delay)
120 .min(self.max_delay);
121 Some(delay)
122 }
123
124 #[must_use]
128 pub fn step(&self, attempt: u32, err: &BrainError) -> RetryStep {
129 if attempt < self.max_attempts && err.is_retryable() {
130 let server_floor = err.retry_after();
131 let delay = self.backoff(attempt, server_floor).map(|delay| {
136 jittered(
137 delay,
138 server_floor.unwrap_or(Duration::ZERO).min(self.max_delay),
139 )
140 });
141 RetryStep::Retry(delay)
142 } else {
143 RetryStep::GiveUp
144 }
145 }
146}
147
148impl Default for RetryPolicy {
149 fn default() -> Self {
151 Self {
152 max_attempts: 3,
153 base_delay: Duration::from_millis(100),
154 max_delay: Duration::from_secs(5),
155 }
156 }
157}
158
159pub async fn with_retry<T, F, Fut>(policy: &RetryPolicy, mut op: F) -> Result<T>
164where
165 F: FnMut() -> Fut,
166 Fut: Future<Output = Result<T>>,
167{
168 let mut attempt: u32 = 1;
169 loop {
170 match op().await {
171 Ok(value) => return Ok(value),
172 Err(err) => match policy.step(attempt, &err) {
173 RetryStep::Retry(delay) => {
174 if let Some(delay) = delay {
175 tokio::time::sleep(delay).await;
176 }
177 attempt += 1;
178 }
179 RetryStep::GiveUp => return Err(err),
180 },
181 }
182 }
183}
184
185pub async fn with_default_retry<T, F, Fut>(op: F) -> Result<T>
187where
188 F: FnMut() -> Fut,
189 Fut: Future<Output = Result<T>>,
190{
191 with_retry(&RetryPolicy::default(), op).await
192}
193
194#[cfg(test)]
195mod tests {
196 use super::*;
197 use std::cell::Cell;
198
199 #[test]
200 fn exponential_backoff_doubles_and_caps() {
201 let p = RetryPolicy::new(5, Duration::from_millis(100), Duration::from_secs(1));
202 assert_eq!(p.backoff(1, None), Some(Duration::from_millis(100)));
203 assert_eq!(p.backoff(2, None), Some(Duration::from_millis(200)));
204 assert_eq!(p.backoff(3, None), Some(Duration::from_millis(400)));
205 assert_eq!(p.backoff(4, None), Some(Duration::from_millis(800)));
206 assert_eq!(p.backoff(5, None), Some(Duration::from_secs(1)));
208 }
209
210 #[test]
211 fn server_retry_after_overrides_and_is_capped() {
212 let p = RetryPolicy::new(3, Duration::from_millis(100), Duration::from_secs(2));
213 assert_eq!(
214 p.backoff(1, Some(Duration::from_millis(500))),
215 Some(Duration::from_millis(500))
216 );
217 assert_eq!(
218 p.backoff(1, Some(Duration::from_secs(99))),
219 Some(Duration::from_secs(2)),
220 "server hint is capped at max_delay"
221 );
222 }
223
224 #[test]
225 fn jitter_stays_within_bounds_and_honors_the_floor() {
226 let delay = Duration::from_millis(800);
227 for _ in 0..1000 {
229 let j = jittered(delay, Duration::ZERO);
230 assert!(j <= delay, "jitter must not exceed the base delay");
231 }
232 let floor = Duration::from_millis(200);
234 for _ in 0..1000 {
235 let j = jittered(delay, floor);
236 assert!(j >= floor, "jitter must never wake before the server floor");
237 assert!(j <= delay, "jitter must not exceed the base delay");
238 }
239 }
240
241 #[test]
242 fn jitter_with_zero_span_returns_the_floor() {
243 let d = Duration::from_millis(500);
245 assert_eq!(jittered(d, d), d);
246 assert_eq!(jittered(d, Duration::from_secs(99)), d);
248 }
249
250 #[test]
251 fn step_jitters_within_the_scheduled_delay() {
252 let p = RetryPolicy::new(5, Duration::from_millis(100), Duration::from_secs(1));
253 for _ in 0..1000 {
255 match p.step(4, &BrainError::Closed) {
256 RetryStep::Retry(Some(d)) => assert!(d <= Duration::from_millis(800)),
257 other => panic!("expected a jittered retry, got {other:?}"),
258 }
259 }
260 }
261
262 #[test]
263 fn none_policy_means_no_backoff() {
264 let p = RetryPolicy::none();
265 assert_eq!(p.max_attempts, 1);
266 assert_eq!(p.backoff(1, None), None);
267 }
268
269 #[tokio::test]
270 async fn retries_until_success_then_stops() {
271 let calls = Cell::new(0u32);
272 let policy = RetryPolicy::new(5, Duration::ZERO, Duration::ZERO);
274 let out: Result<u32> = with_retry(&policy, || {
275 let n = calls.get() + 1;
276 calls.set(n);
277 async move {
278 if n < 3 {
279 Err(BrainError::Closed)
280 } else {
281 Ok(n)
282 }
283 }
284 })
285 .await;
286 assert_eq!(out.unwrap(), 3);
287 assert_eq!(calls.get(), 3);
288 }
289
290 #[tokio::test]
291 async fn does_not_retry_a_non_retryable_error() {
292 let calls = Cell::new(0u32);
293 let policy = RetryPolicy::new(5, Duration::ZERO, Duration::ZERO);
294 let out: Result<u32> = with_retry(&policy, || {
295 calls.set(calls.get() + 1);
296 async move { Err(BrainError::Protocol("nope".into())) }
297 })
298 .await;
299 assert!(matches!(out, Err(BrainError::Protocol(_))));
300 assert_eq!(calls.get(), 1, "a non-retryable error must not retry");
301 }
302
303 #[tokio::test]
304 async fn gives_up_after_max_attempts() {
305 let calls = Cell::new(0u32);
306 let policy = RetryPolicy::new(3, Duration::ZERO, Duration::ZERO);
307 let out: Result<u32> = with_retry(&policy, || {
308 calls.set(calls.get() + 1);
309 async move { Err(BrainError::Closed) }
310 })
311 .await;
312 assert!(matches!(out, Err(BrainError::Closed)));
313 assert_eq!(calls.get(), 3, "exactly max_attempts tries");
314 }
315}