chrono_machines 0.5.2 → 0.8.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 +72 -0
- data/Cargo.lock +2 -14
- data/README.md +26 -0
- data/ext/chrono_machines_native/core/Cargo.toml +7 -4
- data/ext/chrono_machines_native/core/src/backoff.rs +58 -92
- data/ext/chrono_machines_native/core/src/dsl.rs +1 -38
- data/ext/chrono_machines_native/core/src/lib.rs +36 -149
- data/ext/chrono_machines_native/core/src/retry.rs +340 -905
- data/ext/chrono_machines_native/core/src/sleep.rs +65 -31
- data/ext/chrono_machines_native/ffi/Cargo.toml +1 -1
- data/ext/chrono_machines_native/ffi/src/lib.rs +3 -68
- data/lib/chrono_machines/configuration.rb +1 -0
- data/lib/chrono_machines/executor.rb +25 -1
- data/lib/chrono_machines/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c6cbe05e9e40deb1bf5b846c57800d1f4439caf6cd753883562a7f911a453640
|
|
4
|
+
data.tar.gz: ae0a5c30e033a6bdd974ff9ea8bbac5220f2aa316a9b6724897cefcdd23971cf
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3d99472379c0ff2a7adf1a4035698e6d7f18f5e763096f389bb8ccb12d273a55e97bd4ffc62cca04b52436ed64f8cd294c76a8aacd6a0ca11982df7b9ee7a07c
|
|
7
|
+
data.tar.gz: 7e6d0d99d5c074b978f9c3595245c1aa37ffaaf214e74d6ccbb75d16436c8bcd76383ca6d92abb265caf8cd359099e289d7d20cf2b3d0caf0d2e2a21db009b5b
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,77 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [0.8.0](https://github.com/seuros/chrono_machines/compare/chrono_machines/v0.5.0...chrono_machines/v0.8.0) (2026-09-14)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### ⚠ BREAKING CHANGES
|
|
7
|
+
|
|
8
|
+
* **native:** add async retry driver ([#21](https://github.com/seuros/chrono_machines/issues/21))
|
|
9
|
+
|
|
10
|
+
### Features
|
|
11
|
+
|
|
12
|
+
* **core:** delay_from - server-hinted retry delays (HTTP Retry-After) ([#25](https://github.com/seuros/chrono_machines/issues/25)) ([2dcf987](https://github.com/seuros/chrono_machines/commit/2dcf9871939b70c053144e9fc296914a3b6105e5))
|
|
13
|
+
* **core:** re-export rand so BackoffStrategy is implementable downstream ([dc1cf49](https://github.com/seuros/chrono_machines/commit/dc1cf494df0124032bec6aee6e4f6a33e43abbfb))
|
|
14
|
+
* fix release ([ad40d8f](https://github.com/seuros/chrono_machines/commit/ad40d8f5b6629e49ba2d5f0cb7f213a1cc1e30c7))
|
|
15
|
+
* **native:** add async retry driver ([#21](https://github.com/seuros/chrono_machines/issues/21)) ([766f207](https://github.com/seuros/chrono_machines/commit/766f20799bcc0c36f7ca6a904fadff227549fee7))
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
### Bug Fixes
|
|
19
|
+
|
|
20
|
+
* ignore native_speedup.rb in Zeitwerk loader ([#19](https://github.com/seuros/chrono_machines/issues/19)) ([491cc2d](https://github.com/seuros/chrono_machines/commit/491cc2d3f88786cf6d0c6af68dba7d2511fa3f36))
|
|
21
|
+
* package workspace Cargo manifest so source builds work ([#17](https://github.com/seuros/chrono_machines/issues/17)) ([ac0ddad](https://github.com/seuros/chrono_machines/commit/ac0ddad15d26528771825240a05ed8c84fb61520))
|
|
22
|
+
* trigger release ([02184d5](https://github.com/seuros/chrono_machines/commit/02184d51f6831f53ae6142ea5092710d456363d5))
|
|
23
|
+
|
|
24
|
+
## [0.5.0](https://github.com/seuros/chrono_machines/compare/chrono_machines/v0.5.0...chrono_machines/v0.5.0) (2026-09-14)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
### ⚠ BREAKING CHANGES
|
|
28
|
+
|
|
29
|
+
* **native:** add async retry driver ([#21](https://github.com/seuros/chrono_machines/issues/21))
|
|
30
|
+
|
|
31
|
+
### Features
|
|
32
|
+
|
|
33
|
+
* **core:** delay_from - server-hinted retry delays (HTTP Retry-After) ([#25](https://github.com/seuros/chrono_machines/issues/25)) ([2dcf987](https://github.com/seuros/chrono_machines/commit/2dcf9871939b70c053144e9fc296914a3b6105e5))
|
|
34
|
+
* **core:** re-export rand so BackoffStrategy is implementable downstream ([dc1cf49](https://github.com/seuros/chrono_machines/commit/dc1cf494df0124032bec6aee6e4f6a33e43abbfb))
|
|
35
|
+
* fix release ([ad40d8f](https://github.com/seuros/chrono_machines/commit/ad40d8f5b6629e49ba2d5f0cb7f213a1cc1e30c7))
|
|
36
|
+
* **native:** add async retry driver ([#21](https://github.com/seuros/chrono_machines/issues/21)) ([766f207](https://github.com/seuros/chrono_machines/commit/766f20799bcc0c36f7ca6a904fadff227549fee7))
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
### Bug Fixes
|
|
40
|
+
|
|
41
|
+
* ignore native_speedup.rb in Zeitwerk loader ([#19](https://github.com/seuros/chrono_machines/issues/19)) ([491cc2d](https://github.com/seuros/chrono_machines/commit/491cc2d3f88786cf6d0c6af68dba7d2511fa3f36))
|
|
42
|
+
* package workspace Cargo manifest so source builds work ([#17](https://github.com/seuros/chrono_machines/issues/17)) ([ac0ddad](https://github.com/seuros/chrono_machines/commit/ac0ddad15d26528771825240a05ed8c84fb61520))
|
|
43
|
+
|
|
44
|
+
## [0.5.0](https://github.com/seuros/chrono_machines/compare/chrono_machines/v0.5.0...chrono_machines/v0.5.0) (2026-08-23)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
### ⚠ BREAKING CHANGES
|
|
48
|
+
|
|
49
|
+
* **native:** add async retry driver ([#21](https://github.com/seuros/chrono_machines/issues/21))
|
|
50
|
+
|
|
51
|
+
### Features
|
|
52
|
+
|
|
53
|
+
* **core:** re-export rand so BackoffStrategy is implementable downstream ([dc1cf49](https://github.com/seuros/chrono_machines/commit/dc1cf494df0124032bec6aee6e4f6a33e43abbfb))
|
|
54
|
+
* fix release ([ad40d8f](https://github.com/seuros/chrono_machines/commit/ad40d8f5b6629e49ba2d5f0cb7f213a1cc1e30c7))
|
|
55
|
+
* **native:** add async retry driver ([#21](https://github.com/seuros/chrono_machines/issues/21)) ([766f207](https://github.com/seuros/chrono_machines/commit/766f20799bcc0c36f7ca6a904fadff227549fee7))
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
### Bug Fixes
|
|
59
|
+
|
|
60
|
+
* ignore native_speedup.rb in Zeitwerk loader ([#19](https://github.com/seuros/chrono_machines/issues/19)) ([491cc2d](https://github.com/seuros/chrono_machines/commit/491cc2d3f88786cf6d0c6af68dba7d2511fa3f36))
|
|
61
|
+
* package workspace Cargo manifest so source builds work ([#17](https://github.com/seuros/chrono_machines/issues/17)) ([ac0ddad](https://github.com/seuros/chrono_machines/commit/ac0ddad15d26528771825240a05ed8c84fb61520))
|
|
62
|
+
|
|
63
|
+
## [0.5.0](https://github.com/seuros/chrono_machines/compare/chrono_machines/v0.5.2...chrono_machines/v0.5.0) (2026-08-23)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
### ⚠ BREAKING CHANGES
|
|
67
|
+
|
|
68
|
+
* **native:** add async retry driver ([#21](https://github.com/seuros/chrono_machines/issues/21))
|
|
69
|
+
|
|
70
|
+
### Features
|
|
71
|
+
|
|
72
|
+
* fix release ([ad40d8f](https://github.com/seuros/chrono_machines/commit/ad40d8f5b6629e49ba2d5f0cb7f213a1cc1e30c7))
|
|
73
|
+
* **native:** add async retry driver ([#21](https://github.com/seuros/chrono_machines/issues/21)) ([766f207](https://github.com/seuros/chrono_machines/commit/766f20799bcc0c36f7ca6a904fadff227549fee7))
|
|
74
|
+
|
|
3
75
|
## [0.5.2](https://github.com/seuros/chrono_machines/compare/chrono_machines/v0.5.1...chrono_machines/v0.5.2) (2026-08-07)
|
|
4
76
|
|
|
5
77
|
|
data/Cargo.lock
CHANGED
|
@@ -17,17 +17,6 @@ version = "1.0.102"
|
|
|
17
17
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
18
18
|
checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
|
|
19
19
|
|
|
20
|
-
[[package]]
|
|
21
|
-
name = "async-trait"
|
|
22
|
-
version = "0.1.89"
|
|
23
|
-
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
24
|
-
checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb"
|
|
25
|
-
dependencies = [
|
|
26
|
-
"proc-macro2",
|
|
27
|
-
"quote",
|
|
28
|
-
"syn",
|
|
29
|
-
]
|
|
30
|
-
|
|
31
20
|
[[package]]
|
|
32
21
|
name = "bindgen"
|
|
33
22
|
version = "0.72.1"
|
|
@@ -80,16 +69,15 @@ dependencies = [
|
|
|
80
69
|
|
|
81
70
|
[[package]]
|
|
82
71
|
name = "chrono-machines"
|
|
83
|
-
version = "0.
|
|
72
|
+
version = "0.6.0"
|
|
84
73
|
dependencies = [
|
|
85
|
-
"async-trait",
|
|
86
74
|
"rand",
|
|
87
75
|
"tokio",
|
|
88
76
|
]
|
|
89
77
|
|
|
90
78
|
[[package]]
|
|
91
79
|
name = "chrono_machines_native"
|
|
92
|
-
version = "0.
|
|
80
|
+
version = "0.5.0"
|
|
93
81
|
dependencies = [
|
|
94
82
|
"chrono-machines",
|
|
95
83
|
"magnus",
|
data/README.md
CHANGED
|
@@ -195,6 +195,32 @@ ChronoMachines.retry(
|
|
|
195
195
|
end
|
|
196
196
|
```
|
|
197
197
|
|
|
198
|
+
### Server-Hinted Delays (`delay_from`)
|
|
199
|
+
|
|
200
|
+
When the server tells you when to come back - HTTP 429 with `Retry-After` - guessing with exponential backoff is rude. `delay_from` lets the exception dictate the next delay:
|
|
201
|
+
|
|
202
|
+
```ruby
|
|
203
|
+
ChronoMachines.retry(
|
|
204
|
+
max_attempts: 5,
|
|
205
|
+
max_delay: 30,
|
|
206
|
+
retryable_exceptions: [MyClient::RateLimitError],
|
|
207
|
+
delay_from: ->(exception:, attempt:) {
|
|
208
|
+
exception.respond_to?(:retry_after) ? exception.retry_after : nil
|
|
209
|
+
}
|
|
210
|
+
) do
|
|
211
|
+
call_rate_limited_api
|
|
212
|
+
end
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
The hook's return value controls the retry:
|
|
216
|
+
|
|
217
|
+
- `nil` - no hint; fall back to the policy's backoff strategy
|
|
218
|
+
- `Numeric` - sleep exactly that many seconds (no jitter: the server picked the time, honor it)
|
|
219
|
+
- a `Numeric` **beyond `max_delay`** - halt: the server asked for more patience than this policy allows, so the original exception propagates and the caller decides (background job, `Retry-After` response header, ...)
|
|
220
|
+
- `:halt` or `false` - stop retrying immediately, propagate the original exception
|
|
221
|
+
|
|
222
|
+
Anything else raises `ArgumentError` - a broken hook fails loudly instead of silently mangling your retry cadence.
|
|
223
|
+
|
|
198
224
|
## The Science of Temporal Jitter
|
|
199
225
|
|
|
200
226
|
ChronoMachines implements **full jitter** exponential backoff:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[package]
|
|
2
2
|
name = "chrono-machines"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.8.0"
|
|
4
4
|
edition = "2024"
|
|
5
5
|
authors = ["Abdelkader Boudih <terminale@gmail.com>"]
|
|
6
6
|
license = "MIT"
|
|
@@ -9,6 +9,7 @@ repository = "https://github.com/seuros/chrono_machines"
|
|
|
9
9
|
keywords = ["retry", "backoff", "exponential", "jitter", "resilience"]
|
|
10
10
|
categories = ["no-std", "algorithms"]
|
|
11
11
|
readme = "README.md"
|
|
12
|
+
exclude = ["tests/", "src/**/tests.rs"]
|
|
12
13
|
|
|
13
14
|
[lib]
|
|
14
15
|
name = "chrono_machines"
|
|
@@ -18,11 +19,13 @@ crate-type = ["lib"]
|
|
|
18
19
|
default = ["std"]
|
|
19
20
|
std = ["alloc", "rand/std", "rand/thread_rng"]
|
|
20
21
|
alloc = []
|
|
21
|
-
|
|
22
|
+
# Async retry driver. Needs `alloc` for the builder's boxed callbacks, but not
|
|
23
|
+
# `std`: the futures are native `async fn` in trait, so there is no runtime
|
|
24
|
+
# dependency and nothing to box per sleep.
|
|
25
|
+
async = ["alloc"]
|
|
22
26
|
|
|
23
27
|
[dependencies]
|
|
24
28
|
rand = { version = "0.10", default-features = false, features = ["std_rng"] }
|
|
25
|
-
async-trait = { version = "0.1", optional = true }
|
|
26
29
|
|
|
27
30
|
[dev-dependencies]
|
|
28
|
-
tokio = { version = "1", features = ["rt", "macros", "time"] }
|
|
31
|
+
tokio = { version = "1", features = ["rt", "rt-multi-thread", "macros", "time"] }
|
|
@@ -55,6 +55,39 @@ fn apply_jitter<R: Rng>(base: f64, jitter_factor: f64, rng: &mut R) -> u64 {
|
|
|
55
55
|
}
|
|
56
56
|
|
|
57
57
|
/// Trait for backoff strategies that calculate delays between retry attempts
|
|
58
|
+
///
|
|
59
|
+
/// The three strategies in this module cover the usual shapes. Implement it
|
|
60
|
+
/// yourself when the delay depends on something they cannot see — a server's
|
|
61
|
+
/// `Retry-After`, a token bucket, a circuit breaker's cool-down.
|
|
62
|
+
///
|
|
63
|
+
/// [`delay`](BackoffStrategy::delay) is generic over [`Rng`], so an
|
|
64
|
+
/// implementation has to name that trait. Take it from
|
|
65
|
+
/// [`chrono_machines::rand`](crate::rand) rather than adding your own `rand`
|
|
66
|
+
/// dependency; a version skew between the two yields two distinct `Rng` traits
|
|
67
|
+
/// and a mismatch error that points nowhere near the cause.
|
|
68
|
+
///
|
|
69
|
+
/// ```rust
|
|
70
|
+
/// use chrono_machines::{rand::Rng, BackoffStrategy};
|
|
71
|
+
///
|
|
72
|
+
/// /// Retries as fast as the loop will go — for tests, where sleeping is waste.
|
|
73
|
+
/// struct Immediate {
|
|
74
|
+
/// max_attempts: u8,
|
|
75
|
+
/// }
|
|
76
|
+
///
|
|
77
|
+
/// impl BackoffStrategy for Immediate {
|
|
78
|
+
/// fn delay<R: Rng>(&self, attempt: u8, _rng: &mut R) -> Option<u64> {
|
|
79
|
+
/// self.should_retry(attempt).then_some(0)
|
|
80
|
+
/// }
|
|
81
|
+
///
|
|
82
|
+
/// fn should_retry(&self, attempt: u8) -> bool {
|
|
83
|
+
/// attempt < self.max_attempts
|
|
84
|
+
/// }
|
|
85
|
+
///
|
|
86
|
+
/// fn max_attempts(&self) -> u8 {
|
|
87
|
+
/// self.max_attempts
|
|
88
|
+
/// }
|
|
89
|
+
/// }
|
|
90
|
+
/// ```
|
|
58
91
|
pub trait BackoffStrategy {
|
|
59
92
|
/// Calculate the delay in milliseconds for the given attempt number
|
|
60
93
|
///
|
|
@@ -81,6 +114,14 @@ pub trait BackoffStrategy {
|
|
|
81
114
|
|
|
82
115
|
/// Maximum number of retry attempts permitted by this strategy.
|
|
83
116
|
fn max_attempts(&self) -> u8;
|
|
117
|
+
|
|
118
|
+
/// Maximum delay this strategy will ever produce, if it has a cap.
|
|
119
|
+
///
|
|
120
|
+
/// A `delay_from` hint beyond this cap halts the retry. `None` (the
|
|
121
|
+
/// default) means uncapped: every hint is honoured.
|
|
122
|
+
fn max_delay_ms(&self) -> Option<u64> {
|
|
123
|
+
None
|
|
124
|
+
}
|
|
84
125
|
}
|
|
85
126
|
|
|
86
127
|
/// Exponential backoff strategy with configurable jitter
|
|
@@ -182,6 +223,10 @@ impl BackoffStrategy for ExponentialBackoff {
|
|
|
182
223
|
fn max_attempts(&self) -> u8 {
|
|
183
224
|
self.max_attempts
|
|
184
225
|
}
|
|
226
|
+
|
|
227
|
+
fn max_delay_ms(&self) -> Option<u64> {
|
|
228
|
+
Some(self.max_delay_ms)
|
|
229
|
+
}
|
|
185
230
|
}
|
|
186
231
|
|
|
187
232
|
/// Constant backoff strategy with fixed delay
|
|
@@ -350,6 +395,10 @@ impl BackoffStrategy for FibonacciBackoff {
|
|
|
350
395
|
fn max_attempts(&self) -> u8 {
|
|
351
396
|
self.max_attempts
|
|
352
397
|
}
|
|
398
|
+
|
|
399
|
+
fn max_delay_ms(&self) -> Option<u64> {
|
|
400
|
+
Some(self.max_delay_ms)
|
|
401
|
+
}
|
|
353
402
|
}
|
|
354
403
|
|
|
355
404
|
/// Backoff policy that can represent any supported strategy.
|
|
@@ -401,6 +450,14 @@ impl BackoffStrategy for BackoffPolicy {
|
|
|
401
450
|
BackoffPolicy::Fibonacci(policy) => policy.max_attempts(),
|
|
402
451
|
}
|
|
403
452
|
}
|
|
453
|
+
|
|
454
|
+
fn max_delay_ms(&self) -> Option<u64> {
|
|
455
|
+
match self {
|
|
456
|
+
BackoffPolicy::Exponential(policy) => policy.max_delay_ms(),
|
|
457
|
+
BackoffPolicy::Constant(policy) => policy.max_delay_ms(),
|
|
458
|
+
BackoffPolicy::Fibonacci(policy) => policy.max_delay_ms(),
|
|
459
|
+
}
|
|
460
|
+
}
|
|
404
461
|
}
|
|
405
462
|
|
|
406
463
|
impl From<ExponentialBackoff> for BackoffPolicy {
|
|
@@ -422,95 +479,4 @@ impl From<FibonacciBackoff> for BackoffPolicy {
|
|
|
422
479
|
}
|
|
423
480
|
|
|
424
481
|
#[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
|
-
}
|
|
482
|
+
mod tests;
|
|
@@ -68,41 +68,4 @@ where
|
|
|
68
68
|
}
|
|
69
69
|
|
|
70
70
|
#[cfg(test)]
|
|
71
|
-
mod tests
|
|
72
|
-
use super::*;
|
|
73
|
-
use crate::backoff::{BackoffPolicy, ExponentialBackoff};
|
|
74
|
-
use crate::policy::{clear_global_policies, register_global_policy};
|
|
75
|
-
|
|
76
|
-
#[test]
|
|
77
|
-
fn test_retry_with_policy_success() {
|
|
78
|
-
clear_global_policies();
|
|
79
|
-
register_global_policy(
|
|
80
|
-
"default",
|
|
81
|
-
BackoffPolicy::from(ExponentialBackoff::new().max_attempts(2)),
|
|
82
|
-
);
|
|
83
|
-
|
|
84
|
-
let mut attempts = 0;
|
|
85
|
-
let outcome = retry_with_policy("default", || {
|
|
86
|
-
attempts += 1;
|
|
87
|
-
if attempts == 1 {
|
|
88
|
-
Err::<_, &'static str>("fail")
|
|
89
|
-
} else {
|
|
90
|
-
Ok("ok")
|
|
91
|
-
}
|
|
92
|
-
})
|
|
93
|
-
.expect("dsl retry should succeed");
|
|
94
|
-
|
|
95
|
-
assert_eq!(attempts, 2);
|
|
96
|
-
assert_eq!(outcome.into_inner(), "ok");
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
#[test]
|
|
100
|
-
fn test_retry_with_policy_missing() {
|
|
101
|
-
clear_global_policies();
|
|
102
|
-
let result = retry_with_policy::<_, (), &str>("missing", || Ok(()));
|
|
103
|
-
match result {
|
|
104
|
-
Err(DslError::PolicyMissing(name)) => assert_eq!(name, "missing"),
|
|
105
|
-
_ => panic!("expected policy missing error"),
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
}
|
|
71
|
+
mod tests;
|
|
@@ -43,12 +43,36 @@ pub mod policy;
|
|
|
43
43
|
pub mod retry;
|
|
44
44
|
pub mod sleep;
|
|
45
45
|
|
|
46
|
+
/// The `rand` crate this one was built against.
|
|
47
|
+
///
|
|
48
|
+
/// [`BackoffStrategy::delay`] is generic over [`rand::Rng`], which puts that
|
|
49
|
+
/// trait in the public API: a downstream strategy cannot be written without
|
|
50
|
+
/// naming it. Re-exporting it here means implementors do not have to add their
|
|
51
|
+
/// own `rand` dependency and keep its version in lockstep with this crate's by
|
|
52
|
+
/// hand — a mismatch there produces two incompatible `Rng` traits and an error
|
|
53
|
+
/// that says nothing about the real cause.
|
|
54
|
+
///
|
|
55
|
+
/// ```rust
|
|
56
|
+
/// use chrono_machines::{rand::Rng, BackoffStrategy};
|
|
57
|
+
///
|
|
58
|
+
/// struct Immediate;
|
|
59
|
+
///
|
|
60
|
+
/// impl BackoffStrategy for Immediate {
|
|
61
|
+
/// fn delay<R: Rng>(&self, attempt: u8, _rng: &mut R) -> Option<u64> {
|
|
62
|
+
/// self.should_retry(attempt).then_some(0)
|
|
63
|
+
/// }
|
|
64
|
+
/// fn should_retry(&self, attempt: u8) -> bool { attempt < self.max_attempts() }
|
|
65
|
+
/// fn max_attempts(&self) -> u8 { 3 }
|
|
66
|
+
/// }
|
|
67
|
+
/// ```
|
|
68
|
+
pub use ::rand;
|
|
69
|
+
|
|
46
70
|
pub use backoff::{
|
|
47
|
-
|
|
48
|
-
|
|
71
|
+
BackoffPolicy, BackoffStrategy, ConstantBackoff, ExponentialBackoff, FibonacciBackoff,
|
|
72
|
+
fibonacci,
|
|
49
73
|
};
|
|
50
74
|
#[cfg(feature = "std")]
|
|
51
|
-
pub use dsl::{builder_for_policy, retry_with_policy
|
|
75
|
+
pub use dsl::{DslError, builder_for_policy, retry_with_policy};
|
|
52
76
|
#[cfg(any(feature = "std", feature = "alloc"))]
|
|
53
77
|
pub use policy::PolicyRegistry;
|
|
54
78
|
#[cfg(feature = "std")]
|
|
@@ -56,15 +80,21 @@ pub use policy::{
|
|
|
56
80
|
clear_global_policies, get_global_policy, list_global_policies, register_global_policy,
|
|
57
81
|
remove_global_policy,
|
|
58
82
|
};
|
|
83
|
+
#[cfg(feature = "async")]
|
|
84
|
+
pub use retry::AsyncRetryable;
|
|
59
85
|
#[cfg(any(feature = "std", feature = "alloc"))]
|
|
60
|
-
pub use retry::{
|
|
86
|
+
pub use retry::{
|
|
87
|
+
DelayHint, RetryBuilder, RetryContext, RetryError, RetryOutcome, Retryable, RetryableExt,
|
|
88
|
+
};
|
|
89
|
+
#[cfg(feature = "async")]
|
|
90
|
+
pub use sleep::AsyncSleeper;
|
|
61
91
|
#[cfg(feature = "std")]
|
|
62
92
|
pub use sleep::StdSleeper;
|
|
63
93
|
pub use sleep::{FnSleeper, Sleeper};
|
|
64
94
|
|
|
95
|
+
use rand::RngExt;
|
|
65
96
|
#[cfg(feature = "std")]
|
|
66
97
|
use rand::rngs::StdRng;
|
|
67
|
-
use rand::RngExt;
|
|
68
98
|
|
|
69
99
|
use rand::Rng;
|
|
70
100
|
|
|
@@ -209,147 +239,4 @@ impl Default for Policy {
|
|
|
209
239
|
}
|
|
210
240
|
|
|
211
241
|
#[cfg(test)]
|
|
212
|
-
mod tests
|
|
213
|
-
use super::*;
|
|
214
|
-
use rand::rngs::StdRng;
|
|
215
|
-
use rand::SeedableRng;
|
|
216
|
-
|
|
217
|
-
#[test]
|
|
218
|
-
fn test_policy_default() {
|
|
219
|
-
let policy = Policy::default();
|
|
220
|
-
assert_eq!(policy.max_attempts, 3);
|
|
221
|
-
assert_eq!(policy.base_delay_ms, 100);
|
|
222
|
-
assert_eq!(policy.multiplier, 2.0);
|
|
223
|
-
assert_eq!(policy.max_delay_ms, 10_000);
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
#[test]
|
|
227
|
-
fn test_calculate_delay_bounds() {
|
|
228
|
-
let policy = Policy {
|
|
229
|
-
max_attempts: 5,
|
|
230
|
-
base_delay_ms: 100,
|
|
231
|
-
multiplier: 2.0,
|
|
232
|
-
max_delay_ms: 1000,
|
|
233
|
-
};
|
|
234
|
-
|
|
235
|
-
let mut rng = StdRng::seed_from_u64(42);
|
|
236
|
-
|
|
237
|
-
// First attempt with full jitter: delay should be between 0 and 100ms
|
|
238
|
-
let delay1 = policy.calculate_delay_with_rng(1, 1.0, &mut rng);
|
|
239
|
-
assert!(delay1 <= 100);
|
|
240
|
-
|
|
241
|
-
// Second attempt with full jitter: delay should be between 0 and 200ms
|
|
242
|
-
let delay2 = policy.calculate_delay_with_rng(2, 1.0, &mut rng);
|
|
243
|
-
assert!(delay2 <= 200);
|
|
244
|
-
|
|
245
|
-
// Fifth attempt with full jitter: delay should be capped at max_delay_ms (1000ms)
|
|
246
|
-
let delay5 = policy.calculate_delay_with_rng(5, 1.0, &mut rng);
|
|
247
|
-
assert!(delay5 <= 1000);
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
#[test]
|
|
251
|
-
fn test_should_retry() {
|
|
252
|
-
let policy = Policy {
|
|
253
|
-
max_attempts: 3,
|
|
254
|
-
..Policy::default()
|
|
255
|
-
};
|
|
256
|
-
|
|
257
|
-
assert!(policy.should_retry(1));
|
|
258
|
-
assert!(policy.should_retry(2));
|
|
259
|
-
assert!(!policy.should_retry(3));
|
|
260
|
-
assert!(!policy.should_retry(4));
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
#[test]
|
|
264
|
-
fn test_max_delay_cap() {
|
|
265
|
-
let policy = Policy {
|
|
266
|
-
max_attempts: 10,
|
|
267
|
-
base_delay_ms: 100,
|
|
268
|
-
multiplier: 2.0,
|
|
269
|
-
max_delay_ms: 500,
|
|
270
|
-
};
|
|
271
|
-
|
|
272
|
-
let mut rng = StdRng::seed_from_u64(42);
|
|
273
|
-
|
|
274
|
-
// High attempt number should still be capped
|
|
275
|
-
let delay = policy.calculate_delay_with_rng(10, 1.0, &mut rng);
|
|
276
|
-
assert!(delay <= 500);
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
#[test]
|
|
280
|
-
fn test_zero_multiplier() {
|
|
281
|
-
let policy = Policy {
|
|
282
|
-
max_attempts: 5,
|
|
283
|
-
base_delay_ms: 100,
|
|
284
|
-
multiplier: 1.0, // No exponential growth
|
|
285
|
-
max_delay_ms: 10_000,
|
|
286
|
-
};
|
|
287
|
-
|
|
288
|
-
let mut rng = StdRng::seed_from_u64(42);
|
|
289
|
-
|
|
290
|
-
// All delays with full jitter should be between 0 and base_delay_ms
|
|
291
|
-
for attempt in 1..=5 {
|
|
292
|
-
let delay = policy.calculate_delay_with_rng(attempt, 1.0, &mut rng);
|
|
293
|
-
assert!(delay <= 100);
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
#[test]
|
|
298
|
-
fn test_jitter_factor() {
|
|
299
|
-
let policy = Policy {
|
|
300
|
-
max_attempts: 5,
|
|
301
|
-
base_delay_ms: 1000,
|
|
302
|
-
multiplier: 1.0,
|
|
303
|
-
max_delay_ms: 10_000,
|
|
304
|
-
};
|
|
305
|
-
|
|
306
|
-
let mut rng = StdRng::seed_from_u64(42);
|
|
307
|
-
|
|
308
|
-
// 10% jitter: delay should be between 900ms (90%) and 1000ms (100%)
|
|
309
|
-
let delay = policy.calculate_delay_with_rng(1, 0.1, &mut rng);
|
|
310
|
-
assert!(
|
|
311
|
-
delay >= 900 && delay <= 1000,
|
|
312
|
-
"delay {} not in range 900-1000",
|
|
313
|
-
delay
|
|
314
|
-
);
|
|
315
|
-
|
|
316
|
-
// No jitter: delay should be exactly base_delay_ms
|
|
317
|
-
let delay = policy.calculate_delay_with_rng(1, 0.0, &mut rng);
|
|
318
|
-
assert_eq!(delay, 1000);
|
|
319
|
-
|
|
320
|
-
// Full jitter: delay should be between 0 and 1000ms
|
|
321
|
-
let delay = policy.calculate_delay_with_rng(1, 1.0, &mut rng);
|
|
322
|
-
assert!(delay <= 1000);
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
#[test]
|
|
326
|
-
fn test_jitter_factor_clamping() {
|
|
327
|
-
let policy = Policy {
|
|
328
|
-
max_attempts: 5,
|
|
329
|
-
base_delay_ms: 1000,
|
|
330
|
-
multiplier: 1.0,
|
|
331
|
-
max_delay_ms: 10_000,
|
|
332
|
-
};
|
|
333
|
-
|
|
334
|
-
let mut rng = StdRng::seed_from_u64(42);
|
|
335
|
-
|
|
336
|
-
// Negative jitter_factor should be clamped to 0.0
|
|
337
|
-
let delay = policy.calculate_delay_with_rng(1, -0.5, &mut rng);
|
|
338
|
-
assert_eq!(delay, 1000, "negative jitter_factor should clamp to 0.0");
|
|
339
|
-
|
|
340
|
-
// jitter_factor > 1.0 should be clamped to 1.0
|
|
341
|
-
let delay = policy.calculate_delay_with_rng(1, 2.0, &mut rng);
|
|
342
|
-
assert!(
|
|
343
|
-
delay <= 1000,
|
|
344
|
-
"jitter_factor > 1.0 should clamp to 1.0, got delay {}",
|
|
345
|
-
delay
|
|
346
|
-
);
|
|
347
|
-
|
|
348
|
-
// Extreme values should still be clamped
|
|
349
|
-
let delay = policy.calculate_delay_with_rng(1, 999.0, &mut rng);
|
|
350
|
-
assert!(delay <= 1000, "extreme jitter_factor should be clamped");
|
|
351
|
-
|
|
352
|
-
let delay = policy.calculate_delay_with_rng(1, -999.0, &mut rng);
|
|
353
|
-
assert_eq!(delay, 1000, "extreme negative should clamp to 0.0");
|
|
354
|
-
}
|
|
355
|
-
}
|
|
242
|
+
mod tests;
|