Skip to main content

wme_client/
retry.rs

1use std::time::Duration;
2
3/// Retry configuration for failed requests.
4#[derive(Debug, Clone)]
5pub struct RetryConfig {
6    /// Maximum number of retries
7    pub max_retries: u32,
8    /// Base delay between retries (exponential backoff)
9    pub base_delay: Duration,
10    /// Maximum delay between retries
11    pub max_delay: Duration,
12    /// HTTP status codes that should trigger a retry
13    pub retryable_status_codes: Vec<u16>,
14    /// Circuit breaker: consecutive failures before opening circuit
15    pub circuit_threshold: u32,
16    /// Circuit breaker: timeout before transitioning from Open to HalfOpen
17    pub circuit_timeout: Duration,
18    /// Jitter factor (0.0-1.0) to add randomness to backoff delays
19    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    /// Create a new retry configuration with default values.
38    pub fn new() -> Self {
39        Self::default()
40    }
41
42    /// Set maximum number of retries.
43    pub fn with_max_retries(mut self, max_retries: u32) -> Self {
44        self.max_retries = max_retries;
45        self
46    }
47
48    /// Set base delay for exponential backoff.
49    pub fn with_base_delay(mut self, delay: Duration) -> Self {
50        self.base_delay = delay;
51        self
52    }
53
54    /// Set maximum delay for exponential backoff.
55    pub fn with_max_delay(mut self, delay: Duration) -> Self {
56        self.max_delay = delay;
57        self
58    }
59
60    /// Set circuit breaker threshold (consecutive failures before opening).
61    pub fn with_circuit_threshold(mut self, threshold: u32) -> Self {
62        self.circuit_threshold = threshold;
63        self
64    }
65
66    /// Set circuit breaker timeout (time before trying again).
67    pub fn with_circuit_timeout(mut self, timeout: Duration) -> Self {
68        self.circuit_timeout = timeout;
69        self
70    }
71
72    /// Set jitter factor (0.0-1.0) to add randomness to backoff.
73    /// This helps prevent thundering herd when services recover.
74    pub fn with_jitter(mut self, jitter: f64) -> Self {
75        self.jitter = jitter.clamp(0.0, 1.0);
76        self
77    }
78
79    /// Calculate delay for a specific retry attempt with exponential backoff and jitter.
80    pub fn delay_for_attempt(&self, attempt: u32) -> Duration {
81        // Exponential backoff: base_delay * 2^attempt
82        let exponential = self.base_delay * 2u32.pow(attempt);
83        let capped = std::cmp::min(exponential, self.max_delay);
84
85        // Add jitter to prevent thundering herd
86        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    /// Check if a status code should trigger a retry.
99    pub fn is_retryable(&self, status: u16) -> bool {
100        self.retryable_status_codes.contains(&status)
101    }
102}
103
104/// Default retry configuration for production use.
105/// More conservative to be a good API citizen.
106impl RetryConfig {
107    /// Production-grade configuration with conservative defaults.
108    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    /// Aggressive retry configuration for development/testing.
121    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    /// Configuration optimized for batch processing with longer delays.
134    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        // Test exponential backoff without jitter
232        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        // 100ms * 2^0 = 100ms
238        assert_eq!(delay0, Duration::from_millis(100));
239        // 100ms * 2^1 = 200ms
240        assert_eq!(delay1, Duration::from_millis(200));
241        // 100ms * 2^2 = 400ms
242        assert_eq!(delay2, Duration::from_millis(400));
243        // 100ms * 2^3 = 800ms
244        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        // After several attempts, should hit the max delay
255        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); // 10% jitter
268
269        // Run multiple times to account for randomness
270        for _ in 0..10 {
271            let delay = config.delay_for_attempt(0);
272            // With 10% jitter on 1000ms, delay should be between 900ms and 1100ms
273            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)); // Request timeout
283        assert!(config.is_retryable(429)); // Too many requests
284        assert!(config.is_retryable(500)); // Internal server error
285        assert!(config.is_retryable(502)); // Bad gateway
286        assert!(config.is_retryable(503)); // Service unavailable
287        assert!(config.is_retryable(504)); // Gateway timeout
288
289        assert!(!config.is_retryable(200)); // OK
290        assert!(!config.is_retryable(201)); // Created
291        assert!(!config.is_retryable(400)); // Bad request
292        assert!(!config.is_retryable(401)); // Unauthorized
293        assert!(!config.is_retryable(403)); // Forbidden
294        assert!(!config.is_retryable(404)); // Not found
295    }
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        // Test default retryable codes are set
305        assert!(config.is_retryable(429));
306        assert!(config.is_retryable(503));
307    }
308}