use actix_ws::Session;
use dashmap::DashMap;
use std::hash::Hash;
use std::sync::Arc;
use std::sync::atomic::{AtomicI64, Ordering};
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use tokio::time;
use tracing::{debug, warn};
struct HeartbeatEntry {
session: Session,
last_pong_ms: AtomicI64,
}
#[derive(Clone)]
pub struct HeartbeatRegistry<K: Eq + Hash + Clone + Send + Sync + 'static> {
inner: Arc<DashMap<K, HeartbeatEntry>>,
}
impl<K: Eq + Hash + Clone + Send + Sync + std::fmt::Display + 'static> HeartbeatRegistry<K> {
pub fn new() -> Self {
Self {
inner: Arc::new(DashMap::new()),
}
}
pub fn register(&self, session_id: K, session: Session) {
self.inner.insert(
session_id,
HeartbeatEntry {
session,
last_pong_ms: AtomicI64::new(now_ms()),
},
);
}
pub fn touch(&self, session_id: &K) {
if let Some(entry) = self.inner.get(session_id) {
entry.last_pong_ms.store(now_ms(), Ordering::Relaxed);
}
}
pub fn remove(&self, session_id: &K) {
self.inner.remove(session_id);
}
pub fn spawn_sweeper(&self, interval: Duration, timeout: Duration) {
let registry = self.clone();
actix_web::rt::spawn(async move {
let mut ticker = time::interval(interval);
let timeout_ms = timeout.as_millis() as i64;
loop {
ticker.tick().await;
let now = now_ms();
let mut stale = Vec::new();
for entry in registry.inner.iter() {
let last = entry.value().last_pong_ms.load(Ordering::Relaxed);
if now - last > timeout_ms {
stale.push(entry.key().clone());
} else {
let mut session = entry.value().session.clone();
actix_web::rt::spawn(async move {
let _ = session.ping(b"").await;
});
}
}
for session_id in stale {
if let Some((_, entry)) = registry.inner.remove(&session_id) {
warn!(%session_id, "heartbeat timeout, force-closing");
let session = entry.session;
let _ = session.close(None).await;
}
}
debug!(active = registry.inner.len(), "heartbeat sweep complete");
}
});
}
}
fn now_ms() -> i64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_millis() as i64
}