1use serde::Serialize;
12
13use crate::memory::{EngineId, PressureLevel};
14use crate::storage::engine::StorageEngine;
15
16use super::core::NodeDbLite;
17use super::lock_ext::LockExt;
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
21#[serde(rename_all = "snake_case")]
22pub enum OverallStatus {
23 Healthy,
25 Degraded,
27 Unhealthy,
29}
30
31#[derive(Debug, Serialize)]
33pub struct HealthStatus {
34 pub status: OverallStatus,
36 pub storage: StorageHealth,
38 pub memory: MemoryHealth,
40 pub engines: EnginesHealth,
42}
43
44#[derive(Debug, Serialize)]
46pub struct StorageHealth {
47 pub accessible: bool,
49}
50
51#[derive(Debug, Serialize)]
53pub struct MemoryHealth {
54 pub budget_bytes: usize,
56 pub used_bytes: usize,
58 pub usage_ratio: f64,
60 pub pressure: &'static str,
62 pub engines: EngineMemoryBreakdown,
64}
65
66#[derive(Debug, Serialize)]
68pub struct EngineMemoryBreakdown {
69 pub hnsw: EngineMemory,
70 pub csr: EngineMemory,
71 pub loro: EngineMemory,
72 pub query: EngineMemory,
73}
74
75#[derive(Debug, Serialize)]
77pub struct EngineMemory {
78 pub budget_bytes: usize,
79 pub used_bytes: usize,
80 pub pressure: &'static str,
81}
82
83#[derive(Debug, Serialize)]
85pub struct EnginesHealth {
86 pub hnsw_collection_count: usize,
88 pub hnsw_total_vectors: usize,
90 pub csr_node_count: usize,
92 pub csr_edge_count: usize,
94 pub crdt_collection_count: usize,
96 pub text_index_count: usize,
98 pub pending_deltas: usize,
100}
101
102fn pressure_str(p: PressureLevel) -> &'static str {
103 match p {
104 PressureLevel::Normal => "normal",
105 PressureLevel::Warning => "warning",
106 PressureLevel::Critical => "critical",
107 }
108}
109
110fn engine_memory(gov: &crate::memory::MemoryGovernor, id: EngineId) -> EngineMemory {
111 EngineMemory {
112 budget_bytes: gov.budget_for(id),
113 used_bytes: gov.usage_for(id),
114 pressure: pressure_str(gov.engine_pressure(id)),
115 }
116}
117
118impl<S: StorageEngine> NodeDbLite<S> {
119 pub fn health(&self) -> HealthStatus {
124 self.update_memory_stats();
126
127 let gov = &self.governor;
128
129 let memory = MemoryHealth {
130 budget_bytes: gov.total_budget(),
131 used_bytes: gov.total_used(),
132 usage_ratio: gov.usage_ratio(),
133 pressure: pressure_str(gov.pressure()),
134 engines: EngineMemoryBreakdown {
135 hnsw: engine_memory(gov, EngineId::Hnsw),
136 csr: engine_memory(gov, EngineId::Csr),
137 loro: engine_memory(gov, EngineId::Loro),
138 query: engine_memory(gov, EngineId::Query),
139 },
140 };
141
142 let (hnsw_count, hnsw_vectors) = {
143 let indices = self.hnsw_indices.lock_or_recover();
144 let count = indices.len();
145 let vectors: usize = indices.values().map(|idx| idx.len()).sum();
146 (count, vectors)
147 };
148
149 let (csr_nodes, csr_edges) = {
150 let csr = self.csr.lock_or_recover();
151 (csr.node_count(), csr.edge_count())
152 };
153
154 let (crdt_collections, pending_deltas) = {
155 let crdt = self.crdt.lock_or_recover();
156 (crdt.collection_names().len(), crdt.pending_count())
157 };
158
159 let text_count = {
160 let text = self.text_indices.lock_or_recover();
161 text.len()
162 };
163
164 let engines = EnginesHealth {
165 hnsw_collection_count: hnsw_count,
166 hnsw_total_vectors: hnsw_vectors,
167 csr_node_count: csr_nodes,
168 csr_edge_count: csr_edges,
169 crdt_collection_count: crdt_collections,
170 text_index_count: text_count,
171 pending_deltas,
172 };
173
174 let overall = match gov.pressure() {
176 PressureLevel::Critical => OverallStatus::Unhealthy,
177 PressureLevel::Warning => OverallStatus::Degraded,
178 PressureLevel::Normal => OverallStatus::Healthy,
179 };
180
181 HealthStatus {
182 status: overall,
183 storage: StorageHealth { accessible: true },
184 memory,
185 engines,
186 }
187 }
188}
189
190#[cfg(test)]
191mod tests {
192 use super::*;
193 use crate::RedbStorage;
194
195 async fn make_db() -> NodeDbLite<RedbStorage> {
196 let storage = RedbStorage::open_in_memory().unwrap();
197 NodeDbLite::open(storage, 1).await.unwrap()
198 }
199
200 #[tokio::test]
201 async fn health_empty_db() {
202 let db = make_db().await;
203 let h = db.health();
204 assert_eq!(h.status, OverallStatus::Healthy);
205 assert!(h.storage.accessible);
206 assert_eq!(h.memory.pressure, "normal");
207 assert_eq!(h.engines.hnsw_collection_count, 0);
208 assert_eq!(h.engines.pending_deltas, 0);
209 }
210
211 #[tokio::test]
212 async fn health_with_data() {
213 use nodedb_client::NodeDb;
214
215 let db = make_db().await;
216 db.vector_insert("vecs", "v1", &[1.0, 0.0, 0.0], None)
217 .await
218 .unwrap();
219 db.graph_insert_edge(
220 &nodedb_types::id::NodeId::new("a"),
221 &nodedb_types::id::NodeId::new("b"),
222 "REL",
223 None,
224 )
225 .await
226 .unwrap();
227
228 let h = db.health();
229 assert_eq!(h.engines.hnsw_collection_count, 1);
230 assert_eq!(h.engines.hnsw_total_vectors, 1);
231 assert!(h.engines.csr_edge_count >= 1);
232 }
233
234 #[tokio::test]
235 async fn health_serializes_to_json() {
236 let db = make_db().await;
237 let h = db.health();
238 let json = serde_json::to_string_pretty(&h).unwrap();
239 assert!(json.contains("\"status\""));
240 assert!(json.contains("\"storage\""));
241 assert!(json.contains("\"memory\""));
242 assert!(json.contains("\"engines\""));
243 }
244
245 #[tokio::test]
246 async fn health_pending_deltas_counted() {
247 use nodedb_client::NodeDb;
248 use nodedb_types::document::Document;
249
250 let db = make_db().await;
251 let doc = Document::new("d1");
252 db.document_put("docs", doc).await.unwrap();
253
254 let h = db.health();
255 assert!(h.engines.pending_deltas > 0);
256 }
257}