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::try_from_secs_f64(shaved.max(0.0)).unwrap_or(self.max_backoff)
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(|s| Duration::try_from_secs_f64(s).unwrap_or(Duration::MAX))
83}
84
85#[cfg(test)]
86mod tests {
87 use super::*;
88
89 #[test]
90 fn huge_retry_after_saturates_instead_of_panicking() {
91 for v in [
92 "18446744073709551616",
93 "1e300",
94 "99999999999999999999999999999999999999",
95 "1.7976931348623157e308",
96 ] {
97 assert_eq!(parse_retry_after(v), Some(Duration::MAX), "{v}");
98 }
99 assert_eq!(parse_retry_after("1.5"), Some(Duration::from_millis(1500)));
100 assert_eq!(parse_retry_after("-1"), None);
101 assert_eq!(parse_retry_after("NaN"), None);
102 assert_eq!(parse_retry_after("inf"), None);
103 }
104
105 #[test]
106 fn delay_caps_a_saturated_retry_after() {
107 let p = RetryPolicy {
108 max_retry_after: Duration::from_secs(30),
109 jitter: 0.0,
110 ..RetryPolicy::default()
111 };
112 assert_eq!(p.delay(1, Some(Duration::MAX)), Duration::from_secs(30));
113 }
114
115 #[test]
116 fn backoff_with_absurd_settings_does_not_panic() {
117 let p = RetryPolicy {
118 initial_backoff: Duration::MAX,
119 max_backoff: Duration::MAX,
120 multiplier: f64::MAX,
121 jitter: 0.0,
122 ..RetryPolicy::default()
123 };
124 let _ = p.backoff(40);
125 }
126}