1use std::time::Duration;
13
14#[derive(Clone, Debug)]
19pub struct BackoffConfig {
20 pub initial_interval: Duration,
22 pub max_interval: Duration,
24 pub jitter_fraction: f64,
27}
28
29impl Default for BackoffConfig {
30 fn default() -> Self {
31 Self {
32 initial_interval: Duration::from_millis(100),
33 max_interval: Duration::from_secs(30),
34 jitter_fraction: 0.25,
35 }
36 }
37}
38
39impl BackoffConfig {
40 pub fn for_neighbor_hello() -> Self {
45 Self {
46 initial_interval: Duration::from_millis(500),
47 max_interval: Duration::from_secs(10),
48 jitter_fraction: 0.3,
49 }
50 }
51
52 pub fn for_swim_probe() -> Self {
56 Self {
57 initial_interval: Duration::from_millis(200),
58 max_interval: Duration::from_secs(5),
59 jitter_fraction: 0.2,
60 }
61 }
62}
63
64#[derive(Clone, Debug)]
69pub struct BackoffState {
70 current: Duration,
71 rng: u32,
73}
74
75impl BackoffState {
76 pub fn new(seed: u32) -> Self {
81 Self {
82 current: Duration::ZERO,
83 rng: if seed == 0 { 0xdeadbeef } else { seed },
84 }
85 }
86
87 pub fn next_failure(&mut self, cfg: &BackoffConfig) -> Duration {
91 if self.current.is_zero() {
92 self.current = cfg.initial_interval;
93 } else {
94 self.current = (self.current * 2).min(cfg.max_interval);
95 }
96 self.apply_jitter(cfg)
97 }
98
99 pub fn reset(&mut self, _cfg: &BackoffConfig) {
104 self.current = Duration::ZERO;
105 }
106
107 pub fn current(&self) -> Duration {
109 self.current
110 }
111
112 fn apply_jitter(&mut self, cfg: &BackoffConfig) -> Duration {
113 let base_ms = self.current.as_millis() as u64;
114 if base_ms == 0 || cfg.jitter_fraction <= 0.0 {
115 return self.current;
116 }
117 self.rng ^= self.rng << 13;
119 self.rng ^= self.rng >> 17;
120 self.rng ^= self.rng << 5;
121 let range_ms = (base_ms as f64 * cfg.jitter_fraction) as u64;
122 let jitter_ms = if range_ms > 0 {
123 (self.rng as u64 % (2 * range_ms)) as i64 - range_ms as i64
124 } else {
125 0
126 };
127 let result_ms = (base_ms as i64 + jitter_ms).max(1) as u64;
128 Duration::from_millis(result_ms)
129 }
130}
131
132#[cfg(test)]
133mod tests {
134 use super::*;
135
136 #[test]
137 fn doubles_on_failure() {
138 let cfg = BackoffConfig {
139 initial_interval: Duration::from_millis(100),
140 max_interval: Duration::from_secs(10),
141 jitter_fraction: 0.0, };
143 let mut state = BackoffState::new(1);
144 let d1 = state.next_failure(&cfg);
145 let d2 = state.next_failure(&cfg);
146 let d3 = state.next_failure(&cfg);
147 assert_eq!(d1, Duration::from_millis(100));
148 assert_eq!(d2, Duration::from_millis(200));
149 assert_eq!(d3, Duration::from_millis(400));
150 }
151
152 #[test]
153 fn capped_at_max() {
154 let cfg = BackoffConfig {
155 initial_interval: Duration::from_millis(100),
156 max_interval: Duration::from_millis(300),
157 jitter_fraction: 0.0,
158 };
159 let mut state = BackoffState::new(1);
160 for _ in 0..10 {
161 state.next_failure(&cfg);
162 }
163 assert_eq!(state.current(), Duration::from_millis(300));
164 }
165
166 #[test]
167 fn reset_goes_to_zero() {
168 let cfg = BackoffConfig {
169 initial_interval: Duration::from_millis(100),
170 max_interval: Duration::from_secs(30),
171 jitter_fraction: 0.0, };
173 let mut state = BackoffState::new(42);
174 state.next_failure(&cfg);
175 state.next_failure(&cfg);
176 state.reset(&cfg);
177 assert_eq!(state.current(), Duration::ZERO);
179 let d = state.next_failure(&cfg);
180 assert_eq!(d, cfg.initial_interval);
181 }
182
183 #[test]
184 fn jitter_stays_in_range() {
185 let cfg = BackoffConfig {
186 initial_interval: Duration::from_millis(1000),
187 max_interval: Duration::from_secs(60),
188 jitter_fraction: 0.25,
189 };
190 let mut state = BackoffState::new(999);
191 for _ in 0..50 {
192 let d = state.next_failure(&cfg);
193 assert!(d >= Duration::from_millis(750), "too low: {d:?}");
195 assert!(d <= Duration::from_millis(1250), "too high: {d:?}");
196 state.reset(&cfg);
197 }
198 }
199}