#![cfg_attr(not(feature = "std"), no_std)]
#![warn(rust_2024_compatibility)]
#![warn(clippy::all)]
#[cfg(feature = "alloc")]
extern crate alloc;
pub mod backoff;
#[cfg(feature = "std")]
pub mod dsl;
#[cfg(any(feature = "std", feature = "alloc"))]
pub mod policy;
pub mod retry;
pub mod sleep;
pub use backoff::{
BackoffPolicy, BackoffStrategy, ConstantBackoff, ExponentialBackoff, FibonacciBackoff,
};
#[cfg(feature = "std")]
pub use dsl::{builder_for_policy, retry_with_policy, DslError};
#[cfg(any(feature = "std", feature = "alloc"))]
pub use policy::PolicyRegistry;
#[cfg(feature = "std")]
pub use policy::{
clear_global_policies, get_global_policy, list_global_policies, register_global_policy,
remove_global_policy,
};
pub use retry::{RetryBuilder, RetryContext, RetryError, RetryOutcome, Retryable, RetryableExt};
#[cfg(feature = "std")]
pub use sleep::StdSleeper;
pub use sleep::{FnSleeper, Sleeper};
#[cfg(feature = "std")]
use rand::rngs::StdRng;
use rand::RngExt;
use rand::Rng;
#[derive(Debug, Clone, Copy)]
pub struct Policy {
pub max_attempts: u8,
pub base_delay_ms: u64,
pub multiplier: f64,
pub max_delay_ms: u64,
}
impl Policy {
pub fn new() -> Self {
Self {
max_attempts: 3,
base_delay_ms: 100,
multiplier: 2.0,
max_delay_ms: 10_000,
}
}
#[cfg(feature = "std")]
pub fn calculate_delay(&self, attempt: u8, jitter_factor: f64) -> u64 {
let mut rng: StdRng = rand::make_rng();
self.calculate_delay_with_rng(attempt, jitter_factor, &mut rng)
}
pub fn calculate_delay_with_rng<R: Rng>(
&self,
attempt: u8,
jitter_factor: f64,
rng: &mut R,
) -> u64 {
let mut jitter_factor = jitter_factor;
if jitter_factor.is_nan() {
jitter_factor = 1.0;
} else {
jitter_factor = jitter_factor.clamp(0.0, 1.0);
}
let exponent = attempt.saturating_sub(1) as i32;
let base_exponential = (self.base_delay_ms as f64) * self.multiplier.powi(exponent);
let capped = base_exponential.min(self.max_delay_ms as f64);
let random_scalar: f64 = rng.random_range(0.0..=1.0);
let jitter_blend = 1.0 - jitter_factor + random_scalar * jitter_factor;
let jittered = capped * jitter_blend;
jittered as u64
}
pub fn should_retry(&self, current_attempt: u8) -> bool {
current_attempt < self.max_attempts
}
}
impl Default for Policy {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use rand::rngs::StdRng;
use rand::SeedableRng;
#[test]
fn test_policy_default() {
let policy = Policy::default();
assert_eq!(policy.max_attempts, 3);
assert_eq!(policy.base_delay_ms, 100);
assert_eq!(policy.multiplier, 2.0);
assert_eq!(policy.max_delay_ms, 10_000);
}
#[test]
fn test_calculate_delay_bounds() {
let policy = Policy {
max_attempts: 5,
base_delay_ms: 100,
multiplier: 2.0,
max_delay_ms: 1000,
};
let mut rng = StdRng::seed_from_u64(42);
let delay1 = policy.calculate_delay_with_rng(1, 1.0, &mut rng);
assert!(delay1 <= 100);
let delay2 = policy.calculate_delay_with_rng(2, 1.0, &mut rng);
assert!(delay2 <= 200);
let delay5 = policy.calculate_delay_with_rng(5, 1.0, &mut rng);
assert!(delay5 <= 1000);
}
#[test]
fn test_should_retry() {
let policy = Policy {
max_attempts: 3,
..Policy::default()
};
assert!(policy.should_retry(1));
assert!(policy.should_retry(2));
assert!(!policy.should_retry(3));
assert!(!policy.should_retry(4));
}
#[test]
fn test_max_delay_cap() {
let policy = Policy {
max_attempts: 10,
base_delay_ms: 100,
multiplier: 2.0,
max_delay_ms: 500,
};
let mut rng = StdRng::seed_from_u64(42);
let delay = policy.calculate_delay_with_rng(10, 1.0, &mut rng);
assert!(delay <= 500);
}
#[test]
fn test_zero_multiplier() {
let policy = Policy {
max_attempts: 5,
base_delay_ms: 100,
multiplier: 1.0, max_delay_ms: 10_000,
};
let mut rng = StdRng::seed_from_u64(42);
for attempt in 1..=5 {
let delay = policy.calculate_delay_with_rng(attempt, 1.0, &mut rng);
assert!(delay <= 100);
}
}
#[test]
fn test_jitter_factor() {
let policy = Policy {
max_attempts: 5,
base_delay_ms: 1000,
multiplier: 1.0,
max_delay_ms: 10_000,
};
let mut rng = StdRng::seed_from_u64(42);
let delay = policy.calculate_delay_with_rng(1, 0.1, &mut rng);
assert!(
delay >= 900 && delay <= 1000,
"delay {} not in range 900-1000",
delay
);
let delay = policy.calculate_delay_with_rng(1, 0.0, &mut rng);
assert_eq!(delay, 1000);
let delay = policy.calculate_delay_with_rng(1, 1.0, &mut rng);
assert!(delay <= 1000);
}
#[test]
fn test_jitter_factor_clamping() {
let policy = Policy {
max_attempts: 5,
base_delay_ms: 1000,
multiplier: 1.0,
max_delay_ms: 10_000,
};
let mut rng = StdRng::seed_from_u64(42);
let delay = policy.calculate_delay_with_rng(1, -0.5, &mut rng);
assert_eq!(delay, 1000, "negative jitter_factor should clamp to 0.0");
let delay = policy.calculate_delay_with_rng(1, 2.0, &mut rng);
assert!(
delay <= 1000,
"jitter_factor > 1.0 should clamp to 1.0, got delay {}",
delay
);
let delay = policy.calculate_delay_with_rng(1, 999.0, &mut rng);
assert!(delay <= 1000, "extreme jitter_factor should be clamped");
let delay = policy.calculate_delay_with_rng(1, -999.0, &mut rng);
assert_eq!(delay, 1000, "extreme negative should clamp to 0.0");
}
}