chrono_machines 0.2.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +67 -0
  3. data/README.md +1 -1
  4. data/ext/chrono_machines_native/core/Cargo.toml +28 -0
  5. data/ext/chrono_machines_native/core/src/backoff.rs +516 -0
  6. data/ext/chrono_machines_native/core/src/dsl.rs +108 -0
  7. data/ext/chrono_machines_native/core/src/lib.rs +355 -0
  8. data/ext/chrono_machines_native/core/src/policy.rs +189 -0
  9. data/ext/chrono_machines_native/core/src/retry.rs +1511 -0
  10. data/ext/chrono_machines_native/core/src/sleep.rs +97 -0
  11. data/ext/chrono_machines_native/extconf.rb +3 -0
  12. data/ext/chrono_machines_native/ffi/Cargo.toml +23 -0
  13. data/ext/chrono_machines_native/ffi/extconf.rb +62 -0
  14. data/ext/chrono_machines_native/ffi/src/lib.rs +192 -0
  15. data/ext/chrono_machines_native/target/debug/build/clang-sys-414c893ed9493fe4/out/common.rs +355 -0
  16. data/ext/chrono_machines_native/target/debug/build/clang-sys-414c893ed9493fe4/out/dynamic.rs +276 -0
  17. data/ext/chrono_machines_native/target/debug/build/clang-sys-414c893ed9493fe4/out/macros.rs +49 -0
  18. data/ext/chrono_machines_native/target/debug/build/clang-sys-b1d0ba329a011cdd/out/common.rs +355 -0
  19. data/ext/chrono_machines_native/target/debug/build/clang-sys-b1d0ba329a011cdd/out/dynamic.rs +276 -0
  20. data/ext/chrono_machines_native/target/debug/build/clang-sys-b1d0ba329a011cdd/out/macros.rs +49 -0
  21. data/ext/chrono_machines_native/target/debug/build/rb-sys-7d416123f8551038/out/bindings-0.9.124-mri-arm64-darwin23-4.0.3.rs +9057 -0
  22. data/ext/chrono_machines_native/target/debug/build/rb-sys-848783778984ec5d/out/bindings-0.9.124-mri-arm64-darwin23-3.4.8.rs +8933 -0
  23. data/ext/chrono_machines_native/target/debug/build/rb-sys-ce17442a66050c01/out/bindings-0.9.128-mri-arm64-darwin23-3.4.8.rs +8933 -0
  24. data/ext/chrono_machines_native/target/package/chrono-machines-0.3.2/Cargo.toml +76 -0
  25. data/ext/chrono_machines_native/target/package/chrono-machines-0.3.2/src/backoff.rs +497 -0
  26. data/ext/chrono_machines_native/target/package/chrono-machines-0.3.2/src/dsl.rs +108 -0
  27. data/ext/chrono_machines_native/target/package/chrono-machines-0.3.2/src/lib.rs +352 -0
  28. data/ext/chrono_machines_native/target/package/chrono-machines-0.3.2/src/policy.rs +189 -0
  29. data/ext/chrono_machines_native/target/package/chrono-machines-0.3.2/src/retry.rs +1489 -0
  30. data/ext/chrono_machines_native/target/package/chrono-machines-0.3.2/src/sleep.rs +97 -0
  31. data/ext/chrono_machines_native/target/package/chrono_machines_native-0.2.3/Cargo.toml +56 -0
  32. data/ext/chrono_machines_native/target/package/chrono_machines_native-0.2.3/src/lib.rs +192 -0
  33. data/lib/chrono_machines/async_support.rb +2 -1
  34. data/lib/chrono_machines/configuration.rb +19 -5
  35. data/lib/chrono_machines/executor.rb +69 -9
  36. data/lib/chrono_machines/native_speedup.rb +61 -0
  37. data/lib/chrono_machines/version.rb +1 -1
  38. data/lib/chrono_machines.rb +1 -1
  39. metadata +40 -7
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8617cc9e32d231563c1f075a8446728ff851ec0ec6c755aa00e807de5adde85d
4
- data.tar.gz: de5c482f0bb089ce720f5cd3f3019a88fa6b149e83061b729d4d6287161601a3
3
+ metadata.gz: 3186aa3d77c3687efa8dcf51f9612c15997f9f5bdd51cf801a87d0a7ea7d28e6
4
+ data.tar.gz: ec4ea0d3b9c8c27828317617ca89e6404587958ba55105e44c57ecdb599d69f6
5
5
  SHA512:
6
- metadata.gz: 482988d7dd362bb4159af9e9651312b84c3a4e2633d9f4e48f00d9208991328d89ff66bcabd9001c3144c29a347b98f7476849499e310cd6530c08066e0b8f4a
7
- data.tar.gz: 99c5ed6434f2e18236257efb87a652094354c24711d3c7aed651dcfd20b0a0fade2ae15214fbf3ea002730e814f696951dfd7f8af964942937ff0d8b1d91f723
6
+ metadata.gz: f7e5f45ab6660cde57d0f6e101ffb0be54c64ad22a73b7ca39c1a13b74f854e9583355e41934b453211a435b0cf7dd716afbaf747c7e20458340f69eb6d76d51
7
+ data.tar.gz: 1fc648e0d7fe4252ecf94482981bfd278612707cc9efba0c2f13a5b6166b3cc49f1ab02732dde83ddf2292fc42f8d6cefb8d2850fc30a4b72d4d523df87e9f22
data/CHANGELOG.md CHANGED
@@ -1,5 +1,72 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.5.0](https://github.com/seuros/chrono_machines/compare/chrono_machines/v0.4.4...chrono_machines/v0.5.0) (2026-06-24)
4
+
5
+
6
+ ### Features
7
+
8
+ * **native:** make core crate no_std capable ([#15](https://github.com/seuros/chrono_machines/issues/15)) ([76129a3](https://github.com/seuros/chrono_machines/commit/76129a31ad75142641e4a29a82424ad26fb9ecd6))
9
+
10
+
11
+ ### Bug Fixes
12
+
13
+ * **native:** use workspace dependency for chrono-machines in ffi ([fdeca2f](https://github.com/seuros/chrono_machines/commit/fdeca2f938e2647107793cfcf4c5c7c77e62e09b))
14
+
15
+ ## [0.4.4](https://github.com/seuros/chrono_machines/compare/chrono_machines/v0.4.3...chrono_machines/v0.4.4) (2026-06-16)
16
+
17
+
18
+ ### Bug Fixes
19
+
20
+ * Upgrade setup-java action to version 5 ([f6dcecc](https://github.com/seuros/chrono_machines/commit/f6dcecc233d4d4f74bacad8bfe893eff981a4737))
21
+
22
+ ## [0.4.3](https://github.com/seuros/chrono_machines/compare/chrono_machines/v0.4.2...chrono_machines/v0.4.3) (2026-06-15)
23
+
24
+
25
+ ### Bug Fixes
26
+
27
+ * **native:** deduplicate backoff jitter, fibonacci, and retry-failure logic ([46f861a](https://github.com/seuros/chrono_machines/commit/46f861acc7e29024fc35dafb39aaec22db33440d))
28
+
29
+ ## [0.4.2](https://github.com/seuros/chrono_machines/compare/chrono_machines/v0.4.1...chrono_machines/v0.4.2) (2026-05-25)
30
+
31
+
32
+ ### Bug Fixes
33
+
34
+ * remove unused Magnus embed feature ([3b0877a](https://github.com/seuros/chrono_machines/commit/3b0877a11f768a135c877980320cae53fa58cd3d))
35
+
36
+ ## [0.4.1](https://github.com/seuros/chrono_machines/compare/chrono_machines/v0.4.0...chrono_machines/v0.4.1) (2026-03-19)
37
+
38
+
39
+ ### Bug Fixes
40
+
41
+ * **native:** upgrade rand to 0.10 ([#10](https://github.com/seuros/chrono_machines/issues/10)) ([d7e2738](https://github.com/seuros/chrono_machines/commit/d7e2738552a98e4f528055f671cfdfbdb02b0c7a))
42
+
43
+ ## [0.4.0](https://github.com/seuros/chrono_machines/compare/chrono_machines/v0.3.0...chrono_machines/v0.4.0) (2025-12-19)
44
+
45
+
46
+ ### Features
47
+
48
+ * add async support, error classification ([#6](https://github.com/seuros/chrono_machines/issues/6)) ([3d6d2c6](https://github.com/seuros/chrono_machines/commit/3d6d2c6e06de58247c74cdcef94e78a099b11e31))
49
+ * add constant and fibonacci backoff strategies with native acceleration ([582ba9c](https://github.com/seuros/chrono_machines/commit/582ba9ccd84072ec4ccf5d3340eced27cc4d1925))
50
+
51
+
52
+ ### Bug Fixes
53
+
54
+ * add Ruby 4.0.0-preview3 to test matrix ([#9](https://github.com/seuros/chrono_machines/issues/9)) ([df8a3a2](https://github.com/seuros/chrono_machines/commit/df8a3a23acf5ea37b115ab7d05936331b4ae2018))
55
+
56
+ ## [0.3.0](https://github.com/seuros/chrono_machines/compare/chrono_machines/v0.2.0...chrono_machines/v0.3.0) (2025-10-14)
57
+
58
+
59
+ ### Features
60
+
61
+ * add COSS (Contribution Open Source Standard) metadata ([de5b64a](https://github.com/seuros/chrono_machines/commit/de5b64af5c443d8987909199a69030fa332fa323))
62
+ * add GitHub Packages native compilation workflow ([0d7e950](https://github.com/seuros/chrono_machines/commit/0d7e9502d13a8de6b47756c27d682339435fe663))
63
+ * Add Rust native speedup with fluent retry DSL ([#4](https://github.com/seuros/chrono_machines/issues/4)) ([ac980bf](https://github.com/seuros/chrono_machines/commit/ac980bfda450b4e63739e4dc3c19513dd989e819))
64
+
65
+
66
+ ### Bug Fixes
67
+
68
+ * CI compilation using extconf.rb instead of rb_sys task ([bd844a5](https://github.com/seuros/chrono_machines/commit/bd844a594f25745edb407695ef36f7e214a29347))
69
+
3
70
  ## [0.2.0](https://github.com/seuros/chrono_machines/compare/chrono_machines-v0.1.0...chrono_machines/v0.2.0) (2025-07-20)
4
71
 
5
72
 
data/README.md CHANGED
@@ -613,6 +613,6 @@ MIT License. See [LICENSE](LICENSE) file for details.
613
613
 
614
614
  ## Author
615
615
 
616
- Built with time and coffee by temporal engineers fighting entropy one retry at a time.
616
+ Built with time and Brawndo (it's got electrolytes) by temporal engineers fighting entropy one retry at a time.
617
617
 
618
618
  **Remember: In the fabric of spacetime, nobody can hear your API timeout. But they can feel your exponential backoff working as intended.**
@@ -0,0 +1,28 @@
1
+ [package]
2
+ name = "chrono-machines"
3
+ version = "0.4.0"
4
+ edition = "2024"
5
+ authors = ["Abdelkader Boudih <terminale@gmail.com>"]
6
+ license = "MIT"
7
+ description = "Exponential, constant, and Fibonacci backoff retry library with full jitter support - no_std compatible"
8
+ repository = "https://github.com/seuros/chrono_machines"
9
+ keywords = ["retry", "backoff", "exponential", "jitter", "resilience"]
10
+ categories = ["no-std", "algorithms"]
11
+ readme = "README.md"
12
+
13
+ [lib]
14
+ name = "chrono_machines"
15
+ crate-type = ["lib"]
16
+
17
+ [features]
18
+ default = ["std"]
19
+ std = ["alloc", "rand/std", "rand/thread_rng"]
20
+ alloc = []
21
+ async = ["std", "dep:async-trait"]
22
+
23
+ [dependencies]
24
+ rand = { version = "0.10", default-features = false, features = ["std_rng"] }
25
+ async-trait = { version = "0.1", optional = true }
26
+
27
+ [dev-dependencies]
28
+ tokio = { version = "1", features = ["rt", "macros", "time"] }
@@ -0,0 +1,516 @@
1
+ //! Backoff strategy implementations for retry mechanisms
2
+ //!
3
+ //! This module provides various backoff strategies to control delay timing
4
+ //! between retry attempts.
5
+
6
+ use rand::Rng;
7
+ use rand::RngExt;
8
+
9
+ /// `f64::powi`, implemented by hand so it works in `core` (`no_std`).
10
+ #[inline]
11
+ pub(crate) fn powi_f64(base: f64, exp: i32) -> f64 {
12
+ if exp == 0 {
13
+ return 1.0;
14
+ }
15
+ let mut acc = 1.0_f64;
16
+ let mut factor = if exp < 0 { 1.0 / base } else { base };
17
+ let mut n = exp.unsigned_abs();
18
+ while n > 0 {
19
+ if n & 1 == 1 {
20
+ acc *= factor;
21
+ }
22
+ factor *= factor;
23
+ n >>= 1;
24
+ }
25
+ acc
26
+ }
27
+
28
+ /// Calculate the nth Fibonacci number (1-indexed): 1, 1, 2, 3, 5, 8, 13, ...
29
+ pub fn fibonacci(n: u8) -> u64 {
30
+ match n {
31
+ 0 => 0,
32
+ 1 | 2 => 1,
33
+ _ => {
34
+ let mut a = 1u64;
35
+ let mut b = 1u64;
36
+ for _ in 2..n {
37
+ let next = a.saturating_add(b);
38
+ a = b;
39
+ b = next;
40
+ }
41
+ b
42
+ }
43
+ }
44
+ }
45
+
46
+ /// Blend a base delay with jitter using the full-jitter algorithm.
47
+ ///
48
+ /// `jitter_factor` is clamped to `[0.0, 1.0]`: `0.0` returns `base` unchanged,
49
+ /// `1.0` yields a uniform value in `[0, base]`.
50
+ fn apply_jitter<R: Rng>(base: f64, jitter_factor: f64, rng: &mut R) -> u64 {
51
+ let jitter_factor = jitter_factor.clamp(0.0, 1.0);
52
+ let random_scalar: f64 = rng.random_range(0.0..=1.0);
53
+ let jitter_blend = 1.0 - jitter_factor + random_scalar * jitter_factor;
54
+ (base * jitter_blend) as u64
55
+ }
56
+
57
+ /// Trait for backoff strategies that calculate delays between retry attempts
58
+ pub trait BackoffStrategy {
59
+ /// Calculate the delay in milliseconds for the given attempt number
60
+ ///
61
+ /// # Arguments
62
+ ///
63
+ /// * `attempt` - Current attempt number (1-indexed)
64
+ /// * `rng` - Random number generator for jitter
65
+ ///
66
+ /// # Returns
67
+ ///
68
+ /// Delay in milliseconds, or `None` if retries should stop
69
+ fn delay<R: Rng>(&self, attempt: u8, rng: &mut R) -> Option<u64>;
70
+
71
+ /// Check if another retry should be attempted
72
+ ///
73
+ /// # Arguments
74
+ ///
75
+ /// * `attempt` - Current attempt number (1-indexed)
76
+ ///
77
+ /// # Returns
78
+ ///
79
+ /// `true` if another retry is allowed, `false` otherwise
80
+ fn should_retry(&self, attempt: u8) -> bool;
81
+
82
+ /// Maximum number of retry attempts permitted by this strategy.
83
+ fn max_attempts(&self) -> u8;
84
+ }
85
+
86
+ /// Exponential backoff strategy with configurable jitter
87
+ ///
88
+ /// Delays grow exponentially: base_delay * multiplier^(attempt-1)
89
+ ///
90
+ /// # Example
91
+ ///
92
+ /// ```rust
93
+ /// use chrono_machines::ExponentialBackoff;
94
+ ///
95
+ /// let backoff = ExponentialBackoff::new()
96
+ /// .base_delay_ms(100)
97
+ /// .multiplier(2.0)
98
+ /// .max_delay_ms(10_000)
99
+ /// .max_attempts(5)
100
+ /// .jitter_factor(1.0); // Full jitter
101
+ /// ```
102
+ #[derive(Debug, Clone, Copy)]
103
+ pub struct ExponentialBackoff {
104
+ /// Maximum number of retry attempts
105
+ pub max_attempts: u8,
106
+ /// Base delay in milliseconds
107
+ pub base_delay_ms: u64,
108
+ /// Exponential backoff multiplier
109
+ pub multiplier: f64,
110
+ /// Maximum delay cap in milliseconds
111
+ pub max_delay_ms: u64,
112
+ /// Jitter factor (0.0 = no jitter, 1.0 = full jitter)
113
+ pub jitter_factor: f64,
114
+ }
115
+
116
+ impl ExponentialBackoff {
117
+ /// Create a new exponential backoff builder with default values
118
+ pub fn new() -> Self {
119
+ Self::default()
120
+ }
121
+
122
+ /// Set the base delay in milliseconds
123
+ pub fn base_delay_ms(mut self, ms: u64) -> Self {
124
+ self.base_delay_ms = ms;
125
+ self
126
+ }
127
+
128
+ /// Set the exponential multiplier
129
+ pub fn multiplier(mut self, multiplier: f64) -> Self {
130
+ self.multiplier = multiplier;
131
+ self
132
+ }
133
+
134
+ /// Set the maximum delay cap in milliseconds
135
+ pub fn max_delay_ms(mut self, ms: u64) -> Self {
136
+ self.max_delay_ms = ms;
137
+ self
138
+ }
139
+
140
+ /// Set the maximum number of attempts
141
+ pub fn max_attempts(mut self, attempts: u8) -> Self {
142
+ self.max_attempts = attempts;
143
+ self
144
+ }
145
+
146
+ /// Set the jitter factor (0.0 = no jitter, 1.0 = full jitter)
147
+ pub fn jitter_factor(mut self, factor: f64) -> Self {
148
+ self.jitter_factor = factor.clamp(0.0, 1.0);
149
+ self
150
+ }
151
+ }
152
+
153
+ impl Default for ExponentialBackoff {
154
+ fn default() -> Self {
155
+ Self {
156
+ max_attempts: 3,
157
+ base_delay_ms: 100,
158
+ multiplier: 2.0,
159
+ max_delay_ms: 10_000,
160
+ jitter_factor: 1.0, // Full jitter by default
161
+ }
162
+ }
163
+ }
164
+
165
+ impl BackoffStrategy for ExponentialBackoff {
166
+ fn delay<R: Rng>(&self, attempt: u8, rng: &mut R) -> Option<u64> {
167
+ if attempt >= self.max_attempts {
168
+ return None;
169
+ }
170
+
171
+ let exponent = attempt.saturating_sub(1) as i32;
172
+ let base_exponential = (self.base_delay_ms as f64) * powi_f64(self.multiplier, exponent);
173
+ let capped = base_exponential.min(self.max_delay_ms as f64);
174
+
175
+ Some(apply_jitter(capped, self.jitter_factor, rng))
176
+ }
177
+
178
+ fn should_retry(&self, attempt: u8) -> bool {
179
+ attempt < self.max_attempts
180
+ }
181
+
182
+ fn max_attempts(&self) -> u8 {
183
+ self.max_attempts
184
+ }
185
+ }
186
+
187
+ /// Constant backoff strategy with fixed delay
188
+ ///
189
+ /// All retry delays are the same constant value.
190
+ ///
191
+ /// # Example
192
+ ///
193
+ /// ```rust
194
+ /// use chrono_machines::ConstantBackoff;
195
+ ///
196
+ /// let backoff = ConstantBackoff::new()
197
+ /// .delay_ms(500)
198
+ /// .max_attempts(5)
199
+ /// .jitter_factor(0.1); // 10% jitter
200
+ /// ```
201
+ #[derive(Debug, Clone, Copy)]
202
+ pub struct ConstantBackoff {
203
+ /// Fixed delay in milliseconds
204
+ pub delay_ms: u64,
205
+ /// Maximum number of retry attempts
206
+ pub max_attempts: u8,
207
+ /// Jitter factor (0.0 = no jitter, 1.0 = full jitter)
208
+ pub jitter_factor: f64,
209
+ }
210
+
211
+ impl ConstantBackoff {
212
+ /// Create a new constant backoff builder with default values
213
+ pub fn new() -> Self {
214
+ Self::default()
215
+ }
216
+
217
+ /// Set the constant delay in milliseconds
218
+ pub fn delay_ms(mut self, ms: u64) -> Self {
219
+ self.delay_ms = ms;
220
+ self
221
+ }
222
+
223
+ /// Set the maximum number of attempts
224
+ pub fn max_attempts(mut self, attempts: u8) -> Self {
225
+ self.max_attempts = attempts;
226
+ self
227
+ }
228
+
229
+ /// Set the jitter factor (0.0 = no jitter, 1.0 = full jitter)
230
+ pub fn jitter_factor(mut self, factor: f64) -> Self {
231
+ self.jitter_factor = factor.clamp(0.0, 1.0);
232
+ self
233
+ }
234
+ }
235
+
236
+ impl Default for ConstantBackoff {
237
+ fn default() -> Self {
238
+ Self {
239
+ delay_ms: 100,
240
+ max_attempts: 3,
241
+ jitter_factor: 0.0, // No jitter for constant by default
242
+ }
243
+ }
244
+ }
245
+
246
+ impl BackoffStrategy for ConstantBackoff {
247
+ fn delay<R: Rng>(&self, attempt: u8, rng: &mut R) -> Option<u64> {
248
+ if attempt >= self.max_attempts {
249
+ return None;
250
+ }
251
+
252
+ Some(apply_jitter(self.delay_ms as f64, self.jitter_factor, rng))
253
+ }
254
+
255
+ fn should_retry(&self, attempt: u8) -> bool {
256
+ attempt < self.max_attempts
257
+ }
258
+
259
+ fn max_attempts(&self) -> u8 {
260
+ self.max_attempts
261
+ }
262
+ }
263
+
264
+ /// Fibonacci backoff strategy
265
+ ///
266
+ /// Delays follow the Fibonacci sequence: 1, 1, 2, 3, 5, 8, 13, ...
267
+ /// Each delay is base_delay_ms * fibonacci(attempt).
268
+ ///
269
+ /// # Example
270
+ ///
271
+ /// ```rust
272
+ /// use chrono_machines::FibonacciBackoff;
273
+ ///
274
+ /// let backoff = FibonacciBackoff::new()
275
+ /// .base_delay_ms(100) // 100ms, 100ms, 200ms, 300ms, 500ms...
276
+ /// .max_delay_ms(5_000)
277
+ /// .max_attempts(8)
278
+ /// .jitter_factor(0.5); // 50% jitter
279
+ /// ```
280
+ #[derive(Debug, Clone, Copy)]
281
+ pub struct FibonacciBackoff {
282
+ /// Base delay in milliseconds (multiplied by Fibonacci number)
283
+ pub base_delay_ms: u64,
284
+ /// Maximum delay cap in milliseconds
285
+ pub max_delay_ms: u64,
286
+ /// Maximum number of retry attempts
287
+ pub max_attempts: u8,
288
+ /// Jitter factor (0.0 = no jitter, 1.0 = full jitter)
289
+ pub jitter_factor: f64,
290
+ }
291
+
292
+ impl FibonacciBackoff {
293
+ /// Create a new Fibonacci backoff builder with default values
294
+ pub fn new() -> Self {
295
+ Self::default()
296
+ }
297
+
298
+ /// Set the base delay in milliseconds
299
+ pub fn base_delay_ms(mut self, ms: u64) -> Self {
300
+ self.base_delay_ms = ms;
301
+ self
302
+ }
303
+
304
+ /// Set the maximum delay cap in milliseconds
305
+ pub fn max_delay_ms(mut self, ms: u64) -> Self {
306
+ self.max_delay_ms = ms;
307
+ self
308
+ }
309
+
310
+ /// Set the maximum number of attempts
311
+ pub fn max_attempts(mut self, attempts: u8) -> Self {
312
+ self.max_attempts = attempts;
313
+ self
314
+ }
315
+
316
+ /// Set the jitter factor (0.0 = no jitter, 1.0 = full jitter)
317
+ pub fn jitter_factor(mut self, factor: f64) -> Self {
318
+ self.jitter_factor = factor.clamp(0.0, 1.0);
319
+ self
320
+ }
321
+ }
322
+
323
+ impl Default for FibonacciBackoff {
324
+ fn default() -> Self {
325
+ Self {
326
+ base_delay_ms: 100,
327
+ max_delay_ms: 10_000,
328
+ max_attempts: 8,
329
+ jitter_factor: 1.0, // Full jitter by default
330
+ }
331
+ }
332
+ }
333
+
334
+ impl BackoffStrategy for FibonacciBackoff {
335
+ fn delay<R: Rng>(&self, attempt: u8, rng: &mut R) -> Option<u64> {
336
+ if attempt >= self.max_attempts {
337
+ return None;
338
+ }
339
+
340
+ let fib = fibonacci(attempt);
341
+ let base = ((self.base_delay_ms as f64) * (fib as f64)).min(self.max_delay_ms as f64);
342
+
343
+ Some(apply_jitter(base, self.jitter_factor, rng))
344
+ }
345
+
346
+ fn should_retry(&self, attempt: u8) -> bool {
347
+ attempt < self.max_attempts
348
+ }
349
+
350
+ fn max_attempts(&self) -> u8 {
351
+ self.max_attempts
352
+ }
353
+ }
354
+
355
+ /// Backoff policy that can represent any supported strategy.
356
+ ///
357
+ /// The enum form makes it possible to store heterogeneous strategies in a
358
+ /// registry or configuration without heap allocation or dynamic dispatch.
359
+ #[derive(Debug, Clone, Copy)]
360
+ pub enum BackoffPolicy {
361
+ /// Exponential backoff policy
362
+ Exponential(ExponentialBackoff),
363
+ /// Constant backoff policy
364
+ Constant(ConstantBackoff),
365
+ /// Fibonacci backoff policy
366
+ Fibonacci(FibonacciBackoff),
367
+ }
368
+
369
+ impl BackoffPolicy {
370
+ /// Return the maximum retry attempts for the wrapped strategy.
371
+ pub fn max_attempts(&self) -> u8 {
372
+ match self {
373
+ BackoffPolicy::Exponential(policy) => policy.max_attempts,
374
+ BackoffPolicy::Constant(policy) => policy.max_attempts,
375
+ BackoffPolicy::Fibonacci(policy) => policy.max_attempts,
376
+ }
377
+ }
378
+ }
379
+
380
+ impl BackoffStrategy for BackoffPolicy {
381
+ fn delay<R: Rng>(&self, attempt: u8, rng: &mut R) -> Option<u64> {
382
+ match self {
383
+ BackoffPolicy::Exponential(policy) => policy.delay(attempt, rng),
384
+ BackoffPolicy::Constant(policy) => policy.delay(attempt, rng),
385
+ BackoffPolicy::Fibonacci(policy) => policy.delay(attempt, rng),
386
+ }
387
+ }
388
+
389
+ fn should_retry(&self, attempt: u8) -> bool {
390
+ match self {
391
+ BackoffPolicy::Exponential(policy) => policy.should_retry(attempt),
392
+ BackoffPolicy::Constant(policy) => policy.should_retry(attempt),
393
+ BackoffPolicy::Fibonacci(policy) => policy.should_retry(attempt),
394
+ }
395
+ }
396
+
397
+ fn max_attempts(&self) -> u8 {
398
+ match self {
399
+ BackoffPolicy::Exponential(policy) => policy.max_attempts(),
400
+ BackoffPolicy::Constant(policy) => policy.max_attempts(),
401
+ BackoffPolicy::Fibonacci(policy) => policy.max_attempts(),
402
+ }
403
+ }
404
+ }
405
+
406
+ impl From<ExponentialBackoff> for BackoffPolicy {
407
+ fn from(value: ExponentialBackoff) -> Self {
408
+ BackoffPolicy::Exponential(value)
409
+ }
410
+ }
411
+
412
+ impl From<ConstantBackoff> for BackoffPolicy {
413
+ fn from(value: ConstantBackoff) -> Self {
414
+ BackoffPolicy::Constant(value)
415
+ }
416
+ }
417
+
418
+ impl From<FibonacciBackoff> for BackoffPolicy {
419
+ fn from(value: FibonacciBackoff) -> Self {
420
+ BackoffPolicy::Fibonacci(value)
421
+ }
422
+ }
423
+
424
+ #[cfg(test)]
425
+ mod tests {
426
+ use super::*;
427
+ use rand::rngs::StdRng;
428
+ use rand::SeedableRng;
429
+
430
+ #[test]
431
+ fn test_exponential_backoff_builder() {
432
+ let backoff = ExponentialBackoff::new()
433
+ .base_delay_ms(200)
434
+ .multiplier(3.0)
435
+ .max_delay_ms(5000)
436
+ .max_attempts(5)
437
+ .jitter_factor(0.5);
438
+
439
+ assert_eq!(backoff.base_delay_ms, 200);
440
+ assert_eq!(backoff.multiplier, 3.0);
441
+ assert_eq!(backoff.max_delay_ms, 5000);
442
+ assert_eq!(backoff.max_attempts, 5);
443
+ assert_eq!(backoff.jitter_factor, 0.5);
444
+ }
445
+
446
+ #[test]
447
+ fn test_exponential_delays() {
448
+ let backoff = ExponentialBackoff::new()
449
+ .base_delay_ms(100)
450
+ .multiplier(2.0)
451
+ .jitter_factor(0.0); // No jitter for predictable testing
452
+
453
+ let mut rng = StdRng::seed_from_u64(42);
454
+
455
+ assert_eq!(backoff.delay(1, &mut rng), Some(100));
456
+ assert_eq!(backoff.delay(2, &mut rng), Some(200));
457
+ assert_eq!(backoff.delay(3, &mut rng), None); // Exceeds max_attempts (default 3)
458
+ }
459
+
460
+ #[test]
461
+ fn test_constant_backoff() {
462
+ let backoff = ConstantBackoff::new()
463
+ .delay_ms(500)
464
+ .max_attempts(4)
465
+ .jitter_factor(0.0);
466
+
467
+ let mut rng = StdRng::seed_from_u64(42);
468
+
469
+ assert_eq!(backoff.delay(1, &mut rng), Some(500));
470
+ assert_eq!(backoff.delay(2, &mut rng), Some(500));
471
+ assert_eq!(backoff.delay(3, &mut rng), Some(500));
472
+ assert_eq!(backoff.delay(4, &mut rng), None);
473
+ }
474
+
475
+ #[test]
476
+ fn test_fibonacci_sequence() {
477
+ assert_eq!(fibonacci(1), 1);
478
+ assert_eq!(fibonacci(2), 1);
479
+ assert_eq!(fibonacci(3), 2);
480
+ assert_eq!(fibonacci(4), 3);
481
+ assert_eq!(fibonacci(5), 5);
482
+ assert_eq!(fibonacci(6), 8);
483
+ assert_eq!(fibonacci(7), 13);
484
+ }
485
+
486
+ #[test]
487
+ fn test_fibonacci_backoff() {
488
+ let backoff = FibonacciBackoff::new()
489
+ .base_delay_ms(100)
490
+ .max_attempts(5)
491
+ .jitter_factor(0.0);
492
+
493
+ let mut rng = StdRng::seed_from_u64(42);
494
+
495
+ assert_eq!(backoff.delay(1, &mut rng), Some(100)); // 100 * 1
496
+ assert_eq!(backoff.delay(2, &mut rng), Some(100)); // 100 * 1
497
+ assert_eq!(backoff.delay(3, &mut rng), Some(200)); // 100 * 2
498
+ assert_eq!(backoff.delay(4, &mut rng), Some(300)); // 100 * 3
499
+ assert_eq!(backoff.delay(5, &mut rng), None); // Exceeds max_attempts
500
+ }
501
+
502
+ #[test]
503
+ fn test_jitter_application() {
504
+ let backoff = ConstantBackoff::new().delay_ms(1000).jitter_factor(1.0); // Full jitter
505
+
506
+ let mut rng = StdRng::seed_from_u64(42);
507
+ let delays: Vec<u64> = (1..10).filter_map(|i| backoff.delay(i, &mut rng)).collect();
508
+
509
+ // With full jitter, delays should vary
510
+ let all_different = delays.windows(2).any(|w| w[0] != w[1]);
511
+ assert!(all_different, "Full jitter should produce varying delays");
512
+
513
+ // All delays should be <= base delay
514
+ assert!(delays.iter().all(|&d| d <= 1000));
515
+ }
516
+ }