wowlab_node/utils/
backoff.rs1use std::time::Duration;
4
5use wowlab_common::{retry::ExponentialSchedule, time::Instant};
6
7const BACKOFF_MULTIPLIER: u32 = 2;
8
9#[derive(Clone, Debug)]
10pub(crate) struct ExponentialBackoff {
11 schedule: ExponentialSchedule,
12 retry_at: Option<Instant>,
13}
14
15impl ExponentialBackoff {
16 pub(crate) fn new(initial: Duration, max: Duration) -> Self {
17 Self {
18 schedule: ExponentialSchedule::new(initial, max, BACKOFF_MULTIPLIER),
19 retry_at: None,
20 }
21 }
22
23 pub(crate) fn next_retry_at(&mut self) -> Instant {
24 let retry_at = Instant::now() + self.schedule.current_delay();
25
26 self.retry_at = Some(retry_at);
27
28 retry_at
29 }
30
31 pub(crate) fn time_until_retry(&self) -> Option<Duration> {
32 self.retry_at
33 .map(|at| at.saturating_duration_since(Instant::now()))
34 }
35
36 pub(crate) fn should_retry(&self) -> bool {
37 self.retry_at.is_some_and(|at| Instant::now() >= at)
38 }
39
40 pub(crate) fn on_retry(&mut self) {
41 self.retry_at = None;
42 let _ = self.schedule.next_delay();
43 }
44
45 pub(crate) fn reset(&mut self) {
46 self.schedule.reset();
47 self.retry_at = None;
48 }
49
50 pub(crate) fn current_backoff(&self) -> Duration {
51 self.schedule.current_delay()
52 }
53}
54
55#[cfg(test)]
56mod tests {
57 use googletest::prelude::*;
58
59 use super::*;
60
61 #[gtest]
62 fn new_uses_initial_delay() -> Result<()> {
63 let backoff = ExponentialBackoff::new(Duration::from_secs(5), Duration::from_secs(300));
64
65 verify_that!(backoff.current_backoff(), eq(Duration::from_secs(5)))
66 }
67
68 #[gtest]
69 fn retries_double_the_delay() -> Result<()> {
70 let mut backoff = ExponentialBackoff::new(Duration::from_secs(1), Duration::from_secs(60));
71
72 verify_that!(backoff.current_backoff(), eq(Duration::from_secs(1)))?;
73
74 backoff.on_retry();
75 verify_that!(backoff.current_backoff(), eq(Duration::from_secs(2)))?;
76
77 backoff.on_retry();
78 verify_that!(backoff.current_backoff(), eq(Duration::from_secs(4)))?;
79
80 backoff.on_retry();
81
82 verify_that!(backoff.current_backoff(), eq(Duration::from_secs(8)))
83 }
84
85 #[gtest]
86 fn retries_stop_at_maximum_delay() -> Result<()> {
87 let mut backoff = ExponentialBackoff::new(Duration::from_secs(32), Duration::from_secs(60));
88
89 backoff.on_retry();
90 verify_that!(backoff.current_backoff(), eq(Duration::from_secs(60)))?;
91
92 backoff.on_retry();
93
94 verify_that!(backoff.current_backoff(), eq(Duration::from_secs(60)))
95 }
96
97 #[gtest]
98 fn reset_restores_initial_delay() -> Result<()> {
99 let mut backoff = ExponentialBackoff::new(Duration::from_secs(5), Duration::from_secs(300));
100
101 backoff.on_retry();
102 backoff.on_retry();
103 verify_that!(backoff.current_backoff(), eq(Duration::from_secs(20)))?;
104
105 backoff.reset();
106
107 verify_that!(backoff.current_backoff(), eq(Duration::from_secs(5)))
108 }
109
110 #[gtest]
111 fn scheduled_retry_becomes_ready_after_delay() -> Result<()> {
112 let mut backoff =
113 ExponentialBackoff::new(Duration::from_millis(10), Duration::from_secs(1));
114
115 verify_false!(backoff.should_retry())?;
116 verify_that!(backoff.time_until_retry(), none())?;
117
118 let _ = backoff.next_retry_at();
119
120 verify_that!(backoff.time_until_retry(), some(anything()))?;
121 verify_false!(backoff.should_retry())?;
122
123 std::thread::sleep(Duration::from_millis(15));
124
125 verify_true!(backoff.should_retry())
126 }
127}