crispy_stalker/
backoff.rs1use std::time::Duration;
8
9#[derive(Debug, Clone)]
11pub struct BackoffConfig {
12 pub max_retries: u32,
14
15 pub backoff_factor: Duration,
17
18 pub max_backoff: Duration,
20}
21
22impl Default for BackoffConfig {
23 fn default() -> Self {
24 Self {
25 max_retries: 3,
26 backoff_factor: Duration::from_secs(1),
27 max_backoff: Duration::from_secs(30),
28 }
29 }
30}
31
32impl BackoffConfig {
33 pub fn delay_for_attempt(&self, attempt: u32) -> Duration {
40 if attempt == 0 {
41 return Duration::ZERO;
42 }
43 let multiplier = 2u64.saturating_pow(attempt - 1);
44 #[allow(clippy::cast_possible_truncation)]
45 let capped = multiplier.min(u64::from(u32::MAX)) as u32;
46 let delay = self.backoff_factor.saturating_mul(capped);
47 delay.min(self.max_backoff)
48 }
49
50 pub fn should_retry(&self, attempt: u32) -> bool {
52 attempt < self.max_retries
53 }
54}
55
56#[cfg(test)]
57mod tests {
58 use super::*;
59
60 #[test]
61 fn delay_doubles_each_attempt() {
62 let config = BackoffConfig {
63 max_retries: 5,
64 backoff_factor: Duration::from_secs(1),
65 max_backoff: Duration::from_secs(60),
66 };
67
68 assert_eq!(config.delay_for_attempt(1), Duration::from_secs(1));
69 assert_eq!(config.delay_for_attempt(2), Duration::from_secs(2));
70 assert_eq!(config.delay_for_attempt(3), Duration::from_secs(4));
71 assert_eq!(config.delay_for_attempt(4), Duration::from_secs(8));
72 assert_eq!(config.delay_for_attempt(5), Duration::from_secs(16));
73 }
74
75 #[test]
76 fn delay_capped_at_max_backoff() {
77 let config = BackoffConfig {
78 max_retries: 10,
79 backoff_factor: Duration::from_secs(1),
80 max_backoff: Duration::from_secs(10),
81 };
82
83 assert_eq!(config.delay_for_attempt(5), Duration::from_secs(10));
85 assert_eq!(config.delay_for_attempt(10), Duration::from_secs(10));
86 }
87
88 #[test]
89 fn delay_zero_for_attempt_zero() {
90 let config = BackoffConfig::default();
91 assert_eq!(config.delay_for_attempt(0), Duration::ZERO);
92 }
93
94 #[test]
95 fn fractional_backoff_factor() {
96 let config = BackoffConfig {
97 max_retries: 3,
98 backoff_factor: Duration::from_millis(500),
99 max_backoff: Duration::from_secs(30),
100 };
101
102 assert_eq!(config.delay_for_attempt(1), Duration::from_millis(500));
103 assert_eq!(config.delay_for_attempt(2), Duration::from_millis(1000));
104 assert_eq!(config.delay_for_attempt(3), Duration::from_millis(2000));
105 }
106
107 #[test]
108 fn should_retry_within_limit() {
109 let config = BackoffConfig {
110 max_retries: 3,
111 ..Default::default()
112 };
113
114 assert!(config.should_retry(1));
115 assert!(config.should_retry(2));
116 assert!(!config.should_retry(3));
117 assert!(!config.should_retry(4));
118 }
119
120 #[test]
121 fn default_config_values() {
122 let config = BackoffConfig::default();
123 assert_eq!(config.max_retries, 3);
124 assert_eq!(config.backoff_factor, Duration::from_secs(1));
125 assert_eq!(config.max_backoff, Duration::from_secs(30));
126 }
127}