Skip to main content

rightkit_http/
retry.rs

1use std::time::Duration;
2
3/// Retry/backoff policy. `max_attempts` counts total tries (1 = no retry).
4#[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    /// Fraction (0.0..=1.0) of each delay randomly shaved off; 0 = deterministic.
11    pub jitter: f64,
12    pub retry_statuses: Vec<u16>,
13    pub honor_retry_after: bool,
14    /// Upper bound applied to a server-supplied Retry-After.
15    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    /// Delay before retry number `retry` (1 = after the first failure).
46    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    /// Delay for a given failure: the larger of backoff and a (capped) Retry-After.
59    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
76/// Parse a Retry-After header (delta-seconds only; HTTP-dates are ignored).
77pub 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}