1use std::future::Future;
6use std::time::Duration;
7
8use rand::Rng;
9
10use crate::error::{Error, Result};
11
12pub const DEFAULT_JITTER_FACTOR: f64 = 0.25;
14pub const DEFAULT_MAX_DELAY: Duration = Duration::from_secs(30);
16pub const DEFAULT_MULTIPLIER: f64 = 2.0;
18pub const MAX_RECONNECT_BACKOFF: Duration = Duration::from_secs(10);
20pub const DEFAULT_MAX_RETRIES: u32 = 3;
22pub const DEFAULT_INITIAL_DELAY: Duration = Duration::from_secs(1);
24
25#[derive(Clone, Debug)]
27pub struct RetryConfig {
28 pub max_retries: u32,
30 pub initial_delay: Duration,
33 pub max_delay: Duration,
35 pub multiplier: f64,
37 pub jitter_factor: f64,
39}
40
41impl Default for RetryConfig {
42 fn default() -> Self {
43 Self {
44 max_retries: DEFAULT_MAX_RETRIES,
45 initial_delay: DEFAULT_INITIAL_DELAY,
46 max_delay: DEFAULT_MAX_DELAY,
47 multiplier: DEFAULT_MULTIPLIER,
48 jitter_factor: DEFAULT_JITTER_FACTOR,
49 }
50 }
51}
52
53impl RetryConfig {
54 pub(crate) fn delay_for_attempt(&self, attempt: u32) -> Duration {
57 let base = self.initial_delay.as_secs_f64() * self.multiplier.powi(attempt as i32 - 1);
58 let capped = base.min(self.max_delay.as_secs_f64());
59 let jitter_span = capped * self.jitter_factor;
60 let lo = (capped - jitter_span).max(0.0);
61 let hi = capped + jitter_span;
62 let secs = if lo >= hi {
63 capped
64 } else {
65 rand::thread_rng().gen_range(lo..hi)
66 };
67 Duration::from_secs_f64(secs)
68 }
69}
70
71pub(crate) async fn run_with_retry<F, Fut, T>(cfg: &RetryConfig, mut op: F) -> Result<T>
77where
78 F: FnMut() -> Fut,
79 Fut: Future<Output = Result<T>>,
80{
81 let mut attempt: u32 = 0;
82 loop {
83 attempt += 1;
84 match op().await {
85 Ok(v) => return Ok(v),
86 Err(e) => {
87 if !e.is_transient() || attempt > cfg.max_retries {
88 if attempt > 1 {
89 return Err(Error::RetryExhausted {
90 attempts: attempt,
91 source: Box::new(e),
92 });
93 }
94 return Err(e);
95 }
96 let computed = cfg.delay_for_attempt(attempt);
97 let delay = e
98 .retry_after()
99 .map(|ra| ra.max(computed))
100 .unwrap_or(computed);
101 crate::timer::sleep(delay).await;
102 }
103 }
104 }
105}
106
107#[cfg(test)]
108mod tests {
109 use super::*;
110 use std::sync::atomic::{AtomicU32, Ordering};
111 use std::sync::Arc;
112
113 fn api(status: u16) -> Error {
114 Error::Api {
115 status,
116 code: None,
117 message: "x".into(),
118 metadata: None,
119 provider: None,
120 retry_after: None,
121 }
122 }
123
124 #[tokio::test(start_paused = true)]
125 async fn succeeds_first_try() {
126 let calls = Arc::new(AtomicU32::new(0));
127 let c = calls.clone();
128 let cfg = RetryConfig::default();
129 let r: Result<u32> = run_with_retry(&cfg, || {
130 let c = c.clone();
131 async move {
132 c.fetch_add(1, Ordering::SeqCst);
133 Ok(42)
134 }
135 })
136 .await;
137 assert_eq!(r.unwrap(), 42);
138 assert_eq!(calls.load(Ordering::SeqCst), 1);
139 }
140
141 #[tokio::test(start_paused = true)]
142 async fn retries_transient_then_succeeds() {
143 let calls = Arc::new(AtomicU32::new(0));
144 let c = calls.clone();
145 let cfg = RetryConfig {
146 max_retries: 3,
147 initial_delay: Duration::from_millis(10),
148 max_delay: Duration::from_millis(50),
149 multiplier: 2.0,
150 jitter_factor: 0.0,
151 };
152 let r: Result<u32> = run_with_retry(&cfg, || {
153 let c = c.clone();
154 async move {
155 let n = c.fetch_add(1, Ordering::SeqCst) + 1;
156 if n < 3 {
157 Err(api(503))
158 } else {
159 Ok(7)
160 }
161 }
162 })
163 .await;
164 assert_eq!(r.unwrap(), 7);
165 assert_eq!(calls.load(Ordering::SeqCst), 3);
166 }
167
168 #[tokio::test(start_paused = true)]
169 async fn non_transient_errors_short_circuit() {
170 let calls = Arc::new(AtomicU32::new(0));
171 let c = calls.clone();
172 let cfg = RetryConfig::default();
173 let r: Result<()> = run_with_retry(&cfg, || {
174 let c = c.clone();
175 async move {
176 c.fetch_add(1, Ordering::SeqCst);
177 Err(api(400))
178 }
179 })
180 .await;
181 assert!(matches!(r, Err(Error::Api { status: 400, .. })));
182 assert_eq!(calls.load(Ordering::SeqCst), 1);
183 }
184
185 #[tokio::test(start_paused = true)]
186 async fn exhausts_budget() {
187 let calls = Arc::new(AtomicU32::new(0));
188 let c = calls.clone();
189 let cfg = RetryConfig {
190 max_retries: 2,
191 initial_delay: Duration::from_millis(1),
192 max_delay: Duration::from_millis(5),
193 multiplier: 2.0,
194 jitter_factor: 0.0,
195 };
196 let r: Result<()> = run_with_retry(&cfg, || {
197 let c = c.clone();
198 async move {
199 c.fetch_add(1, Ordering::SeqCst);
200 Err(api(503))
201 }
202 })
203 .await;
204 match r {
205 Err(Error::RetryExhausted { attempts, source }) => {
206 assert_eq!(attempts, 3);
207 assert!(matches!(*source, Error::Api { status: 503, .. }));
208 }
209 other => panic!("expected RetryExhausted, got {other:?}"),
210 }
211 assert_eq!(calls.load(Ordering::SeqCst), 3);
212 }
213
214 #[test]
215 fn delay_is_within_jitter_window() {
216 let cfg = RetryConfig {
217 max_retries: 5,
218 initial_delay: Duration::from_secs(1),
219 max_delay: Duration::from_secs(30),
220 multiplier: 2.0,
221 jitter_factor: 0.25,
222 };
223 for attempt in 1..=4 {
224 let base = (cfg.initial_delay.as_secs_f64() * 2f64.powi(attempt as i32 - 1))
225 .min(cfg.max_delay.as_secs_f64());
226 let lo = base * 0.75;
227 let hi = base * 1.25;
228 for _ in 0..100 {
229 let d = cfg.delay_for_attempt(attempt).as_secs_f64();
230 assert!(d >= lo - 1e-9 && d <= hi + 1e-9, "{d} not in [{lo},{hi}]");
231 }
232 }
233 }
234
235 #[tokio::test(start_paused = true)]
236 async fn retry_after_overrides_when_larger() {
237 let calls = Arc::new(AtomicU32::new(0));
238 let c = calls.clone();
239 let cfg = RetryConfig {
240 max_retries: 1,
241 initial_delay: Duration::from_millis(1),
242 max_delay: Duration::from_millis(1),
243 multiplier: 2.0,
244 jitter_factor: 0.0,
245 };
246 let start = tokio::time::Instant::now();
247 let r: Result<()> = run_with_retry(&cfg, || {
248 let c = c.clone();
249 async move {
250 let n = c.fetch_add(1, Ordering::SeqCst) + 1;
251 if n == 1 {
252 Err(Error::Api {
253 status: 429,
254 code: None,
255 message: "rate".into(),
256 metadata: None,
257 provider: None,
258 retry_after: Some(Duration::from_secs(5)),
259 })
260 } else {
261 Ok(())
262 }
263 }
264 })
265 .await;
266 assert!(r.is_ok());
267 let elapsed = start.elapsed();
268 assert!(
269 elapsed >= Duration::from_secs(5),
270 "expected >= 5s, got {elapsed:?}"
271 );
272 }
273}