use std::sync::Arc;
use std::sync::atomic::{AtomicU32, Ordering};
use std::time::Duration;
use futures_util::SinkExt;
use rand::rngs::StdRng;
use rand::{Rng, SeedableRng};
use tokio::net::TcpStream;
use tokio::sync::Mutex;
use tokio::task::JoinHandle;
use tokio_tungstenite::MaybeTlsStream;
use tokio_tungstenite::WebSocketStream;
use tokio_tungstenite::tungstenite::Message as WsMessage;
use crate::protocol::hello::Ready;
use crate::transport::frame_codec::encode_frame;
use super::frame_builder::{FrameIdCounter, ping_frame};
type WsWriter =
futures_util::stream::SplitSink<WebSocketStream<MaybeTlsStream<TcpStream>>, WsMessage>;
pub(super) struct HeartbeatState {
pub(super) missed_pongs: AtomicU32,
}
impl HeartbeatState {
pub(super) fn new() -> Self {
Self {
missed_pongs: AtomicU32::new(0),
}
}
pub(super) fn reset_missed(&self) {
self.missed_pongs.store(0, Ordering::SeqCst);
}
pub(super) fn missed(&self) -> u32 {
self.missed_pongs.load(Ordering::SeqCst)
}
}
pub(super) fn spawn_heartbeat(
writer: Arc<Mutex<WsWriter>>,
ready: &Ready,
frame_ids: FrameIdCounter,
) -> (JoinHandle<()>, Arc<HeartbeatState>) {
let state = Arc::new(HeartbeatState::new());
let ping_interval = Duration::from_millis(ready.ping_interval_ms as u64);
let pong_timeout = Duration::from_millis(ready.pong_timeout_ms as u64);
let max_missed = ready.max_missed_pongs;
let jitter_ms = ready.jitter_ms;
let state_clone = Arc::clone(&state);
let handle = tokio::spawn(async move {
heartbeat_loop(
writer,
state_clone,
frame_ids,
ping_interval,
pong_timeout,
max_missed,
jitter_ms,
)
.await;
});
(handle, state)
}
async fn heartbeat_loop(
writer: Arc<Mutex<WsWriter>>,
state: Arc<HeartbeatState>,
frame_ids: FrameIdCounter,
ping_interval: Duration,
pong_timeout: Duration,
max_missed: u32,
jitter_ms: u32,
) {
let mut rng = StdRng::from_os_rng();
let mut consecutive_encode_failures = 0u32;
loop {
let jitter = if jitter_ms > 0 {
Duration::from_millis(rng.random_range(0..=jitter_ms as u64))
} else {
Duration::ZERO
};
let wait = ping_interval.saturating_add(jitter);
tokio::time::sleep(wait).await;
let id = frame_ids.next();
let ping = ping_frame(id);
let encoded = match encode_frame(&ping) {
Ok(b) => b,
Err(e) => {
tracing::warn!(error = %e, "ping encode failed");
consecutive_encode_failures += 1;
if consecutive_encode_failures >= 3 {
tracing::error!("ping encode failed repeatedly; closing connection");
break;
}
continue;
}
};
consecutive_encode_failures = 0;
{
let mut w = writer.lock().await;
if w.send(WsMessage::Binary(encoded.to_vec())).await.is_err() {
break;
}
}
tokio::time::sleep(pong_timeout).await;
if state.missed() == 0 {
continue; }
state.missed_pongs.fetch_add(1, Ordering::SeqCst);
let current = state.missed();
if current >= max_missed {
tracing::warn!(missed = current, "heartbeat timeout — closing connection");
let mut w = writer.lock().await;
let _ = w
.send(WsMessage::Close(Some(
tokio_tungstenite::tungstenite::protocol::CloseFrame {
code: tokio_tungstenite::tungstenite::protocol::frame::coding::CloseCode::Error,
reason: "heartbeat timeout".into(),
},
)))
.await;
break;
}
}
}