mermaid_cli/utils/
retry.rs1use anyhow::Result;
2use std::time::Duration;
3use tracing::debug;
4
5pub struct RetryConfig {
7 pub max_attempts: usize,
8 pub initial_delay_ms: u64,
9 pub max_delay_ms: u64,
10 pub backoff_multiplier: f64,
11}
12
13impl Default for RetryConfig {
14 fn default() -> Self {
15 Self {
16 max_attempts: 3,
17 initial_delay_ms: 100,
18 max_delay_ms: 10_000,
19 backoff_multiplier: 2.0,
20 }
21 }
22}
23
24pub async fn retry_async<F, Fut, T>(operation: F, config: &RetryConfig) -> Result<T>
28where
29 F: Fn() -> Fut,
30 Fut: std::future::Future<Output = Result<T>>,
31{
32 retry_async_if(operation, config, |_| true).await
33}
34
35pub async fn retry_async_if<F, Fut, T, P>(
40 operation: F,
41 config: &RetryConfig,
42 is_retryable: P,
43) -> Result<T>
44where
45 F: Fn() -> Fut,
46 Fut: std::future::Future<Output = Result<T>>,
47 P: Fn(&anyhow::Error) -> bool,
48{
49 let mut attempt = 0;
50 let mut delay_ms = config.initial_delay_ms;
51
52 loop {
53 attempt += 1;
54
55 match operation().await {
56 Ok(result) => return Ok(result),
57 Err(e) if attempt >= config.max_attempts || !is_retryable(&e) => {
59 if attempt >= config.max_attempts {
60 return Err(anyhow::anyhow!(
61 "Operation failed after {} attempts: {}",
62 config.max_attempts,
63 e
64 ));
65 }
66 return Err(e);
68 },
69 Err(e) => {
70 debug!(
71 attempt = attempt,
72 max_attempts = config.max_attempts,
73 delay_ms = delay_ms,
74 "Retry attempt failed: {}",
75 e
76 );
77
78 tokio::time::sleep(Duration::from_millis(jitter(delay_ms))).await;
81
82 delay_ms = ((delay_ms as f64) * config.backoff_multiplier) as u64;
84 delay_ms = delay_ms.min(config.max_delay_ms);
85 },
86 }
87 }
88}
89
90pub(crate) fn jitter(delay_ms: u64) -> u64 {
95 let span = delay_ms / 5;
96 if span == 0 {
97 return delay_ms;
98 }
99 let mut bytes = [0u8; 8];
100 let entropy = match getrandom::fill(&mut bytes) {
101 Ok(()) => u64::from_le_bytes(bytes),
102 Err(_) => return delay_ms,
105 };
106 let offset = entropy % (2 * span + 1);
107 delay_ms - span + offset
108}
109
110#[cfg(test)]
111mod tests {
112 use super::*;
113 use std::sync::Arc;
114 use std::sync::atomic::{AtomicUsize, Ordering};
115
116 #[tokio::test]
117 async fn test_retry_async_success_on_first_try() {
118 let config = RetryConfig::default();
119 let call_count = Arc::new(AtomicUsize::new(0));
120 let call_count_clone = Arc::clone(&call_count);
121
122 let result = retry_async(
123 move || {
124 let count = Arc::clone(&call_count_clone);
125 async move {
126 count.fetch_add(1, Ordering::SeqCst);
127 Ok::<_, anyhow::Error>(42)
128 }
129 },
130 &config,
131 )
132 .await;
133
134 assert!(result.is_ok());
135 assert_eq!(result.unwrap(), 42);
136 assert_eq!(call_count.load(Ordering::SeqCst), 1);
137 }
138
139 #[tokio::test]
140 async fn test_retry_async_success_on_second_try() {
141 let config = RetryConfig {
142 max_attempts: 3,
143 initial_delay_ms: 10,
144 ..Default::default()
145 };
146 let call_count = Arc::new(AtomicUsize::new(0));
147 let call_count_clone = Arc::clone(&call_count);
148
149 let result = retry_async(
150 move || {
151 let count = Arc::clone(&call_count_clone);
152 async move {
153 let current = count.fetch_add(1, Ordering::SeqCst) + 1;
154 if current < 2 {
155 Err(anyhow::anyhow!("Temporary error"))
156 } else {
157 Ok(42)
158 }
159 }
160 },
161 &config,
162 )
163 .await;
164
165 assert!(result.is_ok());
166 assert_eq!(result.unwrap(), 42);
167 assert_eq!(call_count.load(Ordering::SeqCst), 2);
168 }
169
170 #[tokio::test]
171 async fn test_retry_async_fails_after_max_attempts() {
172 let config = RetryConfig {
173 max_attempts: 3,
174 initial_delay_ms: 10,
175 ..Default::default()
176 };
177 let call_count = Arc::new(AtomicUsize::new(0));
178 let call_count_clone = Arc::clone(&call_count);
179
180 let result = retry_async(
181 move || {
182 let count = Arc::clone(&call_count_clone);
183 async move {
184 count.fetch_add(1, Ordering::SeqCst);
185 Err::<i32, _>(anyhow::anyhow!("Persistent error"))
186 }
187 },
188 &config,
189 )
190 .await;
191
192 assert!(result.is_err());
193 assert_eq!(call_count.load(Ordering::SeqCst), 3);
194 }
195
196 #[tokio::test]
197 async fn retry_async_if_skips_nonretryable_errors() {
198 let config = RetryConfig {
201 max_attempts: 5,
202 initial_delay_ms: 1,
203 ..Default::default()
204 };
205 let calls = Arc::new(AtomicUsize::new(0));
206 let cc = Arc::clone(&calls);
207 let result: Result<i32> = retry_async_if(
208 move || {
209 let c = Arc::clone(&cc);
210 async move {
211 c.fetch_add(1, Ordering::SeqCst);
212 Err(anyhow::anyhow!("terminal"))
213 }
214 },
215 &config,
216 |_| false,
217 )
218 .await;
219 assert!(result.is_err());
220 assert_eq!(calls.load(Ordering::SeqCst), 1);
221 }
222
223 #[test]
224 fn jitter_stays_within_band() {
225 for _ in 0..100 {
227 let j = jitter(1000);
228 assert!((800..=1200).contains(&j), "jitter out of band: {j}");
229 }
230 assert_eq!(jitter(3), 3);
232 }
233}