use std::time::{Duration, Instant};
#[derive(Debug, Clone)]
pub struct KeepaliveConfig {
pub interval: Duration,
pub max_missed: u32,
pub enabled: bool,
pub use_ssh_keepalive: bool,
pub response_timeout: Duration,
}
impl Default for KeepaliveConfig {
fn default() -> Self {
Self {
interval: Duration::from_secs(30),
max_missed: 3,
enabled: true,
use_ssh_keepalive: true,
response_timeout: Duration::from_secs(15),
}
}
}
impl KeepaliveConfig {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub const fn interval(mut self, interval: Duration) -> Self {
self.interval = interval;
self
}
#[must_use]
pub const fn max_missed(mut self, max: u32) -> Self {
self.max_missed = max;
self
}
#[must_use]
pub const fn enabled(mut self, enabled: bool) -> Self {
self.enabled = enabled;
self
}
#[must_use]
pub const fn response_timeout(mut self, timeout: Duration) -> Self {
self.response_timeout = timeout;
self
}
#[must_use]
pub const fn use_ssh_keepalive(mut self, use_ssh: bool) -> Self {
self.use_ssh_keepalive = use_ssh;
self
}
#[must_use]
pub fn disabled() -> Self {
Self {
enabled: false,
..Default::default()
}
}
#[must_use]
pub fn high_latency() -> Self {
Self {
interval: Duration::from_secs(60),
max_missed: 5,
response_timeout: Duration::from_secs(30),
..Default::default()
}
}
#[must_use]
pub fn aggressive() -> Self {
Self {
interval: Duration::from_secs(15),
max_missed: 2,
response_timeout: Duration::from_secs(10),
..Default::default()
}
}
}
#[derive(Debug)]
pub struct KeepaliveState {
config: KeepaliveConfig,
last_sent: Option<Instant>,
last_received: Option<Instant>,
missed_count: u32,
alive: bool,
pending_response: bool,
pending_since: Option<Instant>,
total_sent: u64,
total_received: u64,
}
impl KeepaliveState {
#[must_use]
pub const fn new(config: KeepaliveConfig) -> Self {
Self {
config,
last_sent: None,
last_received: None,
missed_count: 0,
alive: true,
pending_response: false,
pending_since: None,
total_sent: 0,
total_received: 0,
}
}
#[must_use]
pub const fn is_enabled(&self) -> bool {
self.config.enabled
}
#[must_use]
pub const fn is_alive(&self) -> bool {
self.alive
}
#[must_use]
pub fn is_keepalive_due(&self) -> bool {
if !self.config.enabled {
return false;
}
if self.pending_response {
return false;
}
match self.last_sent {
Some(last) => last.elapsed() >= self.config.interval,
None => true,
}
}
#[must_use]
pub fn is_response_timed_out(&self) -> bool {
if !self.pending_response {
return false;
}
self.pending_since
.is_some_and(|t| t.elapsed() >= self.config.response_timeout)
}
pub fn record_sent(&mut self) {
let now = Instant::now();
self.last_sent = Some(now);
self.pending_response = true;
self.pending_since = Some(now);
self.total_sent += 1;
}
pub fn record_received(&mut self) {
self.last_received = Some(Instant::now());
self.missed_count = 0;
self.alive = true;
self.pending_response = false;
self.pending_since = None;
self.total_received += 1;
}
pub const fn record_missed(&mut self) {
self.missed_count += 1;
self.pending_response = false;
self.pending_since = None;
if self.missed_count >= self.config.max_missed {
self.alive = false;
}
}
#[must_use]
pub const fn missed_count(&self) -> u32 {
self.missed_count
}
#[must_use]
pub const fn total_sent(&self) -> u64 {
self.total_sent
}
#[must_use]
pub const fn total_received(&self) -> u64 {
self.total_received
}
#[must_use]
pub const fn is_pending(&self) -> bool {
self.pending_response
}
#[must_use]
pub fn time_since_activity(&self) -> Duration {
self.last_received
.or(self.last_sent)
.map_or(Duration::ZERO, |t| t.elapsed())
}
#[must_use]
pub fn time_since_response(&self) -> Option<Duration> {
self.last_received.map(|t| t.elapsed())
}
#[must_use]
pub const fn config(&self) -> &KeepaliveConfig {
&self.config
}
pub const fn reset(&mut self) {
self.last_sent = None;
self.last_received = None;
self.missed_count = 0;
self.alive = true;
self.pending_response = false;
self.pending_since = None;
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum KeepaliveAction {
None,
SendKeepalive,
Timeout,
Disconnect,
}
#[derive(Debug)]
pub struct KeepaliveManager {
state: KeepaliveState,
}
impl KeepaliveManager {
#[must_use]
pub const fn new(config: KeepaliveConfig) -> Self {
Self {
state: KeepaliveState::new(config),
}
}
#[must_use]
pub fn disabled() -> Self {
Self::new(KeepaliveConfig::disabled())
}
#[must_use]
pub const fn state(&self) -> &KeepaliveState {
&self.state
}
pub const fn state_mut(&mut self) -> &mut KeepaliveState {
&mut self.state
}
#[must_use]
pub const fn is_enabled(&self) -> bool {
self.state.is_enabled()
}
#[must_use]
pub const fn is_alive(&self) -> bool {
self.state.is_alive()
}
#[must_use]
pub fn is_due(&self) -> bool {
self.state.is_keepalive_due()
}
#[must_use]
pub fn tick(&mut self) -> KeepaliveAction {
if !self.state.is_enabled() {
return KeepaliveAction::None;
}
if !self.state.is_alive() {
return KeepaliveAction::Disconnect;
}
if self.state.is_response_timed_out() {
return KeepaliveAction::Timeout;
}
if self.state.is_keepalive_due() {
return KeepaliveAction::SendKeepalive;
}
KeepaliveAction::None
}
pub fn record_sent(&mut self) {
self.state.record_sent();
}
pub fn record_response(&mut self) {
self.state.record_received();
}
pub const fn record_timeout(&mut self) {
self.state.record_missed();
}
pub fn handle_response(&mut self) {
self.record_response();
}
pub const fn handle_timeout(&mut self) {
self.record_timeout();
}
#[must_use]
pub const fn should_disconnect(&self) -> bool {
!self.state.is_alive()
}
#[must_use]
pub fn stats(&self) -> KeepaliveStats {
KeepaliveStats {
total_sent: self.state.total_sent(),
total_received: self.state.total_received(),
missed_count: self.state.missed_count(),
is_alive: self.state.is_alive(),
time_since_activity: self.state.time_since_activity(),
}
}
pub const fn reset(&mut self) {
self.state.reset();
}
}
#[derive(Debug, Clone)]
pub struct KeepaliveStats {
pub total_sent: u64,
pub total_received: u64,
pub missed_count: u32,
pub is_alive: bool,
pub time_since_activity: Duration,
}
impl KeepaliveStats {
#[must_use]
pub fn success_rate(&self) -> f64 {
if self.total_sent == 0 {
1.0
} else {
self.total_received as f64 / self.total_sent as f64
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn keepalive_config_builder() {
let config = KeepaliveConfig::new()
.interval(Duration::from_secs(60))
.max_missed(5)
.response_timeout(Duration::from_secs(20))
.use_ssh_keepalive(true)
.enabled(true);
assert_eq!(config.interval, Duration::from_secs(60));
assert_eq!(config.max_missed, 5);
assert_eq!(config.response_timeout, Duration::from_secs(20));
assert!(config.use_ssh_keepalive);
assert!(config.enabled);
}
#[test]
fn keepalive_config_presets() {
let disabled = KeepaliveConfig::disabled();
assert!(!disabled.enabled);
let high_latency = KeepaliveConfig::high_latency();
assert_eq!(high_latency.interval, Duration::from_secs(60));
assert_eq!(high_latency.max_missed, 5);
let aggressive = KeepaliveConfig::aggressive();
assert_eq!(aggressive.interval, Duration::from_secs(15));
assert_eq!(aggressive.max_missed, 2);
}
#[test]
fn keepalive_state_lifecycle() {
let config = KeepaliveConfig::new().max_missed(2);
let mut state = KeepaliveState::new(config);
assert!(state.is_alive());
assert!(!state.is_pending());
assert_eq!(state.total_sent(), 0);
state.record_sent();
assert!(state.is_pending());
assert_eq!(state.total_sent(), 1);
state.record_received();
assert!(!state.is_pending());
assert_eq!(state.total_received(), 1);
assert!(state.is_alive());
}
#[test]
fn keepalive_state_missed() {
let config = KeepaliveConfig::new().max_missed(2);
let mut state = KeepaliveState::new(config);
assert!(state.is_alive());
state.record_missed();
assert!(state.is_alive());
assert_eq!(state.missed_count(), 1);
state.record_missed();
assert!(!state.is_alive());
assert_eq!(state.missed_count(), 2);
}
#[test]
fn keepalive_state_recovery() {
let config = KeepaliveConfig::new().max_missed(2);
let mut state = KeepaliveState::new(config);
state.record_missed();
state.record_received(); assert_eq!(state.missed_count(), 0);
assert!(state.is_alive());
}
#[test]
fn keepalive_manager_disabled() {
let mut manager = KeepaliveManager::disabled();
assert!(!manager.is_enabled());
assert_eq!(manager.tick(), KeepaliveAction::None);
}
#[test]
fn keepalive_manager_tick() {
let config = KeepaliveConfig::new().interval(Duration::from_millis(10));
let mut manager = KeepaliveManager::new(config);
assert_eq!(manager.tick(), KeepaliveAction::SendKeepalive);
manager.record_sent();
assert_eq!(manager.tick(), KeepaliveAction::None);
}
#[test]
fn keepalive_manager_stats() {
let config = KeepaliveConfig::new();
let mut manager = KeepaliveManager::new(config);
manager.record_sent();
manager.record_response();
let stats = manager.stats();
assert_eq!(stats.total_sent, 1);
assert_eq!(stats.total_received, 1);
assert!(stats.is_alive);
assert!((stats.success_rate() - 1.0).abs() < f64::EPSILON);
}
#[test]
fn keepalive_stats_success_rate() {
let stats = KeepaliveStats {
total_sent: 10,
total_received: 8,
missed_count: 0,
is_alive: true,
time_since_activity: Duration::ZERO,
};
assert!((stats.success_rate() - 0.8).abs() < f64::EPSILON);
let empty_stats = KeepaliveStats {
total_sent: 0,
total_received: 0,
missed_count: 0,
is_alive: true,
time_since_activity: Duration::ZERO,
};
assert!((empty_stats.success_rate() - 1.0).abs() < f64::EPSILON);
}
#[test]
fn keepalive_action_variants() {
assert_eq!(KeepaliveAction::None, KeepaliveAction::None);
assert_eq!(
KeepaliveAction::SendKeepalive,
KeepaliveAction::SendKeepalive
);
assert_eq!(KeepaliveAction::Timeout, KeepaliveAction::Timeout);
assert_eq!(KeepaliveAction::Disconnect, KeepaliveAction::Disconnect);
}
#[test]
fn keepalive_manager_reset() {
let config = KeepaliveConfig::new().max_missed(1);
let mut manager = KeepaliveManager::new(config);
manager.record_sent();
manager.record_timeout();
assert!(!manager.is_alive());
manager.reset();
assert!(manager.is_alive());
assert!(!manager.state().is_pending());
}
}