1use std::future::Future;
4
5use tokio::time::{Duration, sleep};
6
7use crate::error::Result;
8
9#[derive(Debug, Clone, Copy, PartialEq)]
13pub struct RetryPolicy {
14 pub max_attempts: u32,
17 pub initial_backoff: Duration,
19 pub max_backoff: Duration,
21 pub multiplier: f64,
23}
24
25impl Default for RetryPolicy {
26 fn default() -> Self {
27 Self {
28 max_attempts: 3,
29 initial_backoff: Duration::from_secs(1),
30 max_backoff: Duration::from_secs(30),
31 multiplier: 2.0,
32 }
33 }
34}
35
36impl RetryPolicy {
37 pub fn backoff(&self, attempt: u32) -> Duration {
40 let attempt = attempt.max(1);
41 let factor = self.multiplier.powi((attempt - 1) as i32);
42 let nanos = self.initial_backoff.as_nanos() as f64 * factor;
43 if nanos > self.max_backoff.as_nanos() as f64 {
44 return self.max_backoff;
45 }
46 Duration::from_nanos(nanos as u64)
48 }
49}
50
51pub async fn retry<F, Fut, T>(policy: RetryPolicy, mut op: F) -> Result<T>
55where
56 F: FnMut() -> Fut,
57 Fut: Future<Output = Result<T>>,
58{
59 let max_attempts = policy.max_attempts.max(1);
60 let mut last_err = None;
61
62 for attempt in 1..=max_attempts {
63 match op().await {
64 Ok(v) => return Ok(v),
65 Err(e) => {
66 if !e.is_retryable() {
67 return Err(e);
68 }
69 if attempt == max_attempts {
70 last_err = Some(e);
71 break;
72 }
73 last_err = Some(e);
74 sleep(policy.backoff(attempt)).await;
75 }
76 }
77 }
78
79 Err(last_err.expect("retry loop always records an error before breaking"))
80}
81
82#[cfg(test)]
83mod tests {
84 use super::*;
85 use crate::error::{Error, Result};
86 use std::sync::Arc;
87 use std::sync::atomic::{AtomicU32, Ordering};
88
89 #[test]
90 fn test_default_policy() {
91 let p = RetryPolicy::default();
92 assert_eq!(p.max_attempts, 3);
93 assert_eq!(p.initial_backoff, Duration::from_secs(1));
94 assert_eq!(p.max_backoff, Duration::from_secs(30));
95 assert_eq!(p.multiplier, 2.0);
96 }
97
98 #[test]
99 fn test_backoff_exponential_and_clamped() {
100 let p = RetryPolicy::default();
101 assert_eq!(p.backoff(1), Duration::from_secs(1));
102 assert_eq!(p.backoff(2), Duration::from_secs(2));
103 assert_eq!(p.backoff(3), Duration::from_secs(4));
104 let p2 = RetryPolicy {
106 max_attempts: 5,
107 initial_backoff: Duration::from_secs(1),
108 max_backoff: Duration::from_secs(5),
109 multiplier: 10.0,
110 };
111 assert_eq!(p2.backoff(3), Duration::from_secs(5)); }
113
114 fn fast_policy() -> RetryPolicy {
115 RetryPolicy {
116 max_attempts: 3,
117 initial_backoff: Duration::from_millis(1),
118 max_backoff: Duration::from_millis(2),
119 multiplier: 2.0,
120 }
121 }
122
123 #[tokio::test]
124 async fn test_retry_succeeds_first_attempt() {
125 let calls = Arc::new(AtomicU32::new(0));
126 let c = calls.clone();
127 let r: Result<i32> = retry(fast_policy(), move || {
128 let c = c.clone();
129 async move {
130 c.fetch_add(1, Ordering::SeqCst);
131 Ok(42)
132 }
133 })
134 .await;
135 assert_eq!(r.unwrap(), 42);
136 assert_eq!(calls.load(Ordering::SeqCst), 1);
137 }
138
139 #[tokio::test]
140 async fn test_retry_retries_until_success() {
141 let calls = Arc::new(AtomicU32::new(0));
142 let c = calls.clone();
143 let r: Result<i32> = retry(fast_policy(), move || {
144 let c = c.clone();
145 async move {
146 let n = c.fetch_add(1, Ordering::SeqCst) + 1;
147 if n < 3 {
148 Err(Error::Connection("transient".into()))
149 } else {
150 Ok(7)
151 }
152 }
153 })
154 .await;
155 assert_eq!(r.unwrap(), 7);
156 assert_eq!(calls.load(Ordering::SeqCst), 3);
157 }
158
159 #[tokio::test]
160 async fn test_retry_stops_on_non_retryable() {
161 let calls = Arc::new(AtomicU32::new(0));
162 let c = calls.clone();
163 let r: Result<i32> = retry(fast_policy(), move || {
164 let c = c.clone();
165 async move {
166 c.fetch_add(1, Ordering::SeqCst);
167 Err(Error::Query {
168 code: "42000".into(),
169 message: "syntax".into(),
170 })
171 }
172 })
173 .await;
174 assert!(r.is_err());
175 assert_eq!(calls.load(Ordering::SeqCst), 1);
176 }
177
178 #[tokio::test]
179 async fn test_retry_exhausts_attempts() {
180 let calls = Arc::new(AtomicU32::new(0));
181 let c = calls.clone();
182 let r: Result<i32> = retry(fast_policy(), move || {
183 let c = c.clone();
184 async move {
185 c.fetch_add(1, Ordering::SeqCst);
186 Err(Error::Timeout)
187 }
188 })
189 .await;
190 assert!(r.is_err());
191 assert_eq!(calls.load(Ordering::SeqCst), 3);
192 }
193}