1use std::time::Duration;
37
38use crate::policies::jitter::JitterPolicy;
39
40#[derive(Clone, Copy, Debug)]
47pub struct BackoffPolicy {
48 pub first: Duration,
50 pub max: Duration,
52 pub factor: f64,
54 pub jitter: JitterPolicy,
56}
57
58impl Default for BackoffPolicy {
59 fn default() -> Self {
64 Self {
65 first: Duration::from_millis(100),
66 max: Duration::from_secs(30),
67 jitter: JitterPolicy::None,
68 factor: 1.0,
69 }
70 }
71}
72
73impl BackoffPolicy {
74 pub fn next(&self, attempt: u32) -> Duration {
85 let max_secs = self.max.as_secs_f64();
86 let clamped_exp = attempt.min(i32::MAX as u32) as i32;
87 let unclamped_secs = self.first.as_secs_f64() * self.factor.powi(clamped_exp);
88
89 let base =
90 if !unclamped_secs.is_finite() || unclamped_secs < 0.0 || unclamped_secs > max_secs {
91 self.max
92 } else {
93 Duration::from_secs_f64(unclamped_secs)
94 };
95
96 match self.jitter {
97 JitterPolicy::Decorrelated => {
98 self.jitter
99 .apply_decorrelated(self.first.min(self.max), base, self.max)
100 }
101 _ => self.jitter.apply(base),
102 }
103 }
104}
105
106#[cfg(test)]
107mod tests {
108 use super::*;
109 use std::time::Duration;
110
111 #[test]
112 fn test_attempt_zero_returns_first() {
113 let policy = BackoffPolicy {
114 first: Duration::from_millis(100),
115 max: Duration::from_secs(30),
116 factor: 2.0,
117 jitter: JitterPolicy::None,
118 };
119 assert_eq!(policy.next(0), Duration::from_millis(100));
120 }
121
122 #[test]
123 fn test_exponential_growth_no_jitter() {
124 let policy = BackoffPolicy {
125 first: Duration::from_millis(100),
126 max: Duration::from_secs(30),
127 factor: 2.0,
128 jitter: JitterPolicy::None,
129 };
130
131 assert_eq!(policy.next(0), Duration::from_millis(100));
132 assert_eq!(policy.next(1), Duration::from_millis(200));
133 assert_eq!(policy.next(2), Duration::from_millis(400));
134 assert_eq!(policy.next(3), Duration::from_millis(800));
135 assert_eq!(policy.next(4), Duration::from_millis(1600));
136 }
137
138 #[test]
139 fn test_constant_factor() {
140 let policy = BackoffPolicy {
141 first: Duration::from_millis(500),
142 max: Duration::from_secs(30),
143 factor: 1.0,
144 jitter: JitterPolicy::None,
145 };
146 for attempt in 0..10 {
147 assert_eq!(
148 policy.next(attempt),
149 Duration::from_millis(500),
150 "attempt {} should be constant at 500ms",
151 attempt
152 );
153 }
154 }
155
156 #[test]
157 fn test_clamped_to_max() {
158 let policy = BackoffPolicy {
159 first: Duration::from_millis(100),
160 max: Duration::from_secs(1),
161 factor: 2.0,
162 jitter: JitterPolicy::None,
163 };
164 assert_eq!(policy.next(10), Duration::from_secs(1));
165 }
166
167 #[test]
168 fn test_first_exceeds_max() {
169 let policy = BackoffPolicy {
170 first: Duration::from_secs(10),
171 max: Duration::from_secs(5),
172 factor: 2.0,
173 jitter: JitterPolicy::None,
174 };
175 assert_eq!(policy.next(0), Duration::from_secs(5));
176 }
177
178 #[test]
179 fn test_full_jitter_no_negative_feedback() {
180 let policy = BackoffPolicy {
181 first: Duration::from_millis(100),
182 max: Duration::from_secs(30),
183 factor: 2.0,
184 jitter: JitterPolicy::Full,
185 };
186
187 for attempt in 5..15 {
188 let base_ms = 100.0 * 2.0f64.powi(attempt as i32);
189 let delay = policy.next(attempt);
190 assert!(
191 delay <= Duration::from_millis(base_ms as u64),
192 "attempt {}: delay {:?} exceeds base {}ms",
193 attempt,
194 delay,
195 base_ms
196 );
197 }
198 }
199
200 #[test]
201 fn test_equal_jitter_no_negative_feedback() {
202 let policy = BackoffPolicy {
203 first: Duration::from_millis(100),
204 max: Duration::from_secs(30),
205 factor: 2.0,
206 jitter: JitterPolicy::Equal,
207 };
208
209 for attempt in 0..15 {
210 let base_ms = (100.0 * 2.0f64.powi(attempt as i32)).min(30_000.0);
211 let half = base_ms / 2.0;
212 let delay = policy.next(attempt);
213 assert!(
214 delay >= Duration::from_millis(half as u64),
215 "attempt {}: delay {:?} < half of base {}ms",
216 attempt,
217 delay,
218 base_ms
219 );
220 assert!(
221 delay <= Duration::from_millis(base_ms as u64),
222 "attempt {}: delay {:?} > base {}ms",
223 attempt,
224 delay,
225 base_ms
226 );
227 }
228 }
229
230 #[test]
231 fn test_decorrelated_jitter_grows_with_attempts() {
232 let policy = BackoffPolicy {
233 first: Duration::from_millis(100),
234 max: Duration::from_secs(30),
235 factor: 2.0,
236 jitter: JitterPolicy::Decorrelated,
237 };
238
239 let mut min_late = Duration::from_secs(999);
240 let mut max_late = Duration::ZERO;
241 for _ in 0..100 {
242 let delay = policy.next(8);
243 min_late = min_late.min(delay);
244 max_late = max_late.max(delay);
245 }
246
247 assert!(
248 min_late >= Duration::from_millis(100),
249 "min_late {:?} below floor",
250 min_late
251 );
252 assert!(
253 max_late >= Duration::from_secs(5),
254 "max_late {:?} suspiciously low, range too narrow",
255 max_late
256 );
257 }
258
259 #[test]
260 fn test_full_jitter_bounds() {
261 let policy = BackoffPolicy {
262 first: Duration::from_millis(1000),
263 max: Duration::from_secs(30),
264 factor: 1.0,
265 jitter: JitterPolicy::Full,
266 };
267 for attempt in 0..50 {
268 let delay = policy.next(attempt);
269 assert!(delay <= Duration::from_millis(1000));
270 }
271 }
272
273 #[test]
274 fn test_equal_jitter_bounds() {
275 let policy = BackoffPolicy {
276 first: Duration::from_millis(1000),
277 max: Duration::from_secs(30),
278 factor: 1.0,
279 jitter: JitterPolicy::Equal,
280 };
281 for attempt in 0..50 {
282 let delay = policy.next(attempt);
283 assert!(delay >= Duration::from_millis(500));
284 assert!(delay <= Duration::from_millis(1000));
285 }
286 }
287
288 #[test]
289 fn test_huge_attempt_clamps_to_max() {
290 let policy = BackoffPolicy {
291 first: Duration::from_millis(100),
292 max: Duration::from_secs(60),
293 factor: 2.0,
294 jitter: JitterPolicy::None,
295 };
296 assert_eq!(policy.next(100), Duration::from_secs(60));
297 }
298
299 #[test]
300 fn test_non_finite_overflow_clamps_to_max() {
301 let policy = BackoffPolicy {
302 first: Duration::from_millis(100),
303 max: Duration::from_secs(10),
304 factor: 2.0,
305 jitter: JitterPolicy::None,
306 };
307 assert_eq!(policy.next(u32::MAX), Duration::from_secs(10));
308 }
309}