exception_collector/
retry.rs1use std::{cell::RefCell, future::Future, time::Duration};
19
20#[derive(Debug)]
36pub struct RetryPolicy {
37 max_retries: u32,
39 base_delay: Duration,
41 attempts: RefCell<u32>,
43}
44
45impl RetryPolicy {
46 #[must_use]
50 pub fn new(max_retries: u32, base_delay: Duration) -> Self {
51 Self {
52 max_retries,
53 base_delay,
54 attempts: RefCell::new(0),
55 }
56 }
57
58 #[must_use]
60 pub fn attempts(&self) -> u32 {
61 *self.attempts.borrow()
62 }
63
64 #[must_use]
69 pub fn backoff_delay(&self, attempt: u32) -> Duration {
70 let factor = 5_u64.saturating_pow(attempt);
71 let secs = self.base_delay.as_secs().saturating_mul(factor);
72 Duration::from_secs(secs)
73 }
74
75 pub fn reset(&self) {
79 *self.attempts.borrow_mut() = 0;
80 }
81}
82
83impl Default for RetryPolicy {
84 fn default() -> Self {
85 Self::new(3, Duration::from_mins(5))
86 }
87}
88
89pub async fn with_retry<F, Fut, T, E>(policy: &RetryPolicy, f: F) -> Result<T, E>
114where
115 F: Fn() -> Fut,
116 Fut: Future<Output = Result<T, E>>,
117{
118 policy.reset();
119
120 let mut attempt: u32 = 0;
121 loop {
122 attempt += 1;
123 *policy.attempts.borrow_mut() = attempt;
124
125 match f().await {
126 Ok(value) => {
127 policy.reset();
128 return Ok(value);
129 }
130 Err(err) => {
131 if attempt > policy.max_retries {
133 return Err(err);
134 }
135 let delay = policy.backoff_delay(attempt - 1);
136 tokio::time::sleep(delay).await;
137 }
138 }
139 }
140}
141
142#[cfg(test)]
145#[allow(
146 clippy::unwrap_used,
147 clippy::unwrap_in_result,
148 clippy::expect_used,
149 clippy::panic,
150 clippy::pedantic,
151 clippy::disallowed_methods,
152 clippy::indexing_slicing,
153 clippy::wildcard_enum_match_arm,
154 reason = "test module relaxes production lint strictness"
155)]
156mod tests {
157 use std::sync::atomic::{AtomicU32, Ordering};
158
159 use super::*;
160
161 #[test]
162 fn test_retry_policy_default_values() {
163 let policy = RetryPolicy::default();
164 assert_eq!(policy.max_retries, 3);
165 assert_eq!(policy.base_delay, Duration::from_mins(5));
166 }
167
168 #[test]
169 fn test_retry_policy_custom_values() {
170 let policy = RetryPolicy::new(5, Duration::from_secs(10));
171 assert_eq!(policy.max_retries, 5);
172 assert_eq!(policy.base_delay, Duration::from_secs(10));
173 }
174
175 #[test]
176 fn test_retry_policy_starts_at_zero_attempts() {
177 let policy = RetryPolicy::default();
178 assert_eq!(policy.attempts(), 0);
179 }
180
181 #[test]
182 fn test_retry_policy_backoff_delay_exponential() {
183 let policy = RetryPolicy::new(3, Duration::from_mins(5));
184
185 assert_eq!(policy.backoff_delay(0), Duration::from_mins(5));
187 assert_eq!(policy.backoff_delay(1), Duration::from_mins(25));
188 assert_eq!(policy.backoff_delay(2), Duration::from_mins(125));
189 }
190
191 #[test]
192 fn test_retry_policy_backoff_delay_protects_overflow() {
193 let policy = RetryPolicy::new(100, Duration::from_secs(u64::MAX));
194 let delay = policy.backoff_delay(10);
195 assert_eq!(delay, Duration::from_secs(u64::MAX));
197 }
198
199 #[test]
200 fn test_retry_policy_reset_clears_attempts() {
201 let policy = RetryPolicy::default();
202 *policy.attempts.borrow_mut() = 5;
203 assert_eq!(policy.attempts(), 5);
204
205 policy.reset();
206 assert_eq!(policy.attempts(), 0);
207 }
208
209 #[tokio::test]
210 async fn test_should_succeed_on_first_attempt() {
211 let policy = RetryPolicy::default();
212 let counter = AtomicU32::new(0);
213
214 let result = with_retry(&policy, || async {
215 counter.fetch_add(1, Ordering::SeqCst);
216 Ok::<_, String>("ok")
217 })
218 .await;
219
220 assert_eq!(result, Ok("ok"));
221 assert_eq!(counter.load(Ordering::SeqCst), 1);
222 }
223
224 #[tokio::test]
225 async fn test_should_retry_up_to_max() {
226 let policy = RetryPolicy::new(2, Duration::from_millis(1));
227 let counter = AtomicU32::new(0);
228
229 let result = with_retry(&policy, || async {
230 let attempt = counter.fetch_add(1, Ordering::SeqCst);
231 if attempt < 2 {
232 Err::<(), _>("fail".to_string())
233 } else {
234 Ok::<_, String>(())
235 }
236 })
237 .await;
238
239 assert!(result.is_ok());
240 assert_eq!(counter.load(Ordering::SeqCst), 3);
241 }
242
243 #[tokio::test]
244 async fn test_should_mark_failed_after_exhaustion() {
245 let policy = RetryPolicy::new(2, Duration::from_millis(1));
246
247 let result = with_retry(&policy, || async {
248 Err::<String, _>("permanent_fail".to_string())
249 })
250 .await;
251
252 assert_eq!(result, Err("permanent_fail".to_string()));
253 assert_eq!(policy.attempts(), 3);
254 }
255
256 #[tokio::test]
257 async fn test_should_reset_after_success() {
258 let policy = RetryPolicy::new(3, Duration::from_millis(1));
259 let call_count = AtomicU32::new(0);
260
261 let result = with_retry(&policy, || async {
263 let n = call_count.fetch_add(1, Ordering::SeqCst);
264 if n == 0 {
265 Err::<(), _>("first_fail".to_string())
266 } else {
267 Ok::<_, String>(())
268 }
269 })
270 .await;
271
272 assert!(result.is_ok());
273 assert_eq!(policy.attempts(), 0);
274
275 let result = with_retry(&policy, || async { Ok::<_, String>(()) }).await;
277
278 assert!(result.is_ok());
279 assert_eq!(policy.attempts(), 0);
280 }
281
282 #[tokio::test]
283 async fn test_should_not_retry_when_max_retries_zero() {
284 let policy = RetryPolicy::new(0, Duration::from_millis(1));
285 let counter = AtomicU32::new(0);
286
287 let result = with_retry(&policy, || async {
288 counter.fetch_add(1, Ordering::SeqCst);
289 Err::<String, _>("fail".to_string())
290 })
291 .await;
292
293 assert_eq!(result, Err("fail".to_string()));
294 assert_eq!(counter.load(Ordering::SeqCst), 1);
295 }
296}