Skip to main content

ubiquity_core/
retry.rs

1//! Retry logic with exponential backoff
2
3use std::time::Duration;
4use std::future::Future;
5use tokio::time::sleep;
6use tracing::{warn, debug};
7
8/// Retry configuration
9#[derive(Debug, Clone)]
10pub struct RetryConfig {
11    pub max_attempts: u32,
12    pub initial_delay: Duration,
13    pub max_delay: Duration,
14    pub exponential_base: f32,
15    pub jitter: bool,
16}
17
18impl Default for RetryConfig {
19    fn default() -> Self {
20        Self {
21            max_attempts: 3,
22            initial_delay: Duration::from_secs(1),
23            max_delay: Duration::from_secs(60),
24            exponential_base: 2.0,
25            jitter: true,
26        }
27    }
28}
29
30/// Execute a future with retry logic
31pub async fn with_retry<F, Fut, T, E>(
32    config: &RetryConfig,
33    operation_name: &str,
34    mut f: F,
35) -> Result<T, E>
36where
37    F: FnMut() -> Fut,
38    Fut: Future<Output = Result<T, E>>,
39    E: std::fmt::Display,
40{
41    let mut attempt = 0;
42    let mut delay = config.initial_delay;
43    
44    loop {
45        attempt += 1;
46        
47        match f().await {
48            Ok(result) => {
49                if attempt > 1 {
50                    debug!(
51                        "Operation '{}' succeeded after {} attempts",
52                        operation_name, attempt
53                    );
54                }
55                return Ok(result);
56            }
57            Err(err) => {
58                if attempt >= config.max_attempts {
59                    warn!(
60                        "Operation '{}' failed after {} attempts: {}",
61                        operation_name, attempt, err
62                    );
63                    return Err(err);
64                }
65                
66                warn!(
67                    "Operation '{}' failed (attempt {}/{}): {}. Retrying in {:?}",
68                    operation_name, attempt, config.max_attempts, err, delay
69                );
70                
71                sleep(delay).await;
72                
73                // Calculate next delay with exponential backoff
74                delay = calculate_next_delay(delay, config);
75            }
76        }
77    }
78}
79
80fn calculate_next_delay(current: Duration, config: &RetryConfig) -> Duration {
81    let mut next = current.mul_f32(config.exponential_base);
82    
83    // Apply jitter if enabled
84    if config.jitter {
85        use rand::Rng;
86        let mut rng = rand::thread_rng();
87        let jitter_factor = rng.gen_range(0.5..1.5);
88        next = next.mul_f32(jitter_factor);
89    }
90    
91    // Cap at max delay
92    next.min(config.max_delay)
93}
94
95/// Retry policy for specific error types
96#[derive(Debug, Clone, Copy, PartialEq, Eq)]
97pub enum RetryPolicy {
98    /// Always retry
99    Always,
100    /// Never retry
101    Never,
102    /// Retry only on specific conditions
103    Conditional,
104}
105
106/// Trait for determining if an error is retryable
107pub trait Retryable {
108    fn is_retryable(&self) -> bool;
109    fn retry_policy(&self) -> RetryPolicy {
110        if self.is_retryable() {
111            RetryPolicy::Always
112        } else {
113            RetryPolicy::Never
114        }
115    }
116}
117
118impl Retryable for crate::UbiquityError {
119    fn is_retryable(&self) -> bool {
120        match self {
121            // Network errors are typically retryable
122            Self::SocketError(_) => true,
123            Self::MeshError(msg) => {
124                // Retry on connection errors
125                msg.contains("connect") || msg.contains("timeout") || msg.contains("refused")
126            }
127            // These are not retryable
128            Self::ConsciousnessTooLow(_) => false,
129            Self::CoherenceLoss(_) => false,
130            Self::AgentNotFound(_) => false,
131            Self::SerializationError(_) => false,
132            Self::ConfigError(_) => false,
133            Self::TaskExecutionError(_) => false,
134            Self::DatabaseError(_) => false,
135            Self::LLMError(msg) => {
136                // Retry on rate limits or temporary failures
137                msg.contains("rate limit") || msg.contains("timeout") || msg.contains("temporary")
138            }
139            Self::RateLimitError(_) => true, // Always retry rate limit errors
140            Self::AuthenticationError(_) => false, // Don't retry auth errors
141            Self::Other(_) => false,
142            // Add missing variants
143            Self::CommandExecution(_) => false,
144            Self::Timeout(_) => true, // Timeouts might be retryable
145            Self::NotFound(_) => false,
146            Self::Internal(_) => false,
147            Self::Network(_) => true, // Network errors are retryable
148            Self::Serialization(_) => false,
149            Self::Configuration(_) => false,
150            Self::ResourceExhausted(_) => true, // Can retry after backoff
151            Self::CloudExecution(_) => true, // Cloud issues might be temporary
152        }
153    }
154}
155
156/// Retry builder for more complex scenarios
157pub struct RetryBuilder {
158    config: RetryConfig,
159}
160
161impl RetryBuilder {
162    pub fn new() -> Self {
163        Self {
164            config: RetryConfig::default(),
165        }
166    }
167    
168    pub fn max_attempts(mut self, attempts: u32) -> Self {
169        self.config.max_attempts = attempts;
170        self
171    }
172    
173    pub fn initial_delay(mut self, delay: Duration) -> Self {
174        self.config.initial_delay = delay;
175        self
176    }
177    
178    pub fn max_delay(mut self, delay: Duration) -> Self {
179        self.config.max_delay = delay;
180        self
181    }
182    
183    pub fn exponential_base(mut self, base: f32) -> Self {
184        self.config.exponential_base = base;
185        self
186    }
187    
188    pub fn with_jitter(mut self, jitter: bool) -> Self {
189        self.config.jitter = jitter;
190        self
191    }
192    
193    pub async fn run<F, Fut, T, E>(
194        self,
195        operation_name: &str,
196        f: F,
197    ) -> Result<T, E>
198    where
199        F: FnMut() -> Fut,
200        Fut: Future<Output = Result<T, E>>,
201        E: std::fmt::Display,
202    {
203        with_retry(&self.config, operation_name, f).await
204    }
205}
206
207#[cfg(test)]
208mod tests {
209    use super::*;
210    use std::sync::atomic::{AtomicU32, Ordering};
211    use std::sync::Arc;
212    
213    #[tokio::test]
214    async fn test_retry_success_first_attempt() {
215        let config = RetryConfig::default();
216        let attempts = Arc::new(AtomicU32::new(0));
217        let attempts_clone = attempts.clone();
218        
219        let result = with_retry(&config, "test", || async {
220            attempts_clone.fetch_add(1, Ordering::SeqCst);
221            Ok::<_, &'static str>("success")
222        }).await;
223        
224        assert_eq!(result, Ok("success"));
225        assert_eq!(attempts.load(Ordering::SeqCst), 1);
226    }
227    
228    #[tokio::test]
229    async fn test_retry_success_after_failures() {
230        let config = RetryConfig {
231            initial_delay: Duration::from_millis(10),
232            ..Default::default()
233        };
234        
235        let attempts = Arc::new(AtomicU32::new(0));
236        let attempts_clone = attempts.clone();
237        
238        let result = with_retry(&config, "test", || async {
239            let attempt = attempts_clone.fetch_add(1, Ordering::SeqCst);
240            if attempt < 2 {
241                Err("temporary failure")
242            } else {
243                Ok("success")
244            }
245        }).await;
246        
247        assert_eq!(result, Ok("success"));
248        assert_eq!(attempts.load(Ordering::SeqCst), 3);
249    }
250    
251    #[tokio::test]
252    async fn test_retry_exhausted() {
253        let config = RetryConfig {
254            max_attempts: 2,
255            initial_delay: Duration::from_millis(10),
256            ..Default::default()
257        };
258        
259        let attempts = Arc::new(AtomicU32::new(0));
260        let attempts_clone = attempts.clone();
261        
262        let result = with_retry(&config, "test", || async {
263            attempts_clone.fetch_add(1, Ordering::SeqCst);
264            Err::<(), _>("persistent failure")
265        }).await;
266        
267        assert_eq!(result, Err("persistent failure"));
268        assert_eq!(attempts.load(Ordering::SeqCst), 2);
269    }
270}