1use std::future::Future;
28use std::time::Duration;
29
30use rand::Rng;
31use tokio::time::sleep;
32use tracing::{debug, warn};
33
34use crate::error::{Error, Result};
35
36#[derive(Debug, Clone)]
38pub struct RetryConfig {
39 pub max_retries: u32,
41 pub initial_delay: Duration,
43 pub max_delay: Duration,
45 pub backoff_multiplier: f64,
47 pub jitter: bool,
49}
50
51impl Default for RetryConfig {
52 fn default() -> Self {
53 Self {
54 max_retries: 3,
55 initial_delay: Duration::from_millis(100),
56 max_delay: Duration::from_secs(5),
57 backoff_multiplier: 2.0,
58 jitter: true,
59 }
60 }
61}
62
63impl RetryConfig {
64 pub fn new(max_retries: u32) -> Self {
66 Self {
67 max_retries,
68 ..Default::default()
69 }
70 }
71
72 pub fn none() -> Self {
74 Self {
75 max_retries: 0,
76 ..Default::default()
77 }
78 }
79
80 pub fn aggressive() -> Self {
82 Self {
83 max_retries: 5,
84 initial_delay: Duration::from_millis(50),
85 max_delay: Duration::from_secs(10),
86 backoff_multiplier: 1.5,
87 jitter: true,
88 }
89 }
90
91 fn delay_for_attempt(&self, attempt: u32) -> Duration {
93 let base_delay =
94 self.initial_delay.as_secs_f64() * self.backoff_multiplier.powi(attempt as i32);
95 let capped_delay = base_delay.min(self.max_delay.as_secs_f64());
96
97 let final_delay = if self.jitter {
98 let jitter_factor = 1.0 + (rand::rng().random::<f64>() * 0.25);
100 capped_delay * jitter_factor
101 } else {
102 capped_delay
103 };
104
105 Duration::from_secs_f64(final_delay)
106 }
107}
108
109pub async fn with_retry<F, Fut, T>(
121 config: &RetryConfig,
122 operation_name: &str,
123 operation: F,
124) -> Result<T>
125where
126 F: Fn() -> Fut,
127 Fut: Future<Output = Result<T>>,
128{
129 let mut last_error = None;
130
131 for attempt in 0..=config.max_retries {
132 match operation().await {
133 Ok(result) => {
134 if attempt > 0 {
135 debug!("{} succeeded after {} retries", operation_name, attempt);
136 }
137 return Ok(result);
138 }
139 Err(e) => {
140 if !is_retryable(&e) {
141 return Err(e);
142 }
143
144 last_error = Some(e);
145
146 if attempt < config.max_retries {
147 let delay = config.delay_for_attempt(attempt);
148 warn!(
149 "{} failed (attempt {}/{}), retrying in {:?}",
150 operation_name,
151 attempt + 1,
152 config.max_retries + 1,
153 delay
154 );
155 sleep(delay).await;
156 }
157 }
158 }
159 }
160
161 Err(last_error
162 .unwrap_or_else(|| Error::InvalidData("Operation failed with no error".to_string())))
163}
164
165fn is_retryable(error: &Error) -> bool {
167 use crate::error::ConnectionFailureReason;
168
169 match error {
170 Error::Timeout { .. } => true,
172 Error::Bluetooth(_) => true,
174 Error::ConnectionFailed { reason, .. } => {
176 matches!(
177 reason,
178 ConnectionFailureReason::OutOfRange
179 | ConnectionFailureReason::Timeout
180 | ConnectionFailureReason::BleError(_)
181 | ConnectionFailureReason::Other(_)
182 )
183 }
184 Error::NotConnected => true,
186 Error::WriteFailed { .. } => true,
188 Error::InvalidData(_) => false,
190 Error::InvalidHistoryData { .. } => false,
192 Error::InvalidReadingFormat { .. } => false,
194 Error::DeviceNotFound(_) => false,
196 Error::CharacteristicNotFound { .. } => false,
198 Error::Cancelled => false,
200 Error::Io(_) => true,
202 Error::InvalidConfig(_) => false,
204 }
205}
206
207#[cfg(test)]
208mod tests {
209 use super::*;
210 use crate::error::{ConnectionFailureReason, DeviceNotFoundReason};
211 use std::sync::Arc;
212 use std::sync::atomic::{AtomicU32, Ordering};
213
214 #[test]
215 fn test_retry_config_default() {
216 let config = RetryConfig::default();
217 assert_eq!(config.max_retries, 3);
218 assert!(config.jitter);
219 }
220
221 #[test]
222 fn test_retry_config_none() {
223 let config = RetryConfig::none();
224 assert_eq!(config.max_retries, 0);
225 }
226
227 #[test]
228 fn test_delay_calculation() {
229 let config = RetryConfig {
230 initial_delay: Duration::from_millis(100),
231 backoff_multiplier: 2.0,
232 max_delay: Duration::from_secs(10),
233 jitter: false,
234 max_retries: 5,
235 };
236
237 assert_eq!(config.delay_for_attempt(0), Duration::from_millis(100));
238 assert_eq!(config.delay_for_attempt(1), Duration::from_millis(200));
239 assert_eq!(config.delay_for_attempt(2), Duration::from_millis(400));
240 }
241
242 #[test]
243 fn test_is_retryable() {
244 assert!(is_retryable(&Error::Timeout {
245 operation: "test".to_string(),
246 duration: Duration::from_secs(1),
247 }));
248 assert!(is_retryable(&Error::ConnectionFailed {
249 device_id: None,
250 reason: ConnectionFailureReason::Other("test".to_string()),
251 }));
252 assert!(is_retryable(&Error::NotConnected));
253 assert!(!is_retryable(&Error::InvalidData("test".to_string())));
254 assert!(!is_retryable(&Error::DeviceNotFound(
255 DeviceNotFoundReason::NotFound {
256 identifier: "test".to_string()
257 }
258 )));
259 }
260
261 #[tokio::test]
262 async fn test_with_retry_immediate_success() {
263 let config = RetryConfig::new(3);
264 let result = with_retry(&config, "test", || async { Ok::<_, Error>(42) }).await;
265 assert_eq!(result.unwrap(), 42);
266 }
267
268 #[tokio::test]
269 async fn test_with_retry_eventual_success() {
270 let config = RetryConfig {
271 max_retries: 3,
272 initial_delay: Duration::from_millis(1),
273 jitter: false,
274 ..Default::default()
275 };
276
277 let attempts = Arc::new(AtomicU32::new(0));
278 let attempts_clone = Arc::clone(&attempts);
279
280 let result: Result<i32> = with_retry(&config, "test", || {
281 let attempts = Arc::clone(&attempts_clone);
282 async move {
283 let count = attempts.fetch_add(1, Ordering::SeqCst);
284 if count < 2 {
285 Err(Error::ConnectionFailed {
286 device_id: None,
287 reason: ConnectionFailureReason::Other("transient error".to_string()),
288 })
289 } else {
290 Ok(42)
291 }
292 }
293 })
294 .await;
295
296 assert_eq!(result.unwrap(), 42);
297 assert_eq!(attempts.load(Ordering::SeqCst), 3);
298 }
299
300 #[tokio::test]
301 async fn test_with_retry_all_fail() {
302 let config = RetryConfig {
303 max_retries: 2,
304 initial_delay: Duration::from_millis(1),
305 jitter: false,
306 ..Default::default()
307 };
308
309 let attempts = Arc::new(AtomicU32::new(0));
310 let attempts_clone = Arc::clone(&attempts);
311
312 let result: Result<i32> = with_retry(&config, "test", || {
313 let attempts = Arc::clone(&attempts_clone);
314 async move {
315 attempts.fetch_add(1, Ordering::SeqCst);
316 Err::<i32, _>(Error::ConnectionFailed {
317 device_id: None,
318 reason: ConnectionFailureReason::Other("persistent error".to_string()),
319 })
320 }
321 })
322 .await;
323
324 assert!(result.is_err());
325 assert_eq!(attempts.load(Ordering::SeqCst), 3); }
327
328 #[tokio::test]
329 async fn test_with_retry_non_retryable_error() {
330 let config = RetryConfig::new(3);
331 let attempts = Arc::new(AtomicU32::new(0));
332 let attempts_clone = Arc::clone(&attempts);
333
334 let result: Result<i32> = with_retry(&config, "test", || {
335 let attempts = Arc::clone(&attempts_clone);
336 async move {
337 attempts.fetch_add(1, Ordering::SeqCst);
338 Err::<i32, _>(Error::InvalidData("not retryable".to_string()))
339 }
340 })
341 .await;
342
343 assert!(result.is_err());
344 assert_eq!(attempts.load(Ordering::SeqCst), 1); }
346}