laminar_connectors/
retry.rs1use std::time::Duration;
8
9use rand::RngExt;
10
11#[derive(Debug, Clone, Copy)]
13pub struct Backoff {
14 initial: Duration,
15 max: Duration,
16 jitter: f64,
20}
21
22impl Backoff {
23 #[must_use]
25 pub const fn new(initial: Duration, max: Duration, jitter: f64) -> Self {
26 Self {
27 initial,
28 max,
29 jitter,
30 }
31 }
32
33 #[must_use]
37 pub const fn broker_reconnect() -> Self {
38 Self::new(Duration::from_secs(1), Duration::from_secs(30), 0.25)
39 }
40
41 #[must_use]
45 pub fn delay(&self, attempt: u32) -> Duration {
46 let shift = attempt.min(30);
49 let factor = 1u64 << shift;
50 let raw_nanos = self
51 .initial
52 .as_nanos()
53 .saturating_mul(u128::from(factor))
54 .min(u128::from(u64::MAX));
55 #[allow(clippy::cast_possible_truncation)]
56 let raw = Duration::from_nanos(raw_nanos as u64).min(self.max);
57
58 if self.jitter <= 0.0 {
59 return raw;
60 }
61 let mut rng = rand::rng();
62 let frac: f64 = rng.random_range(-self.jitter..=self.jitter);
63 #[allow(
64 clippy::cast_precision_loss,
65 clippy::cast_sign_loss,
66 clippy::cast_possible_truncation
67 )]
68 let jittered_nanos = (raw.as_nanos() as f64 * (1.0 + frac)).max(0.0) as u64;
69 Duration::from_nanos(jittered_nanos)
70 }
71}
72
73#[cfg(test)]
74mod tests {
75 use super::*;
76
77 #[test]
78 fn delay_caps_at_max() {
79 let b = Backoff::new(Duration::from_secs(1), Duration::from_secs(30), 0.0);
80 assert_eq!(b.delay(0), Duration::from_secs(1));
81 assert_eq!(b.delay(1), Duration::from_secs(2));
82 assert_eq!(b.delay(4), Duration::from_secs(16));
83 assert_eq!(b.delay(5), Duration::from_secs(30));
84 assert_eq!(b.delay(100), Duration::from_secs(30));
86 assert_eq!(b.delay(u32::MAX), Duration::from_secs(30));
87 }
88
89 #[test]
90 fn jitter_stays_inside_bounds() {
91 let initial = Duration::from_secs(1);
92 let max = Duration::from_secs(60);
93 let b = Backoff::new(initial, max, 0.25);
94 for attempt in 0..7 {
95 let d = b.delay(attempt);
96 let raw = (initial.saturating_mul(1u32 << attempt)).min(max);
97 let lo = raw.as_secs_f64() * 0.74;
98 let hi = raw.as_secs_f64() * 1.26;
99 let actual = d.as_secs_f64();
100 assert!(
101 actual >= lo && actual <= hi,
102 "attempt {attempt}: {actual} not in [{lo}, {hi}]"
103 );
104 }
105 }
106
107 #[test]
108 fn shift_overflow_protected() {
109 let b = Backoff::broker_reconnect();
112 let _ = b.delay(64);
113 let _ = b.delay(u32::MAX);
114 }
115}