wowlab_centrifuge/
backoff.rs1use std::time::Duration;
4
5use rand::RngExt;
6use wowlab_common::retry::ExponentialSchedule;
7
8const DEFAULT_MIN_DELAY: Duration = Duration::from_millis(500);
9const DEFAULT_MAX_DELAY: Duration = Duration::from_secs(20);
10const BACKOFF_MULTIPLIER: u32 = 2;
11
12#[derive(Clone, Debug)]
14pub struct Backoff {
15 min_delay: Duration,
16 max_delay: Duration,
17 schedule: ExponentialSchedule,
18}
19
20impl Backoff {
21 #[must_use]
23 pub fn new(min_delay: Duration, max_delay: Duration) -> Self {
24 Self {
25 min_delay,
26 max_delay,
27 schedule: ExponentialSchedule::new(min_delay, max_delay, BACKOFF_MULTIPLIER),
28 }
29 }
30
31 pub fn next_delay(&mut self) -> Duration {
33 jittered_delay(self.schedule.next_delay(), self.min_delay, self.max_delay)
34 }
35
36 #[must_use]
38 pub fn calculate_delay(&self) -> Duration {
39 jittered_delay(
40 self.schedule.current_delay(),
41 self.min_delay,
42 self.max_delay,
43 )
44 }
45
46 pub fn reset(&mut self) {
48 self.schedule.reset();
49 }
50}
51
52impl Default for Backoff {
53 fn default() -> Self {
54 Self::new(DEFAULT_MIN_DELAY, DEFAULT_MAX_DELAY)
55 }
56}
57
58fn jittered_delay(scheduled_delay: Duration, min_delay: Duration, max_delay: Duration) -> Duration {
59 let duration_millis =
60 |duration: Duration| u64::try_from(duration.as_millis()).unwrap_or(u64::MAX);
61 let base_ms = duration_millis(min_delay);
62 let max_ms = duration_millis(max_delay);
63 let scheduled_ms = duration_millis(scheduled_delay);
64
65 let interval = if scheduled_ms > 0 {
66 rand::rng().random_range(0..=scheduled_ms)
67 } else {
68 0
69 };
70
71 let result = base_ms.saturating_add(interval).min(max_ms);
72
73 Duration::from_millis(result)
74}
75
76#[cfg(test)]
77mod tests {
78 use googletest::prelude::*;
79
80 use super::*;
81
82 #[gtest]
83 fn test_backoff_increases() -> Result<()> {
84 let mut backoff = Backoff::new(Duration::from_millis(100), Duration::from_secs(10));
85
86 let mut max_seen = Duration::ZERO;
87
88 for _ in 0..1000 {
89 backoff.reset();
90
91 for _ in 0..10 {
92 let delay = backoff.next_delay();
93
94 if delay > max_seen {
95 max_seen = delay;
96 }
97 }
98 }
99
100 verify_that!(max_seen, ge(Duration::from_secs(5)))
101 }
102
103 #[gtest]
104 fn test_backoff_respects_max() -> Result<()> {
105 let mut backoff = Backoff::new(Duration::from_millis(100), Duration::from_secs(10));
106
107 for _ in 0..50 {
108 let delay = backoff.next_delay();
109
110 verify_that!(delay, le(Duration::from_secs(10)))?;
111 verify_that!(delay, ge(Duration::from_millis(100)))?;
112 }
113
114 Ok(())
115 }
116
117 #[gtest]
118 fn test_backoff_reset() -> Result<()> {
119 let mut backoff = Backoff::default();
120
121 backoff.next_delay();
122 backoff.next_delay();
123 verify_that!(backoff.calculate_delay(), ge(DEFAULT_MIN_DELAY))?;
124
125 backoff.reset();
126
127 verify_that!(
128 backoff.calculate_delay(),
129 le(DEFAULT_MIN_DELAY.saturating_mul(BACKOFF_MULTIPLIER))
130 )
131 }
132}