use crate::error::Error;
pub const DEFAULT_MAX_RECONNECT_ATTEMPTS: u32 = 5;
pub const DEFAULT_INITIAL_BACKOFF_MS: u64 = 500;
pub const DEFAULT_MAX_BACKOFF_MS: u64 = 30_000;
pub const DEFAULT_BACKOFF_MULTIPLIER: f64 = 2.0;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct RetryPolicy {
pub max_reconnect_attempts: u32,
pub initial_backoff_ms: u64,
pub max_backoff_ms: u64,
pub backoff_multiplier: f64,
}
impl Default for RetryPolicy {
fn default() -> Self {
Self {
max_reconnect_attempts: DEFAULT_MAX_RECONNECT_ATTEMPTS,
initial_backoff_ms: DEFAULT_INITIAL_BACKOFF_MS,
max_backoff_ms: DEFAULT_MAX_BACKOFF_MS,
backoff_multiplier: DEFAULT_BACKOFF_MULTIPLIER,
}
}
}
impl RetryPolicy {
pub fn new(
max_reconnect_attempts: u32,
initial_backoff_ms: u64,
max_backoff_ms: u64,
backoff_multiplier: f64,
) -> Result<Self, Error> {
if max_backoff_ms < initial_backoff_ms {
return Err(Error::Protocol(format!(
"max_backoff_ms must be >= initial_backoff_ms ({max_backoff_ms} < {initial_backoff_ms})"
)));
}
if backoff_multiplier < 1.0 {
return Err(Error::Protocol(format!(
"backoff_multiplier must be >= 1.0, got {backoff_multiplier}"
)));
}
Ok(Self {
max_reconnect_attempts,
initial_backoff_ms,
max_backoff_ms,
backoff_multiplier,
})
}
pub fn backoff_ms(&self, attempt: u32) -> u64 {
let exponent = attempt.saturating_sub(1);
let raw = self.initial_backoff_ms as f64 * self.backoff_multiplier.powi(exponent as i32);
let capped = raw.min(self.max_backoff_ms as f64);
if capped.is_finite() && capped >= 0.0 {
capped as u64
} else {
self.max_backoff_ms
}
}
}
pub fn default_retry_policy() -> RetryPolicy {
RetryPolicy::default()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn defaults_match_the_python_spec() {
let p = RetryPolicy::default();
assert_eq!(p.max_reconnect_attempts, 5);
assert_eq!(p.initial_backoff_ms, 500);
assert_eq!(p.max_backoff_ms, 30_000);
assert_eq!(p.backoff_multiplier, 2.0);
}
#[test]
fn backoff_progression_doubles_and_caps() {
let p = RetryPolicy::default();
assert_eq!(p.backoff_ms(1), 500);
assert_eq!(p.backoff_ms(2), 1000);
assert_eq!(p.backoff_ms(3), 2000);
assert_eq!(p.backoff_ms(4), 4000);
assert_eq!(p.backoff_ms(5), 8000);
assert_eq!(p.backoff_ms(20), 30_000);
}
#[test]
fn backoff_attempt_zero_clamps_to_first() {
assert_eq!(RetryPolicy::default().backoff_ms(0), 500);
}
#[test]
fn constant_backoff_with_multiplier_one() {
let p = RetryPolicy::new(3, 250, 1000, 1.0).unwrap();
assert_eq!(p.backoff_ms(1), 250);
assert_eq!(p.backoff_ms(5), 250);
}
#[test]
fn rejects_max_below_initial() {
let err = RetryPolicy::new(5, 1000, 500, 2.0).unwrap_err();
assert!(format!("{err}").contains("max_backoff_ms must be >= initial_backoff_ms"));
}
#[test]
fn rejects_multiplier_below_one() {
let err = RetryPolicy::new(5, 500, 30_000, 0.5).unwrap_err();
assert!(format!("{err}").contains("backoff_multiplier must be >= 1.0"));
}
#[test]
fn zero_attempts_is_allowed() {
assert_eq!(
RetryPolicy::new(0, 500, 30_000, 2.0)
.unwrap()
.max_reconnect_attempts,
0
);
}
}