1use crate::*;
2use duration_str::*;
3use serde::Deserialize;
4use std::time::Duration;
5
6#[derive(Debug, Clone, Copy, Deserialize, PartialEq)]
7#[serde(tag = "strategy")]
8pub enum BackoffConfig {
10 Constant {
12 #[serde(default = "defaults::delay", deserialize_with = "deserialize_duration")]
16 delay: Duration,
17
18 #[serde(default = "defaults::max_retries")]
22 max_retries: usize,
23
24 #[serde(default = "defaults::jitter_enabled")]
28 jitter_enabled: bool,
29
30 #[serde(default = "defaults::jitter_seed")]
34 jitter_seed: Option<u64>,
35 },
36
37 Exponential {
39 #[serde(default = "defaults::delay", deserialize_with = "deserialize_duration")]
43 initial_delay: Duration,
44
45 #[serde(default = "defaults::factor")]
49 factor: f32,
50
51 #[serde(
55 default = "defaults::max_delay",
56 deserialize_with = "deserialize_duration"
57 )]
58 max_delay: Duration,
59
60 #[serde(default = "defaults::max_retries")]
64 max_retries: usize,
65
66 #[serde(
70 default = "defaults::max_total_delay",
71 deserialize_with = "deserialize_duration"
72 )]
73 max_total_delay: Duration,
74
75 #[serde(default = "defaults::jitter_enabled")]
79 jitter_enabled: bool,
80
81 #[serde(default = "defaults::jitter_seed")]
85 jitter_seed: Option<u64>,
86 },
87
88 Fibonacci {
90 #[serde(default = "defaults::delay", deserialize_with = "deserialize_duration")]
94 initial_delay: Duration,
95
96 #[serde(
100 default = "defaults::max_delay",
101 deserialize_with = "deserialize_duration"
102 )]
103 max_delay: Duration,
104
105 #[serde(default = "defaults::max_retries")]
109 max_retries: usize,
110
111 #[serde(default = "defaults::jitter_enabled")]
115 jitter_enabled: bool,
116
117 #[serde(default = "defaults::jitter_seed")]
121 jitter_seed: Option<u64>,
122 },
123
124 NoBackoff,
126}
127
128impl backon::BackoffBuilder for BackoffConfig {
129 type Backoff = Backoff;
130
131 fn build(self) -> Backoff {
132 match self {
133 BackoffConfig::Constant {
134 delay,
135 max_retries,
136 jitter_enabled,
137 jitter_seed,
138 } => {
139 let mut builder = backon::ConstantBuilder::new()
140 .with_delay(delay)
141 .with_max_times(max_retries);
142
143 if jitter_enabled {
144 builder = builder.with_jitter();
145 }
146
147 if let Some(jitter_seed) = jitter_seed {
148 builder = builder.with_jitter_seed(jitter_seed);
149 }
150
151 Backoff::Constant(builder.build())
152 }
153
154 BackoffConfig::Exponential {
155 initial_delay,
156 factor,
157 max_delay,
158 max_retries,
159 max_total_delay,
160 jitter_enabled,
161 jitter_seed,
162 } => {
163 let mut builder = backon::ExponentialBuilder::new()
164 .with_min_delay(initial_delay)
165 .with_factor(factor)
166 .with_max_delay(max_delay)
167 .with_max_times(max_retries)
168 .with_total_delay(Some(max_total_delay));
169
170 if jitter_enabled {
171 builder = builder.with_jitter();
172 }
173
174 if let Some(jitter_seed) = jitter_seed {
175 builder = builder.with_jitter_seed(jitter_seed);
176 }
177
178 Backoff::Exponential(builder.build())
179 }
180
181 BackoffConfig::Fibonacci {
182 initial_delay,
183 max_delay,
184 max_retries,
185 jitter_enabled,
186 jitter_seed,
187 } => {
188 let mut builder = backon::FibonacciBuilder::new()
189 .with_min_delay(initial_delay)
190 .with_max_delay(max_delay)
191 .with_max_times(max_retries);
192
193 if jitter_enabled {
194 builder = builder.with_jitter();
195 }
196
197 if let Some(jitter_seed) = jitter_seed {
198 builder = builder.with_jitter_seed(jitter_seed);
199 }
200
201 Backoff::Fibonacci(builder.build())
202 }
203
204 BackoffConfig::NoBackoff => Backoff::NoBackoff,
205 }
206 }
207}
208
209pub mod defaults {
211 use std::time::Duration;
212
213 pub const fn delay() -> Duration {
215 Duration::from_millis(500)
216 }
217
218 pub const fn max_retries() -> usize {
220 4
221 }
222
223 pub const fn jitter_enabled() -> bool {
225 true
226 }
227
228 pub const fn jitter_seed() -> Option<u64> {
230 None
231 }
232
233 pub const fn factor() -> f32 {
235 2.0
236 }
237
238 pub const fn max_delay() -> Duration {
240 Duration::from_secs(30)
241 }
242
243 pub const fn max_total_delay() -> Duration {
245 Duration::from_secs(60)
246 }
247}
248
249#[cfg(test)]
250mod tests {
251 use super::*;
252 use backon::BackoffBuilder;
253 use std::time::Duration;
254
255 #[test]
256 fn constant_backoff_config_to_backoff() {
257 let config = BackoffConfig::Constant {
258 delay: Duration::from_secs(1),
259 max_retries: 3,
260 jitter_enabled: false,
261 jitter_seed: None,
262 };
263
264 let backoff = config.build();
265 assert!(matches!(backoff, Backoff::Constant(_)));
266
267 assert_eq!(
268 backoff
269 .take(100)
270 .map(|duration| duration.as_millis())
271 .collect::<Vec<_>>(),
272 vec![1000; 3]
273 );
274 }
275
276 #[test]
277 fn constant_backoff_config_to_backoff_with_jitter() {
278 let config = BackoffConfig::Constant {
279 delay: Duration::from_secs(1),
280 max_retries: 3,
281 jitter_enabled: true,
282 jitter_seed: Some(0),
283 };
284
285 let backoff = config.build();
286 assert!(matches!(backoff, Backoff::Constant(_)));
287
288 assert_eq!(
289 backoff
290 .take(100)
291 .map(|duration| duration.as_millis())
292 .collect::<Vec<_>>(),
293 vec![1552, 1096, 1593]
294 );
295 }
296
297 #[test]
298 fn exponential_backoff_config_to_backoff() {
299 let config = BackoffConfig::Exponential {
300 initial_delay: Duration::from_millis(100),
301 factor: 2_f32,
302 max_delay: Duration::from_millis(800),
303 max_retries: 5,
304 max_total_delay: Duration::from_secs(1000),
305 jitter_enabled: false,
306 jitter_seed: None,
307 };
308
309 let backoff = config.build();
310 assert!(matches!(backoff, Backoff::Exponential(_)));
311
312 assert_eq!(
313 backoff
314 .take(100)
315 .map(|duration| duration.as_millis())
316 .collect::<Vec<_>>(),
317 vec![100, 200, 400, 800, 800]
318 );
319 }
320
321 #[test]
322 fn exponential_backoff_config_to_backoff_with_jitter() {
323 let config = BackoffConfig::Exponential {
324 initial_delay: Duration::from_millis(100),
325 factor: 2_f32,
326 max_delay: Duration::from_millis(800),
327 max_retries: 5,
328 max_total_delay: Duration::from_secs(1000),
329 jitter_enabled: true,
330 jitter_seed: Some(0),
331 };
332
333 let backoff = config.build();
334 assert!(matches!(backoff, Backoff::Exponential(_)));
335
336 assert_eq!(
337 backoff
338 .take(100)
339 .map(|duration| duration.as_millis())
340 .collect::<Vec<_>>(),
341 vec![155, 219, 637, 923, 1102]
342 );
343 }
344
345 #[test]
346 fn exponential_backoff_config_to_backoff_with_max_total_delay() {
347 let config = BackoffConfig::Exponential {
348 initial_delay: Duration::from_millis(100),
349 factor: 2_f32,
350 max_delay: Duration::from_millis(800),
351 max_retries: 5,
352 max_total_delay: Duration::from_millis(1500 + 1),
353 jitter_enabled: false,
354 jitter_seed: None,
355 };
356
357 let backoff = config.build();
358 assert!(matches!(backoff, Backoff::Exponential(_)));
359
360 assert_eq!(
361 backoff
362 .take(100)
363 .map(|duration| duration.as_millis())
364 .collect::<Vec<_>>(),
365 vec![100, 200, 400, 800]
366 );
367 }
368
369 #[test]
370 fn fibonacci_backoff_config_to_backoff() {
371 let config = BackoffConfig::Fibonacci {
372 initial_delay: Duration::from_millis(100),
373 max_delay: Duration::from_millis(800),
374 max_retries: 5,
375 jitter_enabled: false,
376 jitter_seed: None,
377 };
378
379 let backoff = config.build();
380 assert!(matches!(backoff, Backoff::Fibonacci(_)));
381
382 assert_eq!(
383 backoff
384 .take(usize::MAX)
385 .map(|duration| duration.as_millis())
386 .collect::<Vec<_>>(),
387 vec![100, 100, 200, 300, 500]
388 );
389 }
390
391 #[test]
392 fn fibonacci_backoff_config_to_backoff_with_jitter() {
393 let config = BackoffConfig::Fibonacci {
394 initial_delay: Duration::from_millis(100),
395 max_delay: Duration::from_millis(800),
396 max_retries: 5,
397 jitter_enabled: true,
398 jitter_seed: Some(0),
399 };
400
401 let backoff = config.build();
402 assert!(matches!(backoff, Backoff::Fibonacci(_)));
403
404 assert_eq!(
405 backoff
406 .take(usize::MAX)
407 .map(|duration| duration.as_millis())
408 .collect::<Vec<_>>(),
409 vec![155, 109, 259, 315, 537]
410 );
411 }
412
413 #[test]
414 fn no_backoff_backoff_config_to_backoff() {
415 let config = BackoffConfig::NoBackoff;
416
417 let mut backoff = config.build();
418 assert!(matches!(backoff, Backoff::NoBackoff));
419
420 assert!(backoff.next().is_none());
421 }
422}