1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
//! Health checking infrastructure
use mecha10_core::rwlock_collections::RwLockMapExt;
use std::collections::HashMap;
use std::sync::Arc;
use std::time::Duration;
use tokio::sync::RwLock;
/// Health status of a node
#[derive(Clone, Debug, PartialEq)]
pub enum HealthStatus {
/// Node is healthy and operating normally
Healthy,
/// Node is degraded but still functional
Degraded { reason: String },
/// Node is unhealthy and may need restart
Unhealthy { reason: String },
}
impl HealthStatus {
/// Check if status is healthy
pub fn is_healthy(&self) -> bool {
matches!(self, HealthStatus::Healthy)
}
/// Check if status is degraded
pub fn is_degraded(&self) -> bool {
matches!(self, HealthStatus::Degraded { .. })
}
/// Check if status is unhealthy
pub fn is_unhealthy(&self) -> bool {
matches!(self, HealthStatus::Unhealthy { .. })
}
/// Get reason if not healthy
pub fn reason(&self) -> Option<&str> {
match self {
HealthStatus::Healthy => None,
HealthStatus::Degraded { reason } => Some(reason),
HealthStatus::Unhealthy { reason } => Some(reason),
}
}
}
/// Health checker for monitoring node health
pub struct HealthChecker {
checks: Arc<RwLock<HashMap<String, HealthStatus>>>,
interval: Duration,
}
impl HealthChecker {
// duplicate: false positive — matches other unrelated structs' `new()` (e.g. CLI's
// `MessageBuffer`/`LogBuffer`) purely because both move constructor args into fields
// around a fresh `HashMap`/`Arc<RwLock<_>>>`; health monitoring shares no behavior with
// those TUI buffer types worth extracting a helper for.
/// Create a new health checker with specified interval
pub fn new(interval: Duration) -> Self {
Self {
checks: Arc::new(RwLock::new(HashMap::new())),
interval,
}
}
// duplicate: false positive — matches `PerformanceMetrics::record_memory`
// (packages/core/src/profiling.rs) on AST shape only (lock a `RwLock`, insert/mutate);
// unrelated domains, nothing to share.
/// Register a node for health checking
pub async fn register(&self, name: String, initial_status: HealthStatus) {
let mut checks = self.checks.write().await;
checks.insert(name, initial_status);
}
// duplicate: false positive — matches `RecoveryManager::set_policy`
// (packages/core/src/recovery.rs) on AST shape only (write-lock a map, insert); unrelated
// domains (health status vs recovery policy), nothing to share.
/// Update health status for a node
pub async fn update(&self, name: &str, status: HealthStatus) {
let mut checks = self.checks.write().await;
checks.insert(name.to_string(), status);
}
/// Get health status for a specific node
pub async fn check_one(&self, name: &str) -> Option<HealthStatus> {
self.checks.cloned_get(&name.to_string()).await
}
// duplicate: false positive — matches `ConfigWatcher::get_async`, `TopicRegistry::topic_count`,
// `DeadLetterQueue::get_all`/`len` on AST shape only (read-lock then clone/measure the guarded
// value); each clones or measures a different type for an unrelated domain, and there's no
// `RwLockMapExt`-style helper to extract since some of these aren't even keyed maps (e.g.
// `ConfigWatcher<T>` clones a whole `T`).
/// Get health status for all nodes
pub async fn check_all(&self) -> HashMap<String, HealthStatus> {
let checks = self.checks.read().await;
checks.clone()
}
/// Get the check interval
pub fn interval(&self) -> Duration {
self.interval
}
/// Remove a node from health checking
pub async fn unregister(&self, name: &str) {
let mut checks = self.checks.write().await;
checks.remove(name);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn check_one_returns_none_before_registration() {
let checker = HealthChecker::new(Duration::from_secs(1));
assert_eq!(checker.check_one("camera").await, None);
}
#[tokio::test]
async fn check_one_returns_the_registered_status() {
let checker = HealthChecker::new(Duration::from_secs(1));
checker.register("camera".to_string(), HealthStatus::Healthy).await;
assert_eq!(checker.check_one("camera").await, Some(HealthStatus::Healthy));
}
#[tokio::test]
async fn update_overwrites_the_status_seen_by_check_one_and_check_all() {
let checker = HealthChecker::new(Duration::from_secs(1));
checker.register("camera".to_string(), HealthStatus::Healthy).await;
checker
.update(
"camera",
HealthStatus::Degraded {
reason: "low battery".to_string(),
},
)
.await;
assert_eq!(
checker.check_one("camera").await,
Some(HealthStatus::Degraded {
reason: "low battery".to_string()
})
);
assert_eq!(checker.check_all().await.len(), 1);
}
#[tokio::test]
async fn unregister_removes_the_node_from_check_one_and_check_all() {
let checker = HealthChecker::new(Duration::from_secs(1));
checker.register("camera".to_string(), HealthStatus::Healthy).await;
checker.unregister("camera").await;
assert_eq!(checker.check_one("camera").await, None);
assert!(checker.check_all().await.is_empty());
}
}