1use std::{
11 cmp,
12 fmt::Debug,
13 time::{Duration, Instant},
14};
15
16use rand::{Rng, RngExt};
17
18#[derive(Debug, PartialEq, Eq)]
19pub enum RetryAction {
20 OKAY,
21 WAIT,
22}
23
24pub trait RetryPolicy: Debug + PartialEq + Eq {
25 fn max_tries(&self) -> usize;
26 fn current_tries(&self) -> usize;
27
28 fn fail<R: Rng + ?Sized>(&mut self, now: Instant, rng: &mut R);
30 fn succeed(&mut self, now: Instant);
32
33 fn can_try(&self, _now: Instant) -> Option<RetryAction> {
35 if self.current_tries() >= self.max_tries() {
36 None
37 } else {
38 Some(RetryAction::OKAY)
39 }
40 }
41
42 fn is_down(&self) -> bool;
43}
44
45#[derive(Debug, PartialEq, Eq, Clone)]
46pub enum RetryPolicyWrapper {
47 ExponentialBackoff(ExponentialBackoffPolicy),
48}
49
50#[derive(Debug, PartialEq, Eq, Clone)]
51pub struct ExponentialBackoffPolicy {
52 max_tries: usize,
53 current_tries: usize,
54 last_try: Option<Instant>,
59 wait: Duration,
60}
61
62impl ExponentialBackoffPolicy {
63 pub fn new(max_tries: usize) -> Self {
64 ExponentialBackoffPolicy {
65 max_tries,
66 current_tries: 0,
67 last_try: None,
68 wait: Duration::ZERO,
69 }
70 }
71
72 fn since_last_try(&self, now: Instant) -> Duration {
78 self.last_try.map_or(Duration::ZERO, |last_try| {
79 now.saturating_duration_since(last_try)
80 })
81 }
82}
83
84impl RetryPolicy for ExponentialBackoffPolicy {
85 fn max_tries(&self) -> usize {
86 self.max_tries
87 }
88
89 fn current_tries(&self) -> usize {
90 self.current_tries
91 }
92
93 fn fail<R: Rng + ?Sized>(&mut self, now: Instant, rng: &mut R) {
94 if self.since_last_try(now).lt(&self.wait) {
95 return;
97 }
98
99 let max_secs = cmp::max(
100 1,
101 1u64.checked_shl(self.current_tries as u32)
102 .unwrap_or(u64::MAX),
103 );
104 let wait = if max_secs == 1 {
105 1
106 } else {
107 rng.random_range(1..max_secs)
108 };
109
110 self.wait = Duration::from_secs(wait);
111 self.last_try = Some(now);
112 self.current_tries = cmp::min(self.current_tries + 1, self.max_tries);
113 }
114
115 fn succeed(&mut self, now: Instant) {
116 self.wait = Duration::default();
117 self.last_try = Some(now);
118 self.current_tries = 0;
119 }
120
121 fn can_try(&self, now: Instant) -> Option<RetryAction> {
122 let action = if self.since_last_try(now).ge(&self.wait) {
123 RetryAction::OKAY
124 } else {
125 RetryAction::WAIT
126 };
127
128 Some(action)
129 }
130
131 fn is_down(&self) -> bool {
132 self.current_tries() >= self.max_tries()
133 }
134}
135
136#[cfg(test)]
137impl ExponentialBackoffPolicy {
138 pub(crate) fn force_down(&mut self) {
146 self.current_tries = self.max_tries;
147 }
148}
149
150#[cfg(test)]
151impl RetryPolicyWrapper {
152 pub(crate) fn force_down(&mut self) {
154 match self {
155 RetryPolicyWrapper::ExponentialBackoff(p) => p.force_down(),
156 }
157 }
158}
159
160impl From<ExponentialBackoffPolicy> for RetryPolicyWrapper {
161 fn from(val: ExponentialBackoffPolicy) -> Self {
162 RetryPolicyWrapper::ExponentialBackoff(val)
163 }
164}
165
166impl RetryPolicy for RetryPolicyWrapper {
167 fn max_tries(&self) -> usize {
168 match *self {
169 RetryPolicyWrapper::ExponentialBackoff(ref policy) => policy,
170 }
171 .max_tries()
172 }
173
174 fn current_tries(&self) -> usize {
175 match *self {
176 RetryPolicyWrapper::ExponentialBackoff(ref policy) => policy,
177 }
178 .current_tries()
179 }
180
181 fn fail<R: Rng + ?Sized>(&mut self, now: Instant, rng: &mut R) {
182 match *self {
183 RetryPolicyWrapper::ExponentialBackoff(ref mut policy) => policy,
184 }
185 .fail(now, rng)
186 }
187
188 fn succeed(&mut self, now: Instant) {
189 match *self {
190 RetryPolicyWrapper::ExponentialBackoff(ref mut policy) => policy,
191 }
192 .succeed(now)
193 }
194
195 fn can_try(&self, now: Instant) -> Option<RetryAction> {
196 match *self {
197 RetryPolicyWrapper::ExponentialBackoff(ref policy) => policy,
198 }
199 .can_try(now)
200 }
201
202 fn is_down(&self) -> bool {
203 match *self {
204 RetryPolicyWrapper::ExponentialBackoff(ref policy) => policy,
205 }
206 .is_down()
207 }
208}
209
210#[cfg(test)]
211mod tests {
212 use std::time::{Duration, Instant};
213
214 use rand::{SeedableRng, rngs::StdRng};
215
216 use super::{ExponentialBackoffPolicy, RetryAction, RetryPolicy};
217
218 const MAX_FAILS: usize = 10;
219
220 fn rng() -> StdRng {
221 StdRng::seed_from_u64(0x5eed)
222 }
223
224 #[test]
225 fn no_fail() {
226 let policy = ExponentialBackoffPolicy::new(MAX_FAILS);
227 let can_try = policy.can_try(Instant::now());
228
229 assert_eq!(Some(RetryAction::OKAY), can_try)
230 }
231
232 #[test]
233 fn single_fail() {
234 let t0 = Instant::now();
235 let mut policy = ExponentialBackoffPolicy::new(MAX_FAILS);
236 policy.fail(t0, &mut rng());
237 let can_try = policy.can_try(t0);
238
239 assert_eq!(Some(RetryAction::WAIT), can_try)
241 }
242
243 #[test]
244 fn max_fails() {
245 let t0 = Instant::now();
246 let mut rng = rng();
247 let mut policy = ExponentialBackoffPolicy::new(MAX_FAILS);
248
249 for _ in 0..MAX_FAILS {
250 policy.fail(t0, &mut rng);
251 }
252
253 let can_try = policy.can_try(t0);
254
255 assert_eq!(Some(RetryAction::WAIT), can_try)
256 }
257
258 #[test]
259 fn recover_from_fail() {
260 let t0 = Instant::now();
261 let mut rng = rng();
262 let mut policy = ExponentialBackoffPolicy::new(MAX_FAILS);
263
264 for _ in 0..(MAX_FAILS - 1) {
266 policy.fail(t0, &mut rng);
267 }
268
269 policy.succeed(t0);
270 policy.fail(t0, &mut rng);
271 policy.fail(t0, &mut rng);
272 policy.fail(t0, &mut rng);
273
274 let can_try = policy.can_try(t0);
275
276 assert_eq!(Some(RetryAction::WAIT), can_try)
277 }
278
279 #[test]
289 fn the_backoff_window_follows_the_injected_clock() {
290 let t0 = Instant::now();
291 let mut policy = ExponentialBackoffPolicy::new(MAX_FAILS);
292 policy.fail(t0, &mut rng());
293
294 let one_second = Duration::from_secs(1);
295 assert_eq!(
296 policy.can_try(t0 + one_second - Duration::from_nanos(1)),
297 Some(RetryAction::WAIT),
298 "one nanosecond before the window closes, the policy must wait"
299 );
300 assert_eq!(
301 policy.can_try(t0 + one_second),
302 Some(RetryAction::OKAY),
303 "the window closes at t0 + 1s on the injected clock"
304 );
305 }
306
307 fn windows(seed: u64) -> Vec<Duration> {
310 let mut rng = StdRng::seed_from_u64(seed);
311 let mut policy = ExponentialBackoffPolicy::new(MAX_FAILS);
312 let mut now = Instant::now();
313 let mut windows = Vec::with_capacity(MAX_FAILS);
314 while !policy.is_down() {
315 policy.fail(now, &mut rng);
316 windows.push(policy.wait);
317 now += policy.wait;
318 }
319 windows
320 }
321
322 #[test]
328 fn one_seed_yields_one_sequence_of_backoff_windows() {
329 let first = windows(7);
330 assert_eq!(first.len(), MAX_FAILS);
331 assert_eq!(first, windows(7), "one seed must yield one sequence");
332 assert_ne!(
333 first,
334 windows(8),
335 "the jitter must come from the lent RNG, so another seed differs"
336 );
337 }
338}