use rand::RngExt;
use std::time::Duration;
#[derive(Debug, PartialEq)]
pub enum RetryDelayStrategy {
None,
Fixed {
delay: Duration,
},
Random {
min_delay: Duration,
max_delay: Duration,
},
ExponentialBackoff {
initial_delay: Duration,
max_delay: Duration,
multiplier: f64,
},
}
impl RetryDelayStrategy {
#[inline]
pub fn none() -> Self {
RetryDelayStrategy::None
}
#[inline]
pub fn fixed(delay: Duration) -> Self {
RetryDelayStrategy::Fixed { delay }
}
#[inline]
pub fn random(min_delay: Duration, max_delay: Duration) -> Self {
RetryDelayStrategy::Random {
min_delay,
max_delay,
}
}
#[inline]
pub fn exponential_backoff(
initial_delay: Duration,
max_delay: Duration,
multiplier: f64,
) -> Self {
RetryDelayStrategy::ExponentialBackoff {
initial_delay,
max_delay,
multiplier,
}
}
pub fn calculate_delay(&self, attempt: u32, jitter_factor: f64) -> Duration {
let base_delay = match self {
RetryDelayStrategy::None => Duration::ZERO,
RetryDelayStrategy::Fixed { delay } => *delay,
RetryDelayStrategy::Random {
min_delay,
max_delay,
} => {
let mut rng = rand::rng();
let min_nanos = min_delay.as_nanos() as u64;
let max_nanos = max_delay.as_nanos() as u64;
let random_nanos = rng.random_range(min_nanos..=max_nanos);
Duration::from_nanos(random_nanos)
}
RetryDelayStrategy::ExponentialBackoff {
initial_delay,
max_delay,
multiplier,
} => {
let delay_nanos = initial_delay.as_nanos() as f64;
let calculated_nanos = delay_nanos * multiplier.powi((attempt - 1) as i32);
let max_nanos = max_delay.as_nanos() as f64;
let final_nanos = calculated_nanos.min(max_nanos);
Duration::from_nanos(final_nanos as u64)
}
};
if jitter_factor > 0.0 && base_delay > Duration::ZERO {
let mut rng = rand::rng();
let jitter_range = base_delay.as_nanos() as f64 * jitter_factor;
let jitter_nanos = rng.random_range(0.0..=jitter_range);
let total_nanos = base_delay.as_nanos() as f64 + jitter_nanos;
Duration::from_nanos(total_nanos as u64)
} else {
base_delay
}
}
pub fn validate(&self) -> Result<(), String> {
match self {
RetryDelayStrategy::None => Ok(()),
RetryDelayStrategy::Fixed { delay } => {
if delay.is_zero() {
Err("Fixed delay cannot be zero".to_string())
} else {
Ok(())
}
}
RetryDelayStrategy::Random {
min_delay,
max_delay,
} => {
if min_delay.is_zero() {
Err("Random delay minimum cannot be zero".to_string())
} else if *min_delay >= *max_delay {
Err("Random delay minimum must be less than maximum".to_string())
} else {
Ok(())
}
}
RetryDelayStrategy::ExponentialBackoff {
initial_delay,
max_delay,
multiplier,
} => {
if initial_delay.is_zero() {
Err("Exponential backoff initial delay cannot be zero".to_string())
} else if *initial_delay >= *max_delay {
Err(
"Exponential backoff initial delay must be less than maximum delay"
.to_string(),
)
} else if *multiplier <= 1.0 {
Err("Exponential backoff multiplier must be greater than 1.0".to_string())
} else {
Ok(())
}
}
}
}
}
impl Clone for RetryDelayStrategy {
fn clone(&self) -> Self {
match self {
RetryDelayStrategy::None => RetryDelayStrategy::None,
RetryDelayStrategy::Fixed { delay } => RetryDelayStrategy::Fixed { delay: *delay },
RetryDelayStrategy::Random {
min_delay,
max_delay,
} => RetryDelayStrategy::Random {
min_delay: *min_delay,
max_delay: *max_delay,
},
RetryDelayStrategy::ExponentialBackoff {
initial_delay,
max_delay,
multiplier,
} => RetryDelayStrategy::ExponentialBackoff {
initial_delay: *initial_delay,
max_delay: *max_delay,
multiplier: *multiplier,
},
}
}
}
impl Default for RetryDelayStrategy {
fn default() -> Self {
RetryDelayStrategy::ExponentialBackoff {
initial_delay: Duration::from_millis(1000),
max_delay: Duration::from_secs(60),
multiplier: 2.0,
}
}
}