use serde::Serialize;
use nodedb_mem::{EngineId, PressureLevel};
use nodedb_types::error::NodeDbResult;
use crate::storage::engine::StorageEngine;
use super::core::NodeDbLite;
use super::lock_ext::LockExt;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum OverallStatus {
Healthy,
Degraded,
Unhealthy,
}
#[derive(Debug, Serialize)]
pub struct HealthStatus {
pub status: OverallStatus,
pub storage: StorageHealth,
pub memory: MemoryHealth,
pub engines: EnginesHealth,
}
#[derive(Debug, Serialize)]
pub struct StorageHealth {
pub accessible: bool,
}
#[derive(Debug, Serialize)]
pub struct MemoryHealth {
pub budget_bytes: usize,
pub used_bytes: usize,
pub usage_ratio: f64,
pub pressure: &'static str,
pub engines: EngineMemoryBreakdown,
}
#[derive(Debug, Serialize)]
pub struct EngineMemoryBreakdown {
pub hnsw: EngineMemory,
pub csr: EngineMemory,
pub loro: EngineMemory,
pub query: EngineMemory,
}
#[derive(Debug, Serialize)]
pub struct EngineMemory {
pub budget_bytes: usize,
pub used_bytes: usize,
pub pressure: &'static str,
}
#[derive(Debug, Serialize)]
pub struct EnginesHealth {
pub hnsw_collection_count: usize,
pub hnsw_total_vectors: usize,
pub csr_node_count: usize,
pub csr_edge_count: usize,
pub crdt_collection_count: usize,
pub text_index_count: usize,
pub pending_deltas: usize,
}
fn pressure_str(p: PressureLevel) -> &'static str {
match p {
PressureLevel::Normal => "normal",
PressureLevel::Warning => "warning",
PressureLevel::Critical => "critical",
PressureLevel::Emergency => "emergency",
}
}
fn engine_memory(gov: &nodedb_mem::MemoryGovernor, id: EngineId) -> EngineMemory {
let budget = gov.budget(id);
EngineMemory {
budget_bytes: budget.limit(),
used_bytes: budget.allocated(),
pressure: pressure_str(gov.engine_pressure(id)),
}
}
impl<S: StorageEngine> NodeDbLite<S> {
pub fn storage(&self) -> &S {
&self.storage
}
pub async fn compact(&self) -> NodeDbResult<crate::storage::engine::CompactionOutcome> {
Ok(self.storage.compact().await?)
}
pub fn health(&self) -> HealthStatus {
self.update_memory_stats();
let gov = &self.governor;
let memory = MemoryHealth {
budget_bytes: gov.global_ceiling(),
used_bytes: gov.total_allocated(),
usage_ratio: gov.global_utilization_percent() as f64 / 100.0,
pressure: pressure_str(gov.global_pressure()),
engines: EngineMemoryBreakdown {
hnsw: engine_memory(gov, EngineId::Vector),
csr: engine_memory(gov, EngineId::Graph),
loro: engine_memory(gov, EngineId::Crdt),
query: engine_memory(gov, EngineId::Query),
},
};
let (hnsw_count, hnsw_vectors) = {
let indices = self.vector_state.hnsw_indices.lock_or_recover();
let count = indices.len();
let vectors: usize = indices.values().map(|idx| idx.len()).sum();
(count, vectors)
};
let (csr_nodes, csr_edges) = {
let csr_map = self.csr.lock_or_recover();
let nodes: usize = csr_map.values().map(|c| c.node_count()).sum();
let edges: usize = csr_map.values().map(|c| c.edge_count()).sum();
(nodes, edges)
};
let (crdt_collections, pending_deltas) = {
let crdt = self.crdt.lock_or_recover();
(crdt.collection_names().len(), crdt.pending_count())
};
let text_count = {
let fts = self.fts_state.manager.lock_or_recover();
fts.collection_count()
};
let engines = EnginesHealth {
hnsw_collection_count: hnsw_count,
hnsw_total_vectors: hnsw_vectors,
csr_node_count: csr_nodes,
csr_edge_count: csr_edges,
crdt_collection_count: crdt_collections,
text_index_count: text_count,
pending_deltas,
};
let overall = match gov.global_pressure() {
PressureLevel::Emergency => OverallStatus::Unhealthy,
PressureLevel::Critical => OverallStatus::Degraded,
PressureLevel::Warning => OverallStatus::Healthy,
PressureLevel::Normal => OverallStatus::Healthy,
};
HealthStatus {
status: overall,
storage: StorageHealth { accessible: true },
memory,
engines,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::PagedbStorageMem;
async fn make_db() -> std::sync::Arc<NodeDbLite<PagedbStorageMem>> {
let storage = PagedbStorageMem::open_in_memory().await.unwrap();
NodeDbLite::open(storage).await.unwrap()
}
#[tokio::test]
async fn health_empty_db() {
let db = make_db().await;
let h = db.health();
assert_eq!(h.status, OverallStatus::Healthy);
assert!(h.storage.accessible);
assert_eq!(h.memory.pressure, "normal");
assert_eq!(h.engines.hnsw_collection_count, 0);
assert_eq!(h.engines.pending_deltas, 0);
}
#[tokio::test]
async fn health_with_data() {
use nodedb_client::NodeDb;
let db = make_db().await;
db.vector_insert("vecs", "v1", &[1.0, 0.0, 0.0], None)
.await
.unwrap();
db.graph_insert_edge(
"test",
&nodedb_types::id::NodeId::from_validated("a".to_string()),
&nodedb_types::id::NodeId::from_validated("b".to_string()),
"REL",
None,
)
.await
.unwrap();
let h = db.health();
assert_eq!(h.engines.hnsw_collection_count, 1);
assert_eq!(h.engines.hnsw_total_vectors, 1);
assert!(h.engines.csr_edge_count >= 1);
}
#[tokio::test]
async fn health_serializes_to_json() {
let db = make_db().await;
let h = db.health();
let json = sonic_rs::to_string_pretty(&h).unwrap();
assert!(json.contains("\"status\""));
assert!(json.contains("\"storage\""));
assert!(json.contains("\"memory\""));
assert!(json.contains("\"engines\""));
}
#[tokio::test]
async fn health_pending_deltas_counted() {
use nodedb_client::NodeDb;
use nodedb_types::document::Document;
let db = make_db().await;
let doc = Document::new("d1");
db.document_put("docs", doc).await.unwrap();
let h = db.health();
assert!(h.engines.pending_deltas > 0);
}
}