chrono_machines 0.5.2 → 0.9.1
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 +86 -0
- data/Cargo.lock +10 -22
- 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 +3 -3
- data/ext/chrono_machines_native/ffi/src/lib.rs +8 -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 +3 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8784b5114cde3b48cafa9ee9dff5fe69778795235610ecce63a44c73204c28f7
|
|
4
|
+
data.tar.gz: b87ddb52e946b220b0a2552647c85c928aa6b2a0e65e513005a40f1fbfd9e33a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1c04099978047a09d84ad12e52ec228bf632063f228cc227ca993012044eec481956703bdac03e57a81995ebec87b46c515e3859d2095262c56d866be94b3b64
|
|
7
|
+
data.tar.gz: 387168ff2aa02a535b1c2e25ce9a2541afa535786b50e8cd28bce5cea56deee20a3fedd9255b74f886948f3b4d567fd1f39a13d0e5b7d13ecab8e67a8a49a7ae
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,91 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [0.9.1](https://github.com/seuros/chrono_machines/compare/chrono_machines/v0.9.0...chrono_machines/v0.9.1) (2026-10-05)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Bug Fixes
|
|
7
|
+
|
|
8
|
+
* **build:** skip host compile when packaging a cross gem ([5d5b61d](https://github.com/seuros/chrono_machines/commit/5d5b61da68a0815ef3a30f6fb731af844cbd45ee))
|
|
9
|
+
|
|
10
|
+
## [0.9.0](https://github.com/seuros/chrono_machines/compare/chrono_machines/v0.8.0...chrono_machines/v0.9.0) (2026-10-05)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### Features
|
|
14
|
+
|
|
15
|
+
* require Ruby 4.0, magnus 0.9, Ractor-safe native ext ([316d6f5](https://github.com/seuros/chrono_machines/commit/316d6f5e3de0f58e33d0a124eec4efafefb78cab))
|
|
16
|
+
|
|
17
|
+
## [0.8.0](https://github.com/seuros/chrono_machines/compare/chrono_machines/v0.5.0...chrono_machines/v0.8.0) (2026-09-14)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
### ⚠ BREAKING CHANGES
|
|
21
|
+
|
|
22
|
+
* **native:** add async retry driver ([#21](https://github.com/seuros/chrono_machines/issues/21))
|
|
23
|
+
|
|
24
|
+
### Features
|
|
25
|
+
|
|
26
|
+
* **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))
|
|
27
|
+
* **core:** re-export rand so BackoffStrategy is implementable downstream ([dc1cf49](https://github.com/seuros/chrono_machines/commit/dc1cf494df0124032bec6aee6e4f6a33e43abbfb))
|
|
28
|
+
* fix release ([ad40d8f](https://github.com/seuros/chrono_machines/commit/ad40d8f5b6629e49ba2d5f0cb7f213a1cc1e30c7))
|
|
29
|
+
* **native:** add async retry driver ([#21](https://github.com/seuros/chrono_machines/issues/21)) ([766f207](https://github.com/seuros/chrono_machines/commit/766f20799bcc0c36f7ca6a904fadff227549fee7))
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
### Bug Fixes
|
|
33
|
+
|
|
34
|
+
* 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))
|
|
35
|
+
* 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))
|
|
36
|
+
* trigger release ([02184d5](https://github.com/seuros/chrono_machines/commit/02184d51f6831f53ae6142ea5092710d456363d5))
|
|
37
|
+
|
|
38
|
+
## [0.5.0](https://github.com/seuros/chrono_machines/compare/chrono_machines/v0.5.0...chrono_machines/v0.5.0) (2026-09-14)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
### ⚠ BREAKING CHANGES
|
|
42
|
+
|
|
43
|
+
* **native:** add async retry driver ([#21](https://github.com/seuros/chrono_machines/issues/21))
|
|
44
|
+
|
|
45
|
+
### Features
|
|
46
|
+
|
|
47
|
+
* **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))
|
|
48
|
+
* **core:** re-export rand so BackoffStrategy is implementable downstream ([dc1cf49](https://github.com/seuros/chrono_machines/commit/dc1cf494df0124032bec6aee6e4f6a33e43abbfb))
|
|
49
|
+
* fix release ([ad40d8f](https://github.com/seuros/chrono_machines/commit/ad40d8f5b6629e49ba2d5f0cb7f213a1cc1e30c7))
|
|
50
|
+
* **native:** add async retry driver ([#21](https://github.com/seuros/chrono_machines/issues/21)) ([766f207](https://github.com/seuros/chrono_machines/commit/766f20799bcc0c36f7ca6a904fadff227549fee7))
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
### Bug Fixes
|
|
54
|
+
|
|
55
|
+
* 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))
|
|
56
|
+
* 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))
|
|
57
|
+
|
|
58
|
+
## [0.5.0](https://github.com/seuros/chrono_machines/compare/chrono_machines/v0.5.0...chrono_machines/v0.5.0) (2026-08-23)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
### ⚠ BREAKING CHANGES
|
|
62
|
+
|
|
63
|
+
* **native:** add async retry driver ([#21](https://github.com/seuros/chrono_machines/issues/21))
|
|
64
|
+
|
|
65
|
+
### Features
|
|
66
|
+
|
|
67
|
+
* **core:** re-export rand so BackoffStrategy is implementable downstream ([dc1cf49](https://github.com/seuros/chrono_machines/commit/dc1cf494df0124032bec6aee6e4f6a33e43abbfb))
|
|
68
|
+
* fix release ([ad40d8f](https://github.com/seuros/chrono_machines/commit/ad40d8f5b6629e49ba2d5f0cb7f213a1cc1e30c7))
|
|
69
|
+
* **native:** add async retry driver ([#21](https://github.com/seuros/chrono_machines/issues/21)) ([766f207](https://github.com/seuros/chrono_machines/commit/766f20799bcc0c36f7ca6a904fadff227549fee7))
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
### Bug Fixes
|
|
73
|
+
|
|
74
|
+
* 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))
|
|
75
|
+
* 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))
|
|
76
|
+
|
|
77
|
+
## [0.5.0](https://github.com/seuros/chrono_machines/compare/chrono_machines/v0.5.2...chrono_machines/v0.5.0) (2026-08-23)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
### ⚠ BREAKING CHANGES
|
|
81
|
+
|
|
82
|
+
* **native:** add async retry driver ([#21](https://github.com/seuros/chrono_machines/issues/21))
|
|
83
|
+
|
|
84
|
+
### Features
|
|
85
|
+
|
|
86
|
+
* fix release ([ad40d8f](https://github.com/seuros/chrono_machines/commit/ad40d8f5b6629e49ba2d5f0cb7f213a1cc1e30c7))
|
|
87
|
+
* **native:** add async retry driver ([#21](https://github.com/seuros/chrono_machines/issues/21)) ([766f207](https://github.com/seuros/chrono_machines/commit/766f20799bcc0c36f7ca6a904fadff227549fee7))
|
|
88
|
+
|
|
3
89
|
## [0.5.2](https://github.com/seuros/chrono_machines/compare/chrono_machines/v0.5.1...chrono_machines/v0.5.2) (2026-08-07)
|
|
4
90
|
|
|
5
91
|
|
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"
|
|
@@ -69,9 +58,9 @@ checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
|
|
69
58
|
|
|
70
59
|
[[package]]
|
|
71
60
|
name = "chacha20"
|
|
72
|
-
version = "0.10.
|
|
61
|
+
version = "0.10.2"
|
|
73
62
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
74
|
-
checksum = "
|
|
63
|
+
checksum = "65c35e4b699c7e15ccbe7ee35c005e4fc0a278d22238a2857e6ce2dadeda1b06"
|
|
75
64
|
dependencies = [
|
|
76
65
|
"cfg-if",
|
|
77
66
|
"cpufeatures",
|
|
@@ -80,16 +69,15 @@ dependencies = [
|
|
|
80
69
|
|
|
81
70
|
[[package]]
|
|
82
71
|
name = "chrono-machines"
|
|
83
|
-
version = "0.
|
|
72
|
+
version = "0.8.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.9.0"
|
|
93
81
|
dependencies = [
|
|
94
82
|
"chrono-machines",
|
|
95
83
|
"magnus",
|
|
@@ -245,9 +233,9 @@ checksum = "953f07c43838f8e6f9758cab68bf5bed85465e7587ebe0b823f1bcd81978ad3a"
|
|
|
245
233
|
|
|
246
234
|
[[package]]
|
|
247
235
|
name = "magnus"
|
|
248
|
-
version = "0.
|
|
236
|
+
version = "0.9.2"
|
|
249
237
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
250
|
-
checksum = "
|
|
238
|
+
checksum = "b9068710e00d21be762edca59b9c4c8946dbdb29820d4f76ad559afea07982c9"
|
|
251
239
|
dependencies = [
|
|
252
240
|
"magnus-macros",
|
|
253
241
|
"rb-sys",
|
|
@@ -257,9 +245,9 @@ dependencies = [
|
|
|
257
245
|
|
|
258
246
|
[[package]]
|
|
259
247
|
name = "magnus-macros"
|
|
260
|
-
version = "0.
|
|
248
|
+
version = "0.9.1"
|
|
261
249
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
262
|
-
checksum = "
|
|
250
|
+
checksum = "9dafde2eeb66f090e0f326112323eb8324b12cb32b5d1210e541d3df6b0ce77e"
|
|
263
251
|
dependencies = [
|
|
264
252
|
"proc-macro2",
|
|
265
253
|
"quote",
|
|
@@ -330,9 +318,9 @@ checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
|
|
|
330
318
|
|
|
331
319
|
[[package]]
|
|
332
320
|
name = "rand"
|
|
333
|
-
version = "0.10.
|
|
321
|
+
version = "0.10.3"
|
|
334
322
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
335
|
-
checksum = "
|
|
323
|
+
checksum = "65c9fb96cbc91e3478eaae79a69fcd3f1ae4ad052e471fe6732fff548984b4af"
|
|
336
324
|
dependencies = [
|
|
337
325
|
"chacha20",
|
|
338
326
|
"getrandom",
|
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;
|