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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +67 -0
- data/README.md +1 -1
- data/ext/chrono_machines_native/core/Cargo.toml +28 -0
- data/ext/chrono_machines_native/core/src/backoff.rs +516 -0
- data/ext/chrono_machines_native/core/src/dsl.rs +108 -0
- data/ext/chrono_machines_native/core/src/lib.rs +355 -0
- data/ext/chrono_machines_native/core/src/policy.rs +189 -0
- data/ext/chrono_machines_native/core/src/retry.rs +1511 -0
- data/ext/chrono_machines_native/core/src/sleep.rs +97 -0
- data/ext/chrono_machines_native/extconf.rb +3 -0
- data/ext/chrono_machines_native/ffi/Cargo.toml +23 -0
- data/ext/chrono_machines_native/ffi/extconf.rb +62 -0
- data/ext/chrono_machines_native/ffi/src/lib.rs +192 -0
- data/ext/chrono_machines_native/target/debug/build/clang-sys-414c893ed9493fe4/out/common.rs +355 -0
- data/ext/chrono_machines_native/target/debug/build/clang-sys-414c893ed9493fe4/out/dynamic.rs +276 -0
- data/ext/chrono_machines_native/target/debug/build/clang-sys-414c893ed9493fe4/out/macros.rs +49 -0
- data/ext/chrono_machines_native/target/debug/build/clang-sys-b1d0ba329a011cdd/out/common.rs +355 -0
- data/ext/chrono_machines_native/target/debug/build/clang-sys-b1d0ba329a011cdd/out/dynamic.rs +276 -0
- data/ext/chrono_machines_native/target/debug/build/clang-sys-b1d0ba329a011cdd/out/macros.rs +49 -0
- 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
- 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
- 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
- data/ext/chrono_machines_native/target/package/chrono-machines-0.3.2/Cargo.toml +76 -0
- data/ext/chrono_machines_native/target/package/chrono-machines-0.3.2/src/backoff.rs +497 -0
- data/ext/chrono_machines_native/target/package/chrono-machines-0.3.2/src/dsl.rs +108 -0
- data/ext/chrono_machines_native/target/package/chrono-machines-0.3.2/src/lib.rs +352 -0
- data/ext/chrono_machines_native/target/package/chrono-machines-0.3.2/src/policy.rs +189 -0
- data/ext/chrono_machines_native/target/package/chrono-machines-0.3.2/src/retry.rs +1489 -0
- data/ext/chrono_machines_native/target/package/chrono-machines-0.3.2/src/sleep.rs +97 -0
- data/ext/chrono_machines_native/target/package/chrono_machines_native-0.2.3/Cargo.toml +56 -0
- data/ext/chrono_machines_native/target/package/chrono_machines_native-0.2.3/src/lib.rs +192 -0
- data/lib/chrono_machines/async_support.rb +2 -1
- data/lib/chrono_machines/configuration.rb +19 -5
- data/lib/chrono_machines/executor.rb +69 -9
- data/lib/chrono_machines/native_speedup.rb +61 -0
- data/lib/chrono_machines/version.rb +1 -1
- data/lib/chrono_machines.rb +1 -1
- 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(¬ify_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(¬ify_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
|
+
}
|