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 =
match self.backoff_multiplier.is_finite() && 1.0 <= self.backoff_multiplier {
true => {
reconnect_time_ms
* self
.backoff_multiplier
.powi(attempt.min(i32::MAX as _) as _)
}
false => 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 =
match jitter_factor.is_finite() && (0.0..=1.0).contains(&jitter_factor) {
true => jitter_factor,
false => 1.0,
};
sleep_ms = reconnect_time_ms + jitter_factor * (sleep_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());
}
}