actr_runtime/transport/
backoff.rs

1//! Exponential backoff retry strategy
2//!
3//! Provides configurable exponential backoff for connection retries and other
4//! network operations that may fail transiently.
5
6use std::time::Duration;
7
8/// Exponential backoff iterator
9///
10/// Generates increasing delays using exponential backoff algorithm with configurable
11/// initial delay, maximum delay, and retry limit.
12///
13/// # Example
14/// ```
15/// use std::time::Duration;
16/// use actr_runtime::ExponentialBackoff;
17///
18/// let backoff = ExponentialBackoff::new(
19///     Duration::from_millis(100),
20///     Duration::from_secs(30),
21///     Some(10),
22/// );
23///
24/// for (attempt, delay) in backoff.enumerate() {
25///     println!("Attempt {}: waiting {:?}", attempt, delay);
26///     // Retry logic here
27/// }
28/// ```
29#[derive(Debug, Clone)]
30pub struct ExponentialBackoff {
31    /// Current delay duration
32    current: Duration,
33    /// Maximum delay duration (cap)
34    max: Duration,
35    /// Multiplier for exponential growth (default: 2.0)
36    multiplier: f64,
37    /// Current retry count
38    retries: u32,
39    /// Maximum number of retries (None = unlimited)
40    max_retries: Option<u32>,
41}
42
43impl ExponentialBackoff {
44    /// Create new exponential backoff iterator
45    ///
46    /// # Arguments
47    /// - `initial`: Initial delay duration
48    /// - `max`: Maximum delay duration (delays will not exceed this)
49    /// - `max_retries`: Maximum number of retries (None for unlimited)
50    ///
51    /// # Example
52    /// ```
53    /// use std::time::Duration;
54    /// use actr_runtime::ExponentialBackoff;
55    ///
56    /// // Retry up to 5 times with delays: 100ms, 200ms, 400ms, 800ms, 1600ms
57    /// let backoff = ExponentialBackoff::new(
58    ///     Duration::from_millis(100),
59    ///     Duration::from_secs(2),
60    ///     Some(5),
61    /// );
62    /// ```
63    pub fn new(initial: Duration, max: Duration, max_retries: Option<u32>) -> Self {
64        Self {
65            current: initial,
66            max,
67            multiplier: 2.0,
68            retries: 0,
69            max_retries,
70        }
71    }
72
73    /// Create backoff with custom multiplier
74    ///
75    /// # Arguments
76    /// - `initial`: Initial delay duration
77    /// - `max`: Maximum delay duration
78    /// - `max_retries`: Maximum number of retries
79    /// - `multiplier`: Growth multiplier (e.g., 1.5 for slower growth)
80    pub fn with_multiplier(
81        initial: Duration,
82        max: Duration,
83        max_retries: Option<u32>,
84        multiplier: f64,
85    ) -> Self {
86        Self {
87            current: initial,
88            max,
89            multiplier,
90            retries: 0,
91            max_retries,
92        }
93    }
94
95    /// Get current retry count
96    pub fn retry_count(&self) -> u32 {
97        self.retries
98    }
99
100    /// Reset backoff to initial state
101    pub fn reset(&mut self) {
102        self.retries = 0;
103        self.current = Duration::from_millis(100); // Default initial
104    }
105}
106
107impl Iterator for ExponentialBackoff {
108    type Item = Duration;
109
110    fn next(&mut self) -> Option<Duration> {
111        // Check if max retries reached
112        if let Some(max_retries) = self.max_retries {
113            if self.retries >= max_retries {
114                return None;
115            }
116        }
117
118        // Get current delay
119        let delay = self.current;
120
121        // Calculate next delay (exponential growth)
122        let next_millis = (self.current.as_millis() as f64 * self.multiplier) as u64;
123        let next_duration = Duration::from_millis(next_millis);
124
125        // Cap at maximum delay
126        self.current = if next_duration > self.max {
127            self.max
128        } else {
129            next_duration
130        };
131
132        self.retries += 1;
133
134        Some(delay)
135    }
136}
137
138#[cfg(test)]
139mod tests {
140    use super::*;
141
142    #[test]
143    fn test_exponential_backoff_basic() {
144        let mut backoff =
145            ExponentialBackoff::new(Duration::from_millis(100), Duration::from_secs(2), Some(4));
146
147        assert_eq!(backoff.next(), Some(Duration::from_millis(100)));
148        assert_eq!(backoff.next(), Some(Duration::from_millis(200)));
149        assert_eq!(backoff.next(), Some(Duration::from_millis(400)));
150        assert_eq!(backoff.next(), Some(Duration::from_millis(800)));
151        assert_eq!(backoff.next(), None); // Exceeded max_retries
152    }
153
154    #[test]
155    fn test_exponential_backoff_with_cap() {
156        let mut backoff = ExponentialBackoff::new(
157            Duration::from_millis(100),
158            Duration::from_millis(500),
159            Some(5),
160        );
161
162        assert_eq!(backoff.next(), Some(Duration::from_millis(100)));
163        assert_eq!(backoff.next(), Some(Duration::from_millis(200)));
164        assert_eq!(backoff.next(), Some(Duration::from_millis(400)));
165        assert_eq!(backoff.next(), Some(Duration::from_millis(500))); // Capped
166        assert_eq!(backoff.next(), Some(Duration::from_millis(500))); // Still capped
167        assert_eq!(backoff.next(), None);
168    }
169
170    #[test]
171    fn test_exponential_backoff_unlimited() {
172        let mut backoff = ExponentialBackoff::new(
173            Duration::from_millis(50),
174            Duration::from_secs(10),
175            None, // Unlimited retries
176        );
177
178        for i in 0..20 {
179            let delay = backoff.next();
180            assert!(delay.is_some(), "Retry {i} should succeed");
181        }
182    }
183
184    #[test]
185    fn test_custom_multiplier() {
186        let mut backoff = ExponentialBackoff::with_multiplier(
187            Duration::from_millis(100),
188            Duration::from_secs(10),
189            Some(3),
190            1.5, // Slower growth
191        );
192
193        assert_eq!(backoff.next(), Some(Duration::from_millis(100)));
194        assert_eq!(backoff.next(), Some(Duration::from_millis(150)));
195        assert_eq!(backoff.next(), Some(Duration::from_millis(225)));
196        assert_eq!(backoff.next(), None);
197    }
198
199    #[test]
200    fn test_retry_count() {
201        let mut backoff =
202            ExponentialBackoff::new(Duration::from_millis(100), Duration::from_secs(1), None);
203
204        assert_eq!(backoff.retry_count(), 0);
205        backoff.next();
206        assert_eq!(backoff.retry_count(), 1);
207        backoff.next();
208        assert_eq!(backoff.retry_count(), 2);
209    }
210}