1use std::sync::Arc;
7
8use nodedb_types::{DatabaseId, TenantId};
9
10use super::core::MemoryGovernor;
11use crate::engine::EngineId;
12use crate::error::{MemError, Result};
13use crate::over_release::ReleaseIdentity;
14use crate::reservation_token::{ReservationParams, ReservationToken};
15use crate::reserve_scope::{ReserveScope, ReservedLayers};
16
17fn token_from_layers(
20 layers: ReservedLayers,
21 size: usize,
22 db: DatabaseId,
23 tenant: TenantId,
24 engine: EngineId,
25) -> ReservationToken {
26 ReservationToken::new(ReservationParams {
27 global_counter: layers.global,
28 database_counter: layers.database,
29 tenant_counter: layers.tenant,
30 engine_counter: layers.engine,
31 size,
32 db,
33 tenant,
34 engine,
35 })
36}
37
38impl MemoryGovernor {
39 pub fn try_reserve(
53 &self,
54 db: DatabaseId,
55 tenant: TenantId,
56 engine: EngineId,
57 size: usize,
58 ) -> Result<ReservationToken> {
59 let identity = ReleaseIdentity { db, tenant, engine };
60 let mut scope = ReserveScope::new(Arc::clone(&self.global_counter), size, identity);
61
62 scope.try_credit_global()?;
63
64 {
65 let map = self
66 .database_budgets
67 .read()
68 .unwrap_or_else(|p| p.into_inner());
69 if let Some(budget) = map.get(&db) {
70 match budget.try_reserve(size) {
71 Ok(arc) => scope.credit_database(arc),
72 Err(denied) => {
73 return Err(MemError::DatabaseBudgetExhausted {
74 db,
75 requested: size,
76 available: budget.available(),
77 limit: denied.limit,
78 });
79 }
80 }
81 }
82 }
83
84 {
85 let map = self
86 .tenant_budgets
87 .read()
88 .unwrap_or_else(|p| p.into_inner());
89 if let Some(budget) = map.get(&(db, tenant)) {
90 match budget.try_reserve(size) {
91 Ok(arc) => scope.credit_tenant(arc),
92 Err(denied) => {
93 return Err(MemError::TenantBudgetExhausted {
94 db,
95 tenant,
96 requested: size,
97 available: budget.available(),
98 limit: denied.limit,
99 });
100 }
101 }
102 }
103 }
104
105 let engine_budget = &self.budgets[engine.index()];
106 let engine_counter = match engine_budget.try_reserve_arc(size) {
107 Some(arc) => arc,
108 None => {
109 return Err(MemError::BudgetExhausted {
110 engine,
111 requested: size,
112 available: engine_budget.available(),
113 limit: engine_budget.limit(),
114 });
115 }
116 };
117 scope.credit_engine(engine_counter);
118
119 let layers = scope.commit();
120 Ok(token_from_layers(layers, size, db, tenant, engine))
121 }
122
123 pub fn charge(
129 &self,
130 db: DatabaseId,
131 tenant: TenantId,
132 engine: EngineId,
133 size: usize,
134 ) -> ReservationToken {
135 let identity = ReleaseIdentity { db, tenant, engine };
136 let mut scope = ReserveScope::new(Arc::clone(&self.global_counter), size, identity);
137
138 scope.credit_global_unchecked();
139
140 {
141 let map = self
142 .database_budgets
143 .read()
144 .unwrap_or_else(|p| p.into_inner());
145 if let Some(budget) = map.get(&db) {
146 scope.credit_database(budget.credit(size));
147 }
148 }
149
150 {
151 let map = self
152 .tenant_budgets
153 .read()
154 .unwrap_or_else(|p| p.into_inner());
155 if let Some(budget) = map.get(&(db, tenant)) {
156 scope.credit_tenant(budget.credit(size));
157 }
158 }
159
160 let engine_budget = &self.budgets[engine.index()];
161 scope.credit_engine(engine_budget.credit_arc(size));
162
163 let layers = scope.commit();
164 token_from_layers(layers, size, db, tenant, engine)
165 }
166}
167
168#[cfg(test)]
169mod tests {
170 use std::sync::Arc;
171 use std::sync::atomic::Ordering;
172 use std::thread;
173
174 use nodedb_types::{DatabaseId, TenantId};
175
176 use super::*;
177 use crate::engine_limits::EngineLimits;
178 use crate::governor::config::GovernorConfig;
179 use crate::governor::test_support::{db, tenant, test_config};
180
181 #[test]
184 fn reserve_within_budget() {
185 let gov = MemoryGovernor::new(test_config()).unwrap();
186 let tok = gov
187 .try_reserve(db(), tenant(), EngineId::Vector, 1000)
188 .unwrap();
189 assert_eq!(gov.budget(EngineId::Vector).allocated(), 1000);
190 assert_eq!(tok.size(), 1000);
191 }
192
193 #[test]
194 fn reserve_exceeds_engine_budget() {
195 let gov = MemoryGovernor::new(test_config()).unwrap();
196 let err = gov
197 .try_reserve(db(), tenant(), EngineId::Query, 3000)
198 .unwrap_err();
199 assert!(matches!(err, MemError::BudgetExhausted { .. }));
200 }
201
202 #[test]
203 fn reserve_exceeds_global_ceiling() {
204 let gov = MemoryGovernor::new(test_config()).unwrap();
205 let _t1 = gov
207 .try_reserve(db(), tenant(), EngineId::Vector, 4096)
208 .unwrap();
209 let _t2 = gov
210 .try_reserve(db(), tenant(), EngineId::Query, 2048)
211 .unwrap();
212 let _t3 = gov
213 .try_reserve(db(), tenant(), EngineId::Timeseries, 1024)
214 .unwrap();
215 let err = gov
217 .try_reserve(db(), tenant(), EngineId::Timeseries, 2000)
218 .unwrap_err();
219 assert!(matches!(
220 err,
221 MemError::BudgetExhausted { .. } | MemError::GlobalCeilingExceeded { .. }
222 ));
223 }
224
225 #[test]
228 fn raii_release_returns_to_baseline() {
229 let gov = MemoryGovernor::new(test_config()).unwrap();
230
231 {
232 let tok = gov
233 .try_reserve(db(), tenant(), EngineId::Vector, 1000)
234 .unwrap();
235 assert_eq!(gov.budget(EngineId::Vector).allocated(), 1000);
236 assert_eq!(tok.size(), 1000);
237 } assert_eq!(
240 gov.budget(EngineId::Vector).allocated(),
241 0,
242 "engine counter must be returned on drop"
243 );
244 }
245
246 #[test]
249 fn database_cap_denies_even_with_tenant_headroom() {
250 let gov = MemoryGovernor::new(test_config()).unwrap();
251 gov.set_database_budget(db(), 500);
253 gov.set_tenant_budget(db(), tenant(), 4096);
255
256 let err = gov
259 .try_reserve(db(), tenant(), EngineId::Vector, 600)
260 .unwrap_err();
261 assert!(
262 matches!(err, MemError::DatabaseBudgetExhausted { .. }),
263 "expected DatabaseBudgetExhausted, got {err:?}"
264 );
265 }
266
267 #[test]
268 fn global_cap_denies_even_with_database_and_tenant_headroom() {
269 let engine_limits = EngineLimits::zeroed().with(EngineId::Vector, 200);
273 let gov = MemoryGovernor::new(GovernorConfig {
274 global_ceiling: 200,
275 engine_limits,
276 })
277 .unwrap();
278 gov.set_database_budget(db(), 1024);
279 gov.set_tenant_budget(db(), tenant(), 1024);
280
281 let err = gov
282 .try_reserve(db(), tenant(), EngineId::Vector, 300)
283 .unwrap_err();
284 assert!(
285 matches!(err, MemError::GlobalCeilingExceeded { .. }),
286 "expected GlobalCeilingExceeded, got {err:?}"
287 );
288 }
289
290 #[test]
291 fn tenant_cap_denies_with_db_headroom() {
292 let gov = MemoryGovernor::new(test_config()).unwrap();
293 gov.set_database_budget(db(), 4096);
294 gov.set_tenant_budget(db(), tenant(), 300);
295
296 let err = gov
297 .try_reserve(db(), tenant(), EngineId::Vector, 400)
298 .unwrap_err();
299 assert!(
300 matches!(err, MemError::TenantBudgetExhausted { .. }),
301 "expected TenantBudgetExhausted, got {err:?}"
302 );
303 }
304
305 #[test]
308 fn partial_increments_rolled_back_on_db_failure() {
309 let gov = MemoryGovernor::new(test_config()).unwrap();
310 gov.set_database_budget(db(), 50);
311
312 let _ = gov
314 .try_reserve(db(), tenant(), EngineId::Vector, 100)
315 .unwrap_err();
316
317 assert_eq!(
319 gov.global_counter.allocated.load(Ordering::Relaxed),
320 0,
321 "global counter must be rolled back on database-layer failure"
322 );
323 }
324
325 #[test]
326 fn partial_increments_rolled_back_on_tenant_failure() {
327 let gov = MemoryGovernor::new(test_config()).unwrap();
328 gov.set_database_budget(db(), 4096);
329 gov.set_tenant_budget(db(), tenant(), 50);
330
331 let _ = gov
332 .try_reserve(db(), tenant(), EngineId::Vector, 100)
333 .unwrap_err();
334
335 assert_eq!(
337 gov.global_counter.allocated.load(Ordering::Relaxed),
338 0,
339 "global counter must be rolled back on tenant-layer failure"
340 );
341 let db_map = gov.database_budgets.read().unwrap();
342 let db_alloc = db_map[&db()].allocated.load(Ordering::Relaxed);
343 assert_eq!(db_alloc, 0, "database counter must be rolled back");
344 }
345
346 #[test]
349 fn concurrent_reserves_never_exceed_cap() {
350 let limits = EngineLimits::zeroed().with(EngineId::Vector, 10_000);
351 let gov = Arc::new(
352 MemoryGovernor::new(GovernorConfig {
353 global_ceiling: 10_000,
354 engine_limits: limits,
355 })
356 .unwrap(),
357 );
358 gov.set_database_budget(DatabaseId::DEFAULT, 10_000);
359
360 let n_threads = 8;
362 let reserve_size = 1_000;
363 let mut handles = Vec::new();
364
365 for i in 0..n_threads {
366 let gov_clone = Arc::clone(&gov);
367 handles.push(thread::spawn(move || {
368 gov_clone.try_reserve(
369 DatabaseId::DEFAULT,
370 TenantId::new(i as u64),
371 EngineId::Vector,
372 reserve_size,
373 )
374 }));
375 }
376
377 let results: Vec<_> = handles.into_iter().map(|h| h.join().unwrap()).collect();
378 let successful: Vec<_> = results.into_iter().filter_map(|r| r.ok()).collect();
379
380 assert!(
382 successful.len() <= 10,
383 "expected at most 10 successful reservations, got {}",
384 successful.len()
385 );
386
387 let engine_alloc = gov.budget(EngineId::Vector).allocated();
388 assert!(
389 engine_alloc <= 10_000,
390 "engine total {engine_alloc} must not exceed cap 10000"
391 );
392
393 let global_alloc = gov.global_counter.allocated.load(Ordering::Relaxed);
394 assert!(
395 global_alloc <= 10_000,
396 "global total {global_alloc} must not exceed ceiling 10000"
397 );
398 }
399
400 #[test]
407 fn denied_engine_budget_leaves_the_global_counter_untouched() {
408 let gov = MemoryGovernor::new(test_config()).unwrap();
409
410 let err = gov
411 .try_reserve(db(), tenant(), EngineId::Crdt, 1000)
412 .unwrap_err();
413 assert!(matches!(err, MemError::BudgetExhausted { .. }));
414
415 assert_eq!(
416 gov.global_counter.allocated.load(Ordering::Relaxed),
417 0,
418 "global counter must be rolled back when the engine budget denies"
419 );
420 }
421
422 #[test]
423 fn denied_engine_budget_leaves_the_database_counter_untouched() {
424 let gov = MemoryGovernor::new(test_config()).unwrap();
425 gov.set_database_budget(db(), 4096);
426
427 let _ = gov
428 .try_reserve(db(), tenant(), EngineId::Crdt, 1000)
429 .unwrap_err();
430
431 let db_map = gov.database_budgets.read().unwrap();
432 assert_eq!(
433 db_map[&db()].allocated.load(Ordering::Relaxed),
434 0,
435 "database counter must be rolled back when the engine budget denies"
436 );
437 }
438
439 #[test]
440 fn denied_engine_budget_leaves_the_tenant_counter_untouched() {
441 let gov = MemoryGovernor::new(test_config()).unwrap();
442 gov.set_database_budget(db(), 4096);
443 gov.set_tenant_budget(db(), tenant(), 4096);
444
445 let _ = gov
446 .try_reserve(db(), tenant(), EngineId::Crdt, 1000)
447 .unwrap_err();
448
449 let tenant_map = gov.tenant_budgets.read().unwrap();
450 assert_eq!(
451 tenant_map[&(db(), tenant())]
452 .allocated
453 .load(Ordering::Relaxed),
454 0,
455 "tenant counter must be rolled back when the engine budget denies"
456 );
457 }
458
459 #[test]
460 fn rejected_reservations_do_not_consume_the_global_ceiling() {
461 let gov = MemoryGovernor::new(test_config()).unwrap();
462
463 for _ in 0..9 {
465 let _ = gov
466 .try_reserve(db(), tenant(), EngineId::Crdt, 1000)
467 .unwrap_err();
468 }
469
470 let tok = gov.try_reserve(db(), tenant(), EngineId::Vector, 1000);
471 assert!(
472 tok.is_ok(),
473 "rejected reservations must not exhaust the global ceiling, got {:?}",
474 tok.err()
475 );
476 }
477
478 #[test]
479 fn rejected_reservations_do_not_consume_a_database_quota() {
480 let gov = MemoryGovernor::new(test_config()).unwrap();
481 gov.set_database_budget(db(), 2000);
482
483 for _ in 0..2 {
484 let _ = gov
485 .try_reserve(db(), tenant(), EngineId::Crdt, 1000)
486 .unwrap_err();
487 }
488
489 let tok = gov.try_reserve(db(), tenant(), EngineId::Vector, 1000);
490 assert!(
491 tok.is_ok(),
492 "rejected reservations must not exhaust the database quota, got {:?}",
493 tok.err()
494 );
495 }
496}