nautilus_network/
backoff.rs1use std::time::Duration;
25
26use rand::Rng;
27
28#[derive(Clone, Debug)]
29pub struct ExponentialBackoff {
30 delay_initial: Duration,
32 delay_max: Duration,
34 delay_current: Duration,
36 factor: f64,
38 jitter_ms: u64,
40 immediate_first: bool,
42}
43
44impl ExponentialBackoff {
52 #[must_use]
54 pub const fn new(
55 delay_initial: Duration,
56 delay_max: Duration,
57 factor: f64,
58 jitter_ms: u64,
59 immediate_first: bool,
60 ) -> Self {
61 Self {
62 delay_initial,
63 delay_max,
64 delay_current: delay_initial,
65 factor,
66 jitter_ms,
67 immediate_first,
68 }
69 }
70
71 pub fn next_duration(&mut self) -> Duration {
77 if self.immediate_first && self.delay_current == self.delay_initial {
78 self.immediate_first = false;
79 return Duration::ZERO;
80 }
81
82 let jitter = rand::rng().random_range(0..=self.jitter_ms);
84 let delay_with_jitter = self.delay_current + Duration::from_millis(jitter);
85
86 let current_nanos = self.delay_current.as_nanos();
88 let max_nanos = self.delay_max.as_nanos() as u64;
89 let next_nanos = (current_nanos as f64 * self.factor) as u64;
90 self.delay_current = Duration::from_nanos(std::cmp::min(next_nanos, max_nanos));
91
92 delay_with_jitter
93 }
94
95 pub fn reset(&mut self) {
97 self.delay_current = self.delay_initial;
98 }
99
100 #[must_use]
104 pub const fn current_delay(&self) -> Duration {
105 self.delay_current
106 }
107}
108
109#[cfg(test)]
113mod tests {
114 use std::time::Duration;
115
116 use rstest::rstest;
117
118 use super::*;
119
120 #[rstest]
121 fn test_no_jitter_exponential_growth() {
122 let initial = Duration::from_millis(100);
123 let max = Duration::from_millis(1600);
124 let factor = 2.0;
125 let jitter = 0;
126 let mut backoff = ExponentialBackoff::new(initial, max, factor, jitter, false);
127
128 let d1 = backoff.next_duration();
130 assert_eq!(d1, Duration::from_millis(100));
131
132 let d2 = backoff.next_duration();
134 assert_eq!(d2, Duration::from_millis(200));
135
136 let d3 = backoff.next_duration();
138 assert_eq!(d3, Duration::from_millis(400));
139
140 let d4 = backoff.next_duration();
142 assert_eq!(d4, Duration::from_millis(800));
143
144 let d5 = backoff.next_duration();
146 assert_eq!(d5, Duration::from_millis(1600));
147
148 let d6 = backoff.next_duration();
150 assert_eq!(d6, Duration::from_millis(1600));
151 }
152
153 #[rstest]
154 fn test_reset() {
155 let initial = Duration::from_millis(100);
156 let max = Duration::from_millis(1600);
157 let factor = 2.0;
158 let jitter = 0;
159 let mut backoff = ExponentialBackoff::new(initial, max, factor, jitter, false);
160
161 let _ = backoff.next_duration(); backoff.reset();
164 let d = backoff.next_duration();
165 assert_eq!(d, Duration::from_millis(100));
167 }
168
169 #[rstest]
170 fn test_jitter_within_bounds() {
171 let initial = Duration::from_millis(100);
172 let max = Duration::from_millis(1000);
173 let factor = 2.0;
174 let jitter = 50;
175 for _ in 0..10 {
177 let mut backoff = ExponentialBackoff::new(initial, max, factor, jitter, false);
178 let base = backoff.delay_current;
180 let delay = backoff.next_duration();
181 let min_expected = base;
183 let max_expected = base + Duration::from_millis(jitter);
184 assert!(
185 delay >= min_expected,
186 "Delay {delay:?} is less than expected minimum {min_expected:?}"
187 );
188 assert!(
189 delay <= max_expected,
190 "Delay {delay:?} exceeds expected maximum {max_expected:?}"
191 );
192 }
193 }
194
195 #[rstest]
196 fn test_factor_less_than_two() {
197 let initial = Duration::from_millis(100);
198 let max = Duration::from_millis(200);
199 let factor = 1.5;
200 let jitter = 0;
201 let mut backoff = ExponentialBackoff::new(initial, max, factor, jitter, false);
202
203 let d1 = backoff.next_duration();
205 assert_eq!(d1, Duration::from_millis(100));
206
207 let d2 = backoff.next_duration();
209 assert_eq!(d2, Duration::from_millis(150));
210
211 let d3 = backoff.next_duration();
213 assert_eq!(d3, Duration::from_millis(200));
214
215 let d4 = backoff.next_duration();
217 assert_eq!(d4, Duration::from_millis(200));
218 }
219
220 #[rstest]
221 fn test_max_delay_is_respected() {
222 let initial = Duration::from_millis(500);
223 let max = Duration::from_millis(1000);
224 let factor = 3.0;
225 let jitter = 0;
226 let mut backoff = ExponentialBackoff::new(initial, max, factor, jitter, false);
227
228 let d1 = backoff.next_duration();
230 assert_eq!(d1, Duration::from_millis(500));
231
232 let d2 = backoff.next_duration();
234 assert_eq!(d2, Duration::from_millis(1000));
235
236 let d3 = backoff.next_duration();
238 assert_eq!(d3, Duration::from_millis(1000));
239 }
240
241 #[rstest]
242 fn test_current_delay_getter() {
243 let initial = Duration::from_millis(100);
244 let max = Duration::from_millis(1600);
245 let factor = 2.0;
246 let jitter = 0;
247 let mut backoff = ExponentialBackoff::new(initial, max, factor, jitter, false);
248
249 assert_eq!(backoff.current_delay(), initial);
250
251 let _ = backoff.next_duration();
252 assert_eq!(backoff.current_delay(), Duration::from_millis(200));
253
254 let _ = backoff.next_duration();
255 assert_eq!(backoff.current_delay(), Duration::from_millis(400));
256
257 backoff.reset();
258 assert_eq!(backoff.current_delay(), initial);
259 }
260
261 #[rstest]
262 fn test_immediate_first() {
263 let initial = Duration::from_millis(100);
264 let max = Duration::from_millis(1600);
265 let factor = 2.0;
266 let jitter = 0;
267 let mut backoff = ExponentialBackoff::new(initial, max, factor, jitter, true);
268
269 let d1 = backoff.next_duration();
271 assert_eq!(
272 d1,
273 Duration::ZERO,
274 "Expected immediate reconnect (zero delay) on first call"
275 );
276
277 let d2 = backoff.next_duration();
279 assert_eq!(
280 d2, initial,
281 "Expected the delay to be the initial delay after immediate reconnect"
282 );
283
284 let d3 = backoff.next_duration();
286 let expected = initial * 2; assert_eq!(
288 d3, expected,
289 "Expected exponential growth from the initial delay"
290 );
291 }
292}