Skip to main content

chrono_machines/
lib.rs

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")]
35extern crate alloc;
36
37pub mod backoff;
38#[cfg(feature = "std")]
39pub mod dsl;
40#[cfg(any(feature = "std", feature = "alloc"))]
41pub mod policy;
42pub mod retry;
43pub mod sleep;
44
45pub use backoff::{
46    fibonacci, BackoffPolicy, BackoffStrategy, ConstantBackoff, ExponentialBackoff,
47    FibonacciBackoff,
48};
49#[cfg(feature = "std")]
50pub use dsl::{builder_for_policy, retry_with_policy, DslError};
51#[cfg(any(feature = "std", feature = "alloc"))]
52pub use policy::PolicyRegistry;
53#[cfg(feature = "std")]
54pub use policy::{
55    clear_global_policies, get_global_policy, list_global_policies, register_global_policy,
56    remove_global_policy,
57};
58pub use retry::{RetryBuilder, RetryContext, RetryError, RetryOutcome, Retryable, RetryableExt};
59#[cfg(feature = "std")]
60pub use sleep::StdSleeper;
61pub use sleep::{FnSleeper, Sleeper};
62
63#[cfg(feature = "std")]
64use rand::rngs::StdRng;
65use rand::RngExt;
66
67use rand::Rng;
68
69/// Retry policy configuration
70///
71/// Defines the parameters for exponential backoff with jitter.
72#[derive(Debug, Clone, Copy)]
73pub struct Policy {
74    /// Maximum number of retry attempts
75    pub max_attempts: u8,
76
77    /// Base delay in milliseconds
78    pub base_delay_ms: u64,
79
80    /// Exponential backoff multiplier
81    pub multiplier: f64,
82
83    /// Maximum delay cap in milliseconds
84    pub max_delay_ms: u64,
85}
86
87impl Policy {
88    /// Create a new policy with default values
89    ///
90    /// # Default values
91    ///
92    /// - `max_attempts`: 3
93    /// - `base_delay_ms`: 100
94    /// - `multiplier`: 2.0
95    /// - `max_delay_ms`: 10_000
96    pub fn new() -> Self {
97        Self {
98            max_attempts: 3,
99            base_delay_ms: 100,
100            multiplier: 2.0,
101            max_delay_ms: 10_000,
102        }
103    }
104
105    /// Calculate delay with jitter for the given attempt
106    ///
107    /// Applies exponential backoff with configurable jitter to prevent
108    /// thundering herd problems in distributed systems.
109    ///
110    /// # Arguments
111    ///
112    /// * `attempt` - Current attempt number (1-indexed)
113    /// * `jitter_factor` - Jitter multiplier (0.0 = no jitter, 1.0 = full jitter)
114    ///
115    /// # Returns
116    ///
117    /// Delay in milliseconds as a `u64`
118    ///
119    /// # Example
120    ///
121    /// ```rust
122    /// use chrono_machines::Policy;
123    ///
124    /// let policy = Policy::new();
125    /// // Full jitter (default behavior)
126    /// let delay = policy.calculate_delay(1, 1.0);
127    /// assert!(delay <= 100); // First attempt, max is base_delay_ms
128    ///
129    /// // 10% jitter - delay will be 90-100% of base_delay_ms
130    /// let delay = policy.calculate_delay(1, 0.1);
131    /// assert!(delay >= 90 && delay <= 100);
132    /// ```
133    #[cfg(feature = "std")]
134    pub fn calculate_delay(&self, attempt: u8, jitter_factor: f64) -> u64 {
135        let mut rng: StdRng = rand::make_rng();
136        self.calculate_delay_with_rng(attempt, jitter_factor, &mut rng)
137    }
138
139    /// Calculate delay with a provided RNG and custom jitter factor
140    ///
141    /// This method allows for custom RNG implementations and jitter control, useful for:
142    /// - Deterministic testing
143    /// - `no_std` environments with custom RNG sources
144    /// - Performance optimization with specific RNG types
145    /// - Fine-tuning jitter behavior
146    ///
147    /// # Arguments
148    ///
149    /// * `attempt` - Current attempt number (1-indexed)
150    /// * `jitter_factor` - Jitter multiplier (0.0 = no jitter, 1.0 = full jitter)
151    /// * `rng` - Random number generator implementing `Rng`
152    ///
153    /// # Returns
154    ///
155    /// Delay in milliseconds as a `u64`
156    pub fn calculate_delay_with_rng<R: Rng>(
157        &self,
158        attempt: u8,
159        jitter_factor: f64,
160        rng: &mut R,
161    ) -> u64 {
162        // Normalize jitter factor to the inclusive range [0.0, 1.0]
163        let mut jitter_factor = jitter_factor;
164        if jitter_factor.is_nan() {
165            jitter_factor = 1.0;
166        } else {
167            jitter_factor = jitter_factor.clamp(0.0, 1.0);
168        }
169
170        // Calculate base exponential backoff
171        let exponent = attempt.saturating_sub(1) as i32;
172        let base_exponential = (self.base_delay_ms as f64) * self.multiplier.powi(exponent);
173
174        // Cap at max_delay
175        let capped = base_exponential.min(self.max_delay_ms as f64);
176
177        // Apply jitter: blend between deterministic and random delay
178        // jitter_factor of 1.0 = full jitter (0 to base), 0.0 = no jitter (exactly base)
179        // Formula: base * (1 - jitter_factor + rand * jitter_factor)
180        // Example with jitter_factor=0.1: base * (0.9 + rand*0.1) = 90% to 100% of base
181        let random_scalar: f64 = rng.random_range(0.0..=1.0);
182        let jitter_blend = 1.0 - jitter_factor + random_scalar * jitter_factor;
183        let jittered = capped * jitter_blend;
184
185        jittered as u64
186    }
187
188    /// Check if another retry should be attempted
189    ///
190    /// # Arguments
191    ///
192    /// * `current_attempt` - Current attempt number (1-indexed)
193    ///
194    /// # Returns
195    ///
196    /// `true` if another retry is allowed, `false` otherwise
197    pub fn should_retry(&self, current_attempt: u8) -> bool {
198        current_attempt < self.max_attempts
199    }
200}
201
202impl Default for Policy {
203    fn default() -> Self {
204        Self::new()
205    }
206}
207
208#[cfg(test)]
209mod tests {
210    use super::*;
211    use rand::rngs::StdRng;
212    use rand::SeedableRng;
213
214    #[test]
215    fn test_policy_default() {
216        let policy = Policy::default();
217        assert_eq!(policy.max_attempts, 3);
218        assert_eq!(policy.base_delay_ms, 100);
219        assert_eq!(policy.multiplier, 2.0);
220        assert_eq!(policy.max_delay_ms, 10_000);
221    }
222
223    #[test]
224    fn test_calculate_delay_bounds() {
225        let policy = Policy {
226            max_attempts: 5,
227            base_delay_ms: 100,
228            multiplier: 2.0,
229            max_delay_ms: 1000,
230        };
231
232        let mut rng = StdRng::seed_from_u64(42);
233
234        // First attempt with full jitter: delay should be between 0 and 100ms
235        let delay1 = policy.calculate_delay_with_rng(1, 1.0, &mut rng);
236        assert!(delay1 <= 100);
237
238        // Second attempt with full jitter: delay should be between 0 and 200ms
239        let delay2 = policy.calculate_delay_with_rng(2, 1.0, &mut rng);
240        assert!(delay2 <= 200);
241
242        // Fifth attempt with full jitter: delay should be capped at max_delay_ms (1000ms)
243        let delay5 = policy.calculate_delay_with_rng(5, 1.0, &mut rng);
244        assert!(delay5 <= 1000);
245    }
246
247    #[test]
248    fn test_should_retry() {
249        let policy = Policy {
250            max_attempts: 3,
251            ..Policy::default()
252        };
253
254        assert!(policy.should_retry(1));
255        assert!(policy.should_retry(2));
256        assert!(!policy.should_retry(3));
257        assert!(!policy.should_retry(4));
258    }
259
260    #[test]
261    fn test_max_delay_cap() {
262        let policy = Policy {
263            max_attempts: 10,
264            base_delay_ms: 100,
265            multiplier: 2.0,
266            max_delay_ms: 500,
267        };
268
269        let mut rng = StdRng::seed_from_u64(42);
270
271        // High attempt number should still be capped
272        let delay = policy.calculate_delay_with_rng(10, 1.0, &mut rng);
273        assert!(delay <= 500);
274    }
275
276    #[test]
277    fn test_zero_multiplier() {
278        let policy = Policy {
279            max_attempts: 5,
280            base_delay_ms: 100,
281            multiplier: 1.0, // No exponential growth
282            max_delay_ms: 10_000,
283        };
284
285        let mut rng = StdRng::seed_from_u64(42);
286
287        // All delays with full jitter should be between 0 and base_delay_ms
288        for attempt in 1..=5 {
289            let delay = policy.calculate_delay_with_rng(attempt, 1.0, &mut rng);
290            assert!(delay <= 100);
291        }
292    }
293
294    #[test]
295    fn test_jitter_factor() {
296        let policy = Policy {
297            max_attempts: 5,
298            base_delay_ms: 1000,
299            multiplier: 1.0,
300            max_delay_ms: 10_000,
301        };
302
303        let mut rng = StdRng::seed_from_u64(42);
304
305        // 10% jitter: delay should be between 900ms (90%) and 1000ms (100%)
306        let delay = policy.calculate_delay_with_rng(1, 0.1, &mut rng);
307        assert!(
308            delay >= 900 && delay <= 1000,
309            "delay {} not in range 900-1000",
310            delay
311        );
312
313        // No jitter: delay should be exactly base_delay_ms
314        let delay = policy.calculate_delay_with_rng(1, 0.0, &mut rng);
315        assert_eq!(delay, 1000);
316
317        // Full jitter: delay should be between 0 and 1000ms
318        let delay = policy.calculate_delay_with_rng(1, 1.0, &mut rng);
319        assert!(delay <= 1000);
320    }
321
322    #[test]
323    fn test_jitter_factor_clamping() {
324        let policy = Policy {
325            max_attempts: 5,
326            base_delay_ms: 1000,
327            multiplier: 1.0,
328            max_delay_ms: 10_000,
329        };
330
331        let mut rng = StdRng::seed_from_u64(42);
332
333        // Negative jitter_factor should be clamped to 0.0
334        let delay = policy.calculate_delay_with_rng(1, -0.5, &mut rng);
335        assert_eq!(delay, 1000, "negative jitter_factor should clamp to 0.0");
336
337        // jitter_factor > 1.0 should be clamped to 1.0
338        let delay = policy.calculate_delay_with_rng(1, 2.0, &mut rng);
339        assert!(
340            delay <= 1000,
341            "jitter_factor > 1.0 should clamp to 1.0, got delay {}",
342            delay
343        );
344
345        // Extreme values should still be clamped
346        let delay = policy.calculate_delay_with_rng(1, 999.0, &mut rng);
347        assert!(delay <= 1000, "extreme jitter_factor should be clamped");
348
349        let delay = policy.calculate_delay_with_rng(1, -999.0, &mut rng);
350        assert_eq!(delay, 1000, "extreme negative should clamp to 0.0");
351    }
352}