use std::time::Duration;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct RetryConfig {
pub max_attempts: u32,
#[serde(
rename = "initial_delay_ms",
serialize_with = "serialize_duration_ms",
deserialize_with = "deserialize_duration_ms"
)]
pub initial_delay: Duration,
#[serde(
rename = "max_delay_ms",
serialize_with = "serialize_duration_ms",
deserialize_with = "deserialize_duration_ms"
)]
pub max_delay: Duration,
pub multiplier: f64,
}
impl Default for RetryConfig {
fn default() -> Self {
Self {
max_attempts: 3,
initial_delay: Duration::from_millis(100),
max_delay: Duration::from_secs(10),
multiplier: 2.0,
}
}
}
impl RetryConfig {
pub fn builder() -> RetryConfigBuilder {
RetryConfigBuilder::default()
}
pub fn no_retry() -> Self {
Self {
max_attempts: 1,
..Default::default()
}
}
#[allow(
clippy::cast_precision_loss,
clippy::cast_possible_wrap,
clippy::cast_possible_truncation,
clippy::cast_sign_loss
)]
pub fn delay_for_attempt(&self, attempt: u32) -> Duration {
let delay_ms = self.initial_delay.as_millis() as f64 * self.multiplier.powi(attempt as i32);
let delay = Duration::from_millis(delay_ms as u64);
delay.min(self.max_delay)
}
}
#[derive(Debug, Default)]
#[must_use]
pub struct RetryConfigBuilder {
config: RetryConfig,
}
impl RetryConfigBuilder {
pub fn max_attempts(mut self, n: u32) -> Self {
self.config.max_attempts = n;
self
}
pub fn initial_delay(mut self, delay: Duration) -> Self {
self.config.initial_delay = delay;
self
}
pub fn max_delay(mut self, delay: Duration) -> Self {
self.config.max_delay = delay;
self
}
pub fn multiplier(mut self, m: f64) -> Self {
self.config.multiplier = m;
self
}
pub fn build(self) -> RetryConfig {
self.config
}
}
#[allow(clippy::cast_possible_truncation)]
fn serialize_duration_ms<S>(duration: &Duration, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_u64(duration.as_millis() as u64)
}
fn deserialize_duration_ms<'de, D>(deserializer: D) -> Result<Duration, D::Error>
where
D: serde::Deserializer<'de>,
{
let ms = u64::deserialize(deserializer)?;
Ok(Duration::from_millis(ms))
}
#[cfg(test)]
mod tests {
use super::*;
use std::time::Duration;
#[test]
fn retry_config_default_values() {
let config = RetryConfig::default();
assert_eq!(config.max_attempts, 3);
assert_eq!(config.initial_delay, Duration::from_millis(100));
assert_eq!(config.max_delay, Duration::from_secs(10));
assert!((config.multiplier - 2.0_f64).abs() < f64::EPSILON);
}
#[test]
fn retry_config_builder_sets_max_attempts() {
let config = RetryConfig::builder().max_attempts(5).build();
assert_eq!(config.max_attempts, 5);
}
#[test]
fn retry_config_builder_sets_initial_delay() {
let config = RetryConfig::builder()
.initial_delay(Duration::from_millis(500))
.build();
assert_eq!(config.initial_delay, Duration::from_millis(500));
}
#[test]
fn retry_config_builder_sets_max_delay() {
let config = RetryConfig::builder()
.max_delay(Duration::from_secs(30))
.build();
assert_eq!(config.max_delay, Duration::from_secs(30));
}
#[test]
fn retry_config_builder_sets_multiplier() {
let config = RetryConfig::builder().multiplier(1.5).build();
assert!((config.multiplier - 1.5_f64).abs() < f64::EPSILON);
}
#[test]
fn retry_config_delay_for_attempt_increases_exponentially() {
let config = RetryConfig::builder()
.initial_delay(Duration::from_millis(100))
.multiplier(2.0)
.max_delay(Duration::from_secs(10))
.build();
assert_eq!(config.delay_for_attempt(0), Duration::from_millis(100));
assert_eq!(config.delay_for_attempt(1), Duration::from_millis(200));
assert_eq!(config.delay_for_attempt(2), Duration::from_millis(400));
assert_eq!(config.delay_for_attempt(3), Duration::from_millis(800));
}
#[test]
fn retry_config_delay_capped_at_max() {
let config = RetryConfig::builder()
.initial_delay(Duration::from_secs(1))
.multiplier(10.0)
.max_delay(Duration::from_secs(5))
.build();
assert_eq!(config.delay_for_attempt(0), Duration::from_secs(1));
assert_eq!(config.delay_for_attempt(1), Duration::from_secs(5));
assert_eq!(config.delay_for_attempt(2), Duration::from_secs(5));
}
#[test]
fn retry_config_no_retry_returns_single_attempt() {
let config = RetryConfig::no_retry();
assert_eq!(config.max_attempts, 1);
}
#[test]
fn retry_config_serializes_to_json() {
let config = RetryConfig::builder()
.max_attempts(5)
.initial_delay(Duration::from_millis(200))
.build();
let json = serde_json::to_value(&config).unwrap();
assert_eq!(json["max_attempts"], 5);
assert_eq!(json["initial_delay_ms"], 200);
}
#[test]
fn retry_config_deserializes_from_json() {
let json = serde_json::json!({
"max_attempts": 4,
"initial_delay_ms": 500,
"max_delay_ms": 30000,
"multiplier": 1.5
});
let config: RetryConfig = serde_json::from_value(json).unwrap();
assert_eq!(config.max_attempts, 4);
assert_eq!(config.initial_delay, Duration::from_millis(500));
assert_eq!(config.max_delay, Duration::from_secs(30));
assert!((config.multiplier - 1.5_f64).abs() < f64::EPSILON);
}
#[test]
fn retry_config_serde_roundtrip() {
let original = RetryConfig::builder()
.max_attempts(7)
.initial_delay(Duration::from_millis(250))
.max_delay(Duration::from_secs(60))
.multiplier(3.0)
.build();
let json = serde_json::to_string(&original).unwrap();
let deserialized: RetryConfig = serde_json::from_str(&json).unwrap();
assert_eq!(original, deserialized);
}
}