use std::collections::HashMap;
use std::sync::{Arc, Mutex, Weak};
use std::time::{Duration, Instant, SystemTime};
use serde_json::{Value, json};
use crate::ipc::{KernelMessage, MessagePayload, MessageTarget};
use crate::mesh_delivery::PeerCtx;
use crate::mesh_load::LoadSample;
use crate::mesh_runtime::{MeshRuntime, PING_TOPIC, PeerControlSink};
#[derive(Debug, Clone, Copy)]
pub struct LivenessConfig {
pub interval: Duration,
pub timeout: Duration,
}
impl Default for LivenessConfig {
fn default() -> Self {
Self { interval: Duration::from_secs(10), timeout: Duration::from_secs(30) }
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct PeerLiveness {
pub last_seen: SystemTime,
pub rtt_ms: f64,
pub missed: u32,
pub load: Option<LoadSample>,
}
const MAX_OUTSTANDING_PER_PEER: usize = 4;
#[derive(Default)]
struct State {
seq: u64,
outstanding: HashMap<(String, u64), Instant>,
peers: HashMap<String, PeerLiveness>,
}
pub struct Liveness {
rt: Weak<MeshRuntime>,
cfg: LivenessConfig,
state: Mutex<State>,
}
impl Liveness {
pub fn new(rt: &Arc<MeshRuntime>, cfg: LivenessConfig) -> Arc<Self> {
Arc::new(Self { rt: Arc::downgrade(rt), cfg, state: Mutex::new(State::default()) })
}
pub fn start(self: &Arc<Self>) {
let Some(rt) = self.rt.upgrade() else { return };
rt.set_control_sink(PING_TOPIC, self.clone());
let me = Arc::downgrade(self);
let interval = self.cfg.interval;
tokio::spawn(async move {
let mut t = tokio::time::interval(interval);
t.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay);
loop {
t.tick().await;
let Some(me) = me.upgrade() else { break };
me.tick().await;
}
});
}
pub async fn tick(&self) {
let Some(rt) = self.rt.upgrade() else { return };
let targets: Vec<String> =
rt.peer_details().into_iter().filter(|p| p.verified).map(|p| p.node_id).collect();
let pings: Vec<(String, u64)> = {
let mut st = self.state.lock().unwrap();
let timeout = self.cfg.timeout;
let expired: Vec<(String, u64)> =
st.outstanding.iter().filter(|(_, at)| at.elapsed() > timeout).map(|(k, _)| k.clone()).collect();
for k in expired {
st.outstanding.remove(&k);
if let Some(p) = st.peers.get_mut(&k.0) {
p.missed = p.missed.saturating_add(1);
}
}
st.peers.retain(|id, _| targets.contains(id));
let mut out = Vec::new();
for peer in &targets {
let inflight = st.outstanding.keys().filter(|(p, _)| p == peer).count();
if inflight >= MAX_OUTSTANDING_PER_PEER {
continue;
}
st.seq += 1;
let n = st.seq;
st.outstanding.insert((peer.clone(), n), Instant::now());
out.push((peer.clone(), n));
}
out
};
for (peer, n) in pings {
let _ = rt.route_to_remote(&peer, control(json!({"t": "ping", "n": n}))).await;
}
}
pub fn peer(&self, peer: &str) -> Option<PeerLiveness> {
self.state.lock().unwrap().peers.get(peer).cloned()
}
fn on_pong(&self, peer: &str, n: u64, load: Option<LoadSample>) -> bool {
let rtt_ms = {
let mut st = self.state.lock().unwrap();
let Some(sent) = st.outstanding.remove(&(peer.to_string(), n)) else { return false };
let sample = sent.elapsed().as_secs_f64() * 1000.0;
let e = st.peers.entry(peer.to_string()).or_insert(PeerLiveness {
last_seen: SystemTime::now(),
rtt_ms: sample,
missed: 0,
load: None,
});
e.rtt_ms = e.rtt_ms * 0.8 + sample * 0.2;
e.last_seen = SystemTime::now();
e.missed = 0;
e.load = load;
e.rtt_ms
};
if let Some(rt) = self.rt.upgrade() {
rt.record_heartbeat(peer);
}
tracing::trace!(peer, rtt_ms, "mesh pong");
true
}
}
impl PeerControlSink for Liveness {
fn on_peer_control(&self, ctx: &PeerCtx, _conn: u64, payload: &Value) -> Vec<Value> {
if !ctx.node_verified {
return Vec::new();
}
let Some(n) = payload.get("n").and_then(Value::as_u64) else { return Vec::new() };
match payload.get("t").and_then(Value::as_str) {
Some("ping") => {
let mut pong = json!({"t": "pong", "n": n});
if let Some(l) = LoadSample::local() {
pong["load"] = l.to_json();
}
vec![pong]
}
Some("pong") => {
let load = payload.get("load").and_then(LoadSample::from_json);
self.on_pong(&ctx.peer_id, n, load);
Vec::new()
}
_ => Vec::new(),
}
}
}
fn control(value: Value) -> KernelMessage {
KernelMessage::new(0, MessageTarget::Topic(PING_TOPIC.to_string()), MessagePayload::Json(value))
}
#[cfg(test)]
#[path = "mesh_liveness_tests.rs"]
mod tests;