1use std::time::Duration;
9use tokio_retry::Retry;
10use tokio_retry::strategy::{ExponentialBackoff, jitter};
11use tracing::{debug, warn};
12
13#[derive(Debug, Clone)]
15pub struct RetryConfig {
16 pub initial_delay: Duration,
18
19 pub max_delay: Duration,
21
22 pub max_retries: usize,
24
25 pub backoff_multiplier: f64,
27}
28
29impl Default for RetryConfig {
30 fn default() -> Self {
31 Self {
32 initial_delay: Duration::from_millis(100),
33 max_delay: Duration::from_secs(60),
34 max_retries: 10,
35 backoff_multiplier: 2.0,
36 }
37 }
38}
39
40#[derive(Debug, Clone)]
42pub struct BackoffConfig {
43 pub initial: Duration,
44 pub max: Duration,
45 pub multiplier: f64,
46}
47
48impl BackoffConfig {
49 pub fn into_strategy(self) -> impl Iterator<Item = Duration> {
50 let mut current = self.initial.as_millis() as u64;
51 let max_ms = self.max.as_millis() as u64;
52
53 std::iter::from_fn(move || {
54 let _delay = Duration::from_millis(current);
55 current = (current as f64 * self.multiplier) as u64;
56 if current > max_ms {
57 current = max_ms;
58 }
59
60 let jitter = (rand::random::<f64>() * 0.1 - 0.05) * current as f64;
61 Some(Duration::from_millis((current as f64 + jitter) as u64))
62 })
63 }
64}
65
66pub type RetryResult<T> = Result<T, anyhow::Error>;
68
69#[allow(unused_mut)] pub async fn retry_with_backoff<F, Fut, T>(mut operation: F, config: RetryConfig) -> RetryResult<T>
72where
73 F: FnMut() -> Fut,
74 Fut: std::future::Future<Output = RetryResult<T>>,
75{
76 let strategy = config.build_strategy();
77
78 Retry::spawn(strategy, operation).await
79}
80
81impl RetryConfig {
82 pub fn fast() -> Self {
84 Self {
85 initial_delay: Duration::from_millis(50),
86 max_delay: Duration::from_secs(5),
87 max_retries: 5,
88 backoff_multiplier: 2.0,
89 }
90 }
91
92 pub fn slow() -> Self {
94 Self {
95 initial_delay: Duration::from_secs(1),
96 max_delay: Duration::from_secs(300), max_retries: 15,
98 backoff_multiplier: 2.0,
99 }
100 }
101
102 pub fn critical() -> Self {
104 Self {
105 initial_delay: Duration::from_millis(100),
106 max_delay: Duration::from_secs(120), max_retries: 20,
108 backoff_multiplier: 2.0,
109 }
110 }
111
112 pub fn build_strategy(&self) -> impl Iterator<Item = Duration> {
118 let backoff = ExponentialBackoff::from_millis(self.initial_delay.as_millis() as u64)
119 .max_delay(self.max_delay)
120 .take(self.max_retries.saturating_sub(1));
121
122 backoff.map(jitter)
123 }
124}
125
126pub async fn retry_dial<F, Fut, T, E>(
128 peer_id: &str,
129 config: RetryConfig,
130 mut dial_fn: F,
131) -> Result<T, E>
132where
133 F: FnMut() -> Fut,
134 Fut: std::future::Future<Output = Result<T, E>>,
135 E: std::fmt::Display,
136{
137 let mut attempt = 0;
138 let strategy = config.build_strategy();
139
140 for delay in strategy {
141 attempt += 1;
142
143 match dial_fn().await {
144 Ok(result) => {
145 if attempt > 1 {
146 debug!("Dial to {} succeeded on attempt {}", peer_id, attempt);
147 }
148 return Ok(result);
149 }
150 Err(e) => {
151 debug!(
152 "Dial to {} failed (attempt {}): {} - retrying in {:?}",
153 peer_id, attempt, e, delay
154 );
155 tokio::time::sleep(delay).await;
156 }
157 }
158 }
159
160 attempt += 1;
162 dial_fn().await.map_err(|e| {
163 warn!(
164 "Dial to {} failed after {} attempts: {}",
165 peer_id, attempt, e
166 );
167 e
168 })
169}
170
171pub async fn retry_coordinator_discovery<F, Fut, T>(
173 config: RetryConfig,
174 operation: F,
175) -> RetryResult<T>
176where
177 F: FnMut() -> Fut,
178 Fut: std::future::Future<Output = RetryResult<T>>,
179{
180 retry_with_backoff(operation, config).await
181}
182
183#[cfg(test)]
184mod tests {
185 use super::*;
186 use std::sync::Arc;
187 use std::sync::atomic::{AtomicUsize, Ordering};
188
189 #[tokio::test]
190 async fn test_retry_succeeds_eventually() {
191 let attempts = Arc::new(AtomicUsize::new(0));
192 let attempts_clone = attempts.clone();
193
194 let config = RetryConfig {
195 initial_delay: Duration::from_millis(10),
196 max_delay: Duration::from_millis(100),
197 max_retries: 5,
198 backoff_multiplier: 2.0,
199 };
200
201 let result = retry_with_backoff(
202 || {
203 let attempts = attempts_clone.clone();
204 async move {
205 let count = attempts.fetch_add(1, Ordering::SeqCst);
206 if count < 2 {
207 Err(anyhow::anyhow!("Not yet"))
208 } else {
209 Ok("Success")
210 }
211 }
212 },
213 config,
214 )
215 .await;
216
217 assert!(result.is_ok());
218 assert_eq!(result.unwrap(), "Success");
219 assert_eq!(attempts.load(Ordering::SeqCst), 3);
220 }
221
222 #[tokio::test]
223 async fn test_retry_fails_after_max_attempts() {
224 let attempts = Arc::new(AtomicUsize::new(0));
225 let attempts_clone = attempts.clone();
226
227 let config = RetryConfig {
228 initial_delay: Duration::from_millis(10),
229 max_delay: Duration::from_millis(50),
230 max_retries: 3,
231 backoff_multiplier: 2.0,
232 };
233
234 let result = retry_with_backoff(
235 || {
236 let attempts = attempts_clone.clone();
237 async move {
238 attempts.fetch_add(1, Ordering::SeqCst);
239 Err::<(), _>(anyhow::anyhow!("Always fails"))
240 }
241 },
242 config,
243 )
244 .await;
245
246 assert!(result.is_err());
247 assert_eq!(attempts.load(Ordering::SeqCst), 3);
248 }
249
250 #[test]
251 fn test_retry_config_presets() {
252 let fast = RetryConfig::fast();
253 assert_eq!(fast.initial_delay, Duration::from_millis(50));
254 assert_eq!(fast.max_retries, 5);
255
256 let slow = RetryConfig::slow();
257 assert_eq!(slow.initial_delay, Duration::from_secs(1));
258 assert_eq!(slow.max_retries, 15);
259
260 let critical = RetryConfig::critical();
261 assert_eq!(critical.max_retries, 20);
262 }
263
264 #[tokio::test]
265 async fn test_retry_dial_with_logging() {
266 let attempts = Arc::new(AtomicUsize::new(0));
267 let attempts_clone = attempts.clone();
268
269 let config = RetryConfig {
270 initial_delay: Duration::from_millis(10),
271 max_delay: Duration::from_millis(50),
272 max_retries: 3,
273 backoff_multiplier: 2.0,
274 };
275
276 let result = retry_dial("test-peer", config, || {
277 let attempts = attempts_clone.clone();
278 async move {
279 let count = attempts.fetch_add(1, Ordering::SeqCst);
280 if count < 1 {
281 Err(anyhow::anyhow!("Connection refused"))
282 } else {
283 Ok(())
284 }
285 }
286 })
287 .await;
288
289 assert!(result.is_ok());
290 assert_eq!(attempts.load(Ordering::SeqCst), 2);
291 }
292}