use crate::options::SocketOptions;
use std::time::Duration;
#[derive(Debug, Clone)]
pub struct ReconnectState {
base_interval: Duration,
max_interval: Duration,
attempt: u32,
current_interval: Duration,
}
impl ReconnectState {
pub const fn new(options: &SocketOptions) -> Self {
Self {
base_interval: options.reconnect_ivl,
max_interval: options.reconnect_ivl_max,
attempt: 0,
current_interval: options.reconnect_ivl,
}
}
pub fn next_delay(&mut self) -> Duration {
let delay = self.current_interval;
self.attempt += 1;
if self.max_interval.is_zero() || self.max_interval <= self.base_interval {
self.current_interval = self.base_interval;
} else {
self.current_interval = self
.base_interval
.checked_mul(1_u32 << self.attempt.min(10))
.unwrap_or(self.max_interval)
.min(self.max_interval);
}
delay
}
pub fn reset(&mut self) {
self.attempt = 0;
self.current_interval = self.base_interval;
}
#[inline]
#[must_use]
pub const fn attempt(&self) -> u32 {
self.attempt
}
#[inline]
#[must_use]
pub const fn base_interval(&self) -> Duration {
self.base_interval
}
#[inline]
#[must_use]
pub const fn max_interval(&self) -> Duration {
self.max_interval
}
#[inline]
#[must_use]
pub const fn current_interval(&self) -> Duration {
self.current_interval
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ReconnectError {
MaxAttemptsReached { attempts: u32 },
ConnectionFailed { message: String },
Cancelled,
}
impl std::fmt::Display for ReconnectError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::MaxAttemptsReached { attempts } => {
write!(f, "Maximum reconnection attempts reached: {attempts}")
}
Self::ConnectionFailed { message } => {
write!(f, "Connection failed: {message}")
}
Self::Cancelled => {
write!(f, "Reconnection cancelled")
}
}
}
}
impl std::error::Error for ReconnectError {}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_exponential_backoff() {
let options = SocketOptions::default()
.with_reconnect_ivl(Duration::from_millis(100))
.with_reconnect_ivl_max(Duration::from_secs(10));
let mut state = ReconnectState::new(&options);
assert_eq!(state.next_delay(), Duration::from_millis(100));
assert_eq!(state.attempt(), 1);
assert_eq!(state.next_delay(), Duration::from_millis(200));
assert_eq!(state.attempt(), 2);
assert_eq!(state.next_delay(), Duration::from_millis(400));
assert_eq!(state.attempt(), 3);
assert_eq!(state.next_delay(), Duration::from_millis(800));
assert_eq!(state.attempt(), 4);
}
#[test]
fn test_max_interval_cap() {
let options = SocketOptions::default()
.with_reconnect_ivl(Duration::from_millis(100))
.with_reconnect_ivl_max(Duration::from_millis(500));
let mut state = ReconnectState::new(&options);
assert_eq!(state.next_delay(), Duration::from_millis(100));
assert_eq!(state.next_delay(), Duration::from_millis(200));
assert_eq!(state.next_delay(), Duration::from_millis(400));
assert_eq!(state.next_delay(), Duration::from_millis(500));
assert_eq!(state.next_delay(), Duration::from_millis(500));
}
#[test]
fn test_next_delay_caps_before_duration_overflow() {
let options = SocketOptions::default()
.with_reconnect_ivl(Duration::MAX)
.with_reconnect_ivl_max(Duration::from_secs(1));
let mut state = ReconnectState::new(&options);
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
state.next_delay();
}));
assert!(result.is_ok());
}
#[test]
fn test_max_zero_holds_at_base_no_zero_delay_loop() {
let options = SocketOptions::default()
.with_reconnect_ivl(Duration::from_millis(100))
.with_reconnect_ivl_max(Duration::ZERO);
let mut state = ReconnectState::new(&options);
for _ in 0..10 {
assert_eq!(
state.next_delay(),
Duration::from_millis(100),
"max=0 must hold the delay at the base interval, never 0"
);
}
}
#[test]
fn test_max_below_base_holds_at_base() {
let options = SocketOptions::default()
.with_reconnect_ivl(Duration::from_millis(100))
.with_reconnect_ivl_max(Duration::from_millis(50));
let mut state = ReconnectState::new(&options);
for _ in 0..5 {
assert_eq!(state.next_delay(), Duration::from_millis(100));
}
}
#[test]
fn test_reset() {
let options = SocketOptions::default()
.with_reconnect_ivl(Duration::from_millis(100))
.with_reconnect_ivl_max(Duration::from_secs(10));
let mut state = ReconnectState::new(&options);
state.next_delay();
state.next_delay();
state.next_delay();
assert_eq!(state.attempt(), 3);
state.reset();
assert_eq!(state.attempt(), 0);
assert_eq!(state.next_delay(), Duration::from_millis(100));
}
#[test]
fn test_state_accessors() {
let options = SocketOptions::default()
.with_reconnect_ivl(Duration::from_millis(250))
.with_reconnect_ivl_max(Duration::from_secs(5));
let state = ReconnectState::new(&options);
assert_eq!(state.base_interval(), Duration::from_millis(250));
assert_eq!(state.max_interval(), Duration::from_secs(5));
assert_eq!(state.current_interval(), Duration::from_millis(250));
assert_eq!(state.attempt(), 0);
}
}