leptos_query_rs/retry/
mod.rs

1//! Retry logic and error handling for queries
2
3use std::time::Duration;
4use std::future::Future;
5use serde::{Serialize, Deserialize};
6
7/// Error types that can occur during query execution
8#[derive(Debug, Clone, Serialize, Deserialize)]
9pub enum QueryError {
10    /// Network or HTTP errors
11    NetworkError(String),
12    /// Serialization errors
13    SerializationError(String),
14    /// Deserialization errors
15    DeserializationError(String),
16    /// Timeout errors
17    TimeoutError(String),
18    /// Storage errors for persistence
19    StorageError(String),
20    /// Generic error with message
21    GenericError(String),
22}
23
24impl std::fmt::Display for QueryError {
25    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
26        match self {
27            QueryError::NetworkError(msg) => write!(f, "Network error: {}", msg),
28            QueryError::SerializationError(msg) => write!(f, "Serialization error: {}", msg),
29            QueryError::DeserializationError(msg) => write!(f, "Deserialization error: {}", msg),
30            QueryError::TimeoutError(msg) => write!(f, "Timeout error: {}", msg),
31            QueryError::StorageError(msg) => write!(f, "Storage error: {}", msg),
32            QueryError::GenericError(msg) => write!(f, "Error: {}", msg),
33        }
34    }
35}
36
37impl std::error::Error for QueryError {}
38
39/// Configuration for retry behavior
40#[derive(Debug, Clone, Serialize, Deserialize)]
41pub struct RetryConfig {
42    /// Maximum number of retry attempts
43    pub max_retries: usize,
44    /// Base delay between retries
45    pub base_delay: Duration,
46    /// Maximum delay between retries
47    pub max_delay: Duration,
48    /// Whether to use exponential backoff
49    pub exponential_backoff: bool,
50    /// Whether to retry on specific error types
51    pub retry_on_network_errors: bool,
52    pub retry_on_timeout_errors: bool,
53}
54
55impl Default for RetryConfig {
56    fn default() -> Self {
57        Self {
58            max_retries: 3,
59            base_delay: Duration::from_millis(1000),
60            max_delay: Duration::from_secs(30),
61            exponential_backoff: true,
62            retry_on_network_errors: true,
63            retry_on_timeout_errors: true,
64        }
65    }
66}
67
68impl RetryConfig {
69    /// Create a retry config with custom settings
70    pub fn new(max_retries: usize, base_delay: Duration) -> Self {
71        Self {
72            max_retries,
73            base_delay,
74            max_delay: Duration::from_secs(30),
75            exponential_backoff: true,
76            retry_on_network_errors: true,
77            retry_on_timeout_errors: true,
78        }
79    }
80    
81    /// Disable exponential backoff
82    pub fn with_fixed_delay(mut self) -> Self {
83        self.exponential_backoff = false;
84        self
85    }
86    
87    /// Set maximum delay
88    pub fn with_max_delay(mut self, max_delay: Duration) -> Self {
89        self.max_delay = max_delay;
90        self
91    }
92    
93    /// Disable retry on network errors
94    pub fn no_network_retry(mut self) -> Self {
95        self.retry_on_network_errors = false;
96        self
97    }
98    
99    /// Disable retry on timeout errors
100    pub fn no_timeout_retry(mut self) -> Self {
101        self.retry_on_timeout_errors = false;
102        self
103    }
104}
105
106/// Execute a future with retry logic
107pub async fn execute_with_retry<F, Fut, T>(
108    query_fn: F,
109    config: &RetryConfig,
110) -> Result<T, QueryError>
111where
112    F: Fn() -> Fut + Clone,
113    Fut: Future<Output = Result<T, QueryError>>,
114{
115    let mut last_error = None;
116    
117    for attempt in 0..=config.max_retries {
118        match query_fn().await {
119            Ok(result) => return Ok(result),
120            Err(error) => {
121                last_error = Some(error.clone());
122                
123                // Check if we should retry this error
124                if !should_retry_error(&error, config) {
125                    return Err(error);
126                }
127                
128                // Don't retry on the last attempt
129                if attempt == config.max_retries {
130                    break;
131                }
132                
133                // Calculate delay
134                let delay = calculate_delay(attempt, config);
135                
136                // Wait before retrying
137                sleep(delay).await;
138            }
139        }
140    }
141    
142    Err(last_error.unwrap_or_else(|| QueryError::GenericError("Unknown error".to_string())))
143}
144
145/// Check if an error should be retried
146pub fn should_retry_error(error: &QueryError, config: &RetryConfig) -> bool {
147    match error {
148        QueryError::NetworkError(_) => config.retry_on_network_errors,
149        QueryError::TimeoutError(_) => config.retry_on_timeout_errors,
150        QueryError::SerializationError(_) | QueryError::DeserializationError(_) => false,
151        QueryError::GenericError(_) => true,
152        QueryError::StorageError(_) => false, // Storage errors shouldn't be retried
153    }
154}
155
156/// Calculate delay for retry attempt
157fn calculate_delay(attempt: usize, config: &RetryConfig) -> Duration {
158    if config.exponential_backoff {
159        let delay_ms = config.base_delay.as_millis() as u64 * (2_u64.pow(attempt as u32));
160        let delay = Duration::from_millis(delay_ms);
161        delay.min(config.max_delay)
162    } else {
163        config.base_delay
164    }
165}
166
167/// Sleep function that works in both native and WASM environments
168async fn sleep(duration: Duration) {
169    #[cfg(target_arch = "wasm32")]
170    {
171        let promise = js_sys::Promise::new(&mut |resolve, _| {
172            web_sys::window()
173                .unwrap()
174                .set_timeout_with_callback_and_timeout_and_arguments_0(
175                    &resolve, 
176                    duration.as_millis() as i32
177                )
178                .unwrap();
179        });
180        
181        wasm_bindgen_futures::JsFuture::from(promise).await.unwrap();
182    }
183    
184    #[cfg(not(target_arch = "wasm32"))]
185    {
186        tokio::time::sleep(duration).await;
187    }
188}
189
190#[cfg(test)]
191mod tests {
192    use super::*;
193    
194    #[test]
195    fn test_retry_config_builder() {
196        let config = RetryConfig::new(5, Duration::from_secs(2))
197            .with_max_delay(Duration::from_secs(60))
198            .with_fixed_delay()
199            .no_network_retry();
200        
201        assert_eq!(config.max_retries, 5);
202        assert_eq!(config.base_delay, Duration::from_secs(2));
203        assert_eq!(config.max_delay, Duration::from_secs(60));
204        assert!(!config.exponential_backoff);
205        assert!(!config.retry_on_network_errors);
206    }
207    
208    #[test]
209    fn test_should_retry_error() {
210        let config = RetryConfig::default();
211        
212        assert!(should_retry_error(&QueryError::NetworkError("test".to_string()), &config));
213        assert!(should_retry_error(&QueryError::TimeoutError("test".to_string()), &config));
214        assert!(!should_retry_error(&QueryError::SerializationError("test".to_string()), &config));
215    }
216    
217    #[test]
218    fn test_calculate_delay() {
219        let config = RetryConfig::new(3, Duration::from_millis(100));
220        
221        // Exponential backoff
222        assert_eq!(calculate_delay(0, &config), Duration::from_millis(100));
223        assert_eq!(calculate_delay(1, &config), Duration::from_millis(200));
224        assert_eq!(calculate_delay(2, &config), Duration::from_millis(400));
225        
226        // Fixed delay
227        let fixed_config = config.with_fixed_delay();
228        assert_eq!(calculate_delay(0, &fixed_config), Duration::from_millis(100));
229        assert_eq!(calculate_delay(1, &fixed_config), Duration::from_millis(100));
230        assert_eq!(calculate_delay(2, &fixed_config), Duration::from_millis(100));
231    }
232}