1use std::collections::HashSet;
2use std::time::{Duration, SystemTime};
3
4use http::{HeaderMap, StatusCode};
5
6use crate::error::Error;
7
8#[derive(Clone, Debug, PartialEq, Eq)]
10pub struct StatusSet {
11 codes: HashSet<u16>,
12}
13
14impl StatusSet {
15 #[must_use]
17 pub fn new() -> Self {
18 Self {
19 codes: HashSet::new(),
20 }
21 }
22
23 #[must_use]
25 pub fn retryable() -> Self {
26 let mut codes = HashSet::with_capacity(102);
27 codes.insert(408);
28 codes.insert(429);
29 for status in 500..=599 {
30 codes.insert(status);
31 }
32 Self { codes }
33 }
34
35 #[must_use]
37 pub fn from_codes(codes: impl IntoIterator<Item = u16>) -> Self {
38 Self {
39 codes: codes.into_iter().collect(),
40 }
41 }
42
43 pub fn insert(&mut self, code: u16) {
45 self.codes.insert(code);
46 }
47
48 #[must_use]
50 pub fn contains(&self, code: u16) -> bool {
51 self.codes.contains(&code)
52 }
53
54 #[must_use]
56 pub fn contains_status(&self, status: StatusCode) -> bool {
57 self.contains(status.as_u16())
58 }
59}
60
61impl Default for StatusSet {
62 fn default() -> Self {
63 Self::new()
64 }
65}
66
67impl FromIterator<u16> for StatusSet {
68 fn from_iter<T: IntoIterator<Item = u16>>(iter: T) -> Self {
69 Self::from_codes(iter)
70 }
71}
72
73#[derive(Clone, Debug, PartialEq)]
93pub struct RetryPolicy {
94 pub max_retries: u32,
96 pub backoff_initial: Duration,
98 pub backoff_max: Duration,
100 pub backoff_jitter: f64,
102 pub http_statuses: StatusSet,
104 pub respect_retry_after: bool,
106 pub max_retry_after: Duration,
108 pub retry_connection_errors: bool,
110 pub retry_timeouts: bool,
112 pub connect_only_before_send: bool,
117}
118
119impl Default for RetryPolicy {
120 fn default() -> Self {
121 Self {
122 max_retries: 2,
123 backoff_initial: Duration::from_millis(500),
124 backoff_max: Duration::from_secs(5),
125 backoff_jitter: 0.25,
126 http_statuses: StatusSet::retryable(),
127 respect_retry_after: true,
128 max_retry_after: Duration::from_secs(60),
129 retry_connection_errors: true,
130 retry_timeouts: true,
131 connect_only_before_send: false,
132 }
133 }
134}
135
136impl RetryPolicy {
137 #[must_use]
139 pub fn none() -> Self {
140 Self {
141 max_retries: 0,
142 ..Self::default()
143 }
144 }
145
146 #[must_use]
148 pub fn conservative() -> Self {
149 Self {
150 http_statuses: StatusSet::from_codes([408, 429]),
151 retry_timeouts: false,
152 retry_connection_errors: true,
153 connect_only_before_send: true,
154 ..Self::default()
155 }
156 }
157
158 pub(crate) fn validate(&self) -> Result<(), Error> {
159 if !(0.0..=1.0).contains(&self.backoff_jitter) {
160 return Err(Error::InvalidRequest(format!(
161 "`retry.backoff_jitter` must be between 0 and 1, got {}.",
162 self.backoff_jitter
163 )));
164 }
165 Ok(())
166 }
167
168 pub(crate) fn retries_connection(&self, pre_send: bool) -> bool {
169 if !self.retry_connection_errors {
170 return false;
171 }
172 if self.connect_only_before_send {
173 return pre_send;
174 }
175 true
176 }
177
178 #[must_use]
182 pub fn parse_retry_after(headers: &HeaderMap) -> Option<Duration> {
183 Self::parse_retry_after_at(headers, SystemTime::now())
184 }
185
186 #[must_use]
188 pub fn parse_retry_after_at(headers: &HeaderMap, now: SystemTime) -> Option<Duration> {
189 if let Some(value) = headers.get("retry-after-ms") {
190 if let Ok(raw) = value.to_str() {
191 if let Ok(ms) = raw.trim().parse::<f64>() {
192 if ms.is_finite() && ms >= 0.0 {
193 return Some(duration_from_millis_f64(ms));
194 }
195 }
196 }
197 }
198
199 let raw = headers.get("retry-after")?.to_str().ok()?.trim();
200 if let Ok(seconds) = raw.parse::<f64>() {
201 if seconds.is_finite() && seconds >= 0.0 {
202 return Some(duration_from_millis_f64(seconds * 1000.0));
203 }
204 return None;
205 }
206 let date = httpdate::parse_http_date(raw).ok()?;
207 let delay = date.duration_since(now).unwrap_or_default();
208 Some(delay)
209 }
210
211 #[must_use]
213 pub fn delay_after_failure(&self, attempt: u32, headers: Option<&HeaderMap>) -> Duration {
214 self.delay_after_failure_with(attempt, headers, fastrand::f64)
215 }
216
217 pub fn delay_after_failure_with<F>(
221 &self,
222 attempt: u32,
223 headers: Option<&HeaderMap>,
224 rng: F,
225 ) -> Duration
226 where
227 F: FnOnce() -> f64,
228 {
229 if self.respect_retry_after {
230 if let Some(headers) = headers {
231 if let Some(delay) = Self::parse_retry_after(headers) {
232 if delay <= self.max_retry_after {
233 return delay;
234 }
235 }
236 }
237 }
238
239 let factor = 1u32.checked_shl(attempt).unwrap_or(u32::MAX);
240 let exponential = self
241 .backoff_initial
242 .saturating_mul(factor)
243 .min(self.backoff_max);
244 let exp_ms = exponential.as_secs_f64() * 1000.0;
245 let jitter = rng().clamp(0.0, 1.0) * self.backoff_jitter;
246 duration_from_millis_f64((exp_ms * (1.0 - jitter)).round().max(0.0))
247 }
248}
249
250fn duration_from_millis_f64(ms: f64) -> Duration {
251 let nanos = (ms * 1_000_000.0).round().max(0.0) as u128;
252 let nanos = u64::try_from(nanos).unwrap_or(u64::MAX);
253 Duration::from_nanos(nanos)
254}
255
256#[cfg(test)]
257mod tests {
258 use super::*;
259 use http::HeaderValue;
260
261 fn policy() -> RetryPolicy {
262 RetryPolicy::default()
263 }
264
265 #[test]
266 fn default_statuses_include_408_429_and_5xx() {
267 let s = StatusSet::retryable();
268 assert!(s.contains(408));
269 assert!(s.contains(429));
270 assert!(s.contains(500));
271 assert!(s.contains(529));
272 assert!(s.contains(599));
273 assert!(!s.contains(400));
274 assert!(!s.contains(422));
275 }
276
277 #[test]
278 fn delay_without_headers_zero_jitter_is_initial() {
279 let d = policy().delay_after_failure_with(0, None, || 0.0);
280 assert_eq!(d, Duration::from_millis(500));
281 }
282
283 #[test]
284 fn delay_full_jitter_subtracts_25_percent() {
285 let d = policy().delay_after_failure_with(0, None, || 1.0);
286 assert_eq!(d, Duration::from_millis(375));
287 }
288
289 #[test]
290 fn delay_doubles_per_attempt_until_cap() {
291 let p = policy();
292 assert_eq!(
293 p.delay_after_failure_with(1, None, || 0.0),
294 Duration::from_millis(1000)
295 );
296 assert_eq!(
297 p.delay_after_failure_with(2, None, || 0.0),
298 Duration::from_millis(2000)
299 );
300 assert_eq!(
301 p.delay_after_failure_with(4, None, || 0.0),
302 Duration::from_secs(5)
303 );
304 }
305
306 #[test]
307 fn retry_after_ms_wins_over_backoff() {
308 let mut headers = HeaderMap::new();
309 headers.insert("retry-after-ms", HeaderValue::from_static("120"));
310 let d = policy().delay_after_failure_with(0, Some(&headers), || 0.0);
311 assert_eq!(d, Duration::from_millis(120));
312 }
313
314 #[test]
315 fn retry_after_ms_over_max_falls_back_to_backoff() {
316 let mut headers = HeaderMap::new();
317 headers.insert("retry-after-ms", HeaderValue::from_static("70000"));
318 let d = policy().delay_after_failure_with(0, Some(&headers), || 0.0);
319 assert_eq!(d, Duration::from_millis(500));
320 }
321
322 #[test]
323 fn retry_after_seconds() {
324 let mut headers = HeaderMap::new();
325 headers.insert("retry-after", HeaderValue::from_static("2"));
326 let d = RetryPolicy::parse_retry_after(&headers).unwrap();
327 assert_eq!(d, Duration::from_secs(2));
328 }
329
330 #[test]
331 fn retry_after_http_date() {
332 let now = SystemTime::UNIX_EPOCH + Duration::from_secs(1_000);
333 let later = now + Duration::from_secs(4);
334 let mut headers = HeaderMap::new();
335 headers.insert(
336 "retry-after",
337 HeaderValue::from_str(&httpdate::fmt_http_date(later)).unwrap(),
338 );
339 let d = RetryPolicy::parse_retry_after_at(&headers, now).unwrap();
340 assert_eq!(d, Duration::from_secs(4));
341 }
342
343 #[test]
344 fn past_http_date_clamps_to_zero() {
345 let now = SystemTime::UNIX_EPOCH + Duration::from_secs(1_000);
346 let past = SystemTime::UNIX_EPOCH;
347 let mut headers = HeaderMap::new();
348 headers.insert(
349 "retry-after",
350 HeaderValue::from_str(&httpdate::fmt_http_date(past)).unwrap(),
351 );
352 let d = RetryPolicy::parse_retry_after_at(&headers, now).unwrap();
353 assert_eq!(d, Duration::ZERO);
354 }
355
356 #[test]
357 fn invalid_retry_after_ignored() {
358 let mut headers = HeaderMap::new();
359 headers.insert("retry-after-ms", HeaderValue::from_static("nope"));
360 headers.insert("retry-after", HeaderValue::from_static("also-nope"));
361 assert!(RetryPolicy::parse_retry_after(&headers).is_none());
362 }
363
364 #[test]
365 fn negative_retry_after_ignored() {
366 let mut headers = HeaderMap::new();
367 headers.insert("retry-after", HeaderValue::from_static("-1"));
368 assert!(RetryPolicy::parse_retry_after(&headers).is_none());
369 }
370
371 #[test]
372 fn retry_after_ms_preferred_over_retry_after() {
373 let mut headers = HeaderMap::new();
374 headers.insert("retry-after-ms", HeaderValue::from_static("50"));
375 headers.insert("retry-after", HeaderValue::from_static("9"));
376 let d = RetryPolicy::parse_retry_after(&headers).unwrap();
377 assert_eq!(d, Duration::from_millis(50));
378 }
379
380 #[test]
381 fn none_has_zero_retries() {
382 assert_eq!(RetryPolicy::none().max_retries, 0);
383 }
384
385 #[test]
386 fn conservative_skips_5xx_and_timeouts() {
387 let p = RetryPolicy::conservative();
388 assert!(!p.http_statuses.contains(500));
389 assert!(p.http_statuses.contains(429));
390 assert!(p.http_statuses.contains(408));
391 assert!(!p.retry_timeouts);
392 assert!(p.retry_connection_errors);
393 assert!(p.connect_only_before_send);
394 assert!(p.retries_connection(true));
395 assert!(!p.retries_connection(false));
396 }
397
398 #[test]
399 fn default_retries_all_connection_errors() {
400 let p = RetryPolicy::default();
401 assert!(p.retries_connection(true));
402 assert!(p.retries_connection(false));
403 }
404}