Skip to main content

wowlab_node/utils/
backoff.rs

1//! Exponential backoff for retries.
2
3use 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}