Skip to main content

cloud_lite_core_rs/
retry.rs

1//! Retry and backoff configuration for HTTP requests.
2
3use std::time::Duration;
4
5/// Configuration for retry behavior
6#[derive(Debug, Clone)]
7pub struct RetryConfig {
8    /// Maximum number of retry attempts
9    pub max_retries: u32,
10
11    /// Initial backoff duration
12    pub initial_backoff: Duration,
13
14    /// Maximum backoff duration
15    pub max_backoff: Duration,
16
17    /// Backoff multiplier (exponential backoff factor)
18    pub backoff_multiplier: f64,
19
20    /// Whether to add random jitter to backoff
21    pub jitter: bool,
22
23    /// Whether to retry on 401 (with token refresh)
24    pub retry_on_401: bool,
25}
26
27impl Default for RetryConfig {
28    fn default() -> Self {
29        Self {
30            max_retries: 3,
31            initial_backoff: Duration::from_secs(1),
32            max_backoff: Duration::from_secs(30),
33            backoff_multiplier: 2.0,
34            jitter: true,
35            retry_on_401: true,
36        }
37    }
38}
39
40impl RetryConfig {
41    /// Compute the next backoff duration.
42    ///
43    /// # Arguments
44    ///
45    /// * `current_backoff` - Current backoff duration
46    /// * `retry_after` - Optional retry-after duration from server
47    ///
48    /// # Returns
49    ///
50    /// The duration to wait before the next retry
51    pub fn compute_backoff(
52        &self,
53        current_backoff: Duration,
54        retry_after: Option<Duration>,
55    ) -> Duration {
56        // If server specified retry-after, use that (don't cap it)
57        if let Some(retry_after) = retry_after {
58            return retry_after;
59        }
60
61        // Otherwise use calculated backoff
62        let backoff = current_backoff.min(self.max_backoff);
63
64        // Add jitter if enabled
65        if self.jitter {
66            self.add_jitter(backoff)
67        } else {
68            backoff
69        }
70    }
71
72    fn add_jitter(&self, duration: Duration) -> Duration {
73        use std::collections::hash_map::RandomState;
74        use std::hash::BuildHasher;
75
76        // Simple jitter: ±20% of duration
77        let millis = duration.as_millis() as f64;
78        let jitter_range = millis * 0.2;
79
80        // Use RandomState for quick randomness
81        let random =
82            (RandomState::new().hash_one(std::time::SystemTime::now()) % 100) as f64 / 100.0;
83
84        let jittered = millis + (jitter_range * (random * 2.0 - 1.0));
85        Duration::from_millis(jittered.max(0.0) as u64)
86    }
87}
88
89#[cfg(test)]
90mod tests {
91    use super::*;
92
93    #[test]
94    fn jitter_stays_within_bounds() {
95        let config = RetryConfig::default();
96        let base = Duration::from_secs(10);
97
98        for _ in 0..100 {
99            let jittered = config.add_jitter(base);
100            let millis = jittered.as_millis();
101
102            // Should be within ±20% of base (8s - 12s)
103            assert!(millis >= 8000);
104            assert!(millis <= 12000);
105        }
106    }
107}