1use std::time::Duration;
7
8use crate::error::{CloudError, Result, RetryError};
9
10pub const DEFAULT_MAX_RETRIES: usize = 3;
12
13pub const DEFAULT_INITIAL_BACKOFF: Duration = Duration::from_millis(100);
15
16pub const DEFAULT_MAX_BACKOFF: Duration = Duration::from_secs(30);
18
19pub const DEFAULT_BACKOFF_MULTIPLIER: f64 = 2.0;
21
22#[derive(Debug, Clone)]
24pub struct RetryConfig {
25 pub max_retries: usize,
27 pub initial_backoff: Duration,
29 pub max_backoff: Duration,
31 pub backoff_multiplier: f64,
33 pub jitter: bool,
35 pub circuit_breaker: bool,
37 pub circuit_breaker_threshold: usize,
39 pub circuit_breaker_timeout: Duration,
41}
42
43impl Default for RetryConfig {
44 fn default() -> Self {
45 Self {
46 max_retries: DEFAULT_MAX_RETRIES,
47 initial_backoff: DEFAULT_INITIAL_BACKOFF,
48 max_backoff: DEFAULT_MAX_BACKOFF,
49 backoff_multiplier: DEFAULT_BACKOFF_MULTIPLIER,
50 jitter: true,
51 circuit_breaker: true,
52 circuit_breaker_threshold: 5,
53 circuit_breaker_timeout: Duration::from_secs(60),
54 }
55 }
56}
57
58impl RetryConfig {
59 #[must_use]
61 pub fn new() -> Self {
62 Self::default()
63 }
64
65 #[must_use]
67 pub fn with_max_retries(mut self, max_retries: usize) -> Self {
68 self.max_retries = max_retries;
69 self
70 }
71
72 #[must_use]
74 pub fn with_initial_backoff(mut self, duration: Duration) -> Self {
75 self.initial_backoff = duration;
76 self
77 }
78
79 #[must_use]
81 pub fn with_max_backoff(mut self, duration: Duration) -> Self {
82 self.max_backoff = duration;
83 self
84 }
85
86 #[must_use]
88 pub fn with_backoff_multiplier(mut self, multiplier: f64) -> Self {
89 self.backoff_multiplier = multiplier;
90 self
91 }
92
93 #[must_use]
95 pub fn with_jitter(mut self, jitter: bool) -> Self {
96 self.jitter = jitter;
97 self
98 }
99
100 #[must_use]
102 pub fn with_circuit_breaker(mut self, enabled: bool) -> Self {
103 self.circuit_breaker = enabled;
104 self
105 }
106}
107
108#[derive(Debug, Clone, Copy, PartialEq, Eq)]
110pub enum CircuitState {
111 Closed,
113 Open,
115 HalfOpen,
117}
118
119#[derive(Debug)]
121pub struct CircuitBreaker {
122 state: CircuitState,
124 failure_count: usize,
126 threshold: usize,
128 timeout: Duration,
130 last_failure: Option<std::time::Instant>,
132}
133
134impl CircuitBreaker {
135 #[must_use]
137 pub fn new(threshold: usize, timeout: Duration) -> Self {
138 Self {
139 state: CircuitState::Closed,
140 failure_count: 0,
141 threshold,
142 timeout,
143 last_failure: None,
144 }
145 }
146
147 pub fn allow_request(&mut self) -> Result<()> {
149 match self.state {
150 CircuitState::Closed => Ok(()),
151 CircuitState::Open => {
152 if let Some(last_failure) = self.last_failure {
154 if last_failure.elapsed() >= self.timeout {
155 tracing::info!("Circuit breaker transitioning to half-open state");
156 self.state = CircuitState::HalfOpen;
157 Ok(())
158 } else {
159 Err(CloudError::Retry(RetryError::CircuitBreakerOpen {
160 message: "Circuit breaker is open".to_string(),
161 }))
162 }
163 } else {
164 Ok(())
165 }
166 }
167 CircuitState::HalfOpen => Ok(()),
168 }
169 }
170
171 pub fn record_success(&mut self) {
173 match self.state {
174 CircuitState::Closed => {
175 self.failure_count = 0;
176 }
177 CircuitState::HalfOpen => {
178 tracing::info!("Circuit breaker transitioning to closed state");
179 self.state = CircuitState::Closed;
180 self.failure_count = 0;
181 }
182 CircuitState::Open => {}
183 }
184 }
185
186 pub fn record_failure(&mut self) {
188 self.failure_count += 1;
189 self.last_failure = Some(std::time::Instant::now());
190
191 if self.failure_count >= self.threshold && self.state != CircuitState::Open {
192 tracing::warn!(
193 "Circuit breaker opening after {} failures",
194 self.failure_count
195 );
196 self.state = CircuitState::Open;
197 }
198 }
199
200 #[must_use]
202 pub fn state(&self) -> CircuitState {
203 self.state
204 }
205}
206
207#[derive(Debug)]
209pub struct RetryBudget {
210 tokens: usize,
212 max_tokens: usize,
214 refill_rate: f64,
216 last_refill: std::time::Instant,
218}
219
220impl RetryBudget {
221 #[must_use]
223 pub fn new(max_tokens: usize, refill_rate: f64) -> Self {
224 Self {
225 tokens: max_tokens,
226 max_tokens,
227 refill_rate,
228 last_refill: std::time::Instant::now(),
229 }
230 }
231
232 pub fn try_consume(&mut self) -> Result<()> {
234 self.refill();
235
236 if self.tokens > 0 {
237 self.tokens -= 1;
238 Ok(())
239 } else {
240 Err(CloudError::Retry(RetryError::BudgetExhausted {
241 message: "Retry budget exhausted".to_string(),
242 }))
243 }
244 }
245
246 fn refill(&mut self) {
248 let elapsed = self.last_refill.elapsed();
249 let tokens_to_add = (elapsed.as_secs_f64() * self.refill_rate) as usize;
250
251 if tokens_to_add > 0 {
252 self.tokens = (self.tokens + tokens_to_add).min(self.max_tokens);
253 self.last_refill = std::time::Instant::now();
254 }
255 }
256}
257
258#[derive(Debug)]
260pub struct Backoff {
261 config: RetryConfig,
263 attempt: usize,
265 rng_state: u64,
270}
271
272impl Backoff {
273 #[must_use]
275 pub fn new(config: RetryConfig) -> Self {
276 Self {
277 config,
278 attempt: 0,
279 rng_state: Self::entropy_seed(),
280 }
281 }
282
283 fn entropy_seed() -> u64 {
294 use std::sync::atomic::{AtomicU64, Ordering};
295 use std::time::{SystemTime, UNIX_EPOCH};
296
297 static CALL_COUNTER: AtomicU64 = AtomicU64::new(0);
305 let counter = CALL_COUNTER.fetch_add(1, Ordering::Relaxed);
306
307 let nanos = SystemTime::now()
308 .duration_since(UNIX_EPOCH)
309 .map(|d| d.as_nanos() as u64)
310 .unwrap_or(0);
311 let pid = u64::from(std::process::id());
312 let stack_marker: u8 = 0;
315 let addr = std::ptr::addr_of!(stack_marker) as u64;
316
317 let mixed = nanos
320 ^ pid.wrapping_mul(0x9E37_79B9_7F4A_7C15)
321 ^ addr.rotate_left(17)
322 ^ counter.wrapping_mul(0xD6E8_FEB8_6659_FD93);
323 let mut z = mixed.wrapping_add(0x9E37_79B9_7F4A_7C15);
324 z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
325 z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
326 z ^= z >> 31;
327
328 if z == 0 { 1 } else { z }
332 }
333
334 fn next_random(&mut self) -> f64 {
336 let next = self
337 .rng_state
338 .wrapping_mul(1664525)
339 .wrapping_add(1013904223);
340 self.rng_state = next;
341 (next >> 32) as f64 / u32::MAX as f64
342 }
343
344 #[must_use]
346 pub fn next(&mut self) -> Duration {
347 let base = self.config.initial_backoff.as_secs_f64().mul_add(
348 self.config.backoff_multiplier.powi(self.attempt as i32),
349 0.0,
350 );
351
352 let backoff = if self.config.jitter {
353 let jitter_factor = 1.0 + (self.next_random() * 0.5);
355 base * jitter_factor
356 } else {
357 base
358 };
359
360 self.attempt += 1;
361
362 Duration::from_secs_f64(backoff.min(self.config.max_backoff.as_secs_f64()))
363 }
364
365 pub fn reset(&mut self) {
371 self.attempt = 0;
372 }
373}
374
375#[must_use]
377pub fn is_retryable(error: &CloudError) -> bool {
378 match error {
379 CloudError::Timeout { .. } => true,
380 CloudError::RateLimitExceeded { .. } => true,
381 CloudError::Http(http_error) => match http_error {
382 crate::error::HttpError::Network { .. } => true,
383 crate::error::HttpError::Status { status, .. } => {
384 matches!(
386 *status,
387 500 | 502 | 503 | 504 | 408 | 429 )
389 }
390 _ => false,
391 },
392 CloudError::S3(s3_error) => match s3_error {
393 crate::error::S3Error::Sdk { .. } => true,
394 _ => false,
395 },
396 CloudError::Azure(azure_error) => match azure_error {
397 crate::error::AzureError::Sdk { .. } => true,
398 _ => false,
399 },
400 CloudError::Gcs(gcs_error) => match gcs_error {
401 crate::error::GcsError::Sdk { .. } => true,
402 _ => false,
403 },
404 CloudError::Io(_) => true,
405 _ => false,
406 }
407}
408
409#[cfg(feature = "async")]
411pub struct RetryExecutor {
412 config: RetryConfig,
414 circuit_breaker: Option<CircuitBreaker>,
416 retry_budget: Option<RetryBudget>,
418}
419
420#[cfg(feature = "async")]
421impl RetryExecutor {
422 #[must_use]
424 pub fn new(config: RetryConfig) -> Self {
425 let circuit_breaker = if config.circuit_breaker {
426 Some(CircuitBreaker::new(
427 config.circuit_breaker_threshold,
428 config.circuit_breaker_timeout,
429 ))
430 } else {
431 None
432 };
433
434 let retry_budget = Some(RetryBudget::new(100, 10.0)); Self {
437 config,
438 circuit_breaker,
439 retry_budget,
440 }
441 }
442
443 pub async fn execute<F, Fut, T>(&mut self, mut operation: F) -> Result<T>
445 where
446 F: FnMut() -> Fut,
447 Fut: std::future::Future<Output = Result<T>>,
448 {
449 if let Some(ref mut cb) = self.circuit_breaker {
451 cb.allow_request()?;
452 }
453
454 let mut backoff = Backoff::new(self.config.clone());
455 let mut attempts = 0;
456
457 loop {
458 match operation().await {
459 Ok(result) => {
460 if let Some(ref mut cb) = self.circuit_breaker {
462 cb.record_success();
463 }
464 return Ok(result);
465 }
466 Err(error) => {
467 attempts += 1;
468
469 if !is_retryable(&error) {
471 tracing::warn!("Non-retryable error: {}", error);
472 return Err(error);
473 }
474
475 if attempts > self.config.max_retries {
477 tracing::error!("Max retries ({}) exceeded", self.config.max_retries);
478 if let Some(ref mut cb) = self.circuit_breaker {
479 cb.record_failure();
480 }
481 return Err(CloudError::Retry(RetryError::MaxRetriesExceeded {
482 attempts,
483 }));
484 }
485
486 if let Some(ref mut budget) = self.retry_budget {
488 budget.try_consume()?;
489 }
490
491 let delay = backoff.next();
493 tracing::warn!(
494 "Retry attempt {}/{} after {:?}: {}",
495 attempts,
496 self.config.max_retries,
497 delay,
498 error
499 );
500
501 tokio::time::sleep(delay).await;
502 }
503 }
504 }
505 }
506}
507
508#[cfg(test)]
509mod tests {
510 use super::*;
511
512 #[test]
513 fn test_retry_config_builder() {
514 let config = RetryConfig::new()
515 .with_max_retries(5)
516 .with_initial_backoff(Duration::from_millis(50))
517 .with_backoff_multiplier(3.0)
518 .with_jitter(false);
519
520 assert_eq!(config.max_retries, 5);
521 assert_eq!(config.initial_backoff, Duration::from_millis(50));
522 assert_eq!(config.backoff_multiplier, 3.0);
523 assert!(!config.jitter);
524 }
525
526 #[test]
527 fn test_circuit_breaker_closed() {
528 let mut cb = CircuitBreaker::new(3, Duration::from_secs(60));
529 assert_eq!(cb.state, CircuitState::Closed);
530 assert!(cb.allow_request().is_ok());
531 }
532
533 #[test]
534 fn test_circuit_breaker_opens() {
535 let mut cb = CircuitBreaker::new(3, Duration::from_secs(60));
536
537 cb.record_failure();
539 cb.record_failure();
540 cb.record_failure();
541
542 assert_eq!(cb.state, CircuitState::Open);
543 assert!(cb.allow_request().is_err());
544 }
545
546 #[test]
547 fn test_circuit_breaker_half_open() {
548 let mut cb = CircuitBreaker::new(3, Duration::from_millis(10));
549
550 cb.record_failure();
552 cb.record_failure();
553 cb.record_failure();
554 assert_eq!(cb.state, CircuitState::Open);
555
556 std::thread::sleep(Duration::from_millis(20));
558
559 assert!(cb.allow_request().is_ok());
561 assert_eq!(cb.state, CircuitState::HalfOpen);
562
563 cb.record_success();
565 assert_eq!(cb.state, CircuitState::Closed);
566 }
567
568 #[test]
569 fn test_retry_budget() {
570 let mut budget = RetryBudget::new(10, 100.0);
573
574 for _ in 0..10 {
576 assert!(budget.try_consume().is_ok());
577 }
578
579 assert!(budget.try_consume().is_err());
581
582 std::thread::sleep(Duration::from_millis(50));
584
585 assert!(budget.try_consume().is_ok());
587 }
588
589 #[test]
590 fn test_backoff_exponential() {
591 let config = RetryConfig::new()
592 .with_initial_backoff(Duration::from_millis(100))
593 .with_backoff_multiplier(2.0)
594 .with_jitter(false);
595
596 let mut backoff = Backoff::new(config);
597
598 let d1 = backoff.next();
599 let d2 = backoff.next();
600 let d3 = backoff.next();
601
602 assert!(d1 < d2);
603 assert!(d2 < d3);
604 }
605
606 #[test]
607 fn test_backoff_first_jitter_is_not_deterministic_zero() {
608 let config = RetryConfig::new()
614 .with_initial_backoff(Duration::from_millis(1000))
615 .with_backoff_multiplier(1.0)
616 .with_jitter(true);
617 let mut jittered = Backoff::new(config.clone());
618 let jittered_first = jittered.next();
619
620 let mut unjittered = Backoff::new(config.with_jitter(false));
621 let base_first = unjittered.next();
622
623 assert_ne!(
624 jittered_first, base_first,
625 "first jittered backoff should not exactly equal the unjittered base"
626 );
627 }
628
629 #[test]
630 fn test_backoff_independent_instances_diverge() {
631 let config = RetryConfig::new()
636 .with_initial_backoff(Duration::from_millis(1000))
637 .with_backoff_multiplier(1.0)
638 .with_jitter(true);
639
640 let mut backoff_a = Backoff::new(config.clone());
641 let mut backoff_b = Backoff::new(config);
642
643 let sequence_a: Vec<Duration> = (0..5).map(|_| backoff_a.next()).collect();
644 let sequence_b: Vec<Duration> = (0..5).map(|_| backoff_b.next()).collect();
645
646 assert_ne!(
647 sequence_a, sequence_b,
648 "independently constructed Backoff instances must not produce identical jitter sequences"
649 );
650 }
651
652 #[test]
653 fn test_is_retryable() {
654 let timeout_error = CloudError::Timeout {
655 message: "timeout".to_string(),
656 };
657 assert!(is_retryable(&timeout_error));
658
659 let rate_limit_error = CloudError::RateLimitExceeded {
660 message: "rate limit".to_string(),
661 };
662 assert!(is_retryable(&rate_limit_error));
663
664 let not_found_error = CloudError::NotFound {
665 key: "test".to_string(),
666 };
667 assert!(!is_retryable(¬_found_error));
668 }
669
670 #[cfg(feature = "async")]
671 #[tokio::test]
672 async fn test_retry_executor_success() {
673 use std::sync::atomic::{AtomicUsize, Ordering};
674
675 let config = RetryConfig::new().with_max_retries(3);
676 let mut executor = RetryExecutor::new(config);
677
678 let attempt = std::sync::Arc::new(AtomicUsize::new(0));
679 let attempt_clone = attempt.clone();
680 let result = executor
681 .execute(|| {
682 let attempt = attempt_clone.clone();
683 async move {
684 let current = attempt.fetch_add(1, Ordering::SeqCst) + 1;
685 if current < 2 {
686 Err(CloudError::Timeout {
687 message: "timeout".to_string(),
688 })
689 } else {
690 Ok(42)
691 }
692 }
693 })
694 .await;
695
696 assert!(result.is_ok());
697 assert_eq!(result.ok(), Some(42));
698 assert_eq!(attempt.load(Ordering::SeqCst), 2);
699 }
700
701 #[cfg(feature = "async")]
702 #[tokio::test]
703 async fn test_retry_executor_max_retries() {
704 use std::sync::atomic::{AtomicUsize, Ordering};
705
706 let config = RetryConfig::new().with_max_retries(2);
707 let mut executor = RetryExecutor::new(config);
708
709 let attempt = std::sync::Arc::new(AtomicUsize::new(0));
710 let attempt_clone = attempt.clone();
711 let result: Result<i32> = executor
712 .execute(|| {
713 let attempt = attempt_clone.clone();
714 async move {
715 attempt.fetch_add(1, Ordering::SeqCst);
716 Err(CloudError::Timeout {
717 message: "timeout".to_string(),
718 })
719 }
720 })
721 .await;
722
723 assert!(result.is_err());
724 assert_eq!(attempt.load(Ordering::SeqCst), 3); }
726}