use std::sync::Mutex;
use std::time::{Duration, Instant};
use serde_json::Value;
#[derive(Debug, Clone)]
pub struct DaemonHealth {
pub last_heartbeat: Instant,
pub state: String,
pub loop_id: String,
pub is_alive: bool,
}
#[derive(Debug)]
pub struct HeartbeatTracker {
inner: Mutex<Inner>,
}
#[derive(Debug)]
struct Inner {
last_heartbeat: Option<Instant>,
daemon_state: String,
heartbeat_loop_id: String,
alive_threshold: Duration,
start_time: Instant,
}
impl HeartbeatTracker {
pub fn new() -> Self {
Self::with_threshold(Duration::from_secs(15))
}
pub fn with_threshold(alive_threshold: Duration) -> Self {
Self {
inner: Mutex::new(Inner {
last_heartbeat: None,
daemon_state: String::new(),
heartbeat_loop_id: String::new(),
alive_threshold,
start_time: Instant::now(),
}),
}
}
pub fn update(&self, heartbeat_data: Option<&Value>) {
let mut g = self.inner.lock().expect("heartbeat lock");
g.last_heartbeat = Some(Instant::now());
if let Some(data) = heartbeat_data.and_then(|v| v.as_object()) {
if let Some(state) = data.get("state").and_then(|v| v.as_str()) {
g.daemon_state = state.to_string();
}
if let Some(loop_id) = data.get("loop_id").and_then(|v| v.as_str()) {
g.heartbeat_loop_id = loop_id.to_string();
}
}
}
pub fn note_pong(&self) {
let mut g = self.inner.lock().expect("heartbeat lock");
g.last_heartbeat = Some(Instant::now());
}
pub fn get_health(&self) -> DaemonHealth {
let g = self.inner.lock().expect("heartbeat lock");
let now = Instant::now();
let last = g.last_heartbeat.unwrap_or(g.start_time);
let grace = Duration::from_secs(20);
let is_alive = now.duration_since(last) < g.alive_threshold
|| now.duration_since(g.start_time) < grace;
DaemonHealth {
last_heartbeat: last,
state: g.daemon_state.clone(),
loop_id: g.heartbeat_loop_id.clone(),
is_alive,
}
}
pub fn is_processing(&self) -> bool {
self.inner.lock().expect("heartbeat lock").daemon_state == "running"
}
pub fn is_idle(&self) -> bool {
self.inner.lock().expect("heartbeat lock").daemon_state == "idle"
}
}
impl Default for HeartbeatTracker {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn grace_period_alive() {
let t = HeartbeatTracker::new();
assert!(t.get_health().is_alive);
}
}