nodedb_mem/
over_release.rs1use std::sync::atomic::{AtomicUsize, Ordering};
12
13use nodedb_types::{DatabaseId, TenantId};
14
15use crate::engine::EngineId;
16
17#[derive(Debug, Clone, Copy)]
20pub(crate) struct ReleaseIdentity {
21 pub db: DatabaseId,
22 pub tenant: TenantId,
23 pub engine: EngineId,
24}
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
28pub enum Layer {
29 Global,
30 Database,
31 Tenant,
32 Engine,
33}
34
35#[derive(Debug, Default)]
43pub struct OverRelease {
44 global: AtomicUsize,
45 database: AtomicUsize,
46 tenant: AtomicUsize,
47 engine: AtomicUsize,
48}
49
50impl OverRelease {
51 pub fn new() -> Self {
53 Self::default()
54 }
55
56 pub fn record(&self, layer: Layer, shortfall: usize) {
61 if shortfall == 0 {
62 return;
63 }
64 self.counter(layer).fetch_add(1, Ordering::Relaxed);
65 }
66
67 pub fn global(&self) -> usize {
69 self.global.load(Ordering::Relaxed)
70 }
71
72 pub fn database(&self) -> usize {
74 self.database.load(Ordering::Relaxed)
75 }
76
77 pub fn tenant(&self) -> usize {
79 self.tenant.load(Ordering::Relaxed)
80 }
81
82 pub fn engine(&self) -> usize {
84 self.engine.load(Ordering::Relaxed)
85 }
86
87 pub fn total(&self) -> usize {
94 self.global() + self.database() + self.tenant() + self.engine()
95 }
96
97 fn counter(&self, layer: Layer) -> &AtomicUsize {
98 match layer {
99 Layer::Global => &self.global,
100 Layer::Database => &self.database,
101 Layer::Tenant => &self.tenant,
102 Layer::Engine => &self.engine,
103 }
104 }
105}
106
107pub(crate) fn release_layer(
114 counter: &AtomicUsize,
115 size: usize,
116 layer: Layer,
117 over_release: &OverRelease,
118 id: ReleaseIdentity,
119) -> usize {
120 let shortfall = crate::budget::atomic_saturating_sub(counter, size);
121 if shortfall == 0 {
122 return 0;
123 }
124 over_release.record(layer, shortfall);
125 tracing::warn!(
126 layer = ?layer,
127 requested = size,
128 shortfall,
129 db = ?id.db,
130 tenant = ?id.tenant,
131 engine = ?id.engine,
132 "memory release exceeds allocation (WAL replay or accounting drift)"
133 );
134 shortfall
135}
136
137#[cfg(test)]
138mod tests {
139 use super::*;
140
141 #[test]
142 fn record_ignores_zero_shortfall() {
143 let counters = OverRelease::new();
144 counters.record(Layer::Engine, 0);
145 assert_eq!(counters.engine(), 0);
146 assert_eq!(counters.total(), 0);
147 }
148
149 #[test]
150 fn record_counts_events_not_bytes() {
151 let counters = OverRelease::new();
152 counters.record(Layer::Engine, 10);
153 counters.record(Layer::Engine, 9999);
154 assert_eq!(
155 counters.engine(),
156 2,
157 "record counts the number of over-release events, not shortfall bytes"
158 );
159 }
160
161 #[test]
162 fn each_layer_tracks_independently() {
163 let counters = OverRelease::new();
164 counters.record(Layer::Global, 1);
165 counters.record(Layer::Database, 1);
166 counters.record(Layer::Tenant, 1);
167 counters.record(Layer::Engine, 1);
168
169 assert_eq!(counters.global(), 1);
170 assert_eq!(counters.database(), 1);
171 assert_eq!(counters.tenant(), 1);
172 assert_eq!(counters.engine(), 1);
173 assert_eq!(counters.total(), 4);
174 }
175}