1use std::time::Duration;
25
26use nautilus_core::correctness::{check_in_range_inclusive_f64, check_predicate_true};
27use rand::RngExt;
28
29#[derive(Clone, Debug)]
30pub struct ExponentialBackoff {
31 delay_initial: Duration,
33 delay_max: Duration,
35 delay_current: Duration,
37 factor: f64,
39 jitter_ms: u64,
41 immediate_reconnect: bool,
43 immediate_reconnect_original: bool,
45}
46
47impl ExponentialBackoff {
55 pub fn new(
65 delay_initial: Duration,
66 delay_max: Duration,
67 factor: f64,
68 jitter_ms: u64,
69 immediate_first: bool,
70 ) -> anyhow::Result<Self> {
71 check_predicate_true(!delay_initial.is_zero(), "delay_initial must be non-zero")?;
72 check_predicate_true(
73 delay_max >= delay_initial,
74 "delay_max must be >= delay_initial",
75 )?;
76 check_predicate_true(
77 delay_max.as_nanos() <= u128::from(u64::MAX),
78 "delay_max exceeds maximum representable duration (≈584 years)",
79 )?;
80 check_in_range_inclusive_f64(factor, 1.0, 100.0, "factor")?;
81
82 Ok(Self {
83 delay_initial,
84 delay_max,
85 delay_current: delay_initial,
86 factor,
87 jitter_ms,
88 immediate_reconnect: immediate_first,
89 immediate_reconnect_original: immediate_first,
90 })
91 }
92
93 pub fn next_duration(&mut self) -> Duration {
103 if self.immediate_reconnect && self.delay_current == self.delay_initial {
104 self.immediate_reconnect = false;
105 return Duration::ZERO;
106 }
107
108 let jitter = rand::rng().random_range(0..=self.jitter_ms); let base = std::cmp::min(
113 self.delay_current,
114 self.delay_max
115 .saturating_sub(Duration::from_millis(self.jitter_ms)),
116 );
117 let delay_with_jitter = base + Duration::from_millis(jitter);
118
119 let floor = std::cmp::min(self.delay_initial, self.delay_max);
121 let clamped_delay = delay_with_jitter.clamp(floor, self.delay_max);
122
123 let current_nanos = self.delay_current.as_nanos();
126 let max_nanos = self.delay_max.as_nanos();
127
128 let next_nanos_u128 = if current_nanos > u128::from(u64::MAX) {
130 max_nanos
132 } else {
133 let current_u64 = current_nanos as u64;
134 let next_f64 = current_u64 as f64 * self.factor;
135
136 if next_f64 > u64::MAX as f64 {
138 u128::from(u64::MAX)
139 } else {
140 u128::from(next_f64 as u64)
141 }
142 };
143
144 let clamped = std::cmp::min(next_nanos_u128, max_nanos);
145 let final_nanos = if clamped > u128::from(u64::MAX) {
146 u64::MAX
147 } else {
148 clamped as u64
149 };
150
151 self.delay_current = Duration::from_nanos(final_nanos);
152
153 clamped_delay
154 }
155
156 pub const fn reset(&mut self) {
158 self.delay_current = self.delay_initial;
159 self.immediate_reconnect = self.immediate_reconnect_original;
160 }
161
162 #[must_use]
166 pub const fn current_delay(&self) -> Duration {
167 self.delay_current
168 }
169}
170
171#[cfg(test)]
172mod tests {
173 use std::time::Duration;
174
175 use rstest::rstest;
176
177 use super::*;
178
179 #[rstest]
180 fn test_no_jitter_exponential_growth() {
181 let initial = Duration::from_millis(100);
182 let max = Duration::from_millis(1600);
183 let factor = 2.0;
184 let jitter = 0;
185 let mut backoff = ExponentialBackoff::new(initial, max, factor, jitter, false).unwrap();
186
187 let d1 = backoff.next_duration();
189 assert_eq!(d1, Duration::from_millis(100));
190
191 let d2 = backoff.next_duration();
193 assert_eq!(d2, Duration::from_millis(200));
194
195 let d3 = backoff.next_duration();
197 assert_eq!(d3, Duration::from_millis(400));
198
199 let d4 = backoff.next_duration();
201 assert_eq!(d4, Duration::from_millis(800));
202
203 let d5 = backoff.next_duration();
205 assert_eq!(d5, Duration::from_millis(1600));
206
207 let d6 = backoff.next_duration();
209 assert_eq!(d6, Duration::from_millis(1600));
210 }
211
212 #[rstest]
213 fn test_reset() {
214 let initial = Duration::from_millis(100);
215 let max = Duration::from_millis(1600);
216 let factor = 2.0;
217 let jitter = 0;
218 let mut backoff = ExponentialBackoff::new(initial, max, factor, jitter, false).unwrap();
219
220 let _ = backoff.next_duration(); backoff.reset();
223 let d = backoff.next_duration();
224 assert_eq!(d, Duration::from_millis(100));
226 }
227
228 #[rstest]
229 fn test_jitter_within_bounds() {
230 let initial = Duration::from_millis(100);
231 let max = Duration::from_secs(1);
232 let factor = 2.0;
233 let jitter = 50;
234 for _ in 0..10 {
236 let mut backoff = ExponentialBackoff::new(initial, max, factor, jitter, false).unwrap();
237 let base = backoff.delay_current;
239 let delay = backoff.next_duration();
240 let min_expected = base;
242 let max_expected = base + Duration::from_millis(jitter);
243 assert!(
244 delay >= min_expected,
245 "Delay {delay:?} is less than expected minimum {min_expected:?}"
246 );
247 assert!(
248 delay <= max_expected,
249 "Delay {delay:?} exceeds expected maximum {max_expected:?}"
250 );
251 }
252 }
253
254 #[rstest]
255 fn test_factor_less_than_two() {
256 let initial = Duration::from_millis(100);
257 let max = Duration::from_millis(200);
258 let factor = 1.5;
259 let jitter = 0;
260 let mut backoff = ExponentialBackoff::new(initial, max, factor, jitter, false).unwrap();
261
262 let d1 = backoff.next_duration();
264 assert_eq!(d1, Duration::from_millis(100));
265
266 let d2 = backoff.next_duration();
268 assert_eq!(d2, Duration::from_millis(150));
269
270 let d3 = backoff.next_duration();
272 assert_eq!(d3, Duration::from_millis(200));
273
274 let d4 = backoff.next_duration();
276 assert_eq!(d4, Duration::from_millis(200));
277 }
278
279 #[rstest]
280 fn test_max_delay_is_respected() {
281 let initial = Duration::from_millis(500);
282 let max = Duration::from_secs(1);
283 let factor = 3.0;
284 let jitter = 0;
285 let mut backoff = ExponentialBackoff::new(initial, max, factor, jitter, false).unwrap();
286
287 let d1 = backoff.next_duration();
289 assert_eq!(d1, Duration::from_millis(500));
290
291 let d2 = backoff.next_duration();
293 assert_eq!(d2, Duration::from_secs(1));
294
295 let d3 = backoff.next_duration();
297 assert_eq!(d3, Duration::from_secs(1));
298 }
299
300 #[rstest]
301 fn test_current_delay_getter() {
302 let initial = Duration::from_millis(100);
303 let max = Duration::from_millis(1600);
304 let factor = 2.0;
305 let jitter = 0;
306 let mut backoff = ExponentialBackoff::new(initial, max, factor, jitter, false).unwrap();
307
308 assert_eq!(backoff.current_delay(), initial);
309
310 let _ = backoff.next_duration();
311 assert_eq!(backoff.current_delay(), Duration::from_millis(200));
312
313 let _ = backoff.next_duration();
314 assert_eq!(backoff.current_delay(), Duration::from_millis(400));
315
316 backoff.reset();
317 assert_eq!(backoff.current_delay(), initial);
318 }
319
320 #[rstest]
321 fn test_validation_zero_initial_delay() {
322 let result = ExponentialBackoff::new(Duration::ZERO, Duration::from_secs(1), 2.0, 0, false);
323 assert!(result.is_err());
324 assert!(
325 result
326 .unwrap_err()
327 .to_string()
328 .contains("delay_initial must be non-zero")
329 );
330 }
331
332 #[rstest]
333 fn test_validation_max_less_than_initial() {
334 let result = ExponentialBackoff::new(
335 Duration::from_secs(1),
336 Duration::from_millis(500),
337 2.0,
338 0,
339 false,
340 );
341 assert!(result.is_err());
342 assert!(
343 result
344 .unwrap_err()
345 .to_string()
346 .contains("delay_max must be >= delay_initial")
347 );
348 }
349
350 #[rstest]
351 fn test_validation_factor_too_small() {
352 let result = ExponentialBackoff::new(
353 Duration::from_millis(100),
354 Duration::from_secs(1),
355 0.5,
356 0,
357 false,
358 );
359 assert!(result.is_err());
360 assert!(result.unwrap_err().to_string().contains("factor"));
361 }
362
363 #[rstest]
364 fn test_validation_factor_too_large() {
365 let result = ExponentialBackoff::new(
366 Duration::from_millis(100),
367 Duration::from_secs(1),
368 150.0,
369 0,
370 false,
371 );
372 assert!(result.is_err());
373 assert!(result.unwrap_err().to_string().contains("factor"));
374 }
375
376 #[rstest]
377 fn test_validation_delay_max_exceeds_u64_max_nanos() {
378 let max_valid = Duration::from_nanos(u64::MAX);
381 let too_large = max_valid + Duration::from_nanos(1);
382
383 let result = ExponentialBackoff::new(Duration::from_millis(100), too_large, 2.0, 0, false);
384 assert!(result.is_err());
385 assert!(
386 result
387 .unwrap_err()
388 .to_string()
389 .contains("delay_max exceeds maximum representable duration")
390 );
391 }
392
393 #[rstest]
394 fn test_immediate_first() {
395 let initial = Duration::from_millis(100);
396 let max = Duration::from_millis(1600);
397 let factor = 2.0;
398 let jitter = 0;
399 let mut backoff = ExponentialBackoff::new(initial, max, factor, jitter, true).unwrap();
400
401 let d1 = backoff.next_duration();
403 assert_eq!(
404 d1,
405 Duration::ZERO,
406 "Expected immediate reconnect (zero delay) on first call"
407 );
408
409 let d2 = backoff.next_duration();
411 assert_eq!(
412 d2, initial,
413 "Expected the delay to be the initial delay after immediate reconnect"
414 );
415
416 let d3 = backoff.next_duration();
418 let expected = initial * 2; assert_eq!(
420 d3, expected,
421 "Expected exponential growth from the initial delay"
422 );
423 }
424
425 #[rstest]
426 fn test_reset_restores_immediate_first() {
427 let initial = Duration::from_millis(100);
428 let max = Duration::from_millis(1600);
429 let factor = 2.0;
430 let jitter = 0;
431 let mut backoff = ExponentialBackoff::new(initial, max, factor, jitter, true).unwrap();
432
433 let d1 = backoff.next_duration();
435 assert_eq!(d1, Duration::ZERO);
436
437 let d2 = backoff.next_duration();
439 assert_eq!(d2, initial);
440
441 backoff.reset();
443 let d3 = backoff.next_duration();
444 assert_eq!(
445 d3,
446 Duration::ZERO,
447 "Reset should restore immediate_first behavior"
448 );
449 }
450
451 #[rstest]
452 fn test_jitter_never_exceeds_max_delay() {
453 let initial = Duration::from_millis(100);
454 let max = Duration::from_secs(1);
455 let factor = 2.0;
456 let jitter = 500;
457
458 let mut backoff = ExponentialBackoff::new(initial, max, factor, jitter, false).unwrap();
459
460 while backoff.current_delay() < max {
462 backoff.next_duration();
463 }
464
465 for _ in 0..100 {
467 let delay = backoff.next_duration();
468 assert!(
469 delay <= max,
470 "Delay with jitter {delay:?} exceeded max {max:?}"
471 );
472 }
473 }
474
475 #[rstest]
476 fn test_jitter_spreads_delays_at_cap() {
477 let initial = Duration::from_millis(100);
481 let max = Duration::from_secs(1);
482 let mut backoff = ExponentialBackoff::new(initial, max, 2.0, 500, false).unwrap();
483
484 while backoff.current_delay() < max {
485 backoff.next_duration();
486 }
487
488 let mut distinct = std::collections::HashSet::new();
489 for _ in 0..100 {
490 distinct.insert(backoff.next_duration());
491 }
492
493 assert!(
494 distinct.len() >= 2,
495 "Jitter must keep spreading delays once the backoff saturates at the cap"
496 );
497 }
498
499 #[rstest]
500 fn test_jitter_wider_than_max_never_returns_zero_delay() {
501 let max = Duration::from_millis(50);
504 let mut backoff =
505 ExponentialBackoff::new(Duration::from_millis(10), max, 2.0, 100, false).unwrap();
506
507 for _ in 0..200 {
508 let delay = backoff.next_duration();
509 assert!(
510 !delay.is_zero(),
511 "Non-immediate backoff delay must be positive"
512 );
513 assert!(delay <= max, "Delay {delay:?} exceeded max {max:?}");
514 }
515 }
516}