Skip to main content

linkrs_sync/
retry.rs

1//! Retry utility for sync operations
2//!
3//! Provides retry logic for handling transient failures in index synchronization.
4
5use std::time::Duration;
6
7use tokio::time::sleep;
8
9/// Retry configuration
10#[derive(Debug, Clone)]
11pub struct RetryConfig {
12    /// Maximum number of retry attempts
13    pub max_retries: u32,
14    /// Initial delay between retries
15    pub initial_delay: Duration,
16    /// Maximum delay between retries
17    pub max_delay: Duration,
18    /// Backoff multiplier (e.g., 2.0 means exponential backoff doubles each time)
19    pub backoff_multiplier: f64,
20}
21
22impl Default for RetryConfig {
23    fn default() -> Self {
24        Self {
25            max_retries: 3,
26            initial_delay: Duration::from_millis(100),
27            max_delay: Duration::from_secs(10),
28            backoff_multiplier: 2.0,
29        }
30    }
31}
32
33/// Default retry configuration for local (fulltext) operations
34pub fn default_local_retry_config() -> RetryConfig {
35    RetryConfig::new(2, Duration::from_millis(10), Duration::from_millis(500))
36}
37
38/// Default retry configuration for remote (vector) operations
39pub fn default_remote_retry_config() -> RetryConfig {
40    RetryConfig::new(3, Duration::from_millis(50), Duration::from_secs(5))
41}
42
43impl RetryConfig {
44    pub fn new(max_retries: u32, initial_delay: Duration, max_delay: Duration) -> Self {
45        Self {
46            max_retries,
47            initial_delay,
48            max_delay,
49            backoff_multiplier: 2.0,
50        }
51    }
52
53    pub fn with_max_retries(mut self, max_retries: u32) -> Self {
54        self.max_retries = max_retries;
55        self
56    }
57
58    pub fn with_initial_delay(mut self, initial_delay: Duration) -> Self {
59        self.initial_delay = initial_delay;
60        self
61    }
62
63    pub fn with_max_delay(mut self, max_delay: Duration) -> Self {
64        self.max_delay = max_delay;
65        self
66    }
67
68    pub fn with_backoff_multiplier(mut self, multiplier: f64) -> Self {
69        self.backoff_multiplier = multiplier;
70        self
71    }
72}
73
74/// Retry result
75#[derive(Debug)]
76pub enum RetryResult<T, E> {
77    /// Operation succeeded
78    Success(T),
79    /// Operation failed after all retries
80    Failed(E),
81    /// Operation should not be retried
82    DoNotRetry(E),
83}
84
85/// Execute a function with retry logic
86pub async fn with_retry<F, Fut, T, E>(func: F, config: &RetryConfig) -> Result<T, E>
87where
88    F: Fn() -> Fut,
89    Fut: std::future::Future<Output = Result<T, E>>,
90    E: std::fmt::Debug,
91{
92    let mut delay = config.initial_delay;
93    let mut last_error: Option<E> = None;
94
95    for attempt in 0..=config.max_retries {
96        match func().await {
97            Ok(result) => return Ok(result),
98            Err(e) => {
99                last_error = Some(e);
100
101                if attempt < config.max_retries {
102                    log::warn!(
103                        "Operation failed (attempt {}), retrying in {:?}...",
104                        attempt + 1,
105                        delay
106                    );
107                    sleep(delay).await;
108
109                    // Exponential backoff with max delay cap
110                    delay =
111                        std::cmp::min(delay.mul_f64(config.backoff_multiplier), config.max_delay);
112                }
113            }
114        }
115    }
116
117    Err(last_error.unwrap())
118}
119
120/// Execute a function with retry logic and custom error handling
121pub async fn with_retry_and_handler<F, Fut, T, E, H>(
122    func: F,
123    config: &RetryConfig,
124    error_handler: H,
125) -> Result<T, E>
126where
127    F: Fn() -> Fut,
128    Fut: std::future::Future<Output = Result<T, E>>,
129    E: std::fmt::Debug + Clone,
130    H: Fn(&E, u32) -> bool, // Returns true if should retry
131{
132    let mut delay = config.initial_delay;
133    let mut last_error: Option<E> = None;
134
135    for attempt in 0..=config.max_retries {
136        match func().await {
137            Ok(result) => return Ok(result),
138            Err(e) => {
139                // Check if error handler allows retry
140                if !error_handler(&e, attempt + 1) {
141                    return Err(e);
142                }
143
144                last_error = Some(e);
145
146                if attempt < config.max_retries {
147                    log::warn!(
148                        "Operation failed (attempt {}), retrying in {:?}...",
149                        attempt + 1,
150                        delay
151                    );
152                    sleep(delay).await;
153
154                    // Exponential backoff with max delay cap
155                    delay =
156                        std::cmp::min(delay.mul_f64(config.backoff_multiplier), config.max_delay);
157                }
158            }
159        }
160    }
161
162    Err(last_error.unwrap())
163}
164
165#[cfg(test)]
166mod tests {
167    use super::*;
168
169    #[tokio::test]
170    async fn test_retry_success_on_first_attempt() {
171        let config = RetryConfig::default();
172        let attempts = std::sync::Arc::new(tokio::sync::Mutex::new(0));
173        let attempts_clone = attempts.clone();
174
175        let result = with_retry(
176            move || {
177                let attempts = attempts_clone.clone();
178                async move {
179                    let mut count = attempts.lock().await;
180                    *count += 1;
181                    Ok::<_, String>("success".to_string())
182                }
183            },
184            &config,
185        )
186        .await;
187
188        assert!(result.is_ok());
189        assert_eq!(*attempts.lock().await, 1);
190    }
191
192    #[tokio::test]
193    async fn test_retry_eventual_success() {
194        let config = RetryConfig::default();
195        let attempts = std::sync::Arc::new(tokio::sync::Mutex::new(0));
196        let attempts_clone = attempts.clone();
197
198        let result = with_retry(
199            move || {
200                let attempts = attempts_clone.clone();
201                async move {
202                    let mut count = attempts.lock().await;
203                    *count += 1;
204                    if *count < 3 {
205                        Err::<String, _>("temporary error".to_string())
206                    } else {
207                        Ok::<_, String>("success".to_string())
208                    }
209                }
210            },
211            &config,
212        )
213        .await;
214
215        assert!(result.is_ok());
216        assert_eq!(*attempts.lock().await, 3);
217    }
218
219    #[tokio::test]
220    async fn test_retry_exhausted() {
221        let config = RetryConfig::default().with_max_retries(2);
222        let attempts = std::sync::Arc::new(tokio::sync::Mutex::new(0));
223        let attempts_clone = attempts.clone();
224
225        let result = with_retry(
226            move || {
227                let attempts = attempts_clone.clone();
228                async move {
229                    let mut count = attempts.lock().await;
230                    *count += 1;
231                    Err::<String, _>("persistent error".to_string())
232                }
233            },
234            &config,
235        )
236        .await;
237
238        assert!(result.is_err());
239        assert_eq!(*attempts.lock().await, 3); // Initial + 2 retries
240    }
241}