use rand::Rng;
use std::time::{Duration, Instant};
use crate::config::ReconnectPolicy;
pub struct ReconnectionManager {
policy: ReconnectPolicy,
current_delay: Duration,
attempts: usize,
last_attempt: Option<Instant>,
}
impl ReconnectionManager {
pub fn new(policy: ReconnectPolicy) -> Self {
Self {
current_delay: policy.initial_delay,
policy,
attempts: 0,
last_attempt: None,
}
}
pub fn should_reconnect(&self) -> bool {
if !self.policy.enabled {
return false;
}
if let Some(max) = self.policy.max_attempts {
if self.attempts >= max {
return false;
}
}
true
}
pub fn next_delay(&mut self) -> Duration {
let delay = self.current_delay;
let next_delay_secs = (self.current_delay.as_secs_f64() * self.policy.backoff_multiplier)
.min(self.policy.max_delay.as_secs_f64());
self.current_delay = Duration::from_secs_f64(next_delay_secs);
let jitter_range = delay.as_secs_f64() * self.policy.jitter;
let jitter = if jitter_range > 0.0 {
rand::thread_rng().gen_range(-jitter_range..jitter_range)
} else {
0.0
};
let final_delay = Duration::from_secs_f64((delay.as_secs_f64() + jitter).max(0.0));
self.attempts += 1;
self.last_attempt = Some(Instant::now());
final_delay
}
pub fn reset(&mut self) {
self.current_delay = self.policy.initial_delay;
self.attempts = 0;
}
pub fn attempts(&self) -> usize {
self.attempts
}
pub fn time_since_last_attempt(&self) -> Option<Duration> {
self.last_attempt.map(|t| t.elapsed())
}
pub fn is_exhausted(&self) -> bool {
if let Some(max) = self.policy.max_attempts {
self.attempts >= max
} else {
false
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_default_policy() {
let policy = ReconnectPolicy::default();
let mut manager = ReconnectionManager::new(policy);
assert!(manager.should_reconnect());
assert_eq!(manager.attempts(), 0);
let delay1 = manager.next_delay();
assert!(delay1 >= Duration::from_millis(900)); assert!(delay1 <= Duration::from_millis(1100));
assert_eq!(manager.attempts(), 1);
}
#[test]
fn test_exponential_backoff() {
let policy = ReconnectPolicy {
enabled: true,
initial_delay: Duration::from_secs(1),
max_delay: Duration::from_secs(60),
backoff_multiplier: 2.0,
max_attempts: None,
jitter: 0.0, };
let mut manager = ReconnectionManager::new(policy);
assert_eq!(manager.next_delay(), Duration::from_secs(1));
assert_eq!(manager.next_delay(), Duration::from_secs(2));
assert_eq!(manager.next_delay(), Duration::from_secs(4));
assert_eq!(manager.next_delay(), Duration::from_secs(8));
}
#[test]
fn test_max_delay_cap() {
let policy = ReconnectPolicy {
enabled: true,
initial_delay: Duration::from_secs(30),
max_delay: Duration::from_secs(60),
backoff_multiplier: 2.0,
max_attempts: None,
jitter: 0.0,
};
let mut manager = ReconnectionManager::new(policy);
assert_eq!(manager.next_delay(), Duration::from_secs(30));
assert_eq!(manager.next_delay(), Duration::from_secs(60)); assert_eq!(manager.next_delay(), Duration::from_secs(60)); }
#[test]
fn test_max_attempts() {
let policy = ReconnectPolicy {
enabled: true,
initial_delay: Duration::from_secs(1),
max_delay: Duration::from_secs(60),
backoff_multiplier: 2.0,
max_attempts: Some(3),
jitter: 0.0,
};
let mut manager = ReconnectionManager::new(policy);
assert!(manager.should_reconnect());
manager.next_delay();
assert!(manager.should_reconnect());
manager.next_delay();
assert!(manager.should_reconnect());
manager.next_delay();
assert!(!manager.should_reconnect()); assert!(manager.is_exhausted());
}
#[test]
fn test_reset() {
let policy = ReconnectPolicy {
enabled: true,
initial_delay: Duration::from_secs(1),
max_delay: Duration::from_secs(60),
backoff_multiplier: 2.0,
max_attempts: Some(3),
jitter: 0.0,
};
let mut manager = ReconnectionManager::new(policy);
manager.next_delay();
manager.next_delay();
assert_eq!(manager.attempts(), 2);
manager.reset();
assert_eq!(manager.attempts(), 0);
assert!(manager.should_reconnect());
assert_eq!(manager.next_delay(), Duration::from_secs(1));
}
#[test]
fn test_disabled_policy() {
let policy = ReconnectPolicy {
enabled: false,
..Default::default()
};
let manager = ReconnectionManager::new(policy);
assert!(!manager.should_reconnect());
}
}