aranet_core/
retry.rs

1//! Retry logic for BLE operations.
2//!
3//! This module provides configurable retry functionality for handling
4//! transient BLE failures.
5//!
6//! # Example
7//!
8//! ```
9//! use aranet_core::{RetryConfig, with_retry, Error};
10//!
11//! # async fn example() -> Result<(), Error> {
12//! // Configure retry behavior (3 retries with default settings)
13//! let config = RetryConfig::new(3);
14//!
15//! // Or use aggressive settings for unreliable connections
16//! let aggressive = RetryConfig::aggressive();
17//!
18//! // Use with_retry to wrap fallible operations
19//! let result = with_retry(&config, "read_sensor", || async {
20//!     // Your BLE operation here
21//!     Ok::<_, Error>(42)
22//! }).await?;
23//! # Ok(())
24//! # }
25//! ```
26
27use 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/// Configuration for retry behavior.
37#[derive(Debug, Clone)]
38pub struct RetryConfig {
39    /// Maximum number of retry attempts (0 means no retries).
40    pub max_retries: u32,
41    /// Initial delay between retries.
42    pub initial_delay: Duration,
43    /// Maximum delay between retries (for exponential backoff).
44    pub max_delay: Duration,
45    /// Backoff multiplier (1.0 = constant delay, 2.0 = double each time).
46    pub backoff_multiplier: f64,
47    /// Whether to add jitter to delays.
48    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    /// Create a new retry config with custom settings.
65    pub fn new(max_retries: u32) -> Self {
66        Self {
67            max_retries,
68            ..Default::default()
69        }
70    }
71
72    /// No retries.
73    pub fn none() -> Self {
74        Self {
75            max_retries: 0,
76            ..Default::default()
77        }
78    }
79
80    /// Conservative retry settings for unreliable connections.
81    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    /// Calculate delay for a given attempt number.
92    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            // Add up to 25% jitter using proper random number generation
99            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
109/// Execute an async operation with retry logic.
110///
111/// # Arguments
112///
113/// * `config` - Retry configuration
114/// * `operation` - The async operation to retry
115/// * `operation_name` - Name for logging purposes
116///
117/// # Returns
118///
119/// The result of the operation, or the last error if all retries failed.
120pub 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
165/// Check if an error is retryable.
166fn is_retryable(error: &Error) -> bool {
167    use crate::error::ConnectionFailureReason;
168
169    match error {
170        // Timeout errors are usually transient
171        Error::Timeout { .. } => true,
172        // Bluetooth errors are often transient
173        Error::Bluetooth(_) => true,
174        // Connection failed - check the reason
175        Error::ConnectionFailed { reason, .. } => {
176            matches!(
177                reason,
178                ConnectionFailureReason::OutOfRange
179                    | ConnectionFailureReason::Timeout
180                    | ConnectionFailureReason::BleError(_)
181                    | ConnectionFailureReason::Other(_)
182            )
183        }
184        // Not connected errors might be transient
185        Error::NotConnected => true,
186        // Write failures might be transient
187        Error::WriteFailed { .. } => true,
188        // Invalid data is not retryable
189        Error::InvalidData(_) => false,
190        // Invalid history data is not retryable
191        Error::InvalidHistoryData { .. } => false,
192        // Invalid reading format is not retryable
193        Error::InvalidReadingFormat { .. } => false,
194        // Device not found is not retryable
195        Error::DeviceNotFound(_) => false,
196        // Characteristic not found is not retryable
197        Error::CharacteristicNotFound { .. } => false,
198        // Cancelled is not retryable
199        Error::Cancelled => false,
200        // I/O errors might be transient
201        Error::Io(_) => true,
202        // Invalid configuration is not retryable
203        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); // 1 initial + 2 retries
326    }
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); // No retries
345    }
346}