rainy_sdk/
retry.rs

1use crate::{RainyError, Result};
2use std::time::Duration;
3use tokio::time::sleep;
4
5/// Configuration for retry logic with exponential backoff.
6///
7/// `RetryConfig` defines the parameters for retrying failed operations,
8/// such as the maximum number of retries and the delay between attempts.
9#[derive(Debug, Clone)]
10pub struct RetryConfig {
11    /// The maximum number of retry attempts to make.
12    pub max_retries: u32,
13
14    /// The base delay between retries, in milliseconds. This is the starting point
15    /// for the exponential backoff calculation.
16    pub base_delay_ms: u64,
17
18    /// The maximum possible delay between retries, in milliseconds.
19    pub max_delay_ms: u64,
20
21    /// The multiplier for the exponential backoff. Each subsequent delay is
22    /// multiplied by this factor.
23    pub backoff_multiplier: f64,
24
25    /// A flag indicating whether to add a random jitter to the delay time.
26    /// Jitter helps to prevent a "thundering herd" problem in distributed systems.
27    pub jitter: bool,
28}
29
30impl Default for RetryConfig {
31    /// Creates a default `RetryConfig`.
32    ///
33    /// The default settings are:
34    /// - `max_retries`: 3
35    /// - `base_delay_ms`: 1000 (1 second)
36    /// - `max_delay_ms`: 30000 (30 seconds)
37    /// - `backoff_multiplier`: 2.0
38    /// - `jitter`: true
39    fn default() -> Self {
40        Self {
41            max_retries: 3,
42            base_delay_ms: 1000,
43            max_delay_ms: 30000,
44            backoff_multiplier: 2.0,
45            jitter: true,
46        }
47    }
48}
49
50impl RetryConfig {
51    /// Creates a new `RetryConfig` with a specified maximum number of retries
52    /// and default values for other settings.
53    ///
54    /// # Arguments
55    ///
56    /// * `max_retries` - The maximum number of times to retry an operation.
57    pub fn new(max_retries: u32) -> Self {
58        Self {
59            max_retries,
60            ..Default::default()
61        }
62    }
63
64    /// Calculates the delay duration for a specific retry attempt.
65    ///
66    /// The delay is calculated using exponential backoff, and optionally includes jitter.
67    ///
68    /// # Arguments
69    ///
70    /// * `attempt` - The current retry attempt number (starting from 0).
71    ///
72    /// # Returns
73    ///
74    /// A `Duration` to wait before the next attempt.
75    pub fn delay_for_attempt(&self, attempt: u32) -> Duration {
76        let base_delay = self.base_delay_ms as f64;
77        let multiplier = self.backoff_multiplier.powi(attempt as i32);
78        let mut delay = base_delay * multiplier;
79
80        // Add jitter if enabled (±25%)
81        if self.jitter && attempt > 0 {
82            use rand::Rng;
83            let mut rng = rand::thread_rng();
84            let jitter_factor = rng.gen_range(0.75..=1.25);
85            delay *= jitter_factor;
86        }
87
88        // Cap at maximum delay
89        delay = delay.min(self.max_delay_ms as f64);
90
91        Duration::from_millis(delay as u64)
92    }
93}
94
95/// Executes an asynchronous operation with retry logic based on the provided `RetryConfig`.
96///
97/// This function will repeatedly call the `operation` closure until it succeeds,
98/// or until the maximum number of retries is reached.
99///
100/// # Type Parameters
101///
102/// * `F` - The type of the operation, which must be a closure that returns a future.
103/// * `Fut` - The type of the future returned by the closure.
104/// * `T` - The success type of the `Result` returned by the future.
105///
106/// # Arguments
107///
108/// * `config` - The `RetryConfig` to use for the retry logic.
109/// * `operation` - The asynchronous operation to execute.
110///
111/// # Returns
112///
113/// A `Result` containing the success value `T` if the operation succeeds,
114/// or the last `RainyError` if all retry attempts fail.
115pub async fn retry_with_backoff<F, Fut, T>(config: &RetryConfig, operation: F) -> Result<T>
116where
117    F: Fn() -> Fut,
118    Fut: std::future::Future<Output = Result<T>>,
119{
120    let mut last_error = None;
121
122    for attempt in 0..=config.max_retries {
123        match operation().await {
124            Ok(result) => return Ok(result),
125            Err(error) => {
126                // Check if error is retryable
127                if !error.is_retryable() || attempt == config.max_retries {
128                    return Err(error);
129                }
130
131                // Calculate delay for next attempt
132                let delay = config.delay_for_attempt(attempt);
133
134                #[cfg(feature = "tracing")]
135                tracing::warn!(
136                    "Request failed (attempt {}/{}), retrying in {:?}: {}",
137                    attempt + 1,
138                    config.max_retries + 1,
139                    delay,
140                    error
141                );
142
143                last_error = Some(error);
144
145                // Wait before retrying
146                if attempt < config.max_retries {
147                    sleep(delay).await;
148                }
149            }
150        }
151    }
152
153    // This should never be reached, but just in case
154    Err(last_error.unwrap_or_else(|| RainyError::Network {
155        message: "All retry attempts failed".to_string(),
156        retryable: false,
157        source_error: None,
158    }))
159}
160
161#[cfg(test)]
162mod tests {
163    use super::*;
164
165    #[test]
166    fn test_delay_calculation() {
167        let config = RetryConfig::default();
168
169        // Test delay progression
170        let delay0 = config.delay_for_attempt(0);
171        let delay1 = config.delay_for_attempt(1);
172        let delay2 = config.delay_for_attempt(2);
173
174        assert!(delay0.as_millis() >= 1000);
175        assert!(delay1.as_millis() >= delay0.as_millis());
176        assert!(delay2.as_millis() >= delay1.as_millis());
177        assert!(delay2.as_millis() <= 30000);
178    }
179}