1use crate::nullable::Nullable;
2use crate::*;
3use duration_string::DurationString;
4use serde::Deserialize;
5
6#[derive(Debug, Clone, Copy, Deserialize, PartialEq)]
7#[serde(tag = "strategy")]
8pub enum BackoffConfig {
10 Constant {
12 #[serde(default = "defaults::delay")]
16 delay: DurationString,
17
18 #[serde(default = "defaults::max_retries")]
24 max_retries: Nullable<usize>,
25
26 #[serde(default = "defaults::jitter_enabled")]
30 jitter_enabled: bool,
31
32 #[serde(default = "defaults::jitter_seed")]
36 jitter_seed: Option<u64>,
37 },
38
39 Exponential {
41 #[serde(default = "defaults::delay")]
45 initial_delay: DurationString,
46
47 #[serde(default = "defaults::factor")]
51 factor: f32,
52
53 #[serde(default = "defaults::max_delay")]
59 max_delay: Nullable<DurationString>,
60
61 #[serde(default = "defaults::max_retries")]
67 max_retries: Nullable<usize>,
68
69 #[serde(default = "defaults::max_total_delay")]
73 max_total_delay: Option<DurationString>,
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")]
94 initial_delay: DurationString,
95
96 #[serde(default = "defaults::max_delay")]
102 max_delay: Nullable<DurationString>,
103
104 #[serde(default = "defaults::max_retries")]
110 max_retries: Nullable<usize>,
111
112 #[serde(default = "defaults::jitter_enabled")]
116 jitter_enabled: bool,
117
118 #[serde(default = "defaults::jitter_seed")]
122 jitter_seed: Option<u64>,
123 },
124}
125
126impl backon::BackoffBuilder for BackoffConfig {
127 type Backoff = Backoff;
128
129 fn build(self) -> Backoff {
130 match self {
131 BackoffConfig::Constant {
132 delay,
133 max_retries,
134 jitter_enabled,
135 jitter_seed,
136 } => {
137 let mut builder = backon::ConstantBuilder::new().with_delay(delay.into());
138
139 builder = match max_retries {
140 Nullable::Some(max_retries) => builder.with_max_times(max_retries),
141 Nullable::Null => builder.without_max_times(),
142 };
143
144 if jitter_enabled {
145 builder = builder.with_jitter();
146 }
147
148 if let Some(jitter_seed) = jitter_seed {
149 builder = builder.with_jitter_seed(jitter_seed);
150 }
151
152 Backoff::Constant(builder.build())
153 }
154
155 BackoffConfig::Exponential {
156 initial_delay,
157 factor,
158 max_delay,
159 max_retries,
160 max_total_delay,
161 jitter_enabled,
162 jitter_seed,
163 } => {
164 let mut builder = backon::ExponentialBuilder::new()
165 .with_min_delay(initial_delay.into())
166 .with_factor(factor);
167
168 builder = match max_delay {
169 Nullable::Some(max_delay) => builder.with_max_delay(max_delay.into()),
170 Nullable::Null => builder.without_max_delay(),
171 };
172
173 builder = match max_retries {
174 Nullable::Some(max_retries) => builder.with_max_times(max_retries),
175 Nullable::Null => builder.without_max_times(),
176 };
177
178 builder = builder.with_total_delay(max_total_delay.map(Into::into));
179
180 if jitter_enabled {
181 builder = builder.with_jitter();
182 }
183
184 if let Some(jitter_seed) = jitter_seed {
185 builder = builder.with_jitter_seed(jitter_seed);
186 }
187
188 Backoff::Exponential(builder.build())
189 }
190
191 BackoffConfig::Fibonacci {
192 initial_delay,
193 max_delay,
194 max_retries,
195 jitter_enabled,
196 jitter_seed,
197 } => {
198 let mut builder =
199 backon::FibonacciBuilder::new().with_min_delay(initial_delay.into());
200
201 builder = match max_delay {
202 Nullable::Some(max_delay) => builder.with_max_delay(max_delay.into()),
203 Nullable::Null => builder.without_max_delay(),
204 };
205
206 builder = match max_retries {
207 Nullable::Some(max_retries) => builder.with_max_times(max_retries),
208 Nullable::Null => builder.without_max_times(),
209 };
210
211 if jitter_enabled {
212 builder = builder.with_jitter();
213 }
214
215 if let Some(jitter_seed) = jitter_seed {
216 builder = builder.with_jitter_seed(jitter_seed);
217 }
218
219 Backoff::Fibonacci(builder.build())
220 }
221 }
222 }
223}
224
225pub mod defaults {
227 use crate::nullable::Nullable;
228 use duration_string::DurationString;
229 use std::time::Duration;
230
231 pub fn delay() -> DurationString {
233 Duration::from_millis(500).into()
234 }
235
236 pub const fn max_retries() -> Nullable<usize> {
238 Nullable::Some(4)
239 }
240
241 pub const fn jitter_enabled() -> bool {
243 true
244 }
245
246 pub const fn jitter_seed() -> Option<u64> {
248 None
249 }
250
251 pub const fn factor() -> f32 {
253 2.0
254 }
255
256 pub fn max_delay() -> Nullable<DurationString> {
258 Nullable::Some(Duration::from_secs(30).into())
259 }
260
261 pub fn max_total_delay() -> Option<DurationString> {
263 Some(Duration::from_secs(60).into())
264 }
265}
266
267#[cfg(test)]
268mod tests {
269 use super::*;
270 use backon::BackoffBuilder;
271 use std::time::Duration;
272
273 #[test]
274 fn constant_backoff_config_to_backoff() {
275 let config = BackoffConfig::Constant {
276 delay: Duration::from_secs(1).into(),
277 max_retries: Nullable::Some(3),
278 jitter_enabled: false,
279 jitter_seed: None,
280 };
281
282 let backoff = config.build();
283 assert!(matches!(backoff, Backoff::Constant(_)));
284
285 assert_eq!(
286 backoff
287 .take(100)
288 .map(|duration| duration.as_millis())
289 .collect::<Vec<_>>(),
290 vec![1000; 3]
291 );
292 }
293
294 #[test]
295 fn exponential_backoff_config_to_backoff() {
296 let config = BackoffConfig::Exponential {
297 initial_delay: Duration::from_millis(100).into(),
298 factor: 2_f32,
299 max_delay: Nullable::Some(Duration::from_millis(800).into()),
300 max_retries: Nullable::Some(5),
301 max_total_delay: None,
302 jitter_enabled: false,
303 jitter_seed: None,
304 };
305
306 let backoff = config.build();
307 assert!(matches!(backoff, Backoff::Exponential(_)));
308
309 assert_eq!(
310 backoff
311 .take(100)
312 .map(|duration| duration.as_millis())
313 .collect::<Vec<_>>(),
314 vec![100, 200, 400, 800, 800]
315 );
316 }
317
318 #[test]
319 fn exponential_backoff_config_to_backoff_with_max_total_delay() {
320 let config = BackoffConfig::Exponential {
321 initial_delay: Duration::from_millis(100).into(),
322 factor: 2_f32,
323 max_delay: Nullable::Some(Duration::from_millis(800).into()),
324 max_retries: Nullable::Some(5),
325 max_total_delay: Some(Duration::from_millis(1500 + 1).into()),
326 jitter_enabled: false,
327 jitter_seed: None,
328 };
329
330 let backoff = config.build();
331 assert!(matches!(backoff, Backoff::Exponential(_)));
332
333 assert_eq!(
334 backoff
335 .take(100)
336 .map(|duration| duration.as_millis())
337 .collect::<Vec<_>>(),
338 vec![100, 200, 400, 800]
339 );
340 }
341
342 #[test]
343 fn fibonacci_backoff_config_to_backoff() {
344 let config = BackoffConfig::Fibonacci {
345 initial_delay: Duration::from_millis(100).into(),
346 max_delay: Nullable::Some(Duration::from_millis(800).into()),
347 max_retries: Nullable::Some(5),
348 jitter_enabled: false,
349 jitter_seed: None,
350 };
351
352 let backoff = config.build();
353 assert!(matches!(backoff, Backoff::Fibonacci(_)));
354
355 assert_eq!(
356 backoff
357 .take(usize::MAX)
358 .map(|duration| duration.as_millis())
359 .collect::<Vec<_>>(),
360 vec![100, 100, 200, 300, 500]
361 );
362 }
363}