halldyll_core/fetch/
retry.rs

1//! Retry - Retry strategy with exponential backoff and jitter
2
3use rand::Rng;
4use std::time::Duration;
5
6use crate::types::error::Error;
7
8/// Retry policy
9#[derive(Debug, Clone)]
10pub struct RetryPolicy {
11    /// Maximum number of attempts
12    pub max_retries: u32,
13    /// Initial delay (ms)
14    pub initial_delay_ms: u64,
15    /// Backoff multiplier
16    pub multiplier: f64,
17    /// Max jitter (ms)
18    pub max_jitter_ms: u64,
19    /// Max delay (ms)
20    pub max_delay_ms: u64,
21    /// Retry on timeout errors
22    pub retry_on_timeout: bool,
23    /// Retry on connection errors
24    pub retry_on_connection_error: bool,
25    /// Retry on 5xx server errors
26    pub retry_on_5xx: bool,
27    /// Retry on 429 rate limit errors
28    pub retry_on_429: bool,
29}
30
31impl Default for RetryPolicy {
32    fn default() -> Self {
33        Self {
34            max_retries: 3,
35            initial_delay_ms: 1000,
36            multiplier: 2.0,
37            max_jitter_ms: 500,
38            max_delay_ms: 30000,
39            retry_on_timeout: true,
40            retry_on_connection_error: true,
41            retry_on_5xx: true,
42            retry_on_429: true,
43        }
44    }
45}
46
47impl RetryPolicy {
48    /// Calculates the delay for a given attempt (with jitter)
49    pub fn delay_for_attempt(&self, attempt: u32) -> Duration {
50        let base_delay = self.initial_delay_ms as f64 * self.multiplier.powi(attempt as i32);
51        let capped_delay = base_delay.min(self.max_delay_ms as f64);
52
53        // Add jitter
54        let jitter = if self.max_jitter_ms > 0 {
55            let mut rng = rand::thread_rng();
56            rng.gen_range(0..=self.max_jitter_ms)
57        } else {
58            0
59        };
60
61        Duration::from_millis(capped_delay as u64 + jitter)
62    }
63
64    /// Should we retry this error?
65    pub fn should_retry(&self, error: &Error, attempt: u32) -> bool {
66        if attempt >= self.max_retries {
67            return false;
68        }
69
70        match error {
71            Error::Timeout(_) => self.retry_on_timeout,
72            Error::Network(e) => {
73                if e.is_timeout() {
74                    self.retry_on_timeout
75                } else if e.is_connect() {
76                    self.retry_on_connection_error
77                } else if let Some(status) = e.status() {
78                    if status.as_u16() == 429 {
79                        self.retry_on_429
80                    } else if status.is_server_error() {
81                        self.retry_on_5xx
82                    } else {
83                        false
84                    }
85                } else {
86                    self.retry_on_connection_error
87                }
88            }
89            Error::RateLimited(_) => self.retry_on_429,
90            _ => false,
91        }
92    }
93
94    /// Should we retry this HTTP status code?
95    pub fn should_retry_status(&self, status_code: u16, attempt: u32) -> bool {
96        if attempt >= self.max_retries {
97            return false;
98        }
99
100        match status_code {
101            429 => self.retry_on_429,
102            500..=599 => self.retry_on_5xx,
103            _ => false,
104        }
105    }
106}
107
108/// Retry state
109#[derive(Debug)]
110pub struct RetryState {
111    policy: RetryPolicy,
112    attempts: u32,
113}
114
115impl RetryState {
116    /// New state
117    pub fn new(policy: RetryPolicy) -> Self {
118        Self {
119            policy,
120            attempts: 0,
121        }
122    }
123
124    /// Increments the attempts counter
125    pub fn increment(&mut self) {
126        self.attempts += 1;
127    }
128
129    /// Number of attempts
130    pub fn attempts(&self) -> u32 {
131        self.attempts
132    }
133
134    /// Delay before next attempt
135    pub fn next_delay(&self) -> Duration {
136        self.policy.delay_for_attempt(self.attempts)
137    }
138
139    /// Should we retry?
140    pub fn should_retry(&self, error: &Error) -> bool {
141        self.policy.should_retry(error, self.attempts)
142    }
143
144    /// Returns the policy
145    pub fn policy(&self) -> &RetryPolicy {
146        &self.policy
147    }
148}