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}