chrono_machines 0.9.1 → 0.10.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: 8784b5114cde3b48cafa9ee9dff5fe69778795235610ecce63a44c73204c28f7
4
- data.tar.gz: b87ddb52e946b220b0a2552647c85c928aa6b2a0e65e513005a40f1fbfd9e33a
3
+ metadata.gz: 520c4b33c24d8122f82758ed724886bfdd7955b427a98de6432bfd2b85f8355e
4
+ data.tar.gz: 7f31ccc205a392a0f26f932cdebdd13efb56204137598d38e818768f6116da53
5
5
  SHA512:
6
- metadata.gz: 1c04099978047a09d84ad12e52ec228bf632063f228cc227ca993012044eec481956703bdac03e57a81995ebec87b46c515e3859d2095262c56d866be94b3b64
7
- data.tar.gz: 387168ff2aa02a535b1c2e25ce9a2541afa535786b50e8cd28bce5cea56deee20a3fedd9255b74f886948f3b4d567fd1f39a13d0e5b7d13ecab8e67a8a49a7ae
6
+ metadata.gz: 51dbb600f208195e5c42f6cc8eec21069aa361f1c1056df5c960ea331a4335c1f18fb328fb27748d5535d9477fea99ae20f1a5961b1cd53ced563b336bc96542
7
+ data.tar.gz: 7a82f044a7fe6e185399021cb09e141a51e0370a468767df79058cf78567fda1ecc96dc0b916de6154d02b828a4b5b4ac803a5a0aa7e78078c147220383c0bfc
data/CHANGELOG.md CHANGED
@@ -1,5 +1,29 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.10.0](https://github.com/seuros/chrono_machines/compare/chrono_machines/v0.9.2...chrono_machines/v0.10.0) (2026-10-11)
4
+
5
+
6
+ ### Features
7
+
8
+ * expose retry delays without executing or sleeping ([#30](https://github.com/seuros/chrono_machines/issues/30)) ([2f6ce19](https://github.com/seuros/chrono_machines/commit/2f6ce1999c7d2056d3adc8e57e0025c22087ca01))
9
+
10
+
11
+ ### Bug Fixes
12
+
13
+ * reject a jitter random source without #rand ([50734ee](https://github.com/seuros/chrono_machines/commit/50734eee35608d8a8324b38cf38f9ab1cc101c65))
14
+
15
+ ## [0.9.2](https://github.com/seuros/chrono_machines/compare/chrono_machines/v0.9.1...chrono_machines/v0.9.2) (2026-10-10)
16
+
17
+
18
+ ### Bug Fixes
19
+
20
+ * **ci:** attach release gems with gh release upload ([87466ea](https://github.com/seuros/chrono_machines/commit/87466eafe0126e5473ff49f176180eae08337657))
21
+
22
+
23
+ ### Performance Improvements
24
+
25
+ * allocate outside the policy registry lock; adopt clippy pedantic and nursery ([80b40d8](https://github.com/seuros/chrono_machines/commit/80b40d87e5869c74b356a71425639ebf560cec4e))
26
+
3
27
  ## [0.9.1](https://github.com/seuros/chrono_machines/compare/chrono_machines/v0.9.0...chrono_machines/v0.9.1) (2026-10-05)
4
28
 
5
29
 
data/Cargo.lock CHANGED
@@ -69,7 +69,7 @@ dependencies = [
69
69
 
70
70
  [[package]]
71
71
  name = "chrono-machines"
72
- version = "0.8.0"
72
+ version = "0.8.1"
73
73
  dependencies = [
74
74
  "rand",
75
75
  "tokio",
@@ -77,7 +77,7 @@ dependencies = [
77
77
 
78
78
  [[package]]
79
79
  name = "chrono_machines_native"
80
- version = "0.9.0"
80
+ version = "0.9.1"
81
81
  dependencies = [
82
82
  "chrono-machines",
83
83
  "magnus",
data/Cargo.toml CHANGED
@@ -1,7 +1,20 @@
1
1
  [workspace]
2
2
  members = ["ext/chrono_machines_native/core", "ext/chrono_machines_native/ffi"]
3
- resolver = "2"
3
+ resolver = "3"
4
+
5
+ [workspace.package]
6
+ edition = "2024"
7
+ rust-version = "1.99"
4
8
 
5
9
  [workspace.dependencies]
6
10
  chrono-machines = { path = "ext/chrono_machines_native/core" }
7
11
  rand = { version = "0.10", default-features = false, features = ["std_rng", "sys_rng"] }
12
+
13
+ [workspace.lints.rust]
14
+ unsafe_code = "deny"
15
+ unused_must_use = "warn"
16
+
17
+ [workspace.lints.clippy]
18
+ all = "warn"
19
+ pedantic = "warn"
20
+ nursery = "warn"
data/README.md CHANGED
@@ -221,6 +221,39 @@ The hook's return value controls the retry:
221
221
 
222
222
  Anything else raises `ArgumentError` - a broken hook fails loudly instead of silently mangling your retry cadence.
223
223
 
224
+ ### Scheduling retries without sleeping
225
+
226
+ `Executor#next_delay(attempt:, exception: nil)` returns seconds to wait, or `nil` to stop. `attempt: 1` means the initial call has failed; `max_attempts` includes that initial call. It uses the same backoff, jitter, exception rules, and `delay_from` hints as `call`, without executing work, sleeping, or running lifecycle callbacks. The caller owns the attempt count and scheduling. Without an exception, `delay_from` and exception filtering are skipped.
227
+
228
+ For Active Job's `retry_on`, keep the attempt limit in sync with the policy:
229
+
230
+ ```ruby
231
+ class FetchJob < ApplicationJob
232
+ RETRY_POLICY = ChronoMachines::Executor.new(max_attempts: 5, base_delay: 1)
233
+
234
+ retry_on IOError, attempts: 5,
235
+ wait: ->(executions) { RETRY_POLICY.next_delay(attempt: executions) }
236
+ end
237
+ ```
238
+
239
+ `retry_on` calls `wait` only while `executions < attempts`. If the two limits drift apart, `next_delay` returns `nil` and Active Job retries immediately, so keep `attempts:` equal to `max_attempts`.
240
+
241
+ To use exception hints with durable jobs, pass the attempt count to the next job:
242
+
243
+ ```ruby
244
+ def perform(record_id, attempt: 1)
245
+ fetch_record(record_id)
246
+ rescue IOError => error
247
+ delay = RETRY_POLICY.next_delay(attempt: attempt, exception: error)
248
+ raise unless delay
249
+
250
+ self.class.set(wait_until: Time.current + delay)
251
+ .perform_later(record_id, attempt: attempt + 1)
252
+ end
253
+ ```
254
+
255
+ A hint beyond `max_delay` returns `nil`; it is never shortened to the cap. Use `random: Random.new(seed)` for deterministic jitter in tests, with either the Ruby or native backend.
256
+
224
257
  ## The Science of Temporal Jitter
225
258
 
226
259
  ChronoMachines implements **full jitter** exponential backoff:
@@ -239,16 +272,19 @@ ChronoMachines implements **full jitter** exponential backoff:
239
272
 
240
273
  This prevents the "thundering herd" problem where multiple clients retry simultaneously, overwhelming recovering services.
241
274
 
275
+ Jitter is drawn from the `random:` option (Ruby's `Random` by default) on both backends; the native extension computes only the un-jittered base delay.
276
+
242
277
  ## Configuration Reference
243
278
 
244
279
  ### Policy Options
245
280
 
246
281
  | Option | Default | Description |
247
282
  |--------|---------|-------------|
248
- | `max_attempts` | `3` | Maximum number of retry attempts |
283
+ | `max_attempts` | `3` | Maximum number of attempts, including the initial call |
249
284
  | `base_delay` | `0.1` | Initial delay in seconds |
250
285
  | `multiplier` | `2` | Exponential backoff multiplier |
251
286
  | `max_delay` | `10` | Maximum delay cap in seconds |
287
+ | `random` | `Random` | Random source for jitter; accepts `Random.new(seed)` |
252
288
  | `retryable_exceptions` | `[StandardError]` | Array of exception classes to retry |
253
289
  | `on_success` | `nil` | Success callback: `(result:, attempts:)` |
254
290
  | `on_retry` | `nil` | Retry callback: `(exception:, attempt:, next_delay:)` |
@@ -1,7 +1,8 @@
1
1
  [package]
2
2
  name = "chrono-machines"
3
- version = "0.8.0"
4
- edition = "2024"
3
+ version = "0.8.1"
4
+ edition.workspace = true
5
+ rust-version.workspace = true
5
6
  authors = ["Abdelkader Boudih <terminale@gmail.com>"]
6
7
  license = "MIT"
7
8
  description = "Exponential, constant, and Fibonacci backoff retry library with full jitter support - no_std compatible"
@@ -29,3 +30,6 @@ rand = { version = "0.10", default-features = false, features = ["std_rng"] }
29
30
 
30
31
  [dev-dependencies]
31
32
  tokio = { version = "1", features = ["rt", "rt-multi-thread", "macros", "time"] }
33
+
34
+ [lints]
35
+ workspace = true
@@ -8,7 +8,7 @@ use rand::RngExt;
8
8
 
9
9
  /// `f64::powi`, implemented by hand so it works in `core` (`no_std`).
10
10
  #[inline]
11
- pub(crate) fn powi_f64(base: f64, exp: i32) -> f64 {
11
+ fn powi_f64(base: f64, exp: i32) -> f64 {
12
12
  if exp == 0 {
13
13
  return 1.0;
14
14
  }
@@ -25,7 +25,36 @@ pub(crate) fn powi_f64(base: f64, exp: i32) -> f64 {
25
25
  acc
26
26
  }
27
27
 
28
+ /// Widen a delay (or Fibonacci factor) to `f64` for the backoff arithmetic.
29
+ ///
30
+ /// There is no lossless `u64 -> f64` conversion. Rounding only starts above
31
+ /// 2^53 (2^53 ms is ~285,000 years), so every delay that can actually be slept
32
+ /// converts exactly; larger values only ever lose to the `max_delay_ms` cap.
33
+ #[inline]
34
+ #[expect(
35
+ clippy::cast_precision_loss,
36
+ reason = "inherent to u64 -> f64; exact below 2^53"
37
+ )]
38
+ const fn lossy_f64(n: u64) -> f64 {
39
+ n as f64
40
+ }
41
+
42
+ /// Un-jittered exponential delay: `base_delay_ms * multiplier^(attempt - 1)`,
43
+ /// capped at `max_delay_ms`.
44
+ #[inline]
45
+ pub(crate) fn exponential_ms(
46
+ base_delay_ms: u64,
47
+ multiplier: f64,
48
+ max_delay_ms: u64,
49
+ attempt: u8,
50
+ ) -> f64 {
51
+ let exponent = i32::from(attempt.saturating_sub(1));
52
+ let base_exponential = lossy_f64(base_delay_ms) * powi_f64(multiplier, exponent);
53
+ base_exponential.min(lossy_f64(max_delay_ms))
54
+ }
55
+
28
56
  /// Calculate the nth Fibonacci number (1-indexed): 1, 1, 2, 3, 5, 8, 13, ...
57
+ #[must_use]
29
58
  pub fn fibonacci(n: u8) -> u64 {
30
59
  match n {
31
60
  0 => 0,
@@ -47,7 +76,22 @@ pub fn fibonacci(n: u8) -> u64 {
47
76
  ///
48
77
  /// `jitter_factor` is clamped to `[0.0, 1.0]`: `0.0` returns `base` unchanged,
49
78
  /// `1.0` yields a uniform value in `[0, base]`.
50
- fn apply_jitter<R: Rng>(base: f64, jitter_factor: f64, rng: &mut R) -> u64 {
79
+ // Clippy only suggests `mul_add` when `std` provides it; `core` has no
80
+ // `f64::mul_add`, so the suggestion would break the `no_std` build.
81
+ #[cfg_attr(
82
+ feature = "std",
83
+ expect(
84
+ clippy::suboptimal_flops,
85
+ reason = "`f64::mul_add` is std-only (breaks no_std) and would change the rounding"
86
+ )
87
+ )]
88
+ #[expect(
89
+ clippy::cast_possible_truncation,
90
+ clippy::cast_sign_loss,
91
+ reason = "`as` is the saturating float -> int conversion: NaN and negatives \
92
+ become 0, overflow becomes u64::MAX, which is the clamp we want"
93
+ )]
94
+ pub(crate) fn apply_jitter<R: Rng>(base: f64, jitter_factor: f64, rng: &mut R) -> u64 {
51
95
  let jitter_factor = jitter_factor.clamp(0.0, 1.0);
52
96
  let random_scalar: f64 = rng.random_range(0.0..=1.0);
53
97
  let jitter_blend = 1.0 - jitter_factor + random_scalar * jitter_factor;
@@ -126,7 +170,7 @@ pub trait BackoffStrategy {
126
170
 
127
171
  /// Exponential backoff strategy with configurable jitter
128
172
  ///
129
- /// Delays grow exponentially: base_delay * multiplier^(attempt-1)
173
+ /// Delays grow exponentially: `base_delay_ms * multiplier^(attempt - 1)`
130
174
  ///
131
175
  /// # Example
132
176
  ///
@@ -156,36 +200,48 @@ pub struct ExponentialBackoff {
156
200
 
157
201
  impl ExponentialBackoff {
158
202
  /// Create a new exponential backoff builder with default values
159
- pub fn new() -> Self {
160
- Self::default()
203
+ #[must_use]
204
+ pub const fn new() -> Self {
205
+ Self {
206
+ max_attempts: 3,
207
+ base_delay_ms: 100,
208
+ multiplier: 2.0,
209
+ max_delay_ms: 10_000,
210
+ jitter_factor: 1.0, // Full jitter by default
211
+ }
161
212
  }
162
213
 
163
214
  /// Set the base delay in milliseconds
164
- pub fn base_delay_ms(mut self, ms: u64) -> Self {
215
+ #[must_use]
216
+ pub const fn base_delay_ms(mut self, ms: u64) -> Self {
165
217
  self.base_delay_ms = ms;
166
218
  self
167
219
  }
168
220
 
169
221
  /// Set the exponential multiplier
170
- pub fn multiplier(mut self, multiplier: f64) -> Self {
222
+ #[must_use]
223
+ pub const fn multiplier(mut self, multiplier: f64) -> Self {
171
224
  self.multiplier = multiplier;
172
225
  self
173
226
  }
174
227
 
175
228
  /// Set the maximum delay cap in milliseconds
176
- pub fn max_delay_ms(mut self, ms: u64) -> Self {
229
+ #[must_use]
230
+ pub const fn max_delay_ms(mut self, ms: u64) -> Self {
177
231
  self.max_delay_ms = ms;
178
232
  self
179
233
  }
180
234
 
181
235
  /// Set the maximum number of attempts
182
- pub fn max_attempts(mut self, attempts: u8) -> Self {
236
+ #[must_use]
237
+ pub const fn max_attempts(mut self, attempts: u8) -> Self {
183
238
  self.max_attempts = attempts;
184
239
  self
185
240
  }
186
241
 
187
242
  /// Set the jitter factor (0.0 = no jitter, 1.0 = full jitter)
188
- pub fn jitter_factor(mut self, factor: f64) -> Self {
243
+ #[must_use]
244
+ pub const fn jitter_factor(mut self, factor: f64) -> Self {
189
245
  self.jitter_factor = factor.clamp(0.0, 1.0);
190
246
  self
191
247
  }
@@ -193,13 +249,7 @@ impl ExponentialBackoff {
193
249
 
194
250
  impl Default for ExponentialBackoff {
195
251
  fn default() -> Self {
196
- Self {
197
- max_attempts: 3,
198
- base_delay_ms: 100,
199
- multiplier: 2.0,
200
- max_delay_ms: 10_000,
201
- jitter_factor: 1.0, // Full jitter by default
202
- }
252
+ Self::new()
203
253
  }
204
254
  }
205
255
 
@@ -209,9 +259,12 @@ impl BackoffStrategy for ExponentialBackoff {
209
259
  return None;
210
260
  }
211
261
 
212
- let exponent = attempt.saturating_sub(1) as i32;
213
- let base_exponential = (self.base_delay_ms as f64) * powi_f64(self.multiplier, exponent);
214
- let capped = base_exponential.min(self.max_delay_ms as f64);
262
+ let capped = exponential_ms(
263
+ self.base_delay_ms,
264
+ self.multiplier,
265
+ self.max_delay_ms,
266
+ attempt,
267
+ );
215
268
 
216
269
  Some(apply_jitter(capped, self.jitter_factor, rng))
217
270
  }
@@ -255,24 +308,32 @@ pub struct ConstantBackoff {
255
308
 
256
309
  impl ConstantBackoff {
257
310
  /// Create a new constant backoff builder with default values
258
- pub fn new() -> Self {
259
- Self::default()
311
+ #[must_use]
312
+ pub const fn new() -> Self {
313
+ Self {
314
+ delay_ms: 100,
315
+ max_attempts: 3,
316
+ jitter_factor: 0.0, // No jitter for constant by default
317
+ }
260
318
  }
261
319
 
262
320
  /// Set the constant delay in milliseconds
263
- pub fn delay_ms(mut self, ms: u64) -> Self {
321
+ #[must_use]
322
+ pub const fn delay_ms(mut self, ms: u64) -> Self {
264
323
  self.delay_ms = ms;
265
324
  self
266
325
  }
267
326
 
268
327
  /// Set the maximum number of attempts
269
- pub fn max_attempts(mut self, attempts: u8) -> Self {
328
+ #[must_use]
329
+ pub const fn max_attempts(mut self, attempts: u8) -> Self {
270
330
  self.max_attempts = attempts;
271
331
  self
272
332
  }
273
333
 
274
334
  /// Set the jitter factor (0.0 = no jitter, 1.0 = full jitter)
275
- pub fn jitter_factor(mut self, factor: f64) -> Self {
335
+ #[must_use]
336
+ pub const fn jitter_factor(mut self, factor: f64) -> Self {
276
337
  self.jitter_factor = factor.clamp(0.0, 1.0);
277
338
  self
278
339
  }
@@ -280,11 +341,7 @@ impl ConstantBackoff {
280
341
 
281
342
  impl Default for ConstantBackoff {
282
343
  fn default() -> Self {
283
- Self {
284
- delay_ms: 100,
285
- max_attempts: 3,
286
- jitter_factor: 0.0, // No jitter for constant by default
287
- }
344
+ Self::new()
288
345
  }
289
346
  }
290
347
 
@@ -294,7 +351,11 @@ impl BackoffStrategy for ConstantBackoff {
294
351
  return None;
295
352
  }
296
353
 
297
- Some(apply_jitter(self.delay_ms as f64, self.jitter_factor, rng))
354
+ Some(apply_jitter(
355
+ lossy_f64(self.delay_ms),
356
+ self.jitter_factor,
357
+ rng,
358
+ ))
298
359
  }
299
360
 
300
361
  fn should_retry(&self, attempt: u8) -> bool {
@@ -309,7 +370,7 @@ impl BackoffStrategy for ConstantBackoff {
309
370
  /// Fibonacci backoff strategy
310
371
  ///
311
372
  /// Delays follow the Fibonacci sequence: 1, 1, 2, 3, 5, 8, 13, ...
312
- /// Each delay is base_delay_ms * fibonacci(attempt).
373
+ /// Each delay is `base_delay_ms * fibonacci(attempt)`.
313
374
  ///
314
375
  /// # Example
315
376
  ///
@@ -336,30 +397,40 @@ pub struct FibonacciBackoff {
336
397
 
337
398
  impl FibonacciBackoff {
338
399
  /// Create a new Fibonacci backoff builder with default values
339
- pub fn new() -> Self {
340
- Self::default()
400
+ #[must_use]
401
+ pub const fn new() -> Self {
402
+ Self {
403
+ base_delay_ms: 100,
404
+ max_delay_ms: 10_000,
405
+ max_attempts: 8,
406
+ jitter_factor: 1.0, // Full jitter by default
407
+ }
341
408
  }
342
409
 
343
410
  /// Set the base delay in milliseconds
344
- pub fn base_delay_ms(mut self, ms: u64) -> Self {
411
+ #[must_use]
412
+ pub const fn base_delay_ms(mut self, ms: u64) -> Self {
345
413
  self.base_delay_ms = ms;
346
414
  self
347
415
  }
348
416
 
349
417
  /// Set the maximum delay cap in milliseconds
350
- pub fn max_delay_ms(mut self, ms: u64) -> Self {
418
+ #[must_use]
419
+ pub const fn max_delay_ms(mut self, ms: u64) -> Self {
351
420
  self.max_delay_ms = ms;
352
421
  self
353
422
  }
354
423
 
355
424
  /// Set the maximum number of attempts
356
- pub fn max_attempts(mut self, attempts: u8) -> Self {
425
+ #[must_use]
426
+ pub const fn max_attempts(mut self, attempts: u8) -> Self {
357
427
  self.max_attempts = attempts;
358
428
  self
359
429
  }
360
430
 
361
431
  /// Set the jitter factor (0.0 = no jitter, 1.0 = full jitter)
362
- pub fn jitter_factor(mut self, factor: f64) -> Self {
432
+ #[must_use]
433
+ pub const fn jitter_factor(mut self, factor: f64) -> Self {
363
434
  self.jitter_factor = factor.clamp(0.0, 1.0);
364
435
  self
365
436
  }
@@ -367,12 +438,7 @@ impl FibonacciBackoff {
367
438
 
368
439
  impl Default for FibonacciBackoff {
369
440
  fn default() -> Self {
370
- Self {
371
- base_delay_ms: 100,
372
- max_delay_ms: 10_000,
373
- max_attempts: 8,
374
- jitter_factor: 1.0, // Full jitter by default
375
- }
441
+ Self::new()
376
442
  }
377
443
  }
378
444
 
@@ -383,7 +449,8 @@ impl BackoffStrategy for FibonacciBackoff {
383
449
  }
384
450
 
385
451
  let fib = fibonacci(attempt);
386
- let base = ((self.base_delay_ms as f64) * (fib as f64)).min(self.max_delay_ms as f64);
452
+ let base =
453
+ (lossy_f64(self.base_delay_ms) * lossy_f64(fib)).min(lossy_f64(self.max_delay_ms));
387
454
 
388
455
  Some(apply_jitter(base, self.jitter_factor, rng))
389
456
  }
@@ -417,11 +484,12 @@ pub enum BackoffPolicy {
417
484
 
418
485
  impl BackoffPolicy {
419
486
  /// Return the maximum retry attempts for the wrapped strategy.
420
- pub fn max_attempts(&self) -> u8 {
487
+ #[must_use]
488
+ pub const fn max_attempts(&self) -> u8 {
421
489
  match self {
422
- BackoffPolicy::Exponential(policy) => policy.max_attempts,
423
- BackoffPolicy::Constant(policy) => policy.max_attempts,
424
- BackoffPolicy::Fibonacci(policy) => policy.max_attempts,
490
+ Self::Exponential(policy) => policy.max_attempts,
491
+ Self::Constant(policy) => policy.max_attempts,
492
+ Self::Fibonacci(policy) => policy.max_attempts,
425
493
  }
426
494
  }
427
495
  }
@@ -429,52 +497,52 @@ impl BackoffPolicy {
429
497
  impl BackoffStrategy for BackoffPolicy {
430
498
  fn delay<R: Rng>(&self, attempt: u8, rng: &mut R) -> Option<u64> {
431
499
  match self {
432
- BackoffPolicy::Exponential(policy) => policy.delay(attempt, rng),
433
- BackoffPolicy::Constant(policy) => policy.delay(attempt, rng),
434
- BackoffPolicy::Fibonacci(policy) => policy.delay(attempt, rng),
500
+ Self::Exponential(policy) => policy.delay(attempt, rng),
501
+ Self::Constant(policy) => policy.delay(attempt, rng),
502
+ Self::Fibonacci(policy) => policy.delay(attempt, rng),
435
503
  }
436
504
  }
437
505
 
438
506
  fn should_retry(&self, attempt: u8) -> bool {
439
507
  match self {
440
- BackoffPolicy::Exponential(policy) => policy.should_retry(attempt),
441
- BackoffPolicy::Constant(policy) => policy.should_retry(attempt),
442
- BackoffPolicy::Fibonacci(policy) => policy.should_retry(attempt),
508
+ Self::Exponential(policy) => policy.should_retry(attempt),
509
+ Self::Constant(policy) => policy.should_retry(attempt),
510
+ Self::Fibonacci(policy) => policy.should_retry(attempt),
443
511
  }
444
512
  }
445
513
 
446
514
  fn max_attempts(&self) -> u8 {
447
515
  match self {
448
- BackoffPolicy::Exponential(policy) => policy.max_attempts(),
449
- BackoffPolicy::Constant(policy) => policy.max_attempts(),
450
- BackoffPolicy::Fibonacci(policy) => policy.max_attempts(),
516
+ Self::Exponential(policy) => policy.max_attempts(),
517
+ Self::Constant(policy) => policy.max_attempts(),
518
+ Self::Fibonacci(policy) => policy.max_attempts(),
451
519
  }
452
520
  }
453
521
 
454
522
  fn max_delay_ms(&self) -> Option<u64> {
455
523
  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(),
524
+ Self::Exponential(policy) => policy.max_delay_ms(),
525
+ Self::Constant(policy) => policy.max_delay_ms(),
526
+ Self::Fibonacci(policy) => policy.max_delay_ms(),
459
527
  }
460
528
  }
461
529
  }
462
530
 
463
531
  impl From<ExponentialBackoff> for BackoffPolicy {
464
532
  fn from(value: ExponentialBackoff) -> Self {
465
- BackoffPolicy::Exponential(value)
533
+ Self::Exponential(value)
466
534
  }
467
535
  }
468
536
 
469
537
  impl From<ConstantBackoff> for BackoffPolicy {
470
538
  fn from(value: ConstantBackoff) -> Self {
471
- BackoffPolicy::Constant(value)
539
+ Self::Constant(value)
472
540
  }
473
541
  }
474
542
 
475
543
  impl From<FibonacciBackoff> for BackoffPolicy {
476
544
  fn from(value: FibonacciBackoff) -> Self {
477
- BackoffPolicy::Fibonacci(value)
545
+ Self::Fibonacci(value)
478
546
  }
479
547
  }
480
548
 
@@ -22,7 +22,7 @@ pub enum DslError<E> {
22
22
 
23
23
  impl<E> From<RetryError<E>> for DslError<E> {
24
24
  fn from(value: RetryError<E>) -> Self {
25
- DslError::Execution(value)
25
+ Self::Execution(value)
26
26
  }
27
27
  }
28
28
 
@@ -32,8 +32,8 @@ where
32
32
  {
33
33
  fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
34
34
  match self {
35
- DslError::PolicyMissing(name) => write!(f, "retry policy '{}' is not registered", name),
36
- DslError::Execution(err) => write!(f, "{err}"),
35
+ Self::PolicyMissing(name) => write!(f, "retry policy '{name}' is not registered"),
36
+ Self::Execution(err) => write!(f, "{err}"),
37
37
  }
38
38
  }
39
39
  }
@@ -41,6 +41,11 @@ where
41
41
  impl<E> std::error::Error for DslError<E> where E: fmt::Display + std::error::Error {}
42
42
 
43
43
  /// Construct a [`RetryBuilder`] using a named policy from the global registry.
44
+ ///
45
+ /// # Errors
46
+ ///
47
+ /// Returns [`DslError::PolicyMissing`] when no policy is registered under
48
+ /// `policy_name`.
44
49
  pub fn builder_for_policy<F, T, E>(
45
50
  policy_name: &str,
46
51
  operation: F,
@@ -55,6 +60,12 @@ where
55
60
  }
56
61
 
57
62
  /// Execute an operation using a named policy from the global registry.
63
+ ///
64
+ /// # Errors
65
+ ///
66
+ /// Returns [`DslError::PolicyMissing`] when no policy is registered under
67
+ /// `policy_name`, or [`DslError::Execution`] wrapping the [`RetryError`] when
68
+ /// the retry gives up.
58
69
  pub fn retry_with_policy<F, T, E>(
59
70
  policy_name: &str,
60
71
  operation: F,