chrono_machines 0.2.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +67 -0
  3. data/README.md +1 -1
  4. data/ext/chrono_machines_native/core/Cargo.toml +28 -0
  5. data/ext/chrono_machines_native/core/src/backoff.rs +516 -0
  6. data/ext/chrono_machines_native/core/src/dsl.rs +108 -0
  7. data/ext/chrono_machines_native/core/src/lib.rs +355 -0
  8. data/ext/chrono_machines_native/core/src/policy.rs +189 -0
  9. data/ext/chrono_machines_native/core/src/retry.rs +1511 -0
  10. data/ext/chrono_machines_native/core/src/sleep.rs +97 -0
  11. data/ext/chrono_machines_native/extconf.rb +3 -0
  12. data/ext/chrono_machines_native/ffi/Cargo.toml +23 -0
  13. data/ext/chrono_machines_native/ffi/extconf.rb +62 -0
  14. data/ext/chrono_machines_native/ffi/src/lib.rs +192 -0
  15. data/ext/chrono_machines_native/target/debug/build/clang-sys-414c893ed9493fe4/out/common.rs +355 -0
  16. data/ext/chrono_machines_native/target/debug/build/clang-sys-414c893ed9493fe4/out/dynamic.rs +276 -0
  17. data/ext/chrono_machines_native/target/debug/build/clang-sys-414c893ed9493fe4/out/macros.rs +49 -0
  18. data/ext/chrono_machines_native/target/debug/build/clang-sys-b1d0ba329a011cdd/out/common.rs +355 -0
  19. data/ext/chrono_machines_native/target/debug/build/clang-sys-b1d0ba329a011cdd/out/dynamic.rs +276 -0
  20. data/ext/chrono_machines_native/target/debug/build/clang-sys-b1d0ba329a011cdd/out/macros.rs +49 -0
  21. data/ext/chrono_machines_native/target/debug/build/rb-sys-7d416123f8551038/out/bindings-0.9.124-mri-arm64-darwin23-4.0.3.rs +9057 -0
  22. data/ext/chrono_machines_native/target/debug/build/rb-sys-848783778984ec5d/out/bindings-0.9.124-mri-arm64-darwin23-3.4.8.rs +8933 -0
  23. data/ext/chrono_machines_native/target/debug/build/rb-sys-ce17442a66050c01/out/bindings-0.9.128-mri-arm64-darwin23-3.4.8.rs +8933 -0
  24. data/ext/chrono_machines_native/target/package/chrono-machines-0.3.2/Cargo.toml +76 -0
  25. data/ext/chrono_machines_native/target/package/chrono-machines-0.3.2/src/backoff.rs +497 -0
  26. data/ext/chrono_machines_native/target/package/chrono-machines-0.3.2/src/dsl.rs +108 -0
  27. data/ext/chrono_machines_native/target/package/chrono-machines-0.3.2/src/lib.rs +352 -0
  28. data/ext/chrono_machines_native/target/package/chrono-machines-0.3.2/src/policy.rs +189 -0
  29. data/ext/chrono_machines_native/target/package/chrono-machines-0.3.2/src/retry.rs +1489 -0
  30. data/ext/chrono_machines_native/target/package/chrono-machines-0.3.2/src/sleep.rs +97 -0
  31. data/ext/chrono_machines_native/target/package/chrono_machines_native-0.2.3/Cargo.toml +56 -0
  32. data/ext/chrono_machines_native/target/package/chrono_machines_native-0.2.3/src/lib.rs +192 -0
  33. data/lib/chrono_machines/async_support.rb +2 -1
  34. data/lib/chrono_machines/configuration.rb +19 -5
  35. data/lib/chrono_machines/executor.rb +69 -9
  36. data/lib/chrono_machines/native_speedup.rb +61 -0
  37. data/lib/chrono_machines/version.rb +1 -1
  38. data/lib/chrono_machines.rb +1 -1
  39. metadata +40 -7
@@ -0,0 +1,1511 @@
1
+ //! Retry mechanism with fluent builder API
2
+ //!
3
+ //! This module provides a fluent retry API for wrapping fallible operations
4
+ //! with automatic retries and configurable backoff strategies.
5
+
6
+ use crate::backoff::BackoffStrategy;
7
+ use crate::sleep::Sleeper;
8
+ use core::fmt;
9
+ #[cfg(feature = "std")]
10
+ use rand::rngs::StdRng;
11
+
12
+ #[cfg(all(not(feature = "std"), feature = "alloc"))]
13
+ use alloc::boxed::Box;
14
+
15
+ /// Type alias for retry builder with default predicate
16
+ type DefaultRetryBuilder<F, B, T, E> = RetryBuilder<F, B, T, E, fn(&E) -> bool>;
17
+
18
+ /// Type alias for boxed notify callback
19
+ type NotifyCallback<E> = Box<dyn FnMut(&RetryContext<E>)>;
20
+
21
+ /// Type alias for boxed failure callback
22
+ type FailureCallback<E> = Box<dyn FnMut(&RetryError<E>)>;
23
+
24
+ /// Build a terminal [`RetryError`], firing the `on_failure` callback if present.
25
+ fn finalize_failure<E>(
26
+ on_failure: Option<&mut FailureCallback<E>>,
27
+ kind: RetryErrorKind,
28
+ attempt: u8,
29
+ max_attempts: u8,
30
+ cumulative_delay_ms: u64,
31
+ error: E,
32
+ ) -> RetryError<E> {
33
+ let retry_error = RetryError::new(kind, attempt, max_attempts, cumulative_delay_ms, Some(error));
34
+ if let Some(callback) = on_failure {
35
+ callback(&retry_error);
36
+ }
37
+ retry_error
38
+ }
39
+
40
+ /// Reason why a retry operation failed.
41
+ #[derive(Debug, Clone, Copy, PartialEq, Eq)]
42
+ pub enum RetryErrorKind {
43
+ /// The operation exhausted all retry attempts.
44
+ Exhausted,
45
+ /// The error was rejected by the `when` predicate.
46
+ PredicateRejected,
47
+ }
48
+
49
+ /// Context provided to retry callbacks with observability data.
50
+ ///
51
+ /// This struct provides comprehensive information about the current retry attempt,
52
+ /// including timing, delays, and error context.
53
+ #[derive(Debug)]
54
+ pub struct RetryContext<'a, E> {
55
+ /// Current attempt number (1-indexed)
56
+ pub attempt: u8,
57
+ /// Delay in milliseconds before the next retry attempt (None on success or final failure)
58
+ pub next_delay_ms: Option<u64>,
59
+ /// Total milliseconds spent sleeping between attempts so far
60
+ pub cumulative_delay_ms: u64,
61
+ /// Reference to the error that triggered this retry (None on success)
62
+ pub error: Option<&'a E>,
63
+ }
64
+
65
+ /// Rich retry error that carries execution context.
66
+ #[derive(Debug, Clone)]
67
+ pub struct RetryError<E> {
68
+ kind: RetryErrorKind,
69
+ attempts: u8,
70
+ max_attempts: u8,
71
+ cumulative_delay_ms: u64,
72
+ cause: Option<E>,
73
+ }
74
+
75
+ impl<E> RetryError<E> {
76
+ fn new(
77
+ kind: RetryErrorKind,
78
+ attempts: u8,
79
+ max_attempts: u8,
80
+ cumulative_delay_ms: u64,
81
+ cause: Option<E>,
82
+ ) -> Self {
83
+ Self {
84
+ kind,
85
+ attempts,
86
+ max_attempts,
87
+ cumulative_delay_ms,
88
+ cause,
89
+ }
90
+ }
91
+
92
+ /// Retrieve the underlying cause when available.
93
+ pub fn cause(&self) -> Option<&E> {
94
+ self.cause.as_ref()
95
+ }
96
+
97
+ /// Consume the error and return the underlying cause when available.
98
+ pub fn into_cause(self) -> Option<E> {
99
+ self.cause
100
+ }
101
+
102
+ /// Attempt number that produced the terminal outcome (1-indexed).
103
+ pub fn attempts(&self) -> u8 {
104
+ self.attempts
105
+ }
106
+
107
+ /// Maximum attempts allowed by the policy.
108
+ pub fn max_attempts(&self) -> u8 {
109
+ self.max_attempts
110
+ }
111
+
112
+ /// Total time spent in delays before reaching terminal state.
113
+ pub fn cumulative_delay_ms(&self) -> u64 {
114
+ self.cumulative_delay_ms
115
+ }
116
+
117
+ /// Error category.
118
+ pub fn kind(&self) -> RetryErrorKind {
119
+ self.kind
120
+ }
121
+ }
122
+
123
+ impl<E> fmt::Display for RetryError<E>
124
+ where
125
+ E: fmt::Display,
126
+ {
127
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
128
+ match self.kind {
129
+ RetryErrorKind::Exhausted => {
130
+ write!(
131
+ f,
132
+ "retry exhausted after {} of {} attempts",
133
+ self.attempts, self.max_attempts
134
+ )?;
135
+ }
136
+ RetryErrorKind::PredicateRejected => {
137
+ write!(f, "retry aborted by predicate on attempt {}", self.attempts)?;
138
+ }
139
+ }
140
+
141
+ write!(f, " (cumulative delay {}ms)", self.cumulative_delay_ms)?;
142
+
143
+ if let Some(cause) = self.cause.as_ref() {
144
+ write!(f, ": {}", cause)?;
145
+ }
146
+
147
+ Ok(())
148
+ }
149
+ }
150
+
151
+ #[cfg(feature = "std")]
152
+ impl<E> std::error::Error for RetryError<E> where E: std::error::Error {}
153
+
154
+ /// Successful retry result holding metadata about the execution.
155
+ #[derive(Debug)]
156
+ pub struct RetryOutcome<T> {
157
+ value: T,
158
+ attempts: u8,
159
+ cumulative_delay_ms: u64,
160
+ }
161
+
162
+ impl<T> RetryOutcome<T> {
163
+ fn new(value: T, attempts: u8, cumulative_delay_ms: u64) -> Self {
164
+ Self {
165
+ value,
166
+ attempts,
167
+ cumulative_delay_ms,
168
+ }
169
+ }
170
+
171
+ /// Attempt that succeeded (1-indexed).
172
+ pub fn attempts(&self) -> u8 {
173
+ self.attempts
174
+ }
175
+
176
+ /// Total milliseconds spent sleeping between attempts.
177
+ pub fn cumulative_delay_ms(&self) -> u64 {
178
+ self.cumulative_delay_ms
179
+ }
180
+
181
+ /// Borrow the successful value.
182
+ pub fn value(&self) -> &T {
183
+ &self.value
184
+ }
185
+
186
+ /// Consume the outcome and return the successful value.
187
+ pub fn into_inner(self) -> T {
188
+ self.value
189
+ }
190
+ }
191
+
192
+ /// Extension trait that adds `.retry()` method to functions and closures
193
+ ///
194
+ /// This trait is automatically implemented for all `Fn` types that return `Result`.
195
+ ///
196
+ /// # Example
197
+ ///
198
+ /// ```rust
199
+ /// use chrono_machines::{Retryable, ExponentialBackoff};
200
+ ///
201
+ /// fn fetch_data() -> Result<String, std::io::Error> {
202
+ /// // ... operation that might fail
203
+ /// # Ok("data".to_string())
204
+ /// }
205
+ ///
206
+ /// # #[cfg(feature = "std")]
207
+ /// let outcome = fetch_data
208
+ /// .retry(ExponentialBackoff::default())
209
+ /// .call()
210
+ /// .expect("retry succeeded");
211
+ /// assert!(outcome.attempts() >= 1);
212
+ /// ```
213
+ pub trait Retryable<T, E> {
214
+ /// Begin building a retry operation with the given backoff strategy
215
+ ///
216
+ /// # Arguments
217
+ ///
218
+ /// * `backoff` - The backoff strategy to use for retry delays
219
+ ///
220
+ /// # Returns
221
+ ///
222
+ /// A `RetryBuilder` that can be further configured before execution
223
+ fn retry<B: BackoffStrategy>(self, backoff: B) -> DefaultRetryBuilder<Self, B, T, E>
224
+ where
225
+ Self: Sized;
226
+ }
227
+
228
+ impl<F, T, E> Retryable<T, E> for F
229
+ where
230
+ F: FnMut() -> Result<T, E>,
231
+ {
232
+ fn retry<B: BackoffStrategy>(self, backoff: B) -> RetryBuilder<Self, B, T, E, fn(&E) -> bool> {
233
+ RetryBuilder {
234
+ operation: self,
235
+ backoff,
236
+ when: None,
237
+ notify: None,
238
+ on_success: None,
239
+ on_failure: None,
240
+ _phantom_t: core::marker::PhantomData,
241
+ _phantom_e: core::marker::PhantomData,
242
+ }
243
+ }
244
+ }
245
+
246
+ /// Ergonomic extension methods for retry operations
247
+ ///
248
+ /// This trait adds convenience methods that create retry builders with common
249
+ /// backoff strategies using their default configurations.
250
+ ///
251
+ /// # Example
252
+ ///
253
+ /// ```rust
254
+ /// use chrono_machines::RetryableExt;
255
+ ///
256
+ /// fn fetch_data() -> Result<String, std::io::Error> {
257
+ /// // ... operation that might fail
258
+ /// # Ok("data".to_string())
259
+ /// }
260
+ ///
261
+ /// # #[cfg(feature = "std")]
262
+ /// // Before: fetch_data.retry(ExponentialBackoff::default()).call()?;
263
+ /// // After: fetch_data.with_exponential().call()?;
264
+ /// let outcome = fetch_data.with_exponential().call()?;
265
+ /// # Ok::<(), chrono_machines::RetryError<std::io::Error>>(())
266
+ /// ```
267
+ pub trait RetryableExt<T, E>: Retryable<T, E> {
268
+ /// Create a retry builder with exponential backoff using default configuration
269
+ ///
270
+ /// Default configuration:
271
+ /// - max_attempts: 3
272
+ /// - base_delay_ms: 100
273
+ /// - multiplier: 2.0
274
+ /// - max_delay_ms: 10_000
275
+ /// - jitter_factor: 1.0 (full jitter)
276
+ ///
277
+ /// # Returns
278
+ ///
279
+ /// A `RetryBuilder` configured with `ExponentialBackoff::default()`
280
+ ///
281
+ /// # Example
282
+ ///
283
+ /// ```rust
284
+ /// use chrono_machines::RetryableExt;
285
+ ///
286
+ /// fn fetch_api() -> Result<String, std::io::Error> {
287
+ /// Ok("response".to_string())
288
+ /// }
289
+ ///
290
+ /// # #[cfg(feature = "std")]
291
+ /// let outcome = fetch_api.with_exponential().call()?;
292
+ /// # Ok::<(), chrono_machines::RetryError<std::io::Error>>(())
293
+ /// ```
294
+ fn with_exponential(self) -> DefaultRetryBuilder<Self, crate::backoff::ExponentialBackoff, T, E>
295
+ where
296
+ Self: Sized,
297
+ {
298
+ self.retry(crate::backoff::ExponentialBackoff::default())
299
+ }
300
+
301
+ /// Create a retry builder with constant backoff
302
+ ///
303
+ /// # Arguments
304
+ ///
305
+ /// * `delay_ms` - Fixed delay in milliseconds between retry attempts
306
+ ///
307
+ /// Default configuration (besides delay):
308
+ /// - max_attempts: 3
309
+ /// - jitter_factor: 0.0 (no jitter)
310
+ ///
311
+ /// # Returns
312
+ ///
313
+ /// A `RetryBuilder` configured with `ConstantBackoff` using the specified delay
314
+ ///
315
+ /// # Example
316
+ ///
317
+ /// ```rust
318
+ /// use chrono_machines::RetryableExt;
319
+ ///
320
+ /// fn check_status() -> Result<bool, std::io::Error> {
321
+ /// Ok(true)
322
+ /// }
323
+ ///
324
+ /// # #[cfg(feature = "std")]
325
+ /// // Retry with fixed 500ms delay between attempts
326
+ /// let outcome = check_status.with_constant(500).call()?;
327
+ /// # Ok::<(), chrono_machines::RetryError<std::io::Error>>(())
328
+ /// ```
329
+ fn with_constant(self, delay_ms: u64) -> DefaultRetryBuilder<Self, crate::backoff::ConstantBackoff, T, E>
330
+ where
331
+ Self: Sized,
332
+ {
333
+ self.retry(crate::backoff::ConstantBackoff::new().delay_ms(delay_ms))
334
+ }
335
+
336
+ /// Create a retry builder with Fibonacci backoff using default configuration
337
+ ///
338
+ /// Default configuration:
339
+ /// - max_attempts: 8
340
+ /// - base_delay_ms: 100
341
+ /// - max_delay_ms: 10_000
342
+ /// - jitter_factor: 1.0 (full jitter)
343
+ ///
344
+ /// Delays follow the Fibonacci sequence: 100ms, 100ms, 200ms, 300ms, 500ms...
345
+ ///
346
+ /// # Returns
347
+ ///
348
+ /// A `RetryBuilder` configured with `FibonacciBackoff::default()`
349
+ ///
350
+ /// # Example
351
+ ///
352
+ /// ```rust
353
+ /// use chrono_machines::RetryableExt;
354
+ ///
355
+ /// fn connect_database() -> Result<(), std::io::Error> {
356
+ /// Ok(())
357
+ /// }
358
+ ///
359
+ /// # #[cfg(feature = "std")]
360
+ /// let outcome = connect_database.with_fibonacci().call()?;
361
+ /// # Ok::<(), chrono_machines::RetryError<std::io::Error>>(())
362
+ /// ```
363
+ fn with_fibonacci(self) -> DefaultRetryBuilder<Self, crate::backoff::FibonacciBackoff, T, E>
364
+ where
365
+ Self: Sized,
366
+ {
367
+ self.retry(crate::backoff::FibonacciBackoff::default())
368
+ }
369
+ }
370
+
371
+ // Blanket implementation for all Retryable types
372
+ impl<F, T, E> RetryableExt<T, E> for F where F: Retryable<T, E> {}
373
+
374
+ /// Builder for configuring and executing retry operations
375
+ ///
376
+ /// Created by calling `.retry()` on a function or closure.
377
+ /// Provides a fluent API for configuring retry behavior.
378
+ ///
379
+ /// # Type Parameters
380
+ ///
381
+ /// * `F` - The operation function type
382
+ /// * `B` - The backoff strategy type
383
+ /// * `T` - The success return type
384
+ /// * `E` - The error type
385
+ /// * `W` - The when predicate type
386
+ pub struct RetryBuilder<F, B, T, E, W> {
387
+ operation: F,
388
+ backoff: B,
389
+ when: Option<W>,
390
+ notify: Option<NotifyCallback<E>>,
391
+ on_success: Option<NotifyCallback<E>>,
392
+ on_failure: Option<FailureCallback<E>>,
393
+ _phantom_t: core::marker::PhantomData<T>,
394
+ _phantom_e: core::marker::PhantomData<E>,
395
+ }
396
+
397
+ impl<F, B, T, E, W> RetryBuilder<F, B, T, E, W>
398
+ where
399
+ F: FnMut() -> Result<T, E>,
400
+ B: BackoffStrategy,
401
+ W: Fn(&E) -> bool,
402
+ {
403
+ /// Add a conditional predicate that determines if an error should trigger retry
404
+ ///
405
+ /// Only errors where `predicate(&error)` returns `true` will be retried.
406
+ /// Errors that don't match the predicate are returned immediately without retry.
407
+ ///
408
+ /// # Example
409
+ ///
410
+ /// ```rust
411
+ /// use chrono_machines::{Retryable, ExponentialBackoff};
412
+ ///
413
+ /// #[derive(Debug)]
414
+ /// enum MyError {
415
+ /// Retryable,
416
+ /// Fatal,
417
+ /// }
418
+ ///
419
+ /// fn risky_operation() -> Result<String, MyError> {
420
+ /// // ...
421
+ /// # Err(MyError::Retryable)
422
+ /// }
423
+ ///
424
+ /// # #[cfg(feature = "std")]
425
+ /// let result = risky_operation
426
+ /// .retry(ExponentialBackoff::default())
427
+ /// .when(|e| matches!(e, MyError::Retryable))
428
+ /// .call();
429
+ /// ```
430
+ pub fn when<P>(self, predicate: P) -> RetryBuilder<F, B, T, E, P>
431
+ where
432
+ P: Fn(&E) -> bool,
433
+ {
434
+ RetryBuilder {
435
+ operation: self.operation,
436
+ backoff: self.backoff,
437
+ when: Some(predicate),
438
+ notify: self.notify,
439
+ on_success: self.on_success,
440
+ on_failure: self.on_failure,
441
+ _phantom_t: core::marker::PhantomData,
442
+ _phantom_e: core::marker::PhantomData,
443
+ }
444
+ }
445
+
446
+ /// Add a notification callback that's invoked before each retry
447
+ ///
448
+ /// The callback receives a [`RetryContext`] with comprehensive retry state information.
449
+ ///
450
+ /// # Example
451
+ ///
452
+ /// ```rust
453
+ /// use chrono_machines::{Retryable, ExponentialBackoff};
454
+ ///
455
+ /// fn fetch_data() -> Result<String, std::io::Error> {
456
+ /// // ...
457
+ /// # Ok("data".to_string())
458
+ /// }
459
+ ///
460
+ /// # #[cfg(feature = "std")]
461
+ /// let result = fetch_data
462
+ /// .retry(ExponentialBackoff::default())
463
+ /// .notify(|ctx| {
464
+ /// if let Some(err) = ctx.error {
465
+ /// println!(
466
+ /// "Attempt {} failed, retrying after {}ms (cumulative: {}ms): {:?}",
467
+ /// ctx.attempt,
468
+ /// ctx.next_delay_ms.unwrap_or(0),
469
+ /// ctx.cumulative_delay_ms,
470
+ /// err
471
+ /// );
472
+ /// }
473
+ /// })
474
+ /// .call();
475
+ /// ```
476
+ pub fn notify<C>(mut self, callback: C) -> Self
477
+ where
478
+ C: FnMut(&RetryContext<E>) + 'static,
479
+ {
480
+ self.notify = Some(Box::new(callback));
481
+ self
482
+ }
483
+
484
+ /// Execute a callback after a successful attempt.
485
+ ///
486
+ /// The callback receives a [`RetryContext`] with no error (error field is None).
487
+ pub fn on_success<C>(mut self, callback: C) -> Self
488
+ where
489
+ C: FnMut(&RetryContext<E>) + 'static,
490
+ {
491
+ self.on_success = Some(Box::new(callback));
492
+ self
493
+ }
494
+
495
+ /// Execute a callback when the retry process terminates with failure.
496
+ ///
497
+ /// The callback receives the rich [`RetryError`] describing the failure.
498
+ pub fn on_failure<C>(mut self, callback: C) -> Self
499
+ where
500
+ C: FnMut(&RetryError<E>) + 'static,
501
+ {
502
+ self.on_failure = Some(Box::new(callback));
503
+ self
504
+ }
505
+
506
+ /// Execute the retry operation with blocking sleep (requires `std` feature)
507
+ ///
508
+ /// Runs the operation synchronously, retrying with blocking sleep between attempts.
509
+ ///
510
+ /// # Returns
511
+ ///
512
+ /// The final result after all retry attempts (success or final error)
513
+ ///
514
+ /// # Example
515
+ ///
516
+ /// ```rust
517
+ /// use chrono_machines::{Retryable, ExponentialBackoff};
518
+ ///
519
+ /// fn fetch_data() -> Result<String, std::io::Error> {
520
+ /// // ...
521
+ /// # Ok("data".to_string())
522
+ /// }
523
+ ///
524
+ /// # #[cfg(feature = "std")]
525
+ /// let outcome = fetch_data
526
+ /// .retry(ExponentialBackoff::default())
527
+ /// .call()?;
528
+ /// println!("Succeeded after {} attempts", outcome.attempts());
529
+ /// # Ok::<(), chrono_machines::RetryError<std::io::Error>>(())
530
+ /// ```
531
+ #[cfg(feature = "std")]
532
+ pub fn call(self) -> Result<RetryOutcome<T>, RetryError<E>> {
533
+ use crate::sleep::StdSleeper;
534
+ self.call_with_sleeper(StdSleeper)
535
+ }
536
+
537
+ /// Execute the retry operation with a custom sleeper
538
+ ///
539
+ /// This low-level method allows providing a custom sleep implementation,
540
+ /// enabling support for async runtimes, embedded systems, or testing.
541
+ ///
542
+ /// # Arguments
543
+ ///
544
+ /// * `sleeper` - Implementation of the `Sleeper` trait
545
+ ///
546
+ /// # Returns
547
+ ///
548
+ /// The final result after all retry attempts
549
+ ///
550
+ /// # Example
551
+ ///
552
+ /// ```rust
553
+ /// use chrono_machines::{Retryable, ExponentialBackoff, sleep::{FnSleeper, Sleeper}};
554
+ ///
555
+ /// fn fetch_data() -> Result<String, std::io::Error> {
556
+ /// Ok("data".to_string())
557
+ /// }
558
+ ///
559
+ /// fn custom_sleep(ms: u64) {
560
+ /// // Custom sleep implementation
561
+ /// # std::thread::sleep(std::time::Duration::from_millis(ms));
562
+ /// }
563
+ ///
564
+ /// let outcome = fetch_data
565
+ /// .retry(ExponentialBackoff::default())
566
+ /// .call_with_sleeper(FnSleeper(custom_sleep))?;
567
+ /// println!("Total delay {}ms", outcome.cumulative_delay_ms());
568
+ /// # Ok::<(), chrono_machines::RetryError<std::io::Error>>(())
569
+ /// ```
570
+ #[cfg(feature = "std")]
571
+ pub fn call_with_sleeper<S: Sleeper>(
572
+ self,
573
+ sleeper: S,
574
+ ) -> Result<RetryOutcome<T>, RetryError<E>> {
575
+ let rng: StdRng = rand::make_rng();
576
+ self.call_with_sleeper_and_rng(sleeper, rng)
577
+ }
578
+
579
+ /// Execute the retry operation with a custom sleeper and a caller-supplied RNG.
580
+ ///
581
+ /// The `no_std` entry point: callers provide their own [`rand::Rng`] instead
582
+ /// of relying on the `std`-only `make_rng`.
583
+ ///
584
+ /// # Arguments
585
+ ///
586
+ /// * `sleeper` - Implementation of the [`Sleeper`] trait
587
+ /// * `rng` - Random number generator used for jitter
588
+ pub fn call_with_sleeper_and_rng<S: Sleeper, R: rand::Rng>(
589
+ mut self,
590
+ sleeper: S,
591
+ mut rng: R,
592
+ ) -> Result<RetryOutcome<T>, RetryError<E>> {
593
+ let mut attempt = 1u8;
594
+ let max_attempts = self.backoff.max_attempts();
595
+ let mut cumulative_delay_ms: u64 = 0;
596
+
597
+ loop {
598
+ match (self.operation)() {
599
+ Ok(_value) => {
600
+ // Invoke on_success callback with context
601
+ if let Some(ref mut callback) = self.on_success {
602
+ let ctx = RetryContext {
603
+ attempt,
604
+ next_delay_ms: None,
605
+ cumulative_delay_ms,
606
+ error: None,
607
+ };
608
+ callback(&ctx);
609
+ }
610
+ return Ok(RetryOutcome::new(_value, attempt, cumulative_delay_ms));
611
+ }
612
+ Err(error) => {
613
+ // Check if this error should be retried
614
+ if let Some(ref predicate) = self.when
615
+ && !predicate(&error) {
616
+ // Error doesn't match predicate, fail immediately
617
+ return Err(finalize_failure(
618
+ self.on_failure.as_mut(),
619
+ RetryErrorKind::PredicateRejected,
620
+ attempt,
621
+ max_attempts,
622
+ cumulative_delay_ms,
623
+ error,
624
+ ));
625
+ }
626
+
627
+ // Check if we have retries remaining
628
+ if !self.backoff.should_retry(attempt) {
629
+ return Err(finalize_failure(
630
+ self.on_failure.as_mut(),
631
+ RetryErrorKind::Exhausted,
632
+ attempt,
633
+ max_attempts,
634
+ cumulative_delay_ms,
635
+ error,
636
+ ));
637
+ }
638
+
639
+ // Calculate delay
640
+ match self.backoff.delay(attempt, &mut rng) {
641
+ Some(delay_ms) => {
642
+ // Notify if callback is set
643
+ if let Some(ref mut notify) = self.notify {
644
+ let ctx = RetryContext {
645
+ attempt,
646
+ next_delay_ms: Some(delay_ms),
647
+ cumulative_delay_ms,
648
+ error: Some(&error),
649
+ };
650
+ notify(&ctx);
651
+ }
652
+
653
+ // Sleep before retry
654
+ sleeper.sleep_ms(delay_ms);
655
+ cumulative_delay_ms = cumulative_delay_ms.saturating_add(delay_ms);
656
+ attempt = attempt.saturating_add(1);
657
+ }
658
+ None => {
659
+ // Backoff says no more retries
660
+ return Err(finalize_failure(
661
+ self.on_failure.as_mut(),
662
+ RetryErrorKind::Exhausted,
663
+ attempt,
664
+ max_attempts,
665
+ cumulative_delay_ms,
666
+ error,
667
+ ));
668
+ }
669
+ }
670
+ }
671
+ }
672
+ }
673
+ }
674
+ }
675
+
676
+ #[cfg(test)]
677
+ mod tests {
678
+ use super::*;
679
+ use crate::backoff::{ConstantBackoff, ExponentialBackoff};
680
+ use crate::sleep::FnSleeper;
681
+
682
+ #[derive(Debug, PartialEq)]
683
+ enum TestError {
684
+ Retryable,
685
+ Fatal,
686
+ }
687
+
688
+ #[test]
689
+ fn test_retry_success_on_first_attempt() {
690
+ fn always_succeeds() -> Result<i32, TestError> {
691
+ Ok(42)
692
+ }
693
+
694
+ let result = always_succeeds
695
+ .retry(ExponentialBackoff::default())
696
+ .call_with_sleeper(FnSleeper(|_| {}));
697
+
698
+ let outcome = result.expect("retry should succeed");
699
+ assert_eq!(outcome.attempts(), 1);
700
+ assert_eq!(outcome.into_inner(), 42);
701
+ }
702
+
703
+ #[test]
704
+ fn test_retry_success_after_failures() {
705
+ use core::cell::Cell;
706
+
707
+ let attempts = Cell::new(0);
708
+
709
+ let operation = || {
710
+ let current = attempts.get();
711
+ attempts.set(current + 1);
712
+
713
+ if current < 2 {
714
+ Err(TestError::Retryable)
715
+ } else {
716
+ Ok(42)
717
+ }
718
+ };
719
+
720
+ let result = operation
721
+ .retry(ExponentialBackoff::default().max_attempts(3))
722
+ .call_with_sleeper(FnSleeper(|_| {}));
723
+
724
+ let outcome = result.expect("retry should succeed");
725
+ assert_eq!(outcome.attempts(), 3);
726
+ assert_eq!(outcome.into_inner(), 42);
727
+ assert_eq!(attempts.get(), 3);
728
+ }
729
+
730
+ #[test]
731
+ fn test_retry_exhausted() {
732
+ fn always_fails() -> Result<i32, TestError> {
733
+ Err(TestError::Retryable)
734
+ }
735
+
736
+ let result = always_fails
737
+ .retry(ExponentialBackoff::default().max_attempts(3))
738
+ .call_with_sleeper(FnSleeper(|_| {}));
739
+
740
+ let err = result.expect_err("retry should exhaust");
741
+ assert_eq!(err.kind(), RetryErrorKind::Exhausted);
742
+ assert_eq!(err.attempts(), 3);
743
+ assert_eq!(err.max_attempts(), 3);
744
+ assert!(err.cumulative_delay_ms() > 0);
745
+ if let Some(cause) = err.cause() {
746
+ assert_eq!(cause, &TestError::Retryable);
747
+ } else {
748
+ panic!("expected underlying cause");
749
+ }
750
+ }
751
+
752
+ #[test]
753
+ fn test_retry_when_predicate() {
754
+ fn fails_with_fatal() -> Result<i32, TestError> {
755
+ Err(TestError::Fatal)
756
+ }
757
+
758
+ let result = fails_with_fatal
759
+ .retry(ExponentialBackoff::default())
760
+ .when(|e| matches!(e, TestError::Retryable))
761
+ .call_with_sleeper(FnSleeper(|_| {}));
762
+
763
+ // Fatal error should not be retried
764
+ let err = result.expect_err("retry should stop due to predicate");
765
+ assert_eq!(err.kind(), RetryErrorKind::PredicateRejected);
766
+ if let Some(cause) = err.cause() {
767
+ assert_eq!(cause, &TestError::Fatal);
768
+ } else {
769
+ panic!("expected underlying cause");
770
+ }
771
+ }
772
+
773
+ #[test]
774
+ fn test_retry_notify_callback() {
775
+ use core::cell::{Cell, RefCell};
776
+ #[cfg(feature = "std")]
777
+ use std::rc::Rc;
778
+
779
+ #[cfg(not(feature = "std"))]
780
+ use alloc::rc::Rc;
781
+
782
+ let attempts = Cell::new(0);
783
+ let notify_calls = Rc::new(RefCell::new(Vec::new()));
784
+ let notify_calls_clone = Rc::clone(&notify_calls);
785
+
786
+ let operation = || {
787
+ let current = attempts.get();
788
+ attempts.set(current + 1);
789
+
790
+ if current < 2 {
791
+ Err(TestError::Retryable)
792
+ } else {
793
+ Ok(42)
794
+ }
795
+ };
796
+
797
+ let result = operation
798
+ .retry(ExponentialBackoff::default().max_attempts(3))
799
+ .notify(move |ctx| {
800
+ // Track notify calls
801
+ notify_calls_clone.borrow_mut().push((
802
+ ctx.attempt,
803
+ ctx.next_delay_ms,
804
+ ctx.cumulative_delay_ms,
805
+ ctx.error.is_some(),
806
+ ));
807
+ assert!(ctx.attempt >= 1);
808
+ assert!(ctx.next_delay_ms.is_some());
809
+ assert!(ctx.error.is_some());
810
+ })
811
+ .call_with_sleeper(FnSleeper(|_| {}));
812
+
813
+ let outcome = result.expect("retry should succeed");
814
+ assert_eq!(outcome.attempts(), 3);
815
+
816
+ // Verify notify was called twice (for the two failures)
817
+ let calls = notify_calls.borrow();
818
+ assert_eq!(calls.len(), 2);
819
+ assert_eq!(calls[0].0, 1); // First attempt
820
+ assert_eq!(calls[1].0, 2); // Second attempt
821
+ }
822
+
823
+ #[test]
824
+ fn test_on_success_callback_invoked() {
825
+ use core::cell::Cell;
826
+ use core::sync::atomic::{AtomicUsize, Ordering};
827
+
828
+ static SUCCESS_ATTEMPT: AtomicUsize = AtomicUsize::new(0);
829
+ static SUCCESS_CUMULATIVE_DELAY: AtomicUsize = AtomicUsize::new(0);
830
+
831
+ let attempts = Cell::new(0);
832
+
833
+ let operation = || {
834
+ let current = attempts.get();
835
+ attempts.set(current + 1);
836
+
837
+ if current < 1 {
838
+ Err(TestError::Retryable)
839
+ } else {
840
+ Ok(7)
841
+ }
842
+ };
843
+
844
+ SUCCESS_ATTEMPT.store(0, Ordering::SeqCst);
845
+ SUCCESS_CUMULATIVE_DELAY.store(0, Ordering::SeqCst);
846
+
847
+ let outcome = operation
848
+ .retry(ExponentialBackoff::default().max_attempts(3))
849
+ .on_success(|ctx| {
850
+ SUCCESS_ATTEMPT.store(ctx.attempt as usize, Ordering::SeqCst);
851
+ SUCCESS_CUMULATIVE_DELAY.store(ctx.cumulative_delay_ms as usize, Ordering::SeqCst);
852
+ assert!(ctx.error.is_none());
853
+ assert!(ctx.next_delay_ms.is_none());
854
+ })
855
+ .call_with_sleeper(FnSleeper(|_| {}))
856
+ .expect("retry should succeed");
857
+
858
+ assert_eq!(outcome.into_inner(), 7);
859
+ assert_eq!(SUCCESS_ATTEMPT.load(Ordering::SeqCst), 2);
860
+ // Should have some cumulative delay from the first retry
861
+ assert!(SUCCESS_CUMULATIVE_DELAY.load(Ordering::SeqCst) > 0);
862
+ }
863
+
864
+ #[test]
865
+ fn test_on_failure_callback_invoked() {
866
+ use core::sync::atomic::{AtomicUsize, Ordering};
867
+
868
+ static FAILURE_KIND: AtomicUsize = AtomicUsize::new(0);
869
+ static FAILURE_CUMULATIVE_DELAY: AtomicUsize = AtomicUsize::new(0);
870
+
871
+ fn always_fails() -> Result<(), TestError> {
872
+ Err(TestError::Retryable)
873
+ }
874
+
875
+ FAILURE_KIND.store(0, Ordering::SeqCst);
876
+ FAILURE_CUMULATIVE_DELAY.store(0, Ordering::SeqCst);
877
+
878
+ let result = always_fails
879
+ .retry(ExponentialBackoff::default().max_attempts(2))
880
+ .on_failure(|err| {
881
+ let marker = match err.kind() {
882
+ RetryErrorKind::Exhausted => 1,
883
+ RetryErrorKind::PredicateRejected => 2,
884
+ };
885
+ FAILURE_KIND.store(marker, Ordering::SeqCst);
886
+ FAILURE_CUMULATIVE_DELAY.store(err.cumulative_delay_ms() as usize, Ordering::SeqCst);
887
+ })
888
+ .call_with_sleeper(FnSleeper(|_| {}));
889
+
890
+ assert!(result.is_err());
891
+ assert_eq!(FAILURE_KIND.load(Ordering::SeqCst), 1);
892
+ // Should have cumulative delay from retry attempt
893
+ assert!(FAILURE_CUMULATIVE_DELAY.load(Ordering::SeqCst) > 0);
894
+ }
895
+
896
+ #[test]
897
+ fn test_constant_backoff_retry() {
898
+ use core::cell::Cell;
899
+
900
+ let attempts = Cell::new(0);
901
+
902
+ let operation = || {
903
+ let current = attempts.get();
904
+ attempts.set(current + 1);
905
+
906
+ if current < 1 {
907
+ Err(TestError::Retryable)
908
+ } else {
909
+ Ok(42)
910
+ }
911
+ };
912
+
913
+ let result = operation
914
+ .retry(ConstantBackoff::new().delay_ms(10).max_attempts(2))
915
+ .call_with_sleeper(FnSleeper(|_| {}));
916
+
917
+ let outcome = result.expect("retry should succeed");
918
+ assert_eq!(outcome.attempts(), 2);
919
+ assert_eq!(outcome.into_inner(), 42);
920
+ assert_eq!(attempts.get(), 2);
921
+ }
922
+
923
+ #[cfg(feature = "std")]
924
+ #[test]
925
+ fn test_retry_with_std_sleeper() {
926
+ use core::cell::Cell;
927
+
928
+ let attempts = Cell::new(0);
929
+
930
+ let operation = || {
931
+ let current = attempts.get();
932
+ attempts.set(current + 1);
933
+
934
+ if current < 1 {
935
+ Err(TestError::Retryable)
936
+ } else {
937
+ Ok(42)
938
+ }
939
+ };
940
+
941
+ let start = std::time::Instant::now();
942
+ let result = operation
943
+ .retry(
944
+ ConstantBackoff::new()
945
+ .delay_ms(10)
946
+ .max_attempts(2)
947
+ .jitter_factor(0.0),
948
+ )
949
+ .call();
950
+
951
+ let elapsed = start.elapsed();
952
+
953
+ let outcome = result.expect("retry should succeed");
954
+ assert_eq!(outcome.attempts(), 2);
955
+ assert_eq!(outcome.into_inner(), 42);
956
+ assert!(elapsed.as_millis() >= 9); // At least one 10ms sleep
957
+ }
958
+
959
+ #[test]
960
+ fn test_retry_context_comprehensive() {
961
+ use core::cell::{Cell, RefCell};
962
+ #[cfg(feature = "std")]
963
+ use std::rc::Rc;
964
+
965
+ #[cfg(not(feature = "std"))]
966
+ use alloc::rc::Rc;
967
+
968
+ let attempts = Cell::new(0);
969
+ let notify_contexts = Rc::new(RefCell::new(Vec::new()));
970
+ let notify_contexts_clone = Rc::clone(&notify_contexts);
971
+
972
+ let operation = || {
973
+ let current = attempts.get();
974
+ attempts.set(current + 1);
975
+
976
+ if current < 3 {
977
+ Err(TestError::Retryable)
978
+ } else {
979
+ Ok(42)
980
+ }
981
+ };
982
+
983
+ let result = operation
984
+ .retry(ConstantBackoff::new().delay_ms(100).max_attempts(5).jitter_factor(0.0))
985
+ .notify(move |ctx| {
986
+ // Capture context for verification
987
+ notify_contexts_clone.borrow_mut().push((
988
+ ctx.attempt,
989
+ ctx.next_delay_ms,
990
+ ctx.cumulative_delay_ms,
991
+ ));
992
+
993
+ // Verify error is present during notify
994
+ assert!(ctx.error.is_some());
995
+ if let Some(err) = ctx.error {
996
+ assert_eq!(err, &TestError::Retryable);
997
+ }
998
+ })
999
+ .call_with_sleeper(FnSleeper(|_| {}));
1000
+
1001
+ let outcome = result.expect("retry should succeed");
1002
+ assert_eq!(outcome.attempts(), 4);
1003
+
1004
+ // Verify notify contexts
1005
+ let contexts = notify_contexts.borrow();
1006
+ assert_eq!(contexts.len(), 3); // Three failures before success
1007
+
1008
+ // First failure
1009
+ assert_eq!(contexts[0].0, 1);
1010
+ assert_eq!(contexts[0].1, Some(100));
1011
+ assert_eq!(contexts[0].2, 0); // No cumulative delay yet
1012
+
1013
+ // Second failure
1014
+ assert_eq!(contexts[1].0, 2);
1015
+ assert_eq!(contexts[1].1, Some(100));
1016
+ assert_eq!(contexts[1].2, 100); // One delay accumulated
1017
+
1018
+ // Third failure
1019
+ assert_eq!(contexts[2].0, 3);
1020
+ assert_eq!(contexts[2].1, Some(100));
1021
+ assert_eq!(contexts[2].2, 200); // Two delays accumulated
1022
+ }
1023
+
1024
+ #[test]
1025
+ fn test_retry_context_on_success() {
1026
+ use core::cell::{Cell, RefCell};
1027
+ #[cfg(feature = "std")]
1028
+ use std::rc::Rc;
1029
+
1030
+ #[cfg(not(feature = "std"))]
1031
+ use alloc::rc::Rc;
1032
+
1033
+ let attempts = Cell::new(0);
1034
+ let success_context = Rc::new(RefCell::new(None));
1035
+ let success_context_clone = Rc::clone(&success_context);
1036
+
1037
+ let operation = || {
1038
+ let current = attempts.get();
1039
+ attempts.set(current + 1);
1040
+
1041
+ if current < 2 {
1042
+ Err(TestError::Retryable)
1043
+ } else {
1044
+ Ok(100)
1045
+ }
1046
+ };
1047
+
1048
+ let result = operation
1049
+ .retry(ConstantBackoff::new().delay_ms(50).max_attempts(5).jitter_factor(0.0))
1050
+ .on_success(move |ctx| {
1051
+ success_context_clone.borrow_mut().replace((
1052
+ ctx.attempt,
1053
+ ctx.next_delay_ms,
1054
+ ctx.cumulative_delay_ms,
1055
+ ctx.error.is_none(),
1056
+ ));
1057
+ })
1058
+ .call_with_sleeper(FnSleeper(|_| {}));
1059
+
1060
+ let outcome = result.expect("retry should succeed");
1061
+ assert_eq!(outcome.attempts(), 3);
1062
+ assert_eq!(outcome.cumulative_delay_ms(), 100); // 2 delays of 50ms
1063
+
1064
+ // Verify success context
1065
+ let ctx = success_context.borrow();
1066
+ assert!(ctx.is_some());
1067
+ let (attempt, next_delay, cumulative, no_error) = ctx.unwrap();
1068
+ assert_eq!(attempt, 3);
1069
+ assert_eq!(next_delay, None); // No next delay on success
1070
+ assert_eq!(cumulative, 100);
1071
+ assert!(no_error); // Error should be None
1072
+ }
1073
+
1074
+ #[test]
1075
+ fn test_retry_context_cumulative_accuracy() {
1076
+ use core::cell::{Cell, RefCell};
1077
+ #[cfg(feature = "std")]
1078
+ use std::rc::Rc;
1079
+
1080
+ #[cfg(not(feature = "std"))]
1081
+ use alloc::rc::Rc;
1082
+
1083
+ let attempts = Cell::new(0);
1084
+ let cumulative_progression = Rc::new(RefCell::new(Vec::new()));
1085
+ let cumulative_progression_clone = Rc::clone(&cumulative_progression);
1086
+
1087
+ let operation = || {
1088
+ let current = attempts.get();
1089
+ attempts.set(current + 1);
1090
+ Err::<(), TestError>(TestError::Retryable)
1091
+ };
1092
+
1093
+ let _result = operation
1094
+ .retry(ConstantBackoff::new().delay_ms(25).max_attempts(4).jitter_factor(0.0))
1095
+ .notify(move |ctx| {
1096
+ cumulative_progression_clone.borrow_mut().push(ctx.cumulative_delay_ms);
1097
+ })
1098
+ .call_with_sleeper(FnSleeper(|_| {}));
1099
+
1100
+ // Verify cumulative delay progression
1101
+ let progression = cumulative_progression.borrow();
1102
+ assert_eq!(progression.len(), 3); // 3 retries before exhaustion
1103
+ assert_eq!(progression[0], 0); // Before first sleep
1104
+ assert_eq!(progression[1], 25); // After first sleep
1105
+ assert_eq!(progression[2], 50); // After second sleep
1106
+ }
1107
+
1108
+ // ============================================================================
1109
+ // RetryableExt Tests
1110
+ // ============================================================================
1111
+
1112
+ #[test]
1113
+ fn test_with_exponential_success() {
1114
+ use super::RetryableExt;
1115
+
1116
+ fn always_succeeds() -> Result<i32, TestError> {
1117
+ Ok(42)
1118
+ }
1119
+
1120
+ let result = always_succeeds
1121
+ .with_exponential()
1122
+ .call_with_sleeper(FnSleeper(|_| {}));
1123
+
1124
+ let outcome = result.expect("retry should succeed");
1125
+ assert_eq!(outcome.attempts(), 1);
1126
+ assert_eq!(outcome.into_inner(), 42);
1127
+ }
1128
+
1129
+ #[test]
1130
+ fn test_with_exponential_retry_behavior() {
1131
+ use super::RetryableExt;
1132
+ use core::cell::Cell;
1133
+
1134
+ let attempts = Cell::new(0);
1135
+
1136
+ let operation = || {
1137
+ let current = attempts.get();
1138
+ attempts.set(current + 1);
1139
+
1140
+ if current < 2 {
1141
+ Err(TestError::Retryable)
1142
+ } else {
1143
+ Ok(100)
1144
+ }
1145
+ };
1146
+
1147
+ let result = operation
1148
+ .with_exponential()
1149
+ .call_with_sleeper(FnSleeper(|_| {}));
1150
+
1151
+ let outcome = result.expect("retry should succeed");
1152
+ assert_eq!(outcome.attempts(), 3);
1153
+ assert_eq!(outcome.into_inner(), 100);
1154
+ // Verify default max_attempts was used (3)
1155
+ assert_eq!(attempts.get(), 3);
1156
+ }
1157
+
1158
+ #[test]
1159
+ fn test_with_exponential_exhausted() {
1160
+ use super::RetryableExt;
1161
+
1162
+ fn always_fails() -> Result<i32, TestError> {
1163
+ Err(TestError::Retryable)
1164
+ }
1165
+
1166
+ let result = always_fails
1167
+ .with_exponential()
1168
+ .call_with_sleeper(FnSleeper(|_| {}));
1169
+
1170
+ let err = result.expect_err("retry should exhaust");
1171
+ assert_eq!(err.kind(), RetryErrorKind::Exhausted);
1172
+ assert_eq!(err.attempts(), 3); // Default max_attempts
1173
+ assert_eq!(err.max_attempts(), 3);
1174
+ }
1175
+
1176
+ #[test]
1177
+ fn test_with_exponential_chaining() {
1178
+ use super::RetryableExt;
1179
+ use core::cell::Cell;
1180
+
1181
+ let attempts = Cell::new(0);
1182
+
1183
+ let operation = || {
1184
+ let current = attempts.get();
1185
+ attempts.set(current + 1);
1186
+
1187
+ if current < 1 {
1188
+ Err(TestError::Retryable)
1189
+ } else {
1190
+ Ok(7)
1191
+ }
1192
+ };
1193
+
1194
+ // Test that .with_exponential() can be chained with other builder methods
1195
+ let result = operation
1196
+ .with_exponential()
1197
+ .when(|e| matches!(e, TestError::Retryable))
1198
+ .call_with_sleeper(FnSleeper(|_| {}));
1199
+
1200
+ let outcome = result.expect("retry should succeed");
1201
+ assert_eq!(outcome.attempts(), 2);
1202
+ assert_eq!(outcome.into_inner(), 7);
1203
+ }
1204
+
1205
+ #[test]
1206
+ fn test_with_constant_success() {
1207
+ use super::RetryableExt;
1208
+
1209
+ fn always_succeeds() -> Result<i32, TestError> {
1210
+ Ok(99)
1211
+ }
1212
+
1213
+ let result = always_succeeds
1214
+ .with_constant(250)
1215
+ .call_with_sleeper(FnSleeper(|_| {}));
1216
+
1217
+ let outcome = result.expect("retry should succeed");
1218
+ assert_eq!(outcome.attempts(), 1);
1219
+ assert_eq!(outcome.into_inner(), 99);
1220
+ }
1221
+
1222
+ #[test]
1223
+ fn test_with_constant_uses_correct_delay() {
1224
+ use super::RetryableExt;
1225
+ use core::cell::{Cell, RefCell};
1226
+ #[cfg(feature = "std")]
1227
+ use std::rc::Rc;
1228
+
1229
+ #[cfg(not(feature = "std"))]
1230
+ use alloc::rc::Rc;
1231
+
1232
+ let attempts = Cell::new(0);
1233
+ let delays = Rc::new(RefCell::new(Vec::new()));
1234
+ let delays_clone = Rc::clone(&delays);
1235
+
1236
+ let operation = || {
1237
+ let current = attempts.get();
1238
+ attempts.set(current + 1);
1239
+
1240
+ if current < 2 {
1241
+ Err(TestError::Retryable)
1242
+ } else {
1243
+ Ok(42)
1244
+ }
1245
+ };
1246
+
1247
+ let result = operation
1248
+ .with_constant(500)
1249
+ .notify(move |ctx| {
1250
+ if let Some(delay) = ctx.next_delay_ms {
1251
+ delays_clone.borrow_mut().push(delay);
1252
+ }
1253
+ })
1254
+ .call_with_sleeper(FnSleeper(|_| {}));
1255
+
1256
+ let outcome = result.expect("retry should succeed");
1257
+ assert_eq!(outcome.attempts(), 3);
1258
+
1259
+ // Verify constant delay was used (ConstantBackoff default has no jitter)
1260
+ let recorded_delays = delays.borrow();
1261
+ assert_eq!(recorded_delays.len(), 2); // Two retries
1262
+ assert_eq!(recorded_delays[0], 500);
1263
+ assert_eq!(recorded_delays[1], 500);
1264
+ }
1265
+
1266
+ #[test]
1267
+ fn test_with_constant_chaining() {
1268
+ use super::RetryableExt;
1269
+ use core::cell::Cell;
1270
+
1271
+ let attempts = Cell::new(0);
1272
+
1273
+ let operation = || {
1274
+ let current = attempts.get();
1275
+ attempts.set(current + 1);
1276
+
1277
+ if current < 1 {
1278
+ Err(TestError::Fatal)
1279
+ } else {
1280
+ Ok(123)
1281
+ }
1282
+ };
1283
+
1284
+ // Test that .with_constant() can be chained with predicate
1285
+ let result = operation
1286
+ .with_constant(100)
1287
+ .when(|e| matches!(e, TestError::Retryable))
1288
+ .call_with_sleeper(FnSleeper(|_| {}));
1289
+
1290
+ // Should fail immediately due to predicate
1291
+ let err = result.expect_err("retry should fail due to predicate");
1292
+ assert_eq!(err.kind(), RetryErrorKind::PredicateRejected);
1293
+ assert_eq!(err.attempts(), 1);
1294
+ }
1295
+
1296
+ #[test]
1297
+ fn test_with_fibonacci_success() {
1298
+ use super::RetryableExt;
1299
+
1300
+ fn always_succeeds() -> Result<String, TestError> {
1301
+ Ok("success".to_string())
1302
+ }
1303
+
1304
+ let result = always_succeeds
1305
+ .with_fibonacci()
1306
+ .call_with_sleeper(FnSleeper(|_| {}));
1307
+
1308
+ let outcome = result.expect("retry should succeed");
1309
+ assert_eq!(outcome.attempts(), 1);
1310
+ assert_eq!(outcome.into_inner(), "success");
1311
+ }
1312
+
1313
+ #[test]
1314
+ fn test_with_fibonacci_retry_behavior() {
1315
+ use super::RetryableExt;
1316
+ use core::cell::Cell;
1317
+
1318
+ let attempts = Cell::new(0);
1319
+
1320
+ let operation = || {
1321
+ let current = attempts.get();
1322
+ attempts.set(current + 1);
1323
+
1324
+ if current < 3 {
1325
+ Err(TestError::Retryable)
1326
+ } else {
1327
+ Ok(777)
1328
+ }
1329
+ };
1330
+
1331
+ let result = operation
1332
+ .with_fibonacci()
1333
+ .call_with_sleeper(FnSleeper(|_| {}));
1334
+
1335
+ let outcome = result.expect("retry should succeed");
1336
+ assert_eq!(outcome.attempts(), 4);
1337
+ assert_eq!(outcome.into_inner(), 777);
1338
+ }
1339
+
1340
+ #[test]
1341
+ fn test_with_fibonacci_exhausted() {
1342
+ use super::RetryableExt;
1343
+
1344
+ fn always_fails() -> Result<i32, TestError> {
1345
+ Err(TestError::Retryable)
1346
+ }
1347
+
1348
+ let result = always_fails
1349
+ .with_fibonacci()
1350
+ .call_with_sleeper(FnSleeper(|_| {}));
1351
+
1352
+ let err = result.expect_err("retry should exhaust");
1353
+ assert_eq!(err.kind(), RetryErrorKind::Exhausted);
1354
+ assert_eq!(err.attempts(), 8); // Fibonacci default max_attempts
1355
+ assert_eq!(err.max_attempts(), 8);
1356
+ }
1357
+
1358
+ #[test]
1359
+ fn test_with_fibonacci_chaining() {
1360
+ use super::RetryableExt;
1361
+ use core::cell::Cell;
1362
+ use core::sync::atomic::{AtomicUsize, Ordering};
1363
+
1364
+ static SUCCESS_COUNT: AtomicUsize = AtomicUsize::new(0);
1365
+ let attempts = Cell::new(0);
1366
+
1367
+ let operation = || {
1368
+ let current = attempts.get();
1369
+ attempts.set(current + 1);
1370
+
1371
+ if current < 2 {
1372
+ Err(TestError::Retryable)
1373
+ } else {
1374
+ Ok(555)
1375
+ }
1376
+ };
1377
+
1378
+ SUCCESS_COUNT.store(0, Ordering::SeqCst);
1379
+
1380
+ // Test that .with_fibonacci() can be chained with callbacks
1381
+ let result = operation
1382
+ .with_fibonacci()
1383
+ .on_success(|_ctx| {
1384
+ SUCCESS_COUNT.fetch_add(1, Ordering::SeqCst);
1385
+ })
1386
+ .call_with_sleeper(FnSleeper(|_| {}));
1387
+
1388
+ let outcome = result.expect("retry should succeed");
1389
+ assert_eq!(outcome.attempts(), 3);
1390
+ assert_eq!(outcome.into_inner(), 555);
1391
+ assert_eq!(SUCCESS_COUNT.load(Ordering::SeqCst), 1);
1392
+ }
1393
+
1394
+ #[test]
1395
+ fn test_all_extension_methods_produce_working_retries() {
1396
+ use super::RetryableExt;
1397
+ use core::cell::Cell;
1398
+
1399
+ let attempts_exp = Cell::new(0);
1400
+ let op_exp = || {
1401
+ let current = attempts_exp.get();
1402
+ attempts_exp.set(current + 1);
1403
+ if current < 1 {
1404
+ Err(TestError::Retryable)
1405
+ } else {
1406
+ Ok(1)
1407
+ }
1408
+ };
1409
+
1410
+ let attempts_const = Cell::new(0);
1411
+ let op_const = || {
1412
+ let current = attempts_const.get();
1413
+ attempts_const.set(current + 1);
1414
+ if current < 1 {
1415
+ Err(TestError::Retryable)
1416
+ } else {
1417
+ Ok(2)
1418
+ }
1419
+ };
1420
+
1421
+ let attempts_fib = Cell::new(0);
1422
+ let op_fib = || {
1423
+ let current = attempts_fib.get();
1424
+ attempts_fib.set(current + 1);
1425
+ if current < 1 {
1426
+ Err(TestError::Retryable)
1427
+ } else {
1428
+ Ok(3)
1429
+ }
1430
+ };
1431
+
1432
+ // All extension methods should produce working retry builders
1433
+ let r1 = op_exp
1434
+ .with_exponential()
1435
+ .call_with_sleeper(FnSleeper(|_| {}))
1436
+ .expect("exponential retry works");
1437
+
1438
+ let r2 = op_const
1439
+ .with_constant(100)
1440
+ .call_with_sleeper(FnSleeper(|_| {}))
1441
+ .expect("constant retry works");
1442
+
1443
+ let r3 = op_fib
1444
+ .with_fibonacci()
1445
+ .call_with_sleeper(FnSleeper(|_| {}))
1446
+ .expect("fibonacci retry works");
1447
+
1448
+ assert_eq!(r1.into_inner(), 1);
1449
+ assert_eq!(r2.into_inner(), 2);
1450
+ assert_eq!(r3.into_inner(), 3);
1451
+ }
1452
+
1453
+ #[cfg(feature = "std")]
1454
+ #[test]
1455
+ fn test_with_exponential_std_sleeper() {
1456
+ use super::RetryableExt;
1457
+ use core::cell::Cell;
1458
+
1459
+ let attempts = Cell::new(0);
1460
+
1461
+ let operation = || {
1462
+ let current = attempts.get();
1463
+ attempts.set(current + 1);
1464
+
1465
+ if current < 1 {
1466
+ Err(TestError::Retryable)
1467
+ } else {
1468
+ Ok(999)
1469
+ }
1470
+ };
1471
+
1472
+ // Test with std sleeper
1473
+ let result = operation.with_exponential().call();
1474
+
1475
+ let outcome = result.expect("retry should succeed");
1476
+ assert_eq!(outcome.attempts(), 2);
1477
+ assert_eq!(outcome.into_inner(), 999);
1478
+ }
1479
+
1480
+ #[cfg(feature = "std")]
1481
+ #[test]
1482
+ fn test_with_constant_std_sleeper() {
1483
+ use super::RetryableExt;
1484
+
1485
+ fn always_succeeds() -> Result<i32, TestError> {
1486
+ Ok(888)
1487
+ }
1488
+
1489
+ // Test with std sleeper
1490
+ let result = always_succeeds.with_constant(50).call();
1491
+
1492
+ let outcome = result.expect("retry should succeed");
1493
+ assert_eq!(outcome.into_inner(), 888);
1494
+ }
1495
+
1496
+ #[cfg(feature = "std")]
1497
+ #[test]
1498
+ fn test_with_fibonacci_std_sleeper() {
1499
+ use super::RetryableExt;
1500
+
1501
+ fn always_succeeds() -> Result<i32, TestError> {
1502
+ Ok(444)
1503
+ }
1504
+
1505
+ // Test with std sleeper
1506
+ let result = always_succeeds.with_fibonacci().call();
1507
+
1508
+ let outcome = result.expect("retry should succeed");
1509
+ assert_eq!(outcome.into_inner(), 444);
1510
+ }
1511
+ }