1use std::{
7 cell::{Cell, RefCell},
8 rc::Rc,
9};
10
11use crate::db::resource::budget::{
12 ExecutionBudgetExceeded, HardExecutionBudget, HardExecutionContext, READ_HARD_BUDGET,
13 resource_index,
14};
15use icydb_diagnostic_code::{DiagnosticExecutionBudgetResource, DiagnosticExecutionBudgetScope};
16
17const REQUEST_HARD_BUDGET: HardExecutionBudget = READ_HARD_BUDGET
20 .with_limit(DiagnosticExecutionBudgetResource::QueryExecutions, 256)
21 .with_limit(DiagnosticExecutionBudgetResource::PlanningSteps, 1_024)
22 .with_limit(DiagnosticExecutionBudgetResource::PlanCompilations, 256);
23
24thread_local! {
25 static CURRENT_REQUEST_SCOPE: RefCell<Option<RequestExecutionScope>> =
26 const { RefCell::new(None) };
27}
28
29#[derive(Clone, Debug, Eq, PartialEq)]
37pub struct RequestBudgetSnapshot {
38 observed: [u64; DiagnosticExecutionBudgetResource::ALL.len()],
39 limits: [u64; DiagnosticExecutionBudgetResource::ALL.len()],
40}
41
42impl RequestBudgetSnapshot {
43 #[must_use]
45 pub const fn observed(&self, resource: DiagnosticExecutionBudgetResource) -> u64 {
46 self.observed[resource_index(resource)]
47 }
48
49 #[must_use]
51 pub const fn limit(&self, resource: DiagnosticExecutionBudgetResource) -> u64 {
52 self.limits[resource_index(resource)]
53 }
54
55 #[must_use]
60 pub const fn remaining(&self, resource: DiagnosticExecutionBudgetResource) -> u64 {
61 self.limit(resource).saturating_sub(self.observed(resource))
62 }
63}
64
65pub struct RequestExecutionRoot {
71 scope: RequestExecutionScope,
72}
73
74impl RequestExecutionRoot {
75 #[must_use]
77 pub fn request_budget(&self) -> RequestBudgetSnapshot {
78 self.scope.request_budget()
79 }
80
81 #[doc(hidden)]
87 #[must_use]
88 pub fn __new_runtime_root() -> Self {
89 Self::from_budget(REQUEST_HARD_BUDGET)
90 }
91
92 #[doc(hidden)]
98 #[must_use]
99 pub fn __new_or_current_runtime_root() -> Self {
100 current_request_scope().map_or_else(Self::__new_runtime_root, |scope| Self { scope })
101 }
102
103 #[doc(hidden)]
109 pub fn __with_current_scope<T>(&self, run: impl FnOnce() -> T) -> T {
110 let _guard = CurrentRequestScopeGuard::enter(self.scope());
111 run()
112 }
113
114 #[doc(hidden)]
120 #[must_use]
121 pub fn __is_compatible_with_current(&self) -> bool {
122 match current_request_scope() {
123 Some(current) => current.same_counters(&self.scope),
124 None => true,
125 }
126 }
127
128 #[doc(hidden)]
130 #[must_use]
131 pub fn __is_current(&self) -> bool {
132 current_request_scope().is_some_and(|current| current.same_counters(&self.scope))
133 }
134
135 #[cfg(test)]
136 #[must_use]
137 pub(in crate::db) fn new_for_tests(budget: HardExecutionBudget) -> Self {
138 Self::from_budget(budget)
139 }
140
141 fn from_budget(budget: HardExecutionBudget) -> Self {
142 Self {
143 scope: RequestExecutionScope {
144 counters: Rc::new(RequestExecutionCounters {
145 budget,
146 observed: [const { Cell::new(0) };
147 DiagnosticExecutionBudgetResource::ALL.len()],
148 }),
149 },
150 }
151 }
152
153 pub(in crate::db) fn scope(&self) -> RequestExecutionScope {
154 self.scope.clone()
155 }
156
157 #[cfg(test)]
158 #[must_use]
159 pub(in crate::db) fn observed(&self, resource: DiagnosticExecutionBudgetResource) -> u64 {
160 self.scope.observed(resource)
161 }
162}
163
164pub(in crate::db) fn current_request_scope() -> Option<RequestExecutionScope> {
165 CURRENT_REQUEST_SCOPE.with(|current| current.borrow().clone())
166}
167
168struct CurrentRequestScopeGuard {
169 previous: Option<RequestExecutionScope>,
170}
171
172impl CurrentRequestScopeGuard {
173 fn enter(scope: RequestExecutionScope) -> Self {
174 let previous = CURRENT_REQUEST_SCOPE.with(|current| current.replace(Some(scope)));
175 Self { previous }
176 }
177}
178
179impl Drop for CurrentRequestScopeGuard {
180 fn drop(&mut self) {
181 CURRENT_REQUEST_SCOPE.with(|current| {
182 current.replace(self.previous.take());
183 });
184 }
185}
186
187#[derive(Clone)]
189pub(in crate::db) struct RequestExecutionScope {
190 counters: Rc<RequestExecutionCounters>,
191}
192
193impl RequestExecutionScope {
194 pub(in crate::db) fn request_budget(&self) -> RequestBudgetSnapshot {
196 RequestBudgetSnapshot {
197 observed: DiagnosticExecutionBudgetResource::ALL
198 .map(|resource| self.counters.observed[resource_index(resource)].get()),
199 limits: DiagnosticExecutionBudgetResource::ALL
200 .map(|resource| self.counters.budget.limit(resource)),
201 }
202 }
203
204 fn same_counters(&self, other: &Self) -> bool {
205 Rc::ptr_eq(&self.counters, &other.counters)
206 }
207
208 pub(in crate::db) fn charge(
209 &self,
210 context: HardExecutionContext,
211 resource: DiagnosticExecutionBudgetResource,
212 amount: u64,
213 ) -> Result<(), ExecutionBudgetExceeded> {
214 self.counters.charge(context, resource, amount)
215 }
216
217 pub(in crate::db) fn remaining_budget_units(
220 &self,
221 per_unit: &[(DiagnosticExecutionBudgetResource, u64)],
222 ) -> u64 {
223 self.counters
224 .budget
225 .remaining_budget_units(per_unit, |resource| {
226 self.counters.observed[resource_index(resource)].get()
227 })
228 }
229
230 pub(in crate::db) fn can_charge_budget_bundle(
233 &self,
234 charges: &[(DiagnosticExecutionBudgetResource, u64)],
235 ) -> bool {
236 self.counters
237 .budget
238 .can_charge_budget_bundle(charges, |resource| {
239 self.counters.observed[resource_index(resource)].get()
240 })
241 }
242
243 pub(in crate::db) fn try_commit_budget_bundle(
246 &self,
247 charges: &[(DiagnosticExecutionBudgetResource, u64)],
248 ) -> bool {
249 if !self.can_charge_budget_bundle(charges) {
250 return false;
251 }
252 for (resource, amount) in charges {
253 let counter = &self.counters.observed[resource_index(*resource)];
254 counter.set(counter.get().saturating_add(*amount));
255 }
256
257 true
258 }
259
260 #[cfg(test)]
261 fn observed(&self, resource: DiagnosticExecutionBudgetResource) -> u64 {
262 self.counters.observed[resource_index(resource)].get()
263 }
264}
265
266struct RequestExecutionCounters {
267 budget: HardExecutionBudget,
268 observed: [Cell<u64>; DiagnosticExecutionBudgetResource::ALL.len()],
269}
270
271impl RequestExecutionCounters {
272 fn charge(
273 &self,
274 context: HardExecutionContext,
275 resource: DiagnosticExecutionBudgetResource,
276 amount: u64,
277 ) -> Result<(), ExecutionBudgetExceeded> {
278 let index = resource_index(resource);
279 let counter = &self.observed[index];
280 let current = counter.get();
281 let (observed, overflowed) = current.overflowing_add(amount);
282 let observed = if overflowed { u64::MAX } else { observed };
283 counter.set(observed);
284 let limit = self.budget.limit(resource);
285 if overflowed || observed > limit {
286 return Err(ExecutionBudgetExceeded::new(
287 resource,
288 limit,
289 observed,
290 context.with_scope(DiagnosticExecutionBudgetScope::Request),
291 ));
292 }
293
294 Ok(())
295 }
296}
297
298#[cfg(test)]
299mod tests {
300 use super::*;
301
302 #[test]
303 fn request_budget_snapshots_cover_every_resource_without_mutating_the_owner() {
304 let root = RequestExecutionRoot::__new_runtime_root();
305 let initial = root.request_budget();
306 let context = HardExecutionContext::new(
307 DiagnosticExecutionBudgetScope::Execution,
308 icydb_diagnostic_code::DiagnosticExecutionLane::PublicRead,
309 0,
310 );
311 for resource in DiagnosticExecutionBudgetResource::ALL {
312 assert_eq!(initial.observed(resource), 0);
313 assert_eq!(initial.limit(resource), REQUEST_HARD_BUDGET.limit(resource));
314 assert_eq!(initial.remaining(resource), initial.limit(resource));
315 root.scope()
316 .charge(context, resource, resource.raw())
317 .unwrap();
318 }
319 let charged = root.request_budget();
320 assert_eq!(root.request_budget(), charged);
321 for resource in DiagnosticExecutionBudgetResource::ALL {
322 assert_eq!(charged.observed(resource), resource.raw());
323 assert_eq!(
324 charged.remaining(resource),
325 charged.limit(resource) - resource.raw()
326 );
327 assert_eq!(initial.observed(resource), 0, "snapshots are frozen");
328 }
329 root.__with_current_scope(|| {
330 let nested = RequestExecutionRoot::__new_or_current_runtime_root();
331 assert_eq!(nested.request_budget(), charged);
332 });
333 let isolated = RequestExecutionRoot::__new_runtime_root();
334 isolated.__with_current_scope(|| {
335 assert_eq!(root.request_budget(), charged);
336 assert_eq!(isolated.request_budget(), initial);
337 });
338 }
339
340 #[test]
341 fn request_budget_headroom_saturates_after_failed_and_overflowing_charges() {
342 let context = HardExecutionContext::new(
343 DiagnosticExecutionBudgetScope::Execution,
344 icydb_diagnostic_code::DiagnosticExecutionLane::PublicRead,
345 0,
346 );
347 for resource in DiagnosticExecutionBudgetResource::ALL {
348 let root = RequestExecutionRoot::new_for_tests(
349 REQUEST_HARD_BUDGET.with_limit_for_tests(resource, 2),
350 );
351 let scope = root.scope();
352 scope.charge(context, resource, 2).unwrap();
353 assert_eq!(root.request_budget().remaining(resource), 0);
354 let failure = scope.charge(context, resource, 1).unwrap_err();
355 assert_eq!(failure.scope(), DiagnosticExecutionBudgetScope::Request);
356 assert_eq!(failure.observed(), 3);
357 assert_eq!(root.request_budget().observed(resource), 3);
358 assert_eq!(root.request_budget().remaining(resource), 0);
359 let overflow = scope.charge(context, resource, u64::MAX).unwrap_err();
360 assert_eq!(overflow.observed(), u64::MAX);
361 assert_eq!(root.request_budget().observed(resource), u64::MAX);
362 assert_eq!(root.request_budget().remaining(resource), 0);
363 }
364 }
365
366 #[test]
367 fn synchronous_scope_is_installed_then_removed() {
368 assert!(current_request_scope().is_none());
369 let root = RequestExecutionRoot::new_for_tests(REQUEST_HARD_BUDGET);
370
371 root.__with_current_scope(|| {
372 assert!(current_request_scope().is_some());
373 });
374
375 assert!(current_request_scope().is_none());
376 }
377
378 #[test]
379 fn nested_entry_reuses_current_counters() {
380 let resource = DiagnosticExecutionBudgetResource::QueryExecutions;
381 let budget = REQUEST_HARD_BUDGET.with_limit_for_tests(resource, 1);
382 let root = RequestExecutionRoot::new_for_tests(budget);
383 let context = HardExecutionContext::new(
384 DiagnosticExecutionBudgetScope::Execution,
385 icydb_diagnostic_code::DiagnosticExecutionLane::PublicRead,
386 0,
387 );
388
389 root.__with_current_scope(|| {
390 let nested = RequestExecutionRoot::__new_or_current_runtime_root();
391 nested
392 .scope()
393 .charge(context, resource, 1)
394 .expect("first nested charge should fit");
395 let exhausted = root
396 .scope()
397 .charge(context, resource, 1)
398 .expect_err("parent should observe the nested charge");
399
400 assert_eq!(exhausted.scope(), DiagnosticExecutionBudgetScope::Request);
401 assert_eq!(exhausted.observed(), 2);
402 });
403 }
404
405 #[test]
406 fn budget_bundle_preflight_is_non_mutating_and_uses_remaining_capacity() {
407 let entries = DiagnosticExecutionBudgetResource::GroupDistinctEntries;
408 let bytes = DiagnosticExecutionBudgetResource::GroupDistinctStateBytes;
409 let budget = REQUEST_HARD_BUDGET
410 .with_limit_for_tests(entries, 5)
411 .with_limit_for_tests(bytes, 60);
412 let root = RequestExecutionRoot::new_for_tests(budget);
413 let scope = root.scope();
414 let context = HardExecutionContext::new(
415 DiagnosticExecutionBudgetScope::Execution,
416 icydb_diagnostic_code::DiagnosticExecutionLane::PublicRead,
417 0,
418 );
419 scope
420 .charge(context, entries, 2)
421 .expect("initial request charge should fit");
422 scope
423 .charge(context, bytes, 20)
424 .expect("initial request charge should fit");
425
426 assert_eq!(
427 scope.remaining_budget_units(&[(entries, 1), (bytes, 10)]),
428 3
429 );
430 assert!(scope.can_charge_budget_bundle(&[(entries, 3), (bytes, 30)]));
431 assert!(!scope.can_charge_budget_bundle(&[(entries, 4), (bytes, 30)]));
432 assert_eq!(
433 root.observed(entries),
434 2,
435 "preflight must not charge entries"
436 );
437 assert_eq!(root.observed(bytes), 20, "preflight must not charge bytes");
438 assert!(!scope.try_commit_budget_bundle(&[(entries, 4), (bytes, 30)]));
439 assert_eq!(root.observed(entries), 2);
440 assert_eq!(root.observed(bytes), 20);
441 assert!(scope.try_commit_budget_bundle(&[(entries, 3), (bytes, 30)]));
442 assert_eq!(root.observed(entries), 5);
443 assert_eq!(root.observed(bytes), 50);
444 }
445
446 #[test]
447 fn repeated_budget_bundle_resources_use_total_remaining_capacity() {
448 let context = HardExecutionContext::new(
449 DiagnosticExecutionBudgetScope::Execution,
450 icydb_diagnostic_code::DiagnosticExecutionLane::PublicRead,
451 0,
452 );
453 for resource in DiagnosticExecutionBudgetResource::ALL {
454 let root = RequestExecutionRoot::new_for_tests(HardExecutionBudget::uniform_for_tests(
455 10,
456 READ_HARD_BUDGET.failure_headroom(),
457 ));
458 let scope = root.scope();
459 scope.charge(context, resource, 2).unwrap();
460 let before = root.request_budget();
461 let per_unit = [(resource, 2), (resource, 0), (resource, 2)];
462 assert_eq!(scope.remaining_budget_units(&per_unit), 2);
463 assert_eq!(root.request_budget(), before);
464
465 let rejected = [(resource, 5), (resource, 4)];
466 assert!(!scope.can_charge_budget_bundle(&rejected));
467 assert!(!scope.try_commit_budget_bundle(&rejected));
468 assert_eq!(root.request_budget(), before);
469
470 let admitted = [(resource, 5), (resource, 0), (resource, 3)];
471 assert!(scope.can_charge_budget_bundle(&admitted));
472 assert_eq!(root.request_budget(), before);
473 assert!(scope.try_commit_budget_bundle(&admitted));
474 assert_eq!(root.observed(resource), 10);
475 assert_eq!(scope.remaining_budget_units(&per_unit), 0);
476 }
477 }
478
479 #[test]
480 fn explicit_root_compatibility_rejects_a_different_active_root() {
481 let first = RequestExecutionRoot::new_for_tests(REQUEST_HARD_BUDGET);
482 let second = RequestExecutionRoot::new_for_tests(REQUEST_HARD_BUDGET);
483
484 assert!(first.__is_compatible_with_current());
485 assert!(!first.__is_current());
486 first.__with_current_scope(|| {
487 assert!(first.__is_current());
488 assert!(first.__is_compatible_with_current());
489 assert!(!second.__is_compatible_with_current());
490 });
491 assert!(second.__is_compatible_with_current());
492 }
493}