use std::sync::atomic::{AtomicBool, AtomicU8, Ordering};
use tokio::time::{Duration, Instant};
use crate::log_transport;
use crate::logging::LogLevel;
#[derive(Debug, Clone)]
pub struct HeartbeatConfig {
pub interval: Duration,
pub timeout: Duration,
pub max_failures: u8,
}
impl Default for HeartbeatConfig {
fn default() -> Self {
Self {
interval: Duration::from_secs(60),
timeout: Duration::from_secs(10),
max_failures: 3,
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct HeartbeatEvent {
pub ok: bool,
pub latency_ms: Option<u64>,
}
pub struct HeartbeatMonitor {
config: HeartbeatConfig,
channel_id: u8,
label: String,
consecutive_failures: AtomicU8,
tunnel_lost: AtomicBool,
last_success: std::sync::RwLock<Option<Instant>>,
}
impl HeartbeatMonitor {
#[must_use]
pub fn new(config: HeartbeatConfig, channel_id: u8, label: String) -> Self {
Self {
config,
channel_id,
label,
consecutive_failures: AtomicU8::new(0),
tunnel_lost: AtomicBool::new(false),
last_success: std::sync::RwLock::new(None),
}
}
pub fn config(&self) -> &HeartbeatConfig {
&self.config
}
pub fn is_tunnel_lost(&self) -> bool {
self.tunnel_lost.load(Ordering::SeqCst)
}
pub fn consecutive_failures(&self) -> u8 {
self.consecutive_failures.load(Ordering::SeqCst)
}
pub fn last_success(&self) -> Option<Instant> {
*self.last_success.read().unwrap()
}
pub fn record_success(&self) {
self.consecutive_failures.store(0, Ordering::SeqCst);
*self.last_success.write().unwrap() = Some(Instant::now());
log_transport!(
LogLevel::Debug,
"[{}] Heartbeat OK (channel {})",
self.label,
self.channel_id
);
}
pub fn record_failure(&self) -> bool {
let failures = self.consecutive_failures.fetch_add(1, Ordering::SeqCst) + 1;
log_transport!(
LogLevel::Warn,
"[{}] Heartbeat failed (channel {}, attempt {}/{})",
self.label,
self.channel_id,
failures,
self.config.max_failures
);
if failures >= self.config.max_failures {
self.tunnel_lost.store(true, Ordering::SeqCst);
log_transport!(
LogLevel::Error,
"[{}] Tunnel lost: {} consecutive heartbeat failures (channel {})",
self.label,
failures,
self.channel_id
);
return true;
}
false
}
}