1use std::time::Duration;
2
3#[derive(Debug, Clone, PartialEq)]
5pub struct RetryPolicy {
6 pub max_attempts: u32,
7 pub initial_backoff: Duration,
8 pub max_backoff: Duration,
9 pub multiplier: f64,
10 pub jitter: f64,
12 pub retry_statuses: Vec<u16>,
13 pub honor_retry_after: bool,
14 pub max_retry_after: Duration,
16}
17
18impl Default for RetryPolicy {
19 fn default() -> Self {
20 Self {
21 max_attempts: 3,
22 initial_backoff: Duration::from_millis(500),
23 max_backoff: Duration::from_secs(8),
24 multiplier: 2.0,
25 jitter: 0.2,
26 retry_statuses: vec![408, 425, 429, 500, 502, 503, 504],
27 honor_retry_after: true,
28 max_retry_after: Duration::from_secs(30),
29 }
30 }
31}
32
33impl RetryPolicy {
34 pub fn none() -> Self {
35 Self {
36 max_attempts: 1,
37 ..Self::default()
38 }
39 }
40
41 pub fn is_retryable_status(&self, status: u16) -> bool {
42 self.retry_statuses.contains(&status)
43 }
44
45 pub fn backoff(&self, retry: u32) -> Duration {
47 let exp = retry.saturating_sub(1).min(32) as i32;
48 let secs = self.initial_backoff.as_secs_f64() * self.multiplier.powi(exp);
49 let capped = secs.min(self.max_backoff.as_secs_f64());
50 let shaved = if self.jitter > 0.0 {
51 capped * (1.0 - self.jitter.clamp(0.0, 1.0) * random_unit())
52 } else {
53 capped
54 };
55 Duration::from_secs_f64(shaved.max(0.0))
56 }
57
58 pub fn delay(&self, retry: u32, retry_after: Option<Duration>) -> Duration {
60 let base = self.backoff(retry);
61 match (self.honor_retry_after, retry_after) {
62 (true, Some(ra)) => base.max(ra.min(self.max_retry_after)),
63 _ => base,
64 }
65 }
66}
67
68fn random_unit() -> f64 {
69 let mut b = [0u8; 4];
70 if getrandom::fill(&mut b).is_err() {
71 return 0.0;
72 }
73 u32::from_le_bytes(b) as f64 / u32::MAX as f64
74}
75
76pub fn parse_retry_after(value: &str) -> Option<Duration> {
78 let v = value.trim();
79 v.parse::<f64>()
80 .ok()
81 .filter(|s| s.is_finite() && *s >= 0.0)
82 .map(Duration::from_secs_f64)
83}