1use std::sync::Arc;
24use std::sync::atomic::AtomicUsize;
25
26use nodedb_types::{DatabaseId, TenantId};
27
28use crate::engine::EngineId;
29use crate::governor::GlobalCounter;
30use crate::over_release::{Layer, ReleaseIdentity, release_layer};
31
32#[must_use = "dropping a ReservationToken immediately releases the reservation; bind it to a variable"]
37pub struct ReservationToken {
38 pub(crate) global_counter: Arc<GlobalCounter>,
40 pub(crate) database_counter: Option<Arc<AtomicUsize>>,
42 pub(crate) tenant_counter: Option<Arc<AtomicUsize>>,
44 pub(crate) engine_counter: Option<Arc<AtomicUsize>>,
47 pub(crate) size: usize,
49 db: DatabaseId,
51 tenant: TenantId,
52 engine: EngineId,
53}
54
55pub(crate) struct ReservationParams {
60 pub global_counter: Arc<GlobalCounter>,
61 pub database_counter: Option<Arc<AtomicUsize>>,
62 pub tenant_counter: Option<Arc<AtomicUsize>>,
63 pub engine_counter: Option<Arc<AtomicUsize>>,
64 pub size: usize,
65 pub db: DatabaseId,
66 pub tenant: TenantId,
67 pub engine: EngineId,
68}
69
70impl ReservationToken {
71 pub(crate) fn new(params: ReservationParams) -> Self {
73 Self {
74 global_counter: params.global_counter,
75 database_counter: params.database_counter,
76 tenant_counter: params.tenant_counter,
77 engine_counter: params.engine_counter,
78 size: params.size,
79 db: params.db,
80 tenant: params.tenant,
81 engine: params.engine,
82 }
83 }
84
85 pub fn size(&self) -> usize {
87 self.size
88 }
89
90 pub fn database_id(&self) -> DatabaseId {
92 self.db
93 }
94
95 pub fn tenant_id(&self) -> TenantId {
97 self.tenant
98 }
99
100 pub fn engine(&self) -> EngineId {
102 self.engine
103 }
104}
105
106impl Drop for ReservationToken {
107 fn drop(&mut self) {
108 let size = self.size;
109 if size == 0 {
110 return;
111 }
112
113 let identity = ReleaseIdentity {
127 db: self.db,
128 tenant: self.tenant,
129 engine: self.engine,
130 };
131 let over_release = &self.global_counter.over_release;
132 if let Some(ref counter) = self.engine_counter {
133 release_layer(counter, size, Layer::Engine, over_release, identity);
134 }
135 if let Some(ref counter) = self.tenant_counter {
136 release_layer(counter, size, Layer::Tenant, over_release, identity);
137 }
138 if let Some(ref counter) = self.database_counter {
139 release_layer(counter, size, Layer::Database, over_release, identity);
140 }
141 release_layer(
142 &self.global_counter.allocated,
143 size,
144 Layer::Global,
145 over_release,
146 identity,
147 );
148 }
149}
150
151impl std::fmt::Debug for ReservationToken {
152 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
153 f.debug_struct("ReservationToken")
154 .field("size", &self.size)
155 .field("db", &self.db)
156 .field("tenant", &self.tenant)
157 .field("engine", &self.engine)
158 .finish()
159 }
160}
161
162#[cfg(test)]
163mod tests {
164 use std::sync::Arc;
165 use std::sync::atomic::AtomicUsize;
166
167 use nodedb_types::{DatabaseId, TenantId};
168
169 use super::{ReservationParams, ReservationToken};
170 use crate::engine::EngineId;
171 use crate::governor::GlobalCounter;
172
173 fn make_counter(val: usize) -> Arc<AtomicUsize> {
174 Arc::new(AtomicUsize::new(val))
175 }
176
177 fn make_global(val: usize) -> Arc<GlobalCounter> {
178 let global = GlobalCounter::new(1024 * 1024);
179 global
180 .allocated
181 .store(val, std::sync::atomic::Ordering::Relaxed);
182 Arc::new(global)
183 }
184
185 #[test]
186 fn drop_releases_all_four_levels() {
187 let global = make_global(100);
188 let db_ctr = make_counter(100);
189 let tenant_ctr = make_counter(100);
190 let engine_ctr = make_counter(100);
191
192 let token = ReservationToken::new(ReservationParams {
193 global_counter: Arc::clone(&global),
194 database_counter: Some(Arc::clone(&db_ctr)),
195 tenant_counter: Some(Arc::clone(&tenant_ctr)),
196 engine_counter: Some(Arc::clone(&engine_ctr)),
197 size: 100,
198 db: DatabaseId::DEFAULT,
199 tenant: TenantId::new(1),
200 engine: EngineId::Vector,
201 });
202
203 assert_eq!(
204 global.allocated.load(std::sync::atomic::Ordering::Relaxed),
205 100
206 );
207
208 drop(token);
209
210 assert_eq!(
211 global.allocated.load(std::sync::atomic::Ordering::Relaxed),
212 0
213 );
214 assert_eq!(db_ctr.load(std::sync::atomic::Ordering::Relaxed), 0);
215 assert_eq!(tenant_ctr.load(std::sync::atomic::Ordering::Relaxed), 0);
216 assert_eq!(engine_ctr.load(std::sync::atomic::Ordering::Relaxed), 0);
217 }
218
219 #[test]
220 fn drop_with_no_scoped_counters_releases_global() {
221 let global = make_global(200);
222 let token = ReservationToken::new(ReservationParams {
223 global_counter: Arc::clone(&global),
224 database_counter: None,
225 tenant_counter: None,
226 engine_counter: None,
227 size: 200,
228 db: DatabaseId::DEFAULT,
229 tenant: TenantId::new(1),
230 engine: EngineId::Query,
231 });
232 drop(token);
233 assert_eq!(
234 global.allocated.load(std::sync::atomic::Ordering::Relaxed),
235 0
236 );
237 }
238
239 #[test]
240 fn drop_does_not_underflow_a_counter_released_below_size() {
241 let global = make_global(40);
253 let engine_ctr = make_counter(40);
254 let tenant_ctr = make_counter(40);
255
256 let token = ReservationToken::new(ReservationParams {
257 global_counter: Arc::clone(&global),
258 database_counter: None,
259 tenant_counter: Some(Arc::clone(&tenant_ctr)),
260 engine_counter: Some(Arc::clone(&engine_ctr)),
261 size: 40,
262 db: DatabaseId::DEFAULT,
263 tenant: TenantId::new(1),
264 engine: EngineId::Timeseries,
265 });
266
267 engine_ctr.store(0, std::sync::atomic::Ordering::Relaxed);
271 global
272 .allocated
273 .store(0, std::sync::atomic::Ordering::Relaxed);
274
275 drop(token);
276
277 let engine = engine_ctr.load(std::sync::atomic::Ordering::Relaxed);
278 let glob = global.allocated.load(std::sync::atomic::Ordering::Relaxed);
279 let tenant = tenant_ctr.load(std::sync::atomic::Ordering::Relaxed);
280 assert_eq!(
281 engine, 0,
282 "engine counter underflowed to {engine} on token drop — a wrapped \
283 counter reads as 100% utilization (Emergency) forever"
284 );
285 assert_eq!(
286 glob, 0,
287 "global counter underflowed to {glob} on token drop"
288 );
289 assert_eq!(tenant, 0, "tenant counter should release normally to 0");
293 }
294
295 fn generous_governor(per_engine: usize) -> crate::governor::MemoryGovernor {
299 crate::governor::MemoryGovernor::new(crate::governor::GovernorConfig {
300 global_ceiling: per_engine * EngineId::ALL.len(),
301 engine_limits: crate::engine_limits::EngineLimits::uniform(per_engine),
302 })
303 .expect("uniform limits always satisfy the global-ceiling check")
304 }
305
306 #[test]
307 fn second_drop_over_the_same_engine_counter_is_counted() {
308 let global = make_global(100);
315 let engine_ctr = make_counter(100);
316
317 let first = ReservationToken::new(ReservationParams {
318 global_counter: Arc::clone(&global),
319 database_counter: None,
320 tenant_counter: None,
321 engine_counter: Some(Arc::clone(&engine_ctr)),
322 size: 100,
323 db: DatabaseId::DEFAULT,
324 tenant: TenantId::new(1),
325 engine: EngineId::Vector,
326 });
327 let second = ReservationToken::new(ReservationParams {
328 global_counter: Arc::clone(&global),
329 database_counter: None,
330 tenant_counter: None,
331 engine_counter: Some(Arc::clone(&engine_ctr)),
332 size: 100,
333 db: DatabaseId::DEFAULT,
334 tenant: TenantId::new(1),
335 engine: EngineId::Vector,
336 });
337
338 drop(first);
339 assert_eq!(
340 global.over_release.total(),
341 0,
342 "the first drop is a clean release of the only 100 bytes credited"
343 );
344
345 drop(second);
346 assert!(
347 global.over_release.engine() > 0,
348 "the second drop releases 100 bytes the engine counter no \
349 longer holds — the live-path over-release the detector exists \
350 to catch"
351 );
352 assert_eq!(
353 global.over_release.global(),
354 1,
355 "the global counter is credited once and released twice, so it \
356 over-releases alongside the engine layer"
357 );
358 assert_eq!(
359 global.over_release.total(),
360 2,
361 "total() sums the layers, and this drop drifts both the engine \
362 and the global counter"
363 );
364 assert_eq!(
365 global.over_release.database(),
366 0,
367 "a token with no database counter cannot drift that layer"
368 );
369 assert_eq!(global.over_release.tenant(), 0);
370 }
371
372 #[test]
373 fn clean_reserve_and_drop_leaves_every_over_release_counter_at_zero() {
374 let gov = generous_governor(1024);
375 let token = gov
376 .try_reserve(DatabaseId::DEFAULT, TenantId::new(1), EngineId::Vector, 100)
377 .unwrap();
378 drop(token);
379
380 assert_eq!(gov.total_over_release_count(), 0);
381 assert_eq!(gov.global_over_release_count(), 0);
382 assert_eq!(gov.database_over_release_count(), 0);
383 assert_eq!(gov.tenant_over_release_count(), 0);
384 assert_eq!(gov.engine_over_release_count(), 0);
385 }
386
387 #[test]
388 fn over_release_still_clamps_the_counter_to_zero_never_wrapping() {
389 let global = make_global(100);
393 let engine_ctr = make_counter(100);
394
395 let first = ReservationToken::new(ReservationParams {
396 global_counter: Arc::clone(&global),
397 database_counter: None,
398 tenant_counter: None,
399 engine_counter: Some(Arc::clone(&engine_ctr)),
400 size: 100,
401 db: DatabaseId::DEFAULT,
402 tenant: TenantId::new(1),
403 engine: EngineId::Vector,
404 });
405 let second = ReservationToken::new(ReservationParams {
406 global_counter: Arc::clone(&global),
407 database_counter: None,
408 tenant_counter: None,
409 engine_counter: Some(Arc::clone(&engine_ctr)),
410 size: 100,
411 db: DatabaseId::DEFAULT,
412 tenant: TenantId::new(1),
413 engine: EngineId::Vector,
414 });
415 drop(first);
416 drop(second);
417
418 assert_eq!(
419 engine_ctr.load(std::sync::atomic::Ordering::Relaxed),
420 0,
421 "an over-release must saturate at zero, never wrap toward usize::MAX"
422 );
423 }
424
425 #[test]
426 fn zero_size_drop_is_noop() {
427 let global = make_global(0);
428 let token = ReservationToken::new(ReservationParams {
429 global_counter: Arc::clone(&global),
430 database_counter: None,
431 tenant_counter: None,
432 engine_counter: None,
433 size: 0,
434 db: DatabaseId::DEFAULT,
435 tenant: TenantId::new(1),
436 engine: EngineId::Query,
437 });
438 drop(token);
439 assert_eq!(
440 global.allocated.load(std::sync::atomic::Ordering::Relaxed),
441 0
442 );
443 }
444}