Skip to main content

nodedb_mem/governor/
metrics.rs

1// SPDX-License-Identifier: Apache-2.0
2
3//! Read-only reporting surface: utilization, pressure, over-release counts,
4//! and the engine snapshot.
5
6use std::sync::atomic::Ordering;
7
8use super::core::MemoryGovernor;
9use crate::engine::EngineId;
10use crate::pressure::{PressureLevel, PressureThresholds};
11
12impl MemoryGovernor {
13    /// Total memory allocated across all engines (engine-layer sum). A
14    /// separate aggregate from [`global_utilization_percent`](Self::global_utilization_percent),
15    /// which reads the global counter admission enforces the ceiling against.
16    pub fn total_allocated(&self) -> usize {
17        self.budgets.iter().map(|b| b.allocated()).sum()
18    }
19
20    /// Total number of over-release events observed across every layer
21    /// (global, database, tenant, engine). A non-zero value signals at
22    /// least one call site is releasing more bytes than it reserved — the
23    /// "memory release exceeds allocation" warning class. A saturating
24    /// release clamps the affected counter to zero, so this is the only
25    /// post-hoc observable for the bug.
26    pub fn total_over_release_count(&self) -> usize {
27        self.global_counter.over_release.total()
28    }
29
30    /// Over-release events recorded against the global ceiling counter.
31    pub fn global_over_release_count(&self) -> usize {
32        self.global_counter.over_release.global()
33    }
34
35    /// Over-release events recorded against any per-database counter.
36    pub fn database_over_release_count(&self) -> usize {
37        self.global_counter.over_release.database()
38    }
39
40    /// Over-release events recorded against any per-tenant counter.
41    pub fn tenant_over_release_count(&self) -> usize {
42        self.global_counter.over_release.tenant()
43    }
44
45    /// Over-release events recorded against any per-engine counter.
46    pub fn engine_over_release_count(&self) -> usize {
47        self.global_counter.over_release.engine()
48    }
49
50    /// Global utilization as a percentage (0-100). Reads the global counter
51    /// directly — the same quantity admission enforces the ceiling
52    /// against — computed in `u128` so a corrupted count clamps to 100 %
53    /// instead of overflowing.
54    pub fn global_utilization_percent(&self) -> u8 {
55        let allocated = self.global_counter.allocated.load(Ordering::Relaxed);
56        if self.global_ceiling == 0 {
57            return if allocated == 0 { 0 } else { 100 };
58        }
59        ((allocated as u128 * 100) / self.global_ceiling as u128).min(100) as u8
60    }
61
62    /// Current pressure level for a specific engine.
63    pub fn engine_pressure(&self, engine: EngineId) -> PressureLevel {
64        self.thresholds
65            .level_for(self.budgets[engine.index()].utilization_percent())
66    }
67
68    /// Current global pressure level.
69    pub fn global_pressure(&self) -> PressureLevel {
70        self.thresholds.level_for(self.global_utilization_percent())
71    }
72
73    /// Worst-case (highest) pressure level across every engine. Cheap:
74    /// iterates the in-memory budget array and allocates nothing — meant to
75    /// be called once per Data-Plane core-loop tick, unlike
76    /// [`snapshot`](Self::snapshot) which materialises a `Vec`.
77    pub fn worst_engine_pressure(&self) -> PressureLevel {
78        self.budgets
79            .iter()
80            .map(|b| self.thresholds.level_for(b.utilization_percent()))
81            .max()
82            .unwrap_or(PressureLevel::Normal)
83    }
84
85    /// Set custom pressure thresholds.
86    pub fn set_thresholds(&mut self, thresholds: PressureThresholds) {
87        self.thresholds = thresholds;
88    }
89
90    /// Snapshot of all engine budget states (for metrics/debugging).
91    pub fn snapshot(&self) -> Vec<EngineSnapshot> {
92        EngineId::ALL
93            .iter()
94            .zip(self.budgets.iter())
95            .map(|(&engine, budget)| EngineSnapshot {
96                engine,
97                allocated: budget.allocated(),
98                limit: budget.limit(),
99                peak: budget.peak(),
100                rejections: budget.rejections(),
101                utilization_percent: budget.utilization_percent(),
102            })
103            .collect()
104    }
105}
106
107/// Point-in-time snapshot of an engine's memory state.
108#[derive(Debug, Clone)]
109pub struct EngineSnapshot {
110    pub engine: EngineId,
111    pub allocated: usize,
112    pub limit: usize,
113    pub peak: usize,
114    pub rejections: usize,
115    pub utilization_percent: u8,
116}
117
118#[cfg(test)]
119mod tests {
120    use super::*;
121    use crate::governor::test_support::{db, tenant, test_config};
122
123    #[test]
124    fn snapshot_reports_all_engines() {
125        let gov = MemoryGovernor::new(test_config()).unwrap();
126        let _tok = gov
127            .try_reserve(db(), tenant(), EngineId::Vector, 2048)
128            .unwrap();
129
130        let snap = gov.snapshot();
131        assert_eq!(snap.len(), EngineId::COUNT);
132
133        let vector_snap = snap.iter().find(|s| s.engine == EngineId::Vector).unwrap();
134        assert_eq!(vector_snap.allocated, 2048);
135        assert_eq!(vector_snap.limit, 4096);
136        assert_eq!(vector_snap.utilization_percent, 50);
137    }
138
139    #[test]
140    fn engine_pressure_levels() {
141        let gov = MemoryGovernor::new(test_config()).unwrap();
142
143        assert_eq!(gov.engine_pressure(EngineId::Vector), PressureLevel::Normal);
144
145        let _tok1 = gov
146            .try_reserve(db(), tenant(), EngineId::Vector, 2868)
147            .unwrap();
148        assert_eq!(
149            gov.engine_pressure(EngineId::Vector),
150            PressureLevel::Warning
151        );
152    }
153
154    #[test]
155    fn worst_engine_pressure_picks_highest() {
156        let gov = MemoryGovernor::new(test_config()).unwrap();
157        assert_eq!(gov.worst_engine_pressure(), PressureLevel::Normal);
158
159        // Push Query to Critical (2048 limit; 1800 ≈ 87%) while Vector/Timeseries
160        // stay Normal — the worst-case must follow Query.
161        let _tok = gov
162            .try_reserve(db(), tenant(), EngineId::Query, 1800)
163            .unwrap();
164        assert_eq!(gov.engine_pressure(EngineId::Vector), PressureLevel::Normal);
165        assert_eq!(gov.worst_engine_pressure(), PressureLevel::Critical);
166    }
167}