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,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
+ }