use std::time::Duration; use rand::Rng; use tracing::{info, warn}; /// Reconnection strategy with exponential backoff and jitter. pub struct ReconnectStrategy { /// Base delay (default: 1 second). pub base_delay: Duration, /// Maximum delay cap (default: 30 seconds). pub max_delay: Duration, /// Maximum number of attempts before giving up (0 = infinite). pub max_attempts: u32, /// Current attempt counter. attempts: u32, } impl Default for ReconnectStrategy { fn default() -> Self { Self { base_delay: Duration::from_secs(1), max_delay: Duration::from_secs(30), max_attempts: 0, attempts: 0, } } } impl ReconnectStrategy { pub fn new(base_delay: Duration, max_delay: Duration, max_attempts: u32) -> Self { Self { base_delay, max_delay, max_attempts, attempts: 0, } } /// Get the next backoff delay, or None if max attempts exceeded. pub fn next_delay(&mut self) -> Option { if self.max_attempts > 0 && self.attempts >= self.max_attempts { warn!("Max reconnection attempts ({}) exceeded", self.max_attempts); return None; } let base_ms = self.base_delay.as_millis() as u64; let exp_ms = base_ms.saturating_mul(1u64 << self.attempts.min(20)); let max_ms = self.max_delay.as_millis() as u64; let capped_ms = exp_ms.min(max_ms); // Add jitter: ±25% let jitter_range = capped_ms / 4; let jitter = if jitter_range > 0 { rand::thread_rng().gen_range(0..jitter_range * 2) as i64 - jitter_range as i64 } else { 0 }; let final_ms = (capped_ms as i64 + jitter).max(0) as u64; self.attempts += 1; let delay = Duration::from_millis(final_ms); info!( "Reconnect attempt {} in {:?}", self.attempts, delay ); Some(delay) } /// Reset the attempt counter (on successful connection). pub fn reset(&mut self) { self.attempts = 0; } /// Current attempt number. pub fn attempts(&self) -> u32 { self.attempts } } /// Session resume token — opaque blob the client sends to resume a session. #[derive(Debug, Clone)] pub struct SessionToken { pub token: Vec, } impl SessionToken { /// Generate a random session token. pub fn generate() -> Self { let mut token = vec![0u8; 32]; rand::thread_rng().fill(&mut token[..]); Self { token } } pub fn from_bytes(data: Vec) -> Self { Self { token: data } } pub fn as_bytes(&self) -> &[u8] { &self.token } } #[cfg(test)] mod tests { use super::*; #[test] fn exponential_backoff() { let mut strategy = ReconnectStrategy::new( Duration::from_millis(100), Duration::from_secs(5), 5, ); // Should get 5 delays for i in 0..5 { let delay = strategy.next_delay(); assert!(delay.is_some(), "attempt {} should succeed", i); } // 6th should fail assert!(strategy.next_delay().is_none()); } #[test] fn reset_restores_attempts() { let mut strategy = ReconnectStrategy::new( Duration::from_millis(100), Duration::from_secs(5), 2, ); strategy.next_delay(); strategy.next_delay(); assert!(strategy.next_delay().is_none()); strategy.reset(); assert_eq!(strategy.attempts(), 0); assert!(strategy.next_delay().is_some()); } #[test] fn session_token_generation() { let token = SessionToken::generate(); assert_eq!(token.as_bytes().len(), 32); let token2 = SessionToken::generate(); assert_ne!(token.as_bytes(), token2.as_bytes()); // extremely unlikely to be equal } }