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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +67 -0
- data/README.md +1 -1
- data/ext/chrono_machines_native/core/Cargo.toml +28 -0
- data/ext/chrono_machines_native/core/src/backoff.rs +516 -0
- data/ext/chrono_machines_native/core/src/dsl.rs +108 -0
- data/ext/chrono_machines_native/core/src/lib.rs +355 -0
- data/ext/chrono_machines_native/core/src/policy.rs +189 -0
- data/ext/chrono_machines_native/core/src/retry.rs +1511 -0
- data/ext/chrono_machines_native/core/src/sleep.rs +97 -0
- data/ext/chrono_machines_native/extconf.rb +3 -0
- data/ext/chrono_machines_native/ffi/Cargo.toml +23 -0
- data/ext/chrono_machines_native/ffi/extconf.rb +62 -0
- data/ext/chrono_machines_native/ffi/src/lib.rs +192 -0
- data/ext/chrono_machines_native/target/debug/build/clang-sys-414c893ed9493fe4/out/common.rs +355 -0
- data/ext/chrono_machines_native/target/debug/build/clang-sys-414c893ed9493fe4/out/dynamic.rs +276 -0
- data/ext/chrono_machines_native/target/debug/build/clang-sys-414c893ed9493fe4/out/macros.rs +49 -0
- data/ext/chrono_machines_native/target/debug/build/clang-sys-b1d0ba329a011cdd/out/common.rs +355 -0
- data/ext/chrono_machines_native/target/debug/build/clang-sys-b1d0ba329a011cdd/out/dynamic.rs +276 -0
- data/ext/chrono_machines_native/target/debug/build/clang-sys-b1d0ba329a011cdd/out/macros.rs +49 -0
- 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
- 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
- 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
- data/ext/chrono_machines_native/target/package/chrono-machines-0.3.2/Cargo.toml +76 -0
- data/ext/chrono_machines_native/target/package/chrono-machines-0.3.2/src/backoff.rs +497 -0
- data/ext/chrono_machines_native/target/package/chrono-machines-0.3.2/src/dsl.rs +108 -0
- data/ext/chrono_machines_native/target/package/chrono-machines-0.3.2/src/lib.rs +352 -0
- data/ext/chrono_machines_native/target/package/chrono-machines-0.3.2/src/policy.rs +189 -0
- data/ext/chrono_machines_native/target/package/chrono-machines-0.3.2/src/retry.rs +1489 -0
- data/ext/chrono_machines_native/target/package/chrono-machines-0.3.2/src/sleep.rs +97 -0
- data/ext/chrono_machines_native/target/package/chrono_machines_native-0.2.3/Cargo.toml +56 -0
- data/ext/chrono_machines_native/target/package/chrono_machines_native-0.2.3/src/lib.rs +192 -0
- data/lib/chrono_machines/async_support.rb +2 -1
- data/lib/chrono_machines/configuration.rb +19 -5
- data/lib/chrono_machines/executor.rb +69 -9
- data/lib/chrono_machines/native_speedup.rb +61 -0
- data/lib/chrono_machines/version.rb +1 -1
- data/lib/chrono_machines.rb +1 -1
- metadata +40 -7
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3186aa3d77c3687efa8dcf51f9612c15997f9f5bdd51cf801a87d0a7ea7d28e6
|
|
4
|
+
data.tar.gz: ec4ea0d3b9c8c27828317617ca89e6404587958ba55105e44c57ecdb599d69f6
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
|
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
|
+
}
|