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