1use std::time::Duration;
4use std::future::Future;
5use tokio::time::sleep;
6use tracing::{warn, debug};
7
8#[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
30pub 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 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 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 next.min(config.max_delay)
93}
94
95#[derive(Debug, Clone, Copy, PartialEq, Eq)]
97pub enum RetryPolicy {
98 Always,
100 Never,
102 Conditional,
104}
105
106pub 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 Self::SocketError(_) => true,
123 Self::MeshError(msg) => {
124 msg.contains("connect") || msg.contains("timeout") || msg.contains("refused")
126 }
127 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 msg.contains("rate limit") || msg.contains("timeout") || msg.contains("temporary")
138 }
139 Self::RateLimitError(_) => true, Self::AuthenticationError(_) => false, Self::Other(_) => false,
142 Self::CommandExecution(_) => false,
144 Self::Timeout(_) => true, Self::NotFound(_) => false,
146 Self::Internal(_) => false,
147 Self::Network(_) => true, Self::Serialization(_) => false,
149 Self::Configuration(_) => false,
150 Self::ResourceExhausted(_) => true, Self::CloudExecution(_) => true, }
153 }
154}
155
156pub 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}