use nodedb_types::{DatabaseId, TenantId};
use super::core::MemoryGovernor;
use crate::scoped_budget::{clear_scoped_limit, set_scoped_limit};
impl MemoryGovernor {
pub fn set_database_budget(&self, db: DatabaseId, max_bytes: usize) {
let mut map = self
.database_budgets
.write()
.unwrap_or_else(|p| p.into_inner());
set_scoped_limit(&mut map, db, max_bytes);
}
pub fn clear_database_budget(&self, db: DatabaseId) {
let mut map = self
.database_budgets
.write()
.unwrap_or_else(|p| p.into_inner());
clear_scoped_limit(&mut map, &db);
}
pub fn set_tenant_budget(&self, db: DatabaseId, tenant: TenantId, max_bytes: usize) {
let mut map = self
.tenant_budgets
.write()
.unwrap_or_else(|p| p.into_inner());
set_scoped_limit(&mut map, (db, tenant), max_bytes);
}
pub fn clear_tenant_budget(&self, db: DatabaseId, tenant: TenantId) {
let mut map = self
.tenant_budgets
.write()
.unwrap_or_else(|p| p.into_inner());
clear_scoped_limit(&mut map, &(db, tenant));
}
pub fn clear_database_scope(&self, db: DatabaseId) {
{
let mut map = self
.database_budgets
.write()
.unwrap_or_else(|p| p.into_inner());
map.remove(&db);
}
let mut map = self
.tenant_budgets
.write()
.unwrap_or_else(|p| p.into_inner());
map.retain(|(entry_db, _), _| *entry_db != db);
}
pub fn clear_tenant_scope(&self, tenant: TenantId) {
let mut map = self
.tenant_budgets
.write()
.unwrap_or_else(|p| p.into_inner());
map.retain(|(_, entry_tenant), _| *entry_tenant != tenant);
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::engine::EngineId;
use crate::governor::test_support::{db, tenant, test_config};
#[test]
fn raising_a_live_quota_keeps_the_allocated_bytes_tracked() {
let gov = MemoryGovernor::new(test_config()).unwrap();
gov.set_database_budget(db(), 500);
let _held = gov
.try_reserve(db(), tenant(), EngineId::Vector, 400)
.unwrap();
gov.set_database_budget(db(), 1000);
let err = gov
.try_reserve(db(), tenant(), EngineId::Vector, 700)
.unwrap_err();
assert!(
matches!(err, crate::error::MemError::DatabaseBudgetExhausted { .. }),
"raising the quota must keep the held 400 bytes counted, got {err:?}"
);
}
#[test]
fn raising_a_live_tenant_quota_keeps_the_allocated_bytes_tracked() {
let gov = MemoryGovernor::new(test_config()).unwrap();
gov.set_tenant_budget(db(), tenant(), 500);
let _held = gov
.try_reserve(db(), tenant(), EngineId::Vector, 400)
.unwrap();
gov.set_tenant_budget(db(), tenant(), 1000);
let err = gov
.try_reserve(db(), tenant(), EngineId::Vector, 700)
.unwrap_err();
assert!(
matches!(err, crate::error::MemError::TenantBudgetExhausted { .. }),
"raising the quota must keep the held 400 bytes counted, got {err:?}"
);
}
#[test]
fn clearing_a_quota_with_live_tokens_keeps_the_counter() {
let gov = MemoryGovernor::new(test_config()).unwrap();
gov.set_database_budget(db(), 500);
let _held = gov
.try_reserve(db(), tenant(), EngineId::Vector, 400)
.unwrap();
gov.clear_database_budget(db());
gov.set_database_budget(db(), 1000);
let err = gov
.try_reserve(db(), tenant(), EngineId::Vector, 700)
.unwrap_err();
assert!(
matches!(err, crate::error::MemError::DatabaseBudgetExhausted { .. }),
"clear-then-set must keep the held 400 bytes counted, got {err:?}"
);
}
#[test]
fn clearing_a_quota_with_no_live_tokens_drops_the_entry() {
let gov = MemoryGovernor::new(test_config()).unwrap();
gov.set_database_budget(db(), 500);
gov.set_tenant_budget(db(), tenant(), 500);
{
let _tok = gov
.try_reserve(db(), tenant(), EngineId::Vector, 400)
.unwrap();
}
gov.clear_database_budget(db());
gov.clear_tenant_budget(db(), tenant());
assert!(
!gov.database_budgets.read().unwrap().contains_key(&db()),
"database entry must be removed once no token holds its counter"
);
assert!(
!gov.tenant_budgets
.read()
.unwrap()
.contains_key(&(db(), tenant())),
"tenant entry must be removed once no token holds its counter"
);
}
#[test]
fn released_bytes_do_not_count_against_a_later_quota() {
let gov = MemoryGovernor::new(test_config()).unwrap();
gov.set_database_budget(db(), 500);
{
let _tok = gov
.try_reserve(db(), tenant(), EngineId::Vector, 400)
.unwrap();
}
gov.set_database_budget(db(), 500);
let _tok = gov
.try_reserve(db(), tenant(), EngineId::Vector, 500)
.expect("released bytes must not be double-counted");
}
#[test]
fn clear_database_scope_drops_the_database_and_its_tenants() {
let gov = MemoryGovernor::new(test_config()).unwrap();
let other = DatabaseId::new(9);
gov.set_database_budget(db(), 500);
gov.set_tenant_budget(db(), tenant(), 500);
gov.set_database_budget(other, 500);
gov.set_tenant_budget(other, tenant(), 500);
let held = gov
.try_reserve(db(), tenant(), EngineId::Vector, 400)
.unwrap();
gov.clear_database_scope(db());
assert!(!gov.database_budgets.read().unwrap().contains_key(&db()));
assert!(
!gov.tenant_budgets
.read()
.unwrap()
.contains_key(&(db(), tenant())),
"a live token must not keep a dropped scope's entry alive"
);
assert!(gov.database_budgets.read().unwrap().contains_key(&other));
assert!(
gov.tenant_budgets
.read()
.unwrap()
.contains_key(&(other, tenant()))
);
drop(held);
}
#[test]
fn clear_tenant_scope_drops_that_tenant_in_every_database() {
let gov = MemoryGovernor::new(test_config()).unwrap();
let other_db = DatabaseId::new(9);
let other_tenant = TenantId::new(2);
gov.set_tenant_budget(db(), tenant(), 500);
gov.set_tenant_budget(other_db, tenant(), 500);
gov.set_tenant_budget(db(), other_tenant, 500);
gov.clear_tenant_scope(tenant());
assert!(
!gov.tenant_budgets
.read()
.unwrap()
.contains_key(&(db(), tenant()))
);
assert!(
!gov.tenant_budgets
.read()
.unwrap()
.contains_key(&(other_db, tenant()))
);
assert!(
gov.tenant_budgets
.read()
.unwrap()
.contains_key(&(db(), other_tenant))
);
}
}