armature_http_client/
retry.rs1use std::time::Duration;
4
5#[derive(Debug, Clone)]
7pub struct RetryConfig {
8 pub max_attempts: u32,
10 pub backoff: BackoffStrategy,
12 pub retry_status_codes: Vec<u16>,
14 pub retry_on_connection_error: bool,
16 pub retry_on_timeout: bool,
18 pub max_retry_time: Option<Duration>,
20}
21
22impl Default for RetryConfig {
23 fn default() -> Self {
24 Self {
25 max_attempts: 3,
26 backoff: BackoffStrategy::Exponential {
27 initial: Duration::from_millis(100),
28 max: Duration::from_secs(10),
29 multiplier: 2.0,
30 },
31 retry_status_codes: vec![408, 429, 500, 502, 503, 504],
32 retry_on_connection_error: true,
33 retry_on_timeout: true,
34 max_retry_time: Some(Duration::from_secs(60)),
35 }
36 }
37}
38
39impl RetryConfig {
40 pub fn exponential(max_attempts: u32, initial_delay: Duration) -> Self {
42 Self {
43 max_attempts,
44 backoff: BackoffStrategy::Exponential {
45 initial: initial_delay,
46 max: Duration::from_secs(30),
47 multiplier: 2.0,
48 },
49 ..Default::default()
50 }
51 }
52
53 pub fn linear(max_attempts: u32, delay: Duration) -> Self {
55 Self {
56 max_attempts,
57 backoff: BackoffStrategy::Linear {
58 delay,
59 max: Duration::from_secs(30),
60 },
61 ..Default::default()
62 }
63 }
64
65 pub fn constant(max_attempts: u32, delay: Duration) -> Self {
67 Self {
68 max_attempts,
69 backoff: BackoffStrategy::Constant(delay),
70 ..Default::default()
71 }
72 }
73
74 pub fn immediate(max_attempts: u32) -> Self {
76 Self {
77 max_attempts,
78 backoff: BackoffStrategy::None,
79 ..Default::default()
80 }
81 }
82
83 pub fn with_status_codes(mut self, codes: Vec<u16>) -> Self {
85 self.retry_status_codes = codes;
86 self
87 }
88
89 pub fn no_retry_on_connection(mut self) -> Self {
91 self.retry_on_connection_error = false;
92 self
93 }
94
95 pub fn no_retry_on_timeout(mut self) -> Self {
97 self.retry_on_timeout = false;
98 self
99 }
100
101 pub fn with_max_retry_time(mut self, duration: Duration) -> Self {
103 self.max_retry_time = Some(duration);
104 self
105 }
106
107 pub fn delay_for_attempt(&self, attempt: u32) -> Duration {
109 self.backoff.delay_for_attempt(attempt)
110 }
111
112 pub fn should_retry_status(&self, status: u16) -> bool {
114 self.retry_status_codes.contains(&status)
115 }
116}
117
118#[derive(Debug, Clone)]
120pub enum BackoffStrategy {
121 None,
123 Constant(Duration),
125 Linear {
127 delay: Duration,
129 max: Duration,
131 },
132 Exponential {
134 initial: Duration,
136 max: Duration,
138 multiplier: f64,
140 },
141}
142
143impl BackoffStrategy {
144 pub fn delay_for_attempt(&self, attempt: u32) -> Duration {
146 match self {
147 Self::None => Duration::ZERO,
148 Self::Constant(d) => *d,
149 Self::Linear { delay, max } => {
150 let total = delay.saturating_mul(attempt + 1);
151 total.min(*max)
152 }
153 Self::Exponential {
154 initial,
155 max,
156 multiplier,
157 } => {
158 let factor = multiplier.powi(attempt as i32);
159 let millis = (initial.as_millis() as f64 * factor) as u64;
160 Duration::from_millis(millis).min(*max)
161 }
162 }
163 }
164}
165
166pub trait RetryStrategy: Send + Sync {
168 fn should_retry(&self, attempt: u32, error: &crate::HttpClientError) -> bool;
170
171 fn retry_delay(&self, attempt: u32) -> Duration;
173}
174
175impl RetryStrategy for RetryConfig {
176 fn should_retry(&self, attempt: u32, error: &crate::HttpClientError) -> bool {
177 if attempt >= self.max_attempts {
178 return false;
179 }
180
181 match error {
182 crate::HttpClientError::Timeout(_) => self.retry_on_timeout,
183 crate::HttpClientError::Connection(_) => self.retry_on_connection_error,
184 crate::HttpClientError::Response { status, .. } => {
185 self.retry_status_codes.contains(status)
186 }
187 crate::HttpClientError::Http(e) => {
188 if e.is_timeout() {
189 self.retry_on_timeout
190 } else if e.is_connect() {
191 self.retry_on_connection_error
192 } else if let Some(status) = e.status() {
193 self.retry_status_codes.contains(&status.as_u16())
194 } else {
195 false
196 }
197 }
198 _ => false,
199 }
200 }
201
202 fn retry_delay(&self, attempt: u32) -> Duration {
203 self.delay_for_attempt(attempt)
204 }
205}
206
207#[cfg(test)]
208mod tests {
209 use super::*;
210
211 #[test]
212 fn test_exponential_backoff() {
213 let strategy = BackoffStrategy::Exponential {
214 initial: Duration::from_millis(100),
215 max: Duration::from_secs(10),
216 multiplier: 2.0,
217 };
218
219 assert_eq!(strategy.delay_for_attempt(0), Duration::from_millis(100));
220 assert_eq!(strategy.delay_for_attempt(1), Duration::from_millis(200));
221 assert_eq!(strategy.delay_for_attempt(2), Duration::from_millis(400));
222 assert_eq!(strategy.delay_for_attempt(3), Duration::from_millis(800));
223 }
224
225 #[test]
226 fn test_linear_backoff() {
227 let strategy = BackoffStrategy::Linear {
228 delay: Duration::from_millis(100),
229 max: Duration::from_secs(1),
230 };
231
232 assert_eq!(strategy.delay_for_attempt(0), Duration::from_millis(100));
233 assert_eq!(strategy.delay_for_attempt(1), Duration::from_millis(200));
234 assert_eq!(strategy.delay_for_attempt(9), Duration::from_secs(1));
235 }
236
237 #[test]
238 fn test_constant_backoff() {
239 let strategy = BackoffStrategy::Constant(Duration::from_millis(500));
240
241 assert_eq!(strategy.delay_for_attempt(0), Duration::from_millis(500));
242 assert_eq!(strategy.delay_for_attempt(5), Duration::from_millis(500));
243 }
244}