soothe_client/
heartbeat.rs1use std::sync::Mutex;
4use std::time::{Duration, Instant};
5
6use serde_json::Value;
7
8#[derive(Debug, Clone)]
10pub struct DaemonHealth {
11 pub last_heartbeat: Instant,
13 pub state: String,
15 pub loop_id: String,
17 pub is_alive: bool,
19}
20
21#[derive(Debug)]
23pub struct HeartbeatTracker {
24 inner: Mutex<Inner>,
25}
26
27#[derive(Debug)]
28struct Inner {
29 last_heartbeat: Option<Instant>,
30 daemon_state: String,
31 heartbeat_loop_id: String,
32 alive_threshold: Duration,
33 start_time: Instant,
34}
35
36impl HeartbeatTracker {
37 pub fn new() -> Self {
39 Self::with_threshold(Duration::from_secs(15))
40 }
41
42 pub fn with_threshold(alive_threshold: Duration) -> Self {
44 Self {
45 inner: Mutex::new(Inner {
46 last_heartbeat: None,
47 daemon_state: String::new(),
48 heartbeat_loop_id: String::new(),
49 alive_threshold,
50 start_time: Instant::now(),
51 }),
52 }
53 }
54
55 pub fn update(&self, heartbeat_data: Option<&Value>) {
57 let mut g = self.inner.lock().expect("heartbeat lock");
58 g.last_heartbeat = Some(Instant::now());
59 if let Some(data) = heartbeat_data.and_then(|v| v.as_object()) {
60 if let Some(state) = data.get("state").and_then(|v| v.as_str()) {
61 g.daemon_state = state.to_string();
62 }
63 if let Some(loop_id) = data.get("loop_id").and_then(|v| v.as_str()) {
64 g.heartbeat_loop_id = loop_id.to_string();
65 }
66 }
67 }
68
69 pub fn note_pong(&self) {
71 let mut g = self.inner.lock().expect("heartbeat lock");
72 g.last_heartbeat = Some(Instant::now());
73 }
74
75 pub fn get_health(&self) -> DaemonHealth {
77 let g = self.inner.lock().expect("heartbeat lock");
78 let now = Instant::now();
79 let last = g.last_heartbeat.unwrap_or(g.start_time);
80 let grace = Duration::from_secs(20);
81 let is_alive = now.duration_since(last) < g.alive_threshold
82 || now.duration_since(g.start_time) < grace;
83 DaemonHealth {
84 last_heartbeat: last,
85 state: g.daemon_state.clone(),
86 loop_id: g.heartbeat_loop_id.clone(),
87 is_alive,
88 }
89 }
90
91 pub fn is_processing(&self) -> bool {
93 self.inner.lock().expect("heartbeat lock").daemon_state == "running"
94 }
95
96 pub fn is_idle(&self) -> bool {
98 self.inner.lock().expect("heartbeat lock").daemon_state == "idle"
99 }
100}
101
102impl Default for HeartbeatTracker {
103 fn default() -> Self {
104 Self::new()
105 }
106}
107
108#[cfg(test)]
109mod tests {
110 use super::*;
111
112 #[test]
113 fn grace_period_alive() {
114 let t = HeartbeatTracker::new();
115 assert!(t.get_health().is_alive);
116 }
117}