use std::time::Duration;
#[derive(Debug, Clone, Copy)]
pub struct SseRetryConfig {
pub max_retries: u32,
pub max_backoff_ms: u32,
pub min_sleep_ms: u32,
pub backoff_multiplier: f32,
pub jitter: bool,
}
impl SseRetryConfig {
#[inline]
#[must_use]
pub const fn new() -> Self {
Self {
max_retries: 20,
max_backoff_ms: 60_000,
min_sleep_ms: 200,
backoff_multiplier: 2.0,
jitter: true,
}
}
#[inline]
#[must_use]
pub const fn disabled() -> Self {
Self {
max_retries: 0,
..Self::new()
}
}
#[must_use]
pub fn calculate_backoff(&self, reconnect_time_ms: u32, attempt: u32) -> Option<Duration> {
self.calculate_backoff_with_factor(reconnect_time_ms, attempt, fastrand::f32())
}
#[must_use]
pub fn calculate_backoff_with_factor(
&self,
reconnect_time_ms: u32,
attempt: u32,
jitter_factor: f32,
) -> Option<Duration> {
if self.max_retries <= attempt {
return None;
}
debug_assert!(self.min_sleep_ms <= self.max_backoff_ms);
let reconnect_time_ms = reconnect_time_ms.max(self.min_sleep_ms) as f32;
let mut sleep_ms = if self.backoff_multiplier.is_finite() && 1.0 <= self.backoff_multiplier
{
reconnect_time_ms
* self
.backoff_multiplier
.powi(attempt.min(i32::MAX as _) as _)
} else {
reconnect_time_ms
};
if !sleep_ms.is_finite() || (self.max_backoff_ms as f32) <= sleep_ms {
sleep_ms = self.max_backoff_ms as _;
}
if self.jitter && reconnect_time_ms < sleep_ms {
let jitter_factor = if jitter_factor.is_finite() && (0.0..=1.0).contains(&jitter_factor)
{
jitter_factor
} else {
1.0
};
sleep_ms = f32::mul_add(
jitter_factor,
sleep_ms - reconnect_time_ms,
reconnect_time_ms,
);
}
Some(Duration::from_millis(sleep_ms as _))
}
}
impl Default for SseRetryConfig {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn basic_backoff() {
let config = SseRetryConfig::new();
let dur = config.calculate_backoff_with_factor(3000, 0, 0.0);
assert!(dur.is_some());
assert_eq!(dur.unwrap(), Duration::from_millis(3000));
}
#[test]
fn disabled_returns_none() {
let config = SseRetryConfig::disabled();
assert!(config.calculate_backoff_with_factor(3000, 0, 0.5).is_none());
}
#[test]
fn respects_max_retries() {
let config = SseRetryConfig {
max_retries: 3,
..SseRetryConfig::new()
};
assert!(config.calculate_backoff_with_factor(1000, 3, 0.0).is_none());
assert!(config.calculate_backoff_with_factor(1000, 2, 0.0).is_some());
}
}