actionqueue_executor_local/
backoff.rs1use std::time::Duration;
8
9#[derive(Debug, Clone, PartialEq, Eq)]
11pub enum BackoffConfigError {
12 BaseExceedsMax {
14 base: Duration,
16 max: Duration,
18 },
19}
20
21impl std::fmt::Display for BackoffConfigError {
22 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
23 match self {
24 BackoffConfigError::BaseExceedsMax { base, max } => {
25 write!(f, "base delay ({base:?}) must not exceed max delay ({max:?})")
26 }
27 }
28 }
29}
30
31impl std::error::Error for BackoffConfigError {}
32
33pub trait BackoffStrategy: Send + Sync {
35 fn delay_for_attempt(&self, attempt_number: u32) -> Duration;
40}
41
42#[derive(Debug, Clone, Copy, PartialEq, Eq)]
44pub struct FixedBackoff {
45 interval: Duration,
47}
48
49impl FixedBackoff {
50 pub fn new(interval: Duration) -> Self {
52 Self { interval }
53 }
54
55 pub fn interval(&self) -> Duration {
57 self.interval
58 }
59}
60
61impl BackoffStrategy for FixedBackoff {
62 fn delay_for_attempt(&self, _attempt_number: u32) -> Duration {
63 self.interval
64 }
65}
66
67#[derive(Debug, Clone, Copy, PartialEq, Eq)]
71pub struct ExponentialBackoff {
72 base: Duration,
73 max: Duration,
74}
75
76impl ExponentialBackoff {
77 pub fn new(base: Duration, max: Duration) -> Result<Self, BackoffConfigError> {
82 if base > max {
83 return Err(BackoffConfigError::BaseExceedsMax { base, max });
84 }
85 Ok(Self { base, max })
86 }
87
88 pub fn base(&self) -> Duration {
90 self.base
91 }
92
93 pub fn max(&self) -> Duration {
95 self.max
96 }
97}
98
99impl BackoffStrategy for ExponentialBackoff {
100 fn delay_for_attempt(&self, attempt_number: u32) -> Duration {
101 debug_assert!(attempt_number >= 1, "attempt_number is 1-indexed");
102 let exponent = attempt_number.saturating_sub(1);
103 let multiplier = 1u64.checked_shl(exponent).unwrap_or(u64::MAX);
104 let base_millis = u64::try_from(self.base.as_millis()).unwrap_or(u64::MAX);
105 let delay_millis = base_millis.saturating_mul(multiplier);
106 let delay = Duration::from_millis(delay_millis);
107 delay.min(self.max)
108 }
109}
110
111pub fn retry_ready_at(
116 retry_wait_entered_at: u64,
117 attempt_number: u32,
118 strategy: &dyn BackoffStrategy,
119) -> u64 {
120 let delay = strategy.delay_for_attempt(attempt_number);
121 let delay_secs = delay.as_secs().saturating_add(u64::from(delay.subsec_nanos() > 0));
122 retry_wait_entered_at.saturating_add(delay_secs)
123}
124
125#[cfg(test)]
126mod tests {
127 use super::*;
128
129 #[test]
130 fn fixed_backoff_returns_constant_delay() {
131 let strategy = FixedBackoff::new(Duration::from_secs(5));
132
133 assert_eq!(strategy.delay_for_attempt(1), Duration::from_secs(5));
134 assert_eq!(strategy.delay_for_attempt(2), Duration::from_secs(5));
135 assert_eq!(strategy.delay_for_attempt(100), Duration::from_secs(5));
136 }
137
138 #[test]
139 fn exponential_backoff_doubles_per_attempt() {
140 let strategy =
141 ExponentialBackoff::new(Duration::from_secs(1), Duration::from_secs(3600)).unwrap();
142
143 assert_eq!(strategy.delay_for_attempt(1), Duration::from_secs(1)); assert_eq!(strategy.delay_for_attempt(2), Duration::from_secs(2)); assert_eq!(strategy.delay_for_attempt(3), Duration::from_secs(4)); assert_eq!(strategy.delay_for_attempt(4), Duration::from_secs(8)); }
148
149 #[test]
150 fn exponential_backoff_caps_at_max() {
151 let strategy =
152 ExponentialBackoff::new(Duration::from_secs(1), Duration::from_secs(10)).unwrap();
153
154 assert_eq!(strategy.delay_for_attempt(1), Duration::from_secs(1));
155 assert_eq!(strategy.delay_for_attempt(4), Duration::from_secs(8));
156 assert_eq!(strategy.delay_for_attempt(5), Duration::from_secs(10)); assert_eq!(strategy.delay_for_attempt(100), Duration::from_secs(10)); }
159
160 #[test]
161 fn exponential_backoff_saturates_on_overflow() {
162 let strategy =
163 ExponentialBackoff::new(Duration::from_secs(1000), Duration::from_millis(u64::MAX))
164 .unwrap();
165
166 let delay = strategy.delay_for_attempt(u32::MAX);
168 assert!(delay.as_millis() > 0);
169 }
170
171 #[test]
172 fn retry_ready_at_adds_delay_to_current_time() {
173 let strategy = FixedBackoff::new(Duration::from_secs(30));
174
175 assert_eq!(retry_ready_at(1000, 1, &strategy), 1030);
176 assert_eq!(retry_ready_at(1000, 5, &strategy), 1030);
177 }
178
179 #[test]
180 fn retry_ready_at_saturates_at_u64_max() {
181 let strategy = FixedBackoff::new(Duration::from_secs(100));
182
183 assert_eq!(retry_ready_at(u64::MAX - 10, 1, &strategy), u64::MAX);
184 }
185
186 #[test]
187 fn retry_ready_at_rounds_up_sub_second_delay() {
188 let strategy = FixedBackoff::new(Duration::from_millis(500));
189
190 assert_eq!(retry_ready_at(1000, 1, &strategy), 1001);
192 }
193
194 #[test]
195 fn retry_ready_at_exact_seconds_not_rounded_up() {
196 let strategy = FixedBackoff::new(Duration::from_secs(3));
197
198 assert_eq!(retry_ready_at(1000, 1, &strategy), 1003);
200 }
201
202 #[test]
203 fn retry_ready_at_exponential_produces_increasing_delays() {
204 let strategy =
205 ExponentialBackoff::new(Duration::from_secs(10), Duration::from_secs(3600)).unwrap();
206
207 let t1 = retry_ready_at(1000, 1, &strategy);
208 let t2 = retry_ready_at(1000, 2, &strategy);
209 let t3 = retry_ready_at(1000, 3, &strategy);
210
211 assert_eq!(t1, 1010); assert_eq!(t2, 1020); assert_eq!(t3, 1040); assert!(t1 < t2);
215 assert!(t2 < t3);
216 }
217}