Skip to main content

loop_retry/
config.rs

1//! Retry configuration.
2
3use std::time::Duration;
4
5/// Configuration for retry behavior.
6#[derive(Debug, Clone)]
7pub struct RetryConfig {
8    /// Maximum number of retry attempts (0 = no retries, 1 = one retry).
9    pub max_retries: u32,
10
11    /// Initial delay before the first retry.
12    pub initial_delay: Duration,
13
14    /// Maximum delay between retries (caps exponential growth).
15    pub max_delay: Duration,
16
17    /// Multiplier for exponential backoff (e.g., 2.0 doubles each time).
18    pub backoff_multiplier: f64,
19
20    /// Whether to add random jitter to the delay.
21    pub jitter: bool,
22}
23
24impl Default for RetryConfig {
25    fn default() -> Self {
26        Self {
27            max_retries: 3,
28            initial_delay: Duration::from_millis(100),
29            max_delay: Duration::from_secs(5),
30            backoff_multiplier: 2.0,
31            jitter: true,
32        }
33    }
34}
35
36impl RetryConfig {
37    /// Create a config with no retries (try once only).
38    pub fn no_retries() -> Self {
39        Self {
40            max_retries: 0,
41            ..Default::default()
42        }
43    }
44
45    /// Create a config for aggressive retries (10 attempts, fast backoff).
46    pub fn aggressive() -> Self {
47        Self {
48            max_retries: 10,
49            initial_delay: Duration::from_millis(50),
50            max_delay: Duration::from_secs(30),
51            backoff_multiplier: 2.0,
52            jitter: true,
53        }
54    }
55
56    /// Calculate the delay for a given attempt number (0-indexed).
57    pub fn delay_for_attempt(&self, attempt: u32) -> Duration {
58        let base = self.initial_delay.as_millis() as f64;
59        let delay_ms = base * self.backoff_multiplier.powi(attempt as i32);
60        let capped_ms = delay_ms.min(self.max_delay.as_millis() as f64);
61
62        if self.jitter {
63            let jitter_range = capped_ms * 0.1; // 10% jitter
64            let jitter = rand::random::<f64>() * jitter_range;
65            Duration::from_millis((capped_ms + jitter) as u64)
66        } else {
67            Duration::from_millis(capped_ms as u64)
68        }
69    }
70}
71
72#[cfg(test)]
73mod tests {
74    use super::*;
75
76    #[test]
77    fn default_config() {
78        let c = RetryConfig::default();
79        assert_eq!(c.max_retries, 3);
80        assert_eq!(c.initial_delay, Duration::from_millis(100));
81    }
82
83    #[test]
84    fn delay_increases_exponentially() {
85        let c = RetryConfig {
86            jitter: false,
87            ..Default::default()
88        };
89        let d0 = c.delay_for_attempt(0);
90        let d1 = c.delay_for_attempt(1);
91        let d2 = c.delay_for_attempt(2);
92        assert!(d1 > d0);
93        assert!(d2 > d1);
94    }
95
96    #[test]
97    fn delay_respects_max() {
98        let c = RetryConfig {
99            max_delay: Duration::from_millis(200),
100            jitter: false,
101            ..Default::default()
102        };
103        let d = c.delay_for_attempt(100);
104        assert!(d <= Duration::from_millis(200));
105    }
106}