1use std::time::Duration;
2
3use crate::util::epoch_plus;
4
5pub(crate) const GATEWAY_STATUSES: [u16; 3] = [502, 503, 504];
6
7#[derive(Debug, Clone, Copy)]
8#[non_exhaustive]
9pub enum RetryTrigger {
10 ConnectionError,
11 Status(u16),
12 ServerError,
13 Timeout,
14}
15
16#[derive(Debug, Clone)]
17#[non_exhaustive]
18pub struct RetryPolicy {
19 pub(crate) max_retries: u32,
20 pub(crate) initial_backoff: Duration,
21 pub(crate) max_backoff: Duration,
22 pub(crate) max_retry_after: Duration,
23 pub(crate) backoff_factor: f64,
24 pub(crate) jitter: bool,
25 pub(crate) retry_on: Vec<RetryTrigger>,
26 pub(crate) allow_non_idempotent: bool,
27}
28
29impl Default for RetryPolicy {
30 fn default() -> Self {
31 Self::none()
32 }
33}
34
35impl RetryPolicy {
36 pub fn none() -> Self {
37 Self {
38 max_retries: 0,
39 initial_backoff: Duration::from_millis(0),
40 max_backoff: Duration::from_millis(0),
41 max_retry_after: Duration::MAX,
42 backoff_factor: 1.0,
43 jitter: false,
44 retry_on: Vec::new(),
45 allow_non_idempotent: false,
46 }
47 }
48
49 pub fn transient() -> Self {
50 Self {
51 max_retries: 3,
52 initial_backoff: Duration::from_millis(100),
53 max_backoff: Duration::from_secs(1),
54 max_retry_after: Duration::MAX,
55 backoff_factor: 2.0,
56 jitter: true,
57 retry_on: [RetryTrigger::ConnectionError, RetryTrigger::Status(429)]
58 .into_iter()
59 .chain(GATEWAY_STATUSES.map(RetryTrigger::Status))
60 .chain([RetryTrigger::Timeout])
61 .collect(),
62 allow_non_idempotent: false,
63 }
64 }
65
66 pub fn max_retries(mut self, n: u32) -> Self {
67 self.max_retries = n;
68 self
69 }
70
71 pub fn initial_backoff(mut self, d: Duration) -> Self {
72 self.initial_backoff = d;
73 self
74 }
75
76 pub fn max_backoff(mut self, d: Duration) -> Self {
77 self.max_backoff = d;
78 self
79 }
80
81 pub fn backoff_factor(mut self, factor: f64) -> Self {
82 self.backoff_factor = factor;
83 self
84 }
85
86 pub fn jitter(mut self, on: bool) -> Self {
87 self.jitter = on;
88 self
89 }
90
91 pub fn max_retry_after(mut self, d: Duration) -> Self {
92 self.max_retry_after = d;
93 self
94 }
95
96 pub fn on_status(mut self, code: u16) -> Self {
97 self.retry_on.push(RetryTrigger::Status(code));
98 self
99 }
100
101 pub fn retry_on(mut self, triggers: impl IntoIterator<Item = RetryTrigger>) -> Self {
102 self.retry_on = triggers.into_iter().collect();
103 self
104 }
105
106 pub fn allow_non_idempotent(mut self, allow: bool) -> Self {
107 self.allow_non_idempotent = allow;
108 self
109 }
110
111 pub(crate) fn is_none(&self) -> bool {
112 self.max_retries == 0
113 }
114
115 pub(crate) fn matches_status(&self, status: u16) -> bool {
116 for t in &self.retry_on {
117 match *t {
118 RetryTrigger::Status(s) if s == status => return true,
119 RetryTrigger::ServerError if (500..600).contains(&status) => return true,
120 _ => {}
121 }
122 }
123 false
124 }
125
126 pub(crate) fn matches_connection_error(&self) -> bool {
127 self.retry_on
128 .iter()
129 .any(|t| matches!(t, RetryTrigger::ConnectionError))
130 }
131
132 pub(crate) fn matches_timeout(&self) -> bool {
133 self.retry_on
134 .iter()
135 .any(|t| matches!(t, RetryTrigger::Timeout))
136 }
137
138 pub(crate) fn delay(&self, attempt: u32) -> Duration {
139 let base = self.initial_backoff.as_secs_f64();
140 let raw = base * self.backoff_factor.powi(attempt as i32);
141 let capped = raw.min(self.max_backoff.as_secs_f64());
142 let jittered = if self.jitter {
143 capped * cheap_jitter()
144 } else {
145 capped
146 };
147 Duration::from_secs_f64(jittered.max(0.0))
148 }
149}
150
151fn cheap_jitter() -> f64 {
152 use rand::Rng;
153 rand::rng().random_range(0.0..=1.0)
154}
155
156pub(crate) fn parse_retry_after(value: &str) -> Option<Duration> {
157 let value = value.trim();
158 if let Ok(secs) = value.parse::<u64>() {
159 return Some(Duration::from_secs(secs));
160 }
161 let when = parse_imf_fixdate(value)?;
162 Some(
163 when.duration_since(std::time::SystemTime::now())
164 .unwrap_or(Duration::ZERO),
165 )
166}
167
168fn parse_imf_fixdate(s: &str) -> Option<std::time::SystemTime> {
169 let rest = s.split_once(", ")?.1;
170 let mut parts = rest.split_whitespace();
171 let day: u32 = parts.next()?.parse().ok()?;
172 let month = month_num(parts.next()?)?;
173 let year: i32 = parts.next()?.parse().ok()?;
174 let hms = parts.next()?;
175 let tz = parts.next()?;
176 if !tz.eq_ignore_ascii_case("GMT") && !tz.eq_ignore_ascii_case("UTC") {
177 return None;
178 }
179 let mut t = hms.split(':');
180 let hour: u32 = t.next()?.parse().ok()?;
181 let min: u32 = t.next()?.parse().ok()?;
182 let sec: u32 = t.next()?.parse().ok()?;
183 unix_from_ymd_hms(year, month, day, hour, min, sec)
184}
185
186fn month_num(month: &str) -> Option<u32> {
187 Some(match month {
188 "Jan" => 1,
189 "Feb" => 2,
190 "Mar" => 3,
191 "Apr" => 4,
192 "May" => 5,
193 "Jun" => 6,
194 "Jul" => 7,
195 "Aug" => 8,
196 "Sep" => 9,
197 "Oct" => 10,
198 "Nov" => 11,
199 "Dec" => 12,
200 _ => return None,
201 })
202}
203
204fn unix_from_ymd_hms(
205 year: i32,
206 month: u32,
207 day: u32,
208 hour: u32,
209 min: u32,
210 sec: u32,
211) -> Option<std::time::SystemTime> {
212 if !(1..=12).contains(&month) || !(1..=31).contains(&day) || hour > 23 || min > 59 || sec > 59 {
213 return None;
214 }
215 let mut y = year;
216 if month <= 2 {
217 y -= 1;
218 }
219 let era = y.div_euclid(400);
220 let yoe = (y - era * 400) as u32;
221 let shifted = if month > 2 { month - 3 } else { month + 9 };
222 let doy = (153 * shifted + 2) / 5 + day - 1;
223 let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
224 let days = i64::from(era) * 146097 + i64::from(doe) - 719468;
225 let secs = days
226 .checked_mul(86400)?
227 .checked_add(i64::from(hour) * 3600 + i64::from(min) * 60 + i64::from(sec))?;
228 if secs < 0 {
229 return None;
230 }
231 epoch_plus(Duration::from_secs(secs as u64))
232}
233
234#[cfg(test)]
235mod tests;