1use nodedb_types::{DatabaseId, TenantId};
19
20use super::core::MemoryGovernor;
21use crate::scoped_budget::{clear_scoped_limit, set_scoped_limit};
22
23impl MemoryGovernor {
24 pub fn set_database_budget(&self, db: DatabaseId, max_bytes: usize) {
32 let mut map = self
33 .database_budgets
34 .write()
35 .unwrap_or_else(|p| p.into_inner());
36 set_scoped_limit(&mut map, db, max_bytes);
37 }
38
39 pub fn clear_database_budget(&self, db: DatabaseId) {
41 let mut map = self
42 .database_budgets
43 .write()
44 .unwrap_or_else(|p| p.into_inner());
45 clear_scoped_limit(&mut map, &db);
46 }
47
48 pub fn set_tenant_budget(&self, db: DatabaseId, tenant: TenantId, max_bytes: usize) {
52 let mut map = self
53 .tenant_budgets
54 .write()
55 .unwrap_or_else(|p| p.into_inner());
56 set_scoped_limit(&mut map, (db, tenant), max_bytes);
57 }
58
59 pub fn clear_tenant_budget(&self, db: DatabaseId, tenant: TenantId) {
61 let mut map = self
62 .tenant_budgets
63 .write()
64 .unwrap_or_else(|p| p.into_inner());
65 clear_scoped_limit(&mut map, &(db, tenant));
66 }
67
68 pub fn clear_database_scope(&self, db: DatabaseId) {
73 {
74 let mut map = self
75 .database_budgets
76 .write()
77 .unwrap_or_else(|p| p.into_inner());
78 map.remove(&db);
79 }
80 let mut map = self
81 .tenant_budgets
82 .write()
83 .unwrap_or_else(|p| p.into_inner());
84 map.retain(|(entry_db, _), _| *entry_db != db);
85 }
86
87 pub fn clear_tenant_scope(&self, tenant: TenantId) {
90 let mut map = self
91 .tenant_budgets
92 .write()
93 .unwrap_or_else(|p| p.into_inner());
94 map.retain(|(_, entry_tenant), _| *entry_tenant != tenant);
95 }
96}
97
98#[cfg(test)]
99mod tests {
100 use super::*;
101 use crate::engine::EngineId;
102 use crate::governor::test_support::{db, tenant, test_config};
103
104 #[test]
107 fn raising_a_live_quota_keeps_the_allocated_bytes_tracked() {
108 let gov = MemoryGovernor::new(test_config()).unwrap();
109 gov.set_database_budget(db(), 500);
110
111 let _held = gov
112 .try_reserve(db(), tenant(), EngineId::Vector, 400)
113 .unwrap();
114 gov.set_database_budget(db(), 1000);
115
116 let err = gov
118 .try_reserve(db(), tenant(), EngineId::Vector, 700)
119 .unwrap_err();
120 assert!(
121 matches!(err, crate::error::MemError::DatabaseBudgetExhausted { .. }),
122 "raising the quota must keep the held 400 bytes counted, got {err:?}"
123 );
124 }
125
126 #[test]
127 fn raising_a_live_tenant_quota_keeps_the_allocated_bytes_tracked() {
128 let gov = MemoryGovernor::new(test_config()).unwrap();
129 gov.set_tenant_budget(db(), tenant(), 500);
130
131 let _held = gov
132 .try_reserve(db(), tenant(), EngineId::Vector, 400)
133 .unwrap();
134 gov.set_tenant_budget(db(), tenant(), 1000);
135
136 let err = gov
137 .try_reserve(db(), tenant(), EngineId::Vector, 700)
138 .unwrap_err();
139 assert!(
140 matches!(err, crate::error::MemError::TenantBudgetExhausted { .. }),
141 "raising the quota must keep the held 400 bytes counted, got {err:?}"
142 );
143 }
144
145 #[test]
146 fn clearing_a_quota_with_live_tokens_keeps_the_counter() {
147 let gov = MemoryGovernor::new(test_config()).unwrap();
148 gov.set_database_budget(db(), 500);
149
150 let _held = gov
151 .try_reserve(db(), tenant(), EngineId::Vector, 400)
152 .unwrap();
153 gov.clear_database_budget(db());
154 gov.set_database_budget(db(), 1000);
155
156 let err = gov
157 .try_reserve(db(), tenant(), EngineId::Vector, 700)
158 .unwrap_err();
159 assert!(
160 matches!(err, crate::error::MemError::DatabaseBudgetExhausted { .. }),
161 "clear-then-set must keep the held 400 bytes counted, got {err:?}"
162 );
163 }
164
165 #[test]
166 fn clearing_a_quota_with_no_live_tokens_drops_the_entry() {
167 let gov = MemoryGovernor::new(test_config()).unwrap();
168 gov.set_database_budget(db(), 500);
169 gov.set_tenant_budget(db(), tenant(), 500);
170
171 {
172 let _tok = gov
173 .try_reserve(db(), tenant(), EngineId::Vector, 400)
174 .unwrap();
175 }
176 gov.clear_database_budget(db());
177 gov.clear_tenant_budget(db(), tenant());
178
179 assert!(
180 !gov.database_budgets.read().unwrap().contains_key(&db()),
181 "database entry must be removed once no token holds its counter"
182 );
183 assert!(
184 !gov.tenant_budgets
185 .read()
186 .unwrap()
187 .contains_key(&(db(), tenant())),
188 "tenant entry must be removed once no token holds its counter"
189 );
190 }
191
192 #[test]
193 fn released_bytes_do_not_count_against_a_later_quota() {
194 let gov = MemoryGovernor::new(test_config()).unwrap();
195 gov.set_database_budget(db(), 500);
196
197 {
198 let _tok = gov
199 .try_reserve(db(), tenant(), EngineId::Vector, 400)
200 .unwrap();
201 }
202 gov.set_database_budget(db(), 500);
203
204 let _tok = gov
206 .try_reserve(db(), tenant(), EngineId::Vector, 500)
207 .expect("released bytes must not be double-counted");
208 }
209
210 #[test]
211 fn clear_database_scope_drops_the_database_and_its_tenants() {
212 let gov = MemoryGovernor::new(test_config()).unwrap();
213 let other = DatabaseId::new(9);
214 gov.set_database_budget(db(), 500);
215 gov.set_tenant_budget(db(), tenant(), 500);
216 gov.set_database_budget(other, 500);
217 gov.set_tenant_budget(other, tenant(), 500);
218 let held = gov
219 .try_reserve(db(), tenant(), EngineId::Vector, 400)
220 .unwrap();
221
222 gov.clear_database_scope(db());
223
224 assert!(!gov.database_budgets.read().unwrap().contains_key(&db()));
225 assert!(
226 !gov.tenant_budgets
227 .read()
228 .unwrap()
229 .contains_key(&(db(), tenant())),
230 "a live token must not keep a dropped scope's entry alive"
231 );
232 assert!(gov.database_budgets.read().unwrap().contains_key(&other));
233 assert!(
234 gov.tenant_budgets
235 .read()
236 .unwrap()
237 .contains_key(&(other, tenant()))
238 );
239 drop(held);
240 }
241
242 #[test]
243 fn clear_tenant_scope_drops_that_tenant_in_every_database() {
244 let gov = MemoryGovernor::new(test_config()).unwrap();
245 let other_db = DatabaseId::new(9);
246 let other_tenant = TenantId::new(2);
247 gov.set_tenant_budget(db(), tenant(), 500);
248 gov.set_tenant_budget(other_db, tenant(), 500);
249 gov.set_tenant_budget(db(), other_tenant, 500);
250
251 gov.clear_tenant_scope(tenant());
252
253 assert!(
254 !gov.tenant_budgets
255 .read()
256 .unwrap()
257 .contains_key(&(db(), tenant()))
258 );
259 assert!(
260 !gov.tenant_budgets
261 .read()
262 .unwrap()
263 .contains_key(&(other_db, tenant()))
264 );
265 assert!(
266 gov.tenant_budgets
267 .read()
268 .unwrap()
269 .contains_key(&(db(), other_tenant))
270 );
271 }
272}