1use std::time::Duration;
2
3#[derive(Debug, Clone)]
5pub struct RetryConfig {
6 pub max_retries: u32,
8 pub base_delay: Duration,
10 pub max_delay: Duration,
12 pub retryable_status_codes: Vec<u16>,
14 pub circuit_threshold: u32,
16 pub circuit_timeout: Duration,
18 pub jitter: f64,
20}
21
22impl Default for RetryConfig {
23 fn default() -> Self {
24 Self {
25 max_retries: 3,
26 base_delay: Duration::from_millis(500),
27 max_delay: Duration::from_secs(30),
28 retryable_status_codes: vec![408, 429, 500, 502, 503, 504],
29 circuit_threshold: 5,
30 circuit_timeout: Duration::from_secs(60),
31 jitter: 0.1,
32 }
33 }
34}
35
36impl RetryConfig {
37 pub fn new() -> Self {
39 Self::default()
40 }
41
42 pub fn with_max_retries(mut self, max_retries: u32) -> Self {
44 self.max_retries = max_retries;
45 self
46 }
47
48 pub fn with_base_delay(mut self, delay: Duration) -> Self {
50 self.base_delay = delay;
51 self
52 }
53
54 pub fn with_max_delay(mut self, delay: Duration) -> Self {
56 self.max_delay = delay;
57 self
58 }
59
60 pub fn with_circuit_threshold(mut self, threshold: u32) -> Self {
62 self.circuit_threshold = threshold;
63 self
64 }
65
66 pub fn with_circuit_timeout(mut self, timeout: Duration) -> Self {
68 self.circuit_timeout = timeout;
69 self
70 }
71
72 pub fn with_jitter(mut self, jitter: f64) -> Self {
75 self.jitter = jitter.clamp(0.0, 1.0);
76 self
77 }
78
79 pub fn delay_for_attempt(&self, attempt: u32) -> Duration {
81 let exponential = self.base_delay * 2u32.pow(attempt);
83 let capped = std::cmp::min(exponential, self.max_delay);
84
85 if self.jitter > 0.0 {
87 use rand::prelude::*;
88 let mut rng = rand::rng();
89 let jitter_range = capped.as_millis() as f64 * self.jitter;
90 let jitter_amount = rng.random_range(-jitter_range..jitter_range);
91 let jittered_millis = (capped.as_millis() as f64 + jitter_amount).max(0.0) as u64;
92 Duration::from_millis(jittered_millis)
93 } else {
94 capped
95 }
96 }
97
98 pub fn is_retryable(&self, status: u16) -> bool {
100 self.retryable_status_codes.contains(&status)
101 }
102}
103
104impl RetryConfig {
107 pub fn production() -> Self {
109 Self {
110 max_retries: 5,
111 base_delay: Duration::from_secs(1),
112 max_delay: Duration::from_secs(60),
113 retryable_status_codes: vec![408, 429, 500, 502, 503, 504],
114 circuit_threshold: 10,
115 circuit_timeout: Duration::from_secs(120),
116 jitter: 0.25,
117 }
118 }
119
120 pub fn development() -> Self {
122 Self {
123 max_retries: 2,
124 base_delay: Duration::from_millis(100),
125 max_delay: Duration::from_secs(5),
126 retryable_status_codes: vec![408, 429, 500, 502, 503, 504],
127 circuit_threshold: 3,
128 circuit_timeout: Duration::from_secs(10),
129 jitter: 0.0,
130 }
131 }
132
133 pub fn batch_processing() -> Self {
135 Self {
136 max_retries: 10,
137 base_delay: Duration::from_secs(5),
138 max_delay: Duration::from_secs(300),
139 retryable_status_codes: vec![408, 429, 500, 502, 503, 504],
140 circuit_threshold: 20,
141 circuit_timeout: Duration::from_secs(300),
142 jitter: 0.5,
143 }
144 }
145}
146
147#[cfg(test)]
148mod tests {
149 use super::*;
150
151 #[test]
152 fn test_default_config() {
153 let config = RetryConfig::default();
154 assert_eq!(config.max_retries, 3);
155 assert_eq!(config.base_delay, Duration::from_millis(500));
156 assert_eq!(config.max_delay, Duration::from_secs(30));
157 assert_eq!(config.circuit_threshold, 5);
158 assert_eq!(config.circuit_timeout, Duration::from_secs(60));
159 assert_eq!(config.jitter, 0.1);
160 assert!(config.retryable_status_codes.contains(&429));
161 assert!(config.retryable_status_codes.contains(&503));
162 }
163
164 #[test]
165 fn test_production_config() {
166 let config = RetryConfig::production();
167 assert_eq!(config.max_retries, 5);
168 assert_eq!(config.base_delay, Duration::from_secs(1));
169 assert_eq!(config.max_delay, Duration::from_secs(60));
170 assert_eq!(config.circuit_threshold, 10);
171 assert_eq!(config.circuit_timeout, Duration::from_secs(120));
172 assert_eq!(config.jitter, 0.25);
173 }
174
175 #[test]
176 fn test_development_config() {
177 let config = RetryConfig::development();
178 assert_eq!(config.max_retries, 2);
179 assert_eq!(config.base_delay, Duration::from_millis(100));
180 assert_eq!(config.max_delay, Duration::from_secs(5));
181 assert_eq!(config.circuit_threshold, 3);
182 assert_eq!(config.circuit_timeout, Duration::from_secs(10));
183 assert_eq!(config.jitter, 0.0);
184 }
185
186 #[test]
187 fn test_batch_processing_config() {
188 let config = RetryConfig::batch_processing();
189 assert_eq!(config.max_retries, 10);
190 assert_eq!(config.base_delay, Duration::from_secs(5));
191 assert_eq!(config.max_delay, Duration::from_secs(300));
192 assert_eq!(config.circuit_threshold, 20);
193 assert_eq!(config.circuit_timeout, Duration::from_secs(300));
194 assert_eq!(config.jitter, 0.5);
195 }
196
197 #[test]
198 fn test_builder_methods() {
199 let config = RetryConfig::new()
200 .with_max_retries(10)
201 .with_base_delay(Duration::from_secs(2))
202 .with_max_delay(Duration::from_secs(120))
203 .with_circuit_threshold(15)
204 .with_circuit_timeout(Duration::from_secs(180))
205 .with_jitter(0.5);
206
207 assert_eq!(config.max_retries, 10);
208 assert_eq!(config.base_delay, Duration::from_secs(2));
209 assert_eq!(config.max_delay, Duration::from_secs(120));
210 assert_eq!(config.circuit_threshold, 15);
211 assert_eq!(config.circuit_timeout, Duration::from_secs(180));
212 assert_eq!(config.jitter, 0.5);
213 }
214
215 #[test]
216 fn test_jitter_clamping() {
217 let config = RetryConfig::new().with_jitter(1.5);
218 assert_eq!(config.jitter, 1.0);
219
220 let config = RetryConfig::new().with_jitter(-0.5);
221 assert_eq!(config.jitter, 0.0);
222 }
223
224 #[test]
225 fn test_delay_for_attempt_exponential() {
226 let config = RetryConfig::new()
227 .with_base_delay(Duration::from_millis(100))
228 .with_max_delay(Duration::from_secs(10))
229 .with_jitter(0.0);
230
231 let delay0 = config.delay_for_attempt(0);
233 let delay1 = config.delay_for_attempt(1);
234 let delay2 = config.delay_for_attempt(2);
235 let delay3 = config.delay_for_attempt(3);
236
237 assert_eq!(delay0, Duration::from_millis(100));
239 assert_eq!(delay1, Duration::from_millis(200));
241 assert_eq!(delay2, Duration::from_millis(400));
243 assert_eq!(delay3, Duration::from_millis(800));
245 }
246
247 #[test]
248 fn test_delay_for_attempt_max_cap() {
249 let config = RetryConfig::new()
250 .with_base_delay(Duration::from_secs(1))
251 .with_max_delay(Duration::from_secs(5))
252 .with_jitter(0.0);
253
254 let delay5 = config.delay_for_attempt(5);
256 assert_eq!(delay5, Duration::from_secs(5));
257
258 let delay10 = config.delay_for_attempt(10);
259 assert_eq!(delay10, Duration::from_secs(5));
260 }
261
262 #[test]
263 fn test_delay_with_jitter() {
264 let config = RetryConfig::new()
265 .with_base_delay(Duration::from_millis(1000))
266 .with_max_delay(Duration::from_secs(10))
267 .with_jitter(0.1); for _ in 0..10 {
271 let delay = config.delay_for_attempt(0);
272 assert!(delay >= Duration::from_millis(900));
274 assert!(delay <= Duration::from_millis(1100));
275 }
276 }
277
278 #[test]
279 fn test_is_retryable() {
280 let config = RetryConfig::default();
281
282 assert!(config.is_retryable(408)); assert!(config.is_retryable(429)); assert!(config.is_retryable(500)); assert!(config.is_retryable(502)); assert!(config.is_retryable(503)); assert!(config.is_retryable(504)); assert!(!config.is_retryable(200)); assert!(!config.is_retryable(201)); assert!(!config.is_retryable(400)); assert!(!config.is_retryable(401)); assert!(!config.is_retryable(403)); assert!(!config.is_retryable(404)); }
296
297 #[test]
298 fn test_custom_retryable_status_codes() {
299 let config = RetryConfig::new()
300 .with_max_retries(3)
301 .with_base_delay(Duration::from_millis(500))
302 .with_max_delay(Duration::from_secs(30));
303
304 assert!(config.is_retryable(429));
306 assert!(config.is_retryable(503));
307 }
308}