1#![allow(dead_code)]
2use std::collections::HashMap;
9use std::time::{Duration, Instant};
10
11#[derive(Debug, Clone, PartialEq)]
13pub enum BackoffStrategy {
14 Constant(Duration),
16 Linear {
18 base: Duration,
20 },
21 Exponential {
23 base: Duration,
25 max: Duration,
27 },
28 Fibonacci {
30 base: Duration,
32 },
33}
34
35impl BackoffStrategy {
36 #[allow(clippy::cast_precision_loss)]
38 pub fn delay_for_attempt(&self, attempt: u32) -> Duration {
39 match self {
40 Self::Constant(d) => *d,
41 Self::Linear { base } => *base * attempt.max(1),
42 Self::Exponential { base, max } => {
43 let multiplier = 2u64.saturating_pow(attempt);
44 let delay = base.saturating_mul(multiplier as u32);
45 if delay > *max {
46 *max
47 } else {
48 delay
49 }
50 }
51 Self::Fibonacci { base } => {
52 let fib = fibonacci(attempt);
53 base.saturating_mul(fib)
54 }
55 }
56 }
57}
58
59fn fibonacci(n: u32) -> u32 {
61 if n == 0 {
62 return 1;
63 }
64 let mut a: u32 = 1;
65 let mut b: u32 = 1;
66 for _ in 1..n {
67 let next = a.saturating_add(b);
68 a = b;
69 b = next;
70 }
71 b
72}
73
74#[derive(Debug, Clone, PartialEq)]
76pub enum CircuitState {
77 Closed,
79 Open {
81 opened_at: Instant,
83 cooldown: Duration,
85 },
86 HalfOpen {
88 successes_needed: u32,
90 current_successes: u32,
92 },
93}
94
95#[derive(Debug, Clone)]
97pub struct CircuitBreaker {
98 pub failure_threshold: u32,
100 pub cooldown: Duration,
102 pub recovery_threshold: u32,
104 pub consecutive_failures: u32,
106 pub state: CircuitState,
108}
109
110impl CircuitBreaker {
111 pub fn new(failure_threshold: u32, cooldown: Duration, recovery_threshold: u32) -> Self {
113 Self {
114 failure_threshold,
115 cooldown,
116 recovery_threshold,
117 consecutive_failures: 0,
118 state: CircuitState::Closed,
119 }
120 }
121
122 pub fn allow_request(&mut self) -> bool {
124 match &self.state {
125 CircuitState::Closed => true,
126 CircuitState::Open {
127 opened_at,
128 cooldown,
129 } => {
130 if opened_at.elapsed() >= *cooldown {
131 self.state = CircuitState::HalfOpen {
132 successes_needed: self.recovery_threshold,
133 current_successes: 0,
134 };
135 true
136 } else {
137 false
138 }
139 }
140 CircuitState::HalfOpen { .. } => true,
141 }
142 }
143
144 pub fn record_success(&mut self) {
146 self.consecutive_failures = 0;
147 match &self.state {
148 CircuitState::HalfOpen {
149 successes_needed,
150 current_successes,
151 } => {
152 let next = current_successes + 1;
153 if next >= *successes_needed {
154 self.state = CircuitState::Closed;
155 } else {
156 self.state = CircuitState::HalfOpen {
157 successes_needed: *successes_needed,
158 current_successes: next,
159 };
160 }
161 }
162 _ => {
163 self.state = CircuitState::Closed;
164 }
165 }
166 }
167
168 pub fn record_failure(&mut self) {
170 self.consecutive_failures += 1;
171 match &self.state {
172 CircuitState::Closed => {
173 if self.consecutive_failures >= self.failure_threshold {
174 self.state = CircuitState::Open {
175 opened_at: Instant::now(),
176 cooldown: self.cooldown,
177 };
178 }
179 }
180 CircuitState::HalfOpen { .. } => {
181 self.state = CircuitState::Open {
183 opened_at: Instant::now(),
184 cooldown: self.cooldown,
185 };
186 }
187 CircuitState::Open { .. } => {}
188 }
189 }
190}
191
192#[derive(Debug, Clone)]
194pub struct RetryBudget {
195 pub max_retries: u32,
197 pub window: Duration,
199 entries: Vec<Instant>,
201}
202
203impl RetryBudget {
204 pub fn new(max_retries: u32, window: Duration) -> Self {
206 Self {
207 max_retries,
208 window,
209 entries: Vec::new(),
210 }
211 }
212
213 pub fn try_acquire(&mut self) -> bool {
215 let now = Instant::now();
216 self.entries
217 .retain(|t| now.duration_since(*t) < self.window);
218 if self.entries.len() < self.max_retries as usize {
219 self.entries.push(now);
220 true
221 } else {
222 false
223 }
224 }
225
226 pub fn remaining(&mut self) -> u32 {
228 let now = Instant::now();
229 self.entries
230 .retain(|t| now.duration_since(*t) < self.window);
231 self.max_retries.saturating_sub(self.entries.len() as u32)
232 }
233}
234
235#[derive(Debug, Clone, PartialEq)]
237pub enum RetryOutcome {
238 Success,
240 RetryableFailure(String),
242 PermanentFailure(String),
244}
245
246#[derive(Debug)]
248pub struct RetryOrchestrator {
249 pub backoff: BackoffStrategy,
251 pub max_attempts: u32,
253 breakers: HashMap<String, CircuitBreaker>,
255 breaker_config: Option<(u32, Duration, u32)>,
257 pub budget: Option<RetryBudget>,
259 attempts: HashMap<String, u32>,
261}
262
263impl RetryOrchestrator {
264 pub fn new(backoff: BackoffStrategy, max_attempts: u32) -> Self {
266 Self {
267 backoff,
268 max_attempts,
269 breakers: HashMap::new(),
270 breaker_config: None,
271 budget: None,
272 attempts: HashMap::new(),
273 }
274 }
275
276 pub fn with_circuit_breaker(
278 mut self,
279 failure_threshold: u32,
280 cooldown: Duration,
281 recovery_threshold: u32,
282 ) -> Self {
283 self.breaker_config = Some((failure_threshold, cooldown, recovery_threshold));
284 self
285 }
286
287 pub fn with_budget(mut self, max_retries: u32, window: Duration) -> Self {
289 self.budget = Some(RetryBudget::new(max_retries, window));
290 self
291 }
292
293 fn get_breaker(&mut self, key: &str) -> Option<&mut CircuitBreaker> {
295 if let Some((ft, cd, rt)) = self.breaker_config {
296 if !self.breakers.contains_key(key) {
297 self.breakers
298 .insert(key.to_string(), CircuitBreaker::new(ft, cd, rt));
299 }
300 self.breakers.get_mut(key)
301 } else {
302 None
303 }
304 }
305
306 pub fn should_retry(&mut self, task_key: &str, outcome: &RetryOutcome) -> Option<Duration> {
311 if *outcome == RetryOutcome::Success {
312 if let Some(breaker) = self.get_breaker(task_key) {
313 breaker.record_success();
314 }
315 self.attempts.remove(task_key);
316 return None;
317 }
318
319 if let RetryOutcome::PermanentFailure(_) = outcome {
320 if let Some(breaker) = self.get_breaker(task_key) {
321 breaker.record_failure();
322 }
323 self.attempts.remove(task_key);
324 return None;
325 }
326
327 if let Some(breaker) = self.get_breaker(task_key) {
329 breaker.record_failure();
330 if !breaker.allow_request() {
331 return None;
332 }
333 }
334
335 let attempt = self.attempts.entry(task_key.to_string()).or_insert(0);
337 *attempt += 1;
338 if *attempt >= self.max_attempts {
339 return None;
340 }
341
342 if let Some(ref mut budget) = self.budget {
344 if !budget.try_acquire() {
345 return None;
346 }
347 }
348
349 Some(self.backoff.delay_for_attempt(*attempt))
350 }
351
352 pub fn reset(&mut self, task_key: &str) {
354 self.attempts.remove(task_key);
355 self.breakers.remove(task_key);
356 }
357
358 pub fn attempt_count(&self, task_key: &str) -> u32 {
360 self.attempts.get(task_key).copied().unwrap_or(0)
361 }
362}
363
364#[cfg(test)]
365mod tests {
366 use super::*;
367
368 #[test]
369 fn test_constant_backoff() {
370 let strategy = BackoffStrategy::Constant(Duration::from_secs(5));
371 assert_eq!(strategy.delay_for_attempt(0), Duration::from_secs(5));
372 assert_eq!(strategy.delay_for_attempt(3), Duration::from_secs(5));
373 assert_eq!(strategy.delay_for_attempt(100), Duration::from_secs(5));
374 }
375
376 #[test]
377 fn test_linear_backoff() {
378 let strategy = BackoffStrategy::Linear {
379 base: Duration::from_secs(2),
380 };
381 assert_eq!(strategy.delay_for_attempt(0), Duration::from_secs(2));
382 assert_eq!(strategy.delay_for_attempt(1), Duration::from_secs(2));
383 assert_eq!(strategy.delay_for_attempt(3), Duration::from_secs(6));
384 assert_eq!(strategy.delay_for_attempt(5), Duration::from_secs(10));
385 }
386
387 #[test]
388 fn test_exponential_backoff() {
389 let strategy = BackoffStrategy::Exponential {
390 base: Duration::from_millis(100),
391 max: Duration::from_secs(10),
392 };
393 assert_eq!(strategy.delay_for_attempt(0), Duration::from_millis(100));
394 assert_eq!(strategy.delay_for_attempt(1), Duration::from_millis(200));
395 assert_eq!(strategy.delay_for_attempt(2), Duration::from_millis(400));
396 assert!(strategy.delay_for_attempt(30) <= Duration::from_secs(10));
398 }
399
400 #[test]
401 fn test_fibonacci_backoff() {
402 let strategy = BackoffStrategy::Fibonacci {
403 base: Duration::from_millis(100),
404 };
405 assert_eq!(strategy.delay_for_attempt(0), Duration::from_millis(100));
407 assert_eq!(strategy.delay_for_attempt(1), Duration::from_millis(100));
408 assert_eq!(strategy.delay_for_attempt(2), Duration::from_millis(200));
409 assert_eq!(strategy.delay_for_attempt(3), Duration::from_millis(300));
410 assert_eq!(strategy.delay_for_attempt(4), Duration::from_millis(500));
411 }
412
413 #[test]
414 fn test_fibonacci_function() {
415 assert_eq!(fibonacci(0), 1);
416 assert_eq!(fibonacci(1), 1);
417 assert_eq!(fibonacci(2), 2);
418 assert_eq!(fibonacci(3), 3);
419 assert_eq!(fibonacci(4), 5);
420 assert_eq!(fibonacci(5), 8);
421 assert_eq!(fibonacci(6), 13);
422 }
423
424 #[test]
425 fn test_circuit_breaker_closed() {
426 let mut cb = CircuitBreaker::new(3, Duration::from_secs(10), 2);
427 assert!(cb.allow_request());
428 cb.record_failure();
429 assert!(cb.allow_request());
430 cb.record_failure();
431 assert!(cb.allow_request());
432 cb.record_failure();
434 assert!(!cb.allow_request());
435 }
436
437 #[test]
438 fn test_circuit_breaker_recovery() {
439 let mut cb = CircuitBreaker::new(2, Duration::from_millis(1), 1);
440 cb.record_failure();
441 cb.record_failure();
442 assert!(!cb.allow_request());
444 std::thread::sleep(Duration::from_millis(5));
446 assert!(cb.allow_request());
448 cb.record_success();
449 assert_eq!(cb.state, CircuitState::Closed);
450 }
451
452 #[test]
453 fn test_circuit_breaker_half_open_failure() {
454 let mut cb = CircuitBreaker::new(2, Duration::from_millis(1), 2);
455 cb.record_failure();
456 cb.record_failure();
457 std::thread::sleep(Duration::from_millis(5));
458 assert!(cb.allow_request()); cb.record_failure(); assert!(!cb.allow_request());
461 }
462
463 #[test]
464 fn test_retry_budget_allows() {
465 let mut budget = RetryBudget::new(3, Duration::from_secs(60));
466 assert!(budget.try_acquire());
467 assert!(budget.try_acquire());
468 assert!(budget.try_acquire());
469 assert!(!budget.try_acquire());
470 }
471
472 #[test]
473 fn test_retry_budget_remaining() {
474 let mut budget = RetryBudget::new(5, Duration::from_secs(60));
475 assert_eq!(budget.remaining(), 5);
476 budget.try_acquire();
477 budget.try_acquire();
478 assert_eq!(budget.remaining(), 3);
479 }
480
481 #[test]
482 fn test_orchestrator_basic_retry() {
483 let mut orch =
484 RetryOrchestrator::new(BackoffStrategy::Constant(Duration::from_millis(100)), 3);
485 let fail = RetryOutcome::RetryableFailure("err".into());
486 let delay = orch.should_retry("task-1", &fail);
488 assert!(delay.is_some());
489 let delay = orch.should_retry("task-1", &fail);
491 assert!(delay.is_some());
492 let delay = orch.should_retry("task-1", &fail);
494 assert!(delay.is_none());
495 }
496
497 #[test]
498 fn test_orchestrator_success_resets() {
499 let mut orch =
500 RetryOrchestrator::new(BackoffStrategy::Constant(Duration::from_millis(50)), 3);
501 let fail = RetryOutcome::RetryableFailure("err".into());
502 orch.should_retry("t1", &fail);
503 assert_eq!(orch.attempt_count("t1"), 1);
504 orch.should_retry("t1", &RetryOutcome::Success);
505 assert_eq!(orch.attempt_count("t1"), 0);
506 }
507
508 #[test]
509 fn test_orchestrator_permanent_failure() {
510 let mut orch =
511 RetryOrchestrator::new(BackoffStrategy::Constant(Duration::from_millis(50)), 5);
512 let result = orch.should_retry("t1", &RetryOutcome::PermanentFailure("fatal".into()));
513 assert!(result.is_none());
514 }
515
516 #[test]
517 fn test_orchestrator_with_budget() {
518 let mut orch =
519 RetryOrchestrator::new(BackoffStrategy::Constant(Duration::from_millis(50)), 100)
520 .with_budget(2, Duration::from_secs(60));
521 let fail = RetryOutcome::RetryableFailure("err".into());
522 assert!(orch.should_retry("t1", &fail).is_some());
524 assert!(orch.should_retry("t2", &fail).is_some());
525 assert!(orch.should_retry("t3", &fail).is_none());
527 }
528
529 #[test]
530 fn test_orchestrator_reset() {
531 let mut orch =
532 RetryOrchestrator::new(BackoffStrategy::Constant(Duration::from_millis(50)), 3)
533 .with_circuit_breaker(3, Duration::from_secs(10), 1);
534 let fail = RetryOutcome::RetryableFailure("err".into());
535 orch.should_retry("t1", &fail);
536 orch.should_retry("t1", &fail);
537 assert_eq!(orch.attempt_count("t1"), 2);
538 orch.reset("t1");
539 assert_eq!(orch.attempt_count("t1"), 0);
540 }
541}