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,108 @@
|
|
|
1
|
+
//! Ergonomic helpers that mirror the high-level DSL available in the Ruby gem.
|
|
2
|
+
//!
|
|
3
|
+
//! These helpers build on top of the global policy registry (std-only) to allow
|
|
4
|
+
//! concise execution using named policies.
|
|
5
|
+
|
|
6
|
+
use crate::backoff::BackoffPolicy;
|
|
7
|
+
use crate::policy::get_global_policy;
|
|
8
|
+
use crate::retry::{RetryBuilder, RetryError, RetryOutcome, Retryable};
|
|
9
|
+
use std::fmt;
|
|
10
|
+
|
|
11
|
+
/// Type alias for retry builder with policy from global registry
|
|
12
|
+
type PolicyRetryBuilder<F, T, E> = RetryBuilder<F, BackoffPolicy, T, E, fn(&E) -> bool>;
|
|
13
|
+
|
|
14
|
+
/// Errors produced by the DSL helpers.
|
|
15
|
+
#[derive(Debug)]
|
|
16
|
+
pub enum DslError<E> {
|
|
17
|
+
/// Referenced policy name is missing from the global registry.
|
|
18
|
+
PolicyMissing(String),
|
|
19
|
+
/// Underlying retry execution failed.
|
|
20
|
+
Execution(RetryError<E>),
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
impl<E> From<RetryError<E>> for DslError<E> {
|
|
24
|
+
fn from(value: RetryError<E>) -> Self {
|
|
25
|
+
DslError::Execution(value)
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
impl<E> fmt::Display for DslError<E>
|
|
30
|
+
where
|
|
31
|
+
E: fmt::Display,
|
|
32
|
+
{
|
|
33
|
+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
34
|
+
match self {
|
|
35
|
+
DslError::PolicyMissing(name) => write!(f, "retry policy '{}' is not registered", name),
|
|
36
|
+
DslError::Execution(err) => write!(f, "{err}"),
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
impl<E> std::error::Error for DslError<E> where E: fmt::Display + std::error::Error {}
|
|
42
|
+
|
|
43
|
+
/// Construct a [`RetryBuilder`] using a named policy from the global registry.
|
|
44
|
+
pub fn builder_for_policy<F, T, E>(
|
|
45
|
+
policy_name: &str,
|
|
46
|
+
operation: F,
|
|
47
|
+
) -> Result<PolicyRetryBuilder<F, T, E>, DslError<E>>
|
|
48
|
+
where
|
|
49
|
+
F: FnMut() -> Result<T, E>,
|
|
50
|
+
{
|
|
51
|
+
let policy = get_global_policy(policy_name)
|
|
52
|
+
.ok_or_else(|| DslError::PolicyMissing(policy_name.to_string()))?;
|
|
53
|
+
|
|
54
|
+
Ok(operation.retry(policy))
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/// Execute an operation using a named policy from the global registry.
|
|
58
|
+
pub fn retry_with_policy<F, T, E>(
|
|
59
|
+
policy_name: &str,
|
|
60
|
+
operation: F,
|
|
61
|
+
) -> Result<RetryOutcome<T>, DslError<E>>
|
|
62
|
+
where
|
|
63
|
+
F: FnMut() -> Result<T, E>,
|
|
64
|
+
{
|
|
65
|
+
builder_for_policy(policy_name, operation)?
|
|
66
|
+
.call()
|
|
67
|
+
.map_err(DslError::Execution)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
#[cfg(test)]
|
|
71
|
+
mod tests {
|
|
72
|
+
use super::*;
|
|
73
|
+
use crate::backoff::{BackoffPolicy, ExponentialBackoff};
|
|
74
|
+
use crate::policy::{clear_global_policies, register_global_policy};
|
|
75
|
+
|
|
76
|
+
#[test]
|
|
77
|
+
fn test_retry_with_policy_success() {
|
|
78
|
+
clear_global_policies();
|
|
79
|
+
register_global_policy(
|
|
80
|
+
"default",
|
|
81
|
+
BackoffPolicy::from(ExponentialBackoff::new().max_attempts(2)),
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
let mut attempts = 0;
|
|
85
|
+
let outcome = retry_with_policy("default", || {
|
|
86
|
+
attempts += 1;
|
|
87
|
+
if attempts == 1 {
|
|
88
|
+
Err::<_, &'static str>("fail")
|
|
89
|
+
} else {
|
|
90
|
+
Ok("ok")
|
|
91
|
+
}
|
|
92
|
+
})
|
|
93
|
+
.expect("dsl retry should succeed");
|
|
94
|
+
|
|
95
|
+
assert_eq!(attempts, 2);
|
|
96
|
+
assert_eq!(outcome.into_inner(), "ok");
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
#[test]
|
|
100
|
+
fn test_retry_with_policy_missing() {
|
|
101
|
+
clear_global_policies();
|
|
102
|
+
let result = retry_with_policy::<_, (), &str>("missing", || Ok(()));
|
|
103
|
+
match result {
|
|
104
|
+
Err(DslError::PolicyMissing(name)) => assert_eq!(name, "missing"),
|
|
105
|
+
_ => panic!("expected policy missing error"),
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
//! ChronoMachines - Pure Rust exponential backoff and retry library
|
|
2
|
+
//!
|
|
3
|
+
//! This crate provides a lightweight, `no_std` compatible implementation of
|
|
4
|
+
//! exponential backoff with full jitter for retry mechanisms.
|
|
5
|
+
//!
|
|
6
|
+
//! # Features
|
|
7
|
+
//!
|
|
8
|
+
//! - **Full Jitter**: Prevents thundering herd problem
|
|
9
|
+
//! - **no_std compatible**: Works in embedded environments
|
|
10
|
+
//! - **Zero allocation**: Uses stack-only data structures
|
|
11
|
+
//! - **Fast**: Minimal overhead for delay calculations
|
|
12
|
+
//!
|
|
13
|
+
//! # Example
|
|
14
|
+
//!
|
|
15
|
+
//! ```rust
|
|
16
|
+
//! use chrono_machines::Policy;
|
|
17
|
+
//!
|
|
18
|
+
//! let policy = Policy {
|
|
19
|
+
//! max_attempts: 5,
|
|
20
|
+
//! base_delay_ms: 100,
|
|
21
|
+
//! multiplier: 2.0,
|
|
22
|
+
//! max_delay_ms: 10_000,
|
|
23
|
+
//! };
|
|
24
|
+
//!
|
|
25
|
+
//! // Use full jitter (1.0) - recommended for distributed systems
|
|
26
|
+
//! let delay = policy.calculate_delay(1, 1.0);
|
|
27
|
+
//! println!("Wait {}ms before retry", delay);
|
|
28
|
+
//! ```
|
|
29
|
+
|
|
30
|
+
#![cfg_attr(not(feature = "std"), no_std)]
|
|
31
|
+
#![warn(rust_2024_compatibility)]
|
|
32
|
+
#![warn(clippy::all)]
|
|
33
|
+
|
|
34
|
+
#[cfg(feature = "alloc")]
|
|
35
|
+
extern crate alloc;
|
|
36
|
+
|
|
37
|
+
pub mod backoff;
|
|
38
|
+
#[cfg(feature = "std")]
|
|
39
|
+
pub mod dsl;
|
|
40
|
+
#[cfg(any(feature = "std", feature = "alloc"))]
|
|
41
|
+
pub mod policy;
|
|
42
|
+
#[cfg(any(feature = "std", feature = "alloc"))]
|
|
43
|
+
pub mod retry;
|
|
44
|
+
pub mod sleep;
|
|
45
|
+
|
|
46
|
+
pub use backoff::{
|
|
47
|
+
fibonacci, BackoffPolicy, BackoffStrategy, ConstantBackoff, ExponentialBackoff,
|
|
48
|
+
FibonacciBackoff,
|
|
49
|
+
};
|
|
50
|
+
#[cfg(feature = "std")]
|
|
51
|
+
pub use dsl::{builder_for_policy, retry_with_policy, DslError};
|
|
52
|
+
#[cfg(any(feature = "std", feature = "alloc"))]
|
|
53
|
+
pub use policy::PolicyRegistry;
|
|
54
|
+
#[cfg(feature = "std")]
|
|
55
|
+
pub use policy::{
|
|
56
|
+
clear_global_policies, get_global_policy, list_global_policies, register_global_policy,
|
|
57
|
+
remove_global_policy,
|
|
58
|
+
};
|
|
59
|
+
#[cfg(any(feature = "std", feature = "alloc"))]
|
|
60
|
+
pub use retry::{RetryBuilder, RetryContext, RetryError, RetryOutcome, Retryable, RetryableExt};
|
|
61
|
+
#[cfg(feature = "std")]
|
|
62
|
+
pub use sleep::StdSleeper;
|
|
63
|
+
pub use sleep::{FnSleeper, Sleeper};
|
|
64
|
+
|
|
65
|
+
#[cfg(feature = "std")]
|
|
66
|
+
use rand::rngs::StdRng;
|
|
67
|
+
use rand::RngExt;
|
|
68
|
+
|
|
69
|
+
use rand::Rng;
|
|
70
|
+
|
|
71
|
+
/// Retry policy configuration
|
|
72
|
+
///
|
|
73
|
+
/// Defines the parameters for exponential backoff with jitter.
|
|
74
|
+
#[derive(Debug, Clone, Copy)]
|
|
75
|
+
pub struct Policy {
|
|
76
|
+
/// Maximum number of retry attempts
|
|
77
|
+
pub max_attempts: u8,
|
|
78
|
+
|
|
79
|
+
/// Base delay in milliseconds
|
|
80
|
+
pub base_delay_ms: u64,
|
|
81
|
+
|
|
82
|
+
/// Exponential backoff multiplier
|
|
83
|
+
pub multiplier: f64,
|
|
84
|
+
|
|
85
|
+
/// Maximum delay cap in milliseconds
|
|
86
|
+
pub max_delay_ms: u64,
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
impl Policy {
|
|
90
|
+
/// Create a new policy with default values
|
|
91
|
+
///
|
|
92
|
+
/// # Default values
|
|
93
|
+
///
|
|
94
|
+
/// - `max_attempts`: 3
|
|
95
|
+
/// - `base_delay_ms`: 100
|
|
96
|
+
/// - `multiplier`: 2.0
|
|
97
|
+
/// - `max_delay_ms`: 10_000
|
|
98
|
+
pub fn new() -> Self {
|
|
99
|
+
Self {
|
|
100
|
+
max_attempts: 3,
|
|
101
|
+
base_delay_ms: 100,
|
|
102
|
+
multiplier: 2.0,
|
|
103
|
+
max_delay_ms: 10_000,
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/// Calculate delay with jitter for the given attempt
|
|
108
|
+
///
|
|
109
|
+
/// Applies exponential backoff with configurable jitter to prevent
|
|
110
|
+
/// thundering herd problems in distributed systems.
|
|
111
|
+
///
|
|
112
|
+
/// # Arguments
|
|
113
|
+
///
|
|
114
|
+
/// * `attempt` - Current attempt number (1-indexed)
|
|
115
|
+
/// * `jitter_factor` - Jitter multiplier (0.0 = no jitter, 1.0 = full jitter)
|
|
116
|
+
///
|
|
117
|
+
/// # Returns
|
|
118
|
+
///
|
|
119
|
+
/// Delay in milliseconds as a `u64`
|
|
120
|
+
///
|
|
121
|
+
/// # Example
|
|
122
|
+
///
|
|
123
|
+
/// ```rust
|
|
124
|
+
/// use chrono_machines::Policy;
|
|
125
|
+
///
|
|
126
|
+
/// let policy = Policy::new();
|
|
127
|
+
/// // Full jitter (default behavior)
|
|
128
|
+
/// let delay = policy.calculate_delay(1, 1.0);
|
|
129
|
+
/// assert!(delay <= 100); // First attempt, max is base_delay_ms
|
|
130
|
+
///
|
|
131
|
+
/// // 10% jitter - delay will be 90-100% of base_delay_ms
|
|
132
|
+
/// let delay = policy.calculate_delay(1, 0.1);
|
|
133
|
+
/// assert!(delay >= 90 && delay <= 100);
|
|
134
|
+
/// ```
|
|
135
|
+
#[cfg(feature = "std")]
|
|
136
|
+
pub fn calculate_delay(&self, attempt: u8, jitter_factor: f64) -> u64 {
|
|
137
|
+
let mut rng: StdRng = rand::make_rng();
|
|
138
|
+
self.calculate_delay_with_rng(attempt, jitter_factor, &mut rng)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/// Calculate delay with a provided RNG and custom jitter factor
|
|
142
|
+
///
|
|
143
|
+
/// This method allows for custom RNG implementations and jitter control, useful for:
|
|
144
|
+
/// - Deterministic testing
|
|
145
|
+
/// - `no_std` environments with custom RNG sources
|
|
146
|
+
/// - Performance optimization with specific RNG types
|
|
147
|
+
/// - Fine-tuning jitter behavior
|
|
148
|
+
///
|
|
149
|
+
/// # Arguments
|
|
150
|
+
///
|
|
151
|
+
/// * `attempt` - Current attempt number (1-indexed)
|
|
152
|
+
/// * `jitter_factor` - Jitter multiplier (0.0 = no jitter, 1.0 = full jitter)
|
|
153
|
+
/// * `rng` - Random number generator implementing `Rng`
|
|
154
|
+
///
|
|
155
|
+
/// # Returns
|
|
156
|
+
///
|
|
157
|
+
/// Delay in milliseconds as a `u64`
|
|
158
|
+
pub fn calculate_delay_with_rng<R: Rng>(
|
|
159
|
+
&self,
|
|
160
|
+
attempt: u8,
|
|
161
|
+
jitter_factor: f64,
|
|
162
|
+
rng: &mut R,
|
|
163
|
+
) -> u64 {
|
|
164
|
+
// Normalize jitter factor to the inclusive range [0.0, 1.0]
|
|
165
|
+
let mut jitter_factor = jitter_factor;
|
|
166
|
+
if jitter_factor.is_nan() {
|
|
167
|
+
jitter_factor = 1.0;
|
|
168
|
+
} else {
|
|
169
|
+
jitter_factor = jitter_factor.clamp(0.0, 1.0);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Calculate base exponential backoff
|
|
173
|
+
let exponent = attempt.saturating_sub(1) as i32;
|
|
174
|
+
let base_exponential =
|
|
175
|
+
(self.base_delay_ms as f64) * crate::backoff::powi_f64(self.multiplier, exponent);
|
|
176
|
+
|
|
177
|
+
// Cap at max_delay
|
|
178
|
+
let capped = base_exponential.min(self.max_delay_ms as f64);
|
|
179
|
+
|
|
180
|
+
// Apply jitter: blend between deterministic and random delay
|
|
181
|
+
// jitter_factor of 1.0 = full jitter (0 to base), 0.0 = no jitter (exactly base)
|
|
182
|
+
// Formula: base * (1 - jitter_factor + rand * jitter_factor)
|
|
183
|
+
// Example with jitter_factor=0.1: base * (0.9 + rand*0.1) = 90% to 100% of base
|
|
184
|
+
let random_scalar: f64 = rng.random_range(0.0..=1.0);
|
|
185
|
+
let jitter_blend = 1.0 - jitter_factor + random_scalar * jitter_factor;
|
|
186
|
+
let jittered = capped * jitter_blend;
|
|
187
|
+
|
|
188
|
+
jittered as u64
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/// Check if another retry should be attempted
|
|
192
|
+
///
|
|
193
|
+
/// # Arguments
|
|
194
|
+
///
|
|
195
|
+
/// * `current_attempt` - Current attempt number (1-indexed)
|
|
196
|
+
///
|
|
197
|
+
/// # Returns
|
|
198
|
+
///
|
|
199
|
+
/// `true` if another retry is allowed, `false` otherwise
|
|
200
|
+
pub fn should_retry(&self, current_attempt: u8) -> bool {
|
|
201
|
+
current_attempt < self.max_attempts
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
impl Default for Policy {
|
|
206
|
+
fn default() -> Self {
|
|
207
|
+
Self::new()
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
#[cfg(test)]
|
|
212
|
+
mod tests {
|
|
213
|
+
use super::*;
|
|
214
|
+
use rand::rngs::StdRng;
|
|
215
|
+
use rand::SeedableRng;
|
|
216
|
+
|
|
217
|
+
#[test]
|
|
218
|
+
fn test_policy_default() {
|
|
219
|
+
let policy = Policy::default();
|
|
220
|
+
assert_eq!(policy.max_attempts, 3);
|
|
221
|
+
assert_eq!(policy.base_delay_ms, 100);
|
|
222
|
+
assert_eq!(policy.multiplier, 2.0);
|
|
223
|
+
assert_eq!(policy.max_delay_ms, 10_000);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
#[test]
|
|
227
|
+
fn test_calculate_delay_bounds() {
|
|
228
|
+
let policy = Policy {
|
|
229
|
+
max_attempts: 5,
|
|
230
|
+
base_delay_ms: 100,
|
|
231
|
+
multiplier: 2.0,
|
|
232
|
+
max_delay_ms: 1000,
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
let mut rng = StdRng::seed_from_u64(42);
|
|
236
|
+
|
|
237
|
+
// First attempt with full jitter: delay should be between 0 and 100ms
|
|
238
|
+
let delay1 = policy.calculate_delay_with_rng(1, 1.0, &mut rng);
|
|
239
|
+
assert!(delay1 <= 100);
|
|
240
|
+
|
|
241
|
+
// Second attempt with full jitter: delay should be between 0 and 200ms
|
|
242
|
+
let delay2 = policy.calculate_delay_with_rng(2, 1.0, &mut rng);
|
|
243
|
+
assert!(delay2 <= 200);
|
|
244
|
+
|
|
245
|
+
// Fifth attempt with full jitter: delay should be capped at max_delay_ms (1000ms)
|
|
246
|
+
let delay5 = policy.calculate_delay_with_rng(5, 1.0, &mut rng);
|
|
247
|
+
assert!(delay5 <= 1000);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
#[test]
|
|
251
|
+
fn test_should_retry() {
|
|
252
|
+
let policy = Policy {
|
|
253
|
+
max_attempts: 3,
|
|
254
|
+
..Policy::default()
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
assert!(policy.should_retry(1));
|
|
258
|
+
assert!(policy.should_retry(2));
|
|
259
|
+
assert!(!policy.should_retry(3));
|
|
260
|
+
assert!(!policy.should_retry(4));
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
#[test]
|
|
264
|
+
fn test_max_delay_cap() {
|
|
265
|
+
let policy = Policy {
|
|
266
|
+
max_attempts: 10,
|
|
267
|
+
base_delay_ms: 100,
|
|
268
|
+
multiplier: 2.0,
|
|
269
|
+
max_delay_ms: 500,
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
let mut rng = StdRng::seed_from_u64(42);
|
|
273
|
+
|
|
274
|
+
// High attempt number should still be capped
|
|
275
|
+
let delay = policy.calculate_delay_with_rng(10, 1.0, &mut rng);
|
|
276
|
+
assert!(delay <= 500);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
#[test]
|
|
280
|
+
fn test_zero_multiplier() {
|
|
281
|
+
let policy = Policy {
|
|
282
|
+
max_attempts: 5,
|
|
283
|
+
base_delay_ms: 100,
|
|
284
|
+
multiplier: 1.0, // No exponential growth
|
|
285
|
+
max_delay_ms: 10_000,
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
let mut rng = StdRng::seed_from_u64(42);
|
|
289
|
+
|
|
290
|
+
// All delays with full jitter should be between 0 and base_delay_ms
|
|
291
|
+
for attempt in 1..=5 {
|
|
292
|
+
let delay = policy.calculate_delay_with_rng(attempt, 1.0, &mut rng);
|
|
293
|
+
assert!(delay <= 100);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
#[test]
|
|
298
|
+
fn test_jitter_factor() {
|
|
299
|
+
let policy = Policy {
|
|
300
|
+
max_attempts: 5,
|
|
301
|
+
base_delay_ms: 1000,
|
|
302
|
+
multiplier: 1.0,
|
|
303
|
+
max_delay_ms: 10_000,
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
let mut rng = StdRng::seed_from_u64(42);
|
|
307
|
+
|
|
308
|
+
// 10% jitter: delay should be between 900ms (90%) and 1000ms (100%)
|
|
309
|
+
let delay = policy.calculate_delay_with_rng(1, 0.1, &mut rng);
|
|
310
|
+
assert!(
|
|
311
|
+
delay >= 900 && delay <= 1000,
|
|
312
|
+
"delay {} not in range 900-1000",
|
|
313
|
+
delay
|
|
314
|
+
);
|
|
315
|
+
|
|
316
|
+
// No jitter: delay should be exactly base_delay_ms
|
|
317
|
+
let delay = policy.calculate_delay_with_rng(1, 0.0, &mut rng);
|
|
318
|
+
assert_eq!(delay, 1000);
|
|
319
|
+
|
|
320
|
+
// Full jitter: delay should be between 0 and 1000ms
|
|
321
|
+
let delay = policy.calculate_delay_with_rng(1, 1.0, &mut rng);
|
|
322
|
+
assert!(delay <= 1000);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
#[test]
|
|
326
|
+
fn test_jitter_factor_clamping() {
|
|
327
|
+
let policy = Policy {
|
|
328
|
+
max_attempts: 5,
|
|
329
|
+
base_delay_ms: 1000,
|
|
330
|
+
multiplier: 1.0,
|
|
331
|
+
max_delay_ms: 10_000,
|
|
332
|
+
};
|
|
333
|
+
|
|
334
|
+
let mut rng = StdRng::seed_from_u64(42);
|
|
335
|
+
|
|
336
|
+
// Negative jitter_factor should be clamped to 0.0
|
|
337
|
+
let delay = policy.calculate_delay_with_rng(1, -0.5, &mut rng);
|
|
338
|
+
assert_eq!(delay, 1000, "negative jitter_factor should clamp to 0.0");
|
|
339
|
+
|
|
340
|
+
// jitter_factor > 1.0 should be clamped to 1.0
|
|
341
|
+
let delay = policy.calculate_delay_with_rng(1, 2.0, &mut rng);
|
|
342
|
+
assert!(
|
|
343
|
+
delay <= 1000,
|
|
344
|
+
"jitter_factor > 1.0 should clamp to 1.0, got delay {}",
|
|
345
|
+
delay
|
|
346
|
+
);
|
|
347
|
+
|
|
348
|
+
// Extreme values should still be clamped
|
|
349
|
+
let delay = policy.calculate_delay_with_rng(1, 999.0, &mut rng);
|
|
350
|
+
assert!(delay <= 1000, "extreme jitter_factor should be clamped");
|
|
351
|
+
|
|
352
|
+
let delay = policy.calculate_delay_with_rng(1, -999.0, &mut rng);
|
|
353
|
+
assert_eq!(delay, 1000, "extreme negative should clamp to 0.0");
|
|
354
|
+
}
|
|
355
|
+
}
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
//! Named policy management utilities.
|
|
2
|
+
//!
|
|
3
|
+
//! This module introduces a lightweight registry for `BackoffPolicy` values.
|
|
4
|
+
//! Registries can be instantiated locally (requires `alloc`) or accessed via a
|
|
5
|
+
//! global registry when the `std` feature is enabled. The goal is to provide a
|
|
6
|
+
//! convenient way to organise retry policies by name, mirroring the global
|
|
7
|
+
//! configuration style found in higher-level frameworks.
|
|
8
|
+
|
|
9
|
+
use crate::backoff::BackoffPolicy;
|
|
10
|
+
|
|
11
|
+
#[cfg(any(feature = "std", feature = "alloc"))]
|
|
12
|
+
use alloc::string::String;
|
|
13
|
+
#[cfg(any(feature = "std", feature = "alloc"))]
|
|
14
|
+
use alloc::vec::Vec;
|
|
15
|
+
|
|
16
|
+
/// In-memory registry for named [`BackoffPolicy`] values.
|
|
17
|
+
///
|
|
18
|
+
/// This registry performs simple linear lookups over an internal vector. The
|
|
19
|
+
/// design keeps the implementation `no_std`-friendly (when the `alloc` feature
|
|
20
|
+
/// is available) while remaining ergonomic for typical workloads where only a
|
|
21
|
+
/// handful of retry policies are defined.
|
|
22
|
+
#[cfg(any(feature = "std", feature = "alloc"))]
|
|
23
|
+
#[derive(Debug, Clone, Default)]
|
|
24
|
+
pub struct PolicyRegistry {
|
|
25
|
+
entries: Vec<(String, BackoffPolicy)>,
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
#[cfg(any(feature = "std", feature = "alloc"))]
|
|
29
|
+
impl PolicyRegistry {
|
|
30
|
+
/// Create an empty registry.
|
|
31
|
+
pub fn new() -> Self {
|
|
32
|
+
Self::default()
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/// Insert or replace a policy under the given name.
|
|
36
|
+
///
|
|
37
|
+
/// Returns the previously registered policy if one existed.
|
|
38
|
+
pub fn register(
|
|
39
|
+
&mut self,
|
|
40
|
+
name: impl Into<String>,
|
|
41
|
+
policy: BackoffPolicy,
|
|
42
|
+
) -> Option<BackoffPolicy> {
|
|
43
|
+
let name = name.into();
|
|
44
|
+
if let Some((_, existing)) = self
|
|
45
|
+
.entries
|
|
46
|
+
.iter_mut()
|
|
47
|
+
.find(|(existing_name, _)| *existing_name == name)
|
|
48
|
+
{
|
|
49
|
+
let previous = *existing;
|
|
50
|
+
*existing = policy;
|
|
51
|
+
Some(previous)
|
|
52
|
+
} else {
|
|
53
|
+
self.entries.push((name, policy));
|
|
54
|
+
None
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/// Retrieve a policy by name.
|
|
59
|
+
pub fn get(&self, name: &str) -> Option<BackoffPolicy> {
|
|
60
|
+
self.entries
|
|
61
|
+
.iter()
|
|
62
|
+
.find(|(existing_name, _)| existing_name == name)
|
|
63
|
+
.map(|(_, policy)| *policy)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/// Remove a policy by name.
|
|
67
|
+
///
|
|
68
|
+
/// Returns the removed policy when it existed.
|
|
69
|
+
pub fn remove(&mut self, name: &str) -> Option<BackoffPolicy> {
|
|
70
|
+
if let Some(index) = self
|
|
71
|
+
.entries
|
|
72
|
+
.iter()
|
|
73
|
+
.position(|(existing_name, _)| existing_name == name)
|
|
74
|
+
{
|
|
75
|
+
Some(self.entries.swap_remove(index).1)
|
|
76
|
+
} else {
|
|
77
|
+
None
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/// Return all registered policies as `(name, policy)` tuples.
|
|
82
|
+
pub fn all(&self) -> Vec<(String, BackoffPolicy)> {
|
|
83
|
+
self.entries.to_vec()
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/// Clear the registry.
|
|
87
|
+
pub fn clear(&mut self) {
|
|
88
|
+
self.entries.clear();
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
#[cfg(feature = "std")]
|
|
93
|
+
use std::sync::{OnceLock, RwLock};
|
|
94
|
+
|
|
95
|
+
#[cfg(feature = "std")]
|
|
96
|
+
fn global_registry() -> &'static RwLock<PolicyRegistry> {
|
|
97
|
+
static GLOBAL_POLICIES: OnceLock<RwLock<PolicyRegistry>> = OnceLock::new();
|
|
98
|
+
GLOBAL_POLICIES.get_or_init(|| RwLock::new(PolicyRegistry::new()))
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/// Register a policy in the global registry (requires `std`).
|
|
102
|
+
#[cfg(feature = "std")]
|
|
103
|
+
pub fn register_global_policy(
|
|
104
|
+
name: impl Into<String>,
|
|
105
|
+
policy: BackoffPolicy,
|
|
106
|
+
) -> Option<BackoffPolicy> {
|
|
107
|
+
let mut guard = global_registry()
|
|
108
|
+
.write()
|
|
109
|
+
.expect("chronomachines global policy registry poisoned");
|
|
110
|
+
guard.register(name, policy)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/// Fetch a policy from the global registry (requires `std`).
|
|
114
|
+
#[cfg(feature = "std")]
|
|
115
|
+
pub fn get_global_policy(name: &str) -> Option<BackoffPolicy> {
|
|
116
|
+
let guard = global_registry()
|
|
117
|
+
.read()
|
|
118
|
+
.expect("chronomachines global policy registry poisoned");
|
|
119
|
+
guard.get(name)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/// Remove a policy from the global registry (requires `std`).
|
|
123
|
+
#[cfg(feature = "std")]
|
|
124
|
+
pub fn remove_global_policy(name: &str) -> Option<BackoffPolicy> {
|
|
125
|
+
let mut guard = global_registry()
|
|
126
|
+
.write()
|
|
127
|
+
.expect("chronomachines global policy registry poisoned");
|
|
128
|
+
guard.remove(name)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/// List all policies from the global registry (requires `std`).
|
|
132
|
+
#[cfg(feature = "std")]
|
|
133
|
+
pub fn list_global_policies() -> Vec<(String, BackoffPolicy)> {
|
|
134
|
+
let guard = global_registry()
|
|
135
|
+
.read()
|
|
136
|
+
.expect("chronomachines global policy registry poisoned");
|
|
137
|
+
guard.all()
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/// Clear all entries from the global registry (requires `std`).
|
|
141
|
+
#[cfg(feature = "std")]
|
|
142
|
+
pub fn clear_global_policies() {
|
|
143
|
+
let mut guard = global_registry()
|
|
144
|
+
.write()
|
|
145
|
+
.expect("chronomachines global policy registry poisoned");
|
|
146
|
+
guard.clear();
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
#[cfg(all(test, any(feature = "std", feature = "alloc")))]
|
|
150
|
+
mod tests {
|
|
151
|
+
use super::*;
|
|
152
|
+
use crate::backoff::{BackoffPolicy, ExponentialBackoff};
|
|
153
|
+
|
|
154
|
+
#[test]
|
|
155
|
+
fn test_registry_crud() {
|
|
156
|
+
let mut registry = PolicyRegistry::new();
|
|
157
|
+
assert!(registry.get("missing").is_none());
|
|
158
|
+
|
|
159
|
+
let policy = BackoffPolicy::from(ExponentialBackoff::new().max_attempts(5));
|
|
160
|
+
assert!(registry.register("api", policy).is_none());
|
|
161
|
+
assert_eq!(registry.get("api").unwrap().max_attempts(), 5);
|
|
162
|
+
|
|
163
|
+
let new_policy = BackoffPolicy::from(ExponentialBackoff::new().max_attempts(3));
|
|
164
|
+
let replaced = registry.register("api", new_policy);
|
|
165
|
+
assert_eq!(replaced.unwrap().max_attempts(), 5);
|
|
166
|
+
assert_eq!(registry.get("api").unwrap().max_attempts(), 3);
|
|
167
|
+
|
|
168
|
+
let removed = registry.remove("api");
|
|
169
|
+
assert!(removed.is_some());
|
|
170
|
+
assert!(registry.get("api").is_none());
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
#[cfg(feature = "std")]
|
|
174
|
+
#[test]
|
|
175
|
+
fn test_global_registry_roundtrip() {
|
|
176
|
+
clear_global_policies();
|
|
177
|
+
assert!(list_global_policies().is_empty());
|
|
178
|
+
|
|
179
|
+
let policy = BackoffPolicy::from(ExponentialBackoff::new().max_attempts(4));
|
|
180
|
+
assert!(register_global_policy("workers", policy).is_none());
|
|
181
|
+
|
|
182
|
+
let fetched = get_global_policy("workers").unwrap();
|
|
183
|
+
assert_eq!(fetched.max_attempts(), 4);
|
|
184
|
+
|
|
185
|
+
let removed = remove_global_policy("workers").unwrap();
|
|
186
|
+
assert_eq!(removed.max_attempts(), 4);
|
|
187
|
+
assert!(get_global_policy("workers").is_none());
|
|
188
|
+
}
|
|
189
|
+
}
|