use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema)]
pub struct EventRetryConfig {
pub max_attempts: u32,
pub initial_backoff_ms: u64,
pub max_backoff_ms: u64,
pub backoff_multiplier: f64,
}
impl Default for EventRetryConfig {
fn default() -> Self {
Self {
max_attempts: 3,
initial_backoff_ms: 100,
max_backoff_ms: 30_000,
backoff_multiplier: 2.0,
}
}
}
impl EventRetryConfig {
#[must_use]
pub fn no_retry() -> Self {
Self {
max_attempts: 1,
initial_backoff_ms: 0,
max_backoff_ms: 0,
backoff_multiplier: 1.0,
}
}
#[must_use]
#[allow(
clippy::cast_precision_loss,
clippy::cast_possible_truncation,
clippy::cast_sign_loss
)]
pub fn compute_delay_ms(&self, attempt: u32) -> u64 {
let raw = (self.initial_backoff_ms as f64)
* self
.backoff_multiplier
.powi(i32::try_from(attempt).unwrap_or(i32::MAX));
raw.min(self.max_backoff_ms as f64) as u64
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_default_matches_spec() {
let cfg = EventRetryConfig::default();
assert_eq!(cfg.max_attempts, 3);
assert_eq!(cfg.initial_backoff_ms, 100);
assert_eq!(cfg.max_backoff_ms, 30_000);
assert!((cfg.backoff_multiplier - 2.0).abs() < f64::EPSILON);
}
#[test]
fn test_no_retry_helper_is_single_attempt() {
let cfg = EventRetryConfig::no_retry();
assert_eq!(cfg.max_attempts, 1);
}
#[test]
fn test_compute_delay_ms_exponential() {
let cfg = EventRetryConfig {
max_attempts: 5,
initial_backoff_ms: 100,
max_backoff_ms: 30_000,
backoff_multiplier: 2.0,
};
assert_eq!(cfg.compute_delay_ms(0), 100);
assert_eq!(cfg.compute_delay_ms(1), 200);
assert_eq!(cfg.compute_delay_ms(2), 400);
assert_eq!(cfg.compute_delay_ms(3), 800);
}
#[test]
fn test_compute_delay_ms_caps_at_max() {
let cfg = EventRetryConfig {
max_attempts: 10,
initial_backoff_ms: 1000,
max_backoff_ms: 5_000,
backoff_multiplier: 2.0,
};
assert_eq!(cfg.compute_delay_ms(3), 5_000);
}
}