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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a02c5bea1c05535fe666e96b6dc442318ccbb695145b3fffb3f7c259c9c1233b
4
- data.tar.gz: 4a9ad6b5479cfdd7201fa6b52952f4f44743438a2daadd5d12a7e85d76bd3a28
3
+ metadata.gz: c6cbe05e9e40deb1bf5b846c57800d1f4439caf6cd753883562a7f911a453640
4
+ data.tar.gz: ae0a5c30e033a6bdd974ff9ea8bbac5220f2aa316a9b6724897cefcdd23971cf
5
5
  SHA512:
6
- metadata.gz: b367f7d8a63032b1950e0a120485e87bac44f31d7b6a02caf747fb30135a6b6aad99e4ce04724b01eb74ae9fe5464958f8d015ab88e8d5ac8dece43576b3ebaf
7
- data.tar.gz: 42c6b3908d329f78521d073883abb4aefa5dd0d1c53072773ab305daae4f179f9645e5c5eb931ba0c0efb75bb32446501547fd67d3f3baf2f32febe650ae1556
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.3.2"
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.2.3"
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.4.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
- async = ["std", "dep:async-trait"]
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
- fibonacci, BackoffPolicy, BackoffStrategy, ConstantBackoff, ExponentialBackoff,
48
- FibonacciBackoff,
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, DslError};
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::{RetryBuilder, RetryContext, RetryError, RetryOutcome, Retryable, RetryableExt};
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;