1use serde::Serialize;
12
13use nodedb_mem::{EngineId, PressureLevel};
14use nodedb_types::error::NodeDbResult;
15
16use crate::storage::engine::StorageEngine;
17
18use super::core::NodeDbLite;
19use super::lock_ext::LockExt;
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
23#[serde(rename_all = "snake_case")]
24pub enum OverallStatus {
25 Healthy,
27 Degraded,
29 Unhealthy,
31}
32
33#[derive(Debug, Serialize)]
35pub struct HealthStatus {
36 pub status: OverallStatus,
38 pub storage: StorageHealth,
40 pub memory: MemoryHealth,
42 pub engines: EnginesHealth,
44}
45
46#[derive(Debug, Serialize)]
48pub struct StorageHealth {
49 pub accessible: bool,
51}
52
53#[derive(Debug, Serialize)]
55pub struct MemoryHealth {
56 pub budget_bytes: usize,
58 pub used_bytes: usize,
60 pub usage_ratio: f64,
62 pub pressure: &'static str,
64 pub engines: EngineMemoryBreakdown,
66}
67
68#[derive(Debug, Serialize)]
70pub struct EngineMemoryBreakdown {
71 pub hnsw: EngineMemory,
72 pub csr: EngineMemory,
73 pub loro: EngineMemory,
74 pub query: EngineMemory,
75}
76
77#[derive(Debug, Serialize)]
79pub struct EngineMemory {
80 pub budget_bytes: usize,
81 pub used_bytes: usize,
82 pub pressure: &'static str,
83}
84
85#[derive(Debug, Serialize)]
87pub struct EnginesHealth {
88 pub hnsw_collection_count: usize,
90 pub hnsw_total_vectors: usize,
92 pub csr_node_count: usize,
94 pub csr_edge_count: usize,
96 pub crdt_collection_count: usize,
98 pub text_index_count: usize,
100 pub pending_deltas: usize,
102}
103
104fn pressure_str(p: PressureLevel) -> &'static str {
105 match p {
106 PressureLevel::Normal => "normal",
107 PressureLevel::Warning => "warning",
108 PressureLevel::Critical => "critical",
109 PressureLevel::Emergency => "emergency",
110 }
111}
112
113fn engine_memory(gov: &nodedb_mem::MemoryGovernor, id: EngineId) -> EngineMemory {
114 let budget = gov.budget(id);
115 EngineMemory {
116 budget_bytes: budget.limit(),
117 used_bytes: budget.allocated(),
118 pressure: pressure_str(gov.engine_pressure(id)),
119 }
120}
121
122impl<S: StorageEngine> NodeDbLite<S> {
123 pub fn storage(&self) -> &S {
128 &self.storage
129 }
130
131 pub async fn compact(&self) -> NodeDbResult<crate::storage::engine::CompactionOutcome> {
139 Ok(self.storage.compact().await?)
140 }
141
142 pub fn health(&self) -> HealthStatus {
147 self.update_memory_stats();
149
150 let gov = &self.governor;
151
152 let memory = MemoryHealth {
153 budget_bytes: gov.global_ceiling(),
154 used_bytes: gov.total_allocated(),
155 usage_ratio: gov.global_utilization_percent() as f64 / 100.0,
156 pressure: pressure_str(gov.global_pressure()),
157 engines: EngineMemoryBreakdown {
158 hnsw: engine_memory(gov, EngineId::Vector),
159 csr: engine_memory(gov, EngineId::Graph),
160 loro: engine_memory(gov, EngineId::Crdt),
161 query: engine_memory(gov, EngineId::Query),
162 },
163 };
164
165 let (hnsw_count, hnsw_vectors) = {
166 let indices = self.vector_state.hnsw_indices.lock_or_recover();
167 let count = indices.len();
168 let vectors: usize = indices.values().map(|idx| idx.len()).sum();
169 (count, vectors)
170 };
171
172 let (csr_nodes, csr_edges) = {
173 let csr_map = self.csr.lock_or_recover();
174 let nodes: usize = csr_map.values().map(|c| c.node_count()).sum();
175 let edges: usize = csr_map.values().map(|c| c.edge_count()).sum();
176 (nodes, edges)
177 };
178
179 let (crdt_collections, pending_deltas) = {
180 let crdt = self.crdt.lock_or_recover();
181 (crdt.collection_names().len(), crdt.pending_count())
182 };
183
184 let text_count = {
185 let fts = self.fts_state.manager.lock_or_recover();
186 fts.collection_count()
187 };
188
189 let engines = EnginesHealth {
190 hnsw_collection_count: hnsw_count,
191 hnsw_total_vectors: hnsw_vectors,
192 csr_node_count: csr_nodes,
193 csr_edge_count: csr_edges,
194 crdt_collection_count: crdt_collections,
195 text_index_count: text_count,
196 pending_deltas,
197 };
198
199 let overall = match gov.global_pressure() {
203 PressureLevel::Emergency => OverallStatus::Unhealthy,
204 PressureLevel::Critical => OverallStatus::Degraded,
205 PressureLevel::Warning => OverallStatus::Healthy,
206 PressureLevel::Normal => OverallStatus::Healthy,
207 };
208
209 HealthStatus {
210 status: overall,
211 storage: StorageHealth { accessible: true },
212 memory,
213 engines,
214 }
215 }
216}
217
218#[cfg(test)]
219mod tests {
220 use super::*;
221 use crate::PagedbStorageMem;
222
223 async fn make_db() -> std::sync::Arc<NodeDbLite<PagedbStorageMem>> {
224 let storage = PagedbStorageMem::open_in_memory().await.unwrap();
225 NodeDbLite::open(storage).await.unwrap()
226 }
227
228 #[tokio::test]
229 async fn health_empty_db() {
230 let db = make_db().await;
231 let h = db.health();
232 assert_eq!(h.status, OverallStatus::Healthy);
233 assert!(h.storage.accessible);
234 assert_eq!(h.memory.pressure, "normal");
235 assert_eq!(h.engines.hnsw_collection_count, 0);
236 assert_eq!(h.engines.pending_deltas, 0);
237 }
238
239 #[tokio::test]
240 async fn health_with_data() {
241 use nodedb_client::NodeDb;
242
243 let db = make_db().await;
244 db.vector_insert("vecs", "v1", &[1.0, 0.0, 0.0], None)
245 .await
246 .unwrap();
247 db.graph_insert_edge(
248 "test",
249 &nodedb_types::id::NodeId::from_validated("a".to_string()),
250 &nodedb_types::id::NodeId::from_validated("b".to_string()),
251 "REL",
252 None,
253 )
254 .await
255 .unwrap();
256
257 let h = db.health();
258 assert_eq!(h.engines.hnsw_collection_count, 1);
259 assert_eq!(h.engines.hnsw_total_vectors, 1);
260 assert!(h.engines.csr_edge_count >= 1);
261 }
262
263 #[tokio::test]
264 async fn health_serializes_to_json() {
265 let db = make_db().await;
266 let h = db.health();
267 let json = sonic_rs::to_string_pretty(&h).unwrap();
268 assert!(json.contains("\"status\""));
269 assert!(json.contains("\"storage\""));
270 assert!(json.contains("\"memory\""));
271 assert!(json.contains("\"engines\""));
272 }
273
274 #[tokio::test]
275 async fn health_pending_deltas_counted() {
276 use nodedb_client::NodeDb;
277 use nodedb_types::document::Document;
278
279 let db = make_db().await;
280 let doc = Document::new("d1");
281 db.document_put("docs", doc).await.unwrap();
282
283 let h = db.health();
284 assert!(h.engines.pending_deltas > 0);
285 }
286}