1use std::{
7 cell::{Cell, RefCell},
8 rc::Rc,
9};
10
11use crate::db::executor::budget::{
12 ExecutionBudgetExceeded, HardExecutionBudget, HardExecutionContext,
13 HardExecutionFailureHeadroom, resource_index,
14};
15#[cfg(feature = "diagnostics")]
16use crate::db::{
17 diagnostics::{
18 RequestDiagnosticResourceUsage, RequestDiagnostics, RequestDiagnosticsState,
19 RequestQueryPlanEvidence,
20 },
21 session::query::QueryPlanCacheAttribution,
22};
23use icydb_diagnostic_code::{DiagnosticExecutionBudgetResource, DiagnosticExecutionBudgetScope};
24
25const REQUEST_FAILURE_HEADROOM: HardExecutionFailureHeadroom =
26 HardExecutionFailureHeadroom::new(500_000_000, 64 * 1_024);
27const REQUEST_HARD_BUDGET: HardExecutionBudget = HardExecutionBudget::new(
28 [
29 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, 1_000_000, 100_000, 64 * 1_024 * 1_024, 4_500_000_000, ],
51 REQUEST_FAILURE_HEADROOM,
52);
53
54thread_local! {
55 static CURRENT_REQUEST_SCOPE: RefCell<Option<RequestExecutionScope>> =
56 const { RefCell::new(None) };
57}
58
59pub struct RequestExecutionRoot {
65 scope: RequestExecutionScope,
66}
67
68impl RequestExecutionRoot {
69 #[doc(hidden)]
75 #[must_use]
76 pub fn __new_runtime_root() -> Self {
77 Self::from_budget(REQUEST_HARD_BUDGET)
78 }
79
80 #[doc(hidden)]
86 #[must_use]
87 pub fn __new_or_current_runtime_root() -> Self {
88 current_request_scope().map_or_else(Self::__new_runtime_root, |scope| Self { scope })
89 }
90
91 #[doc(hidden)]
97 pub fn __with_current_scope<T>(&self, run: impl FnOnce() -> T) -> T {
98 let _guard = CurrentRequestScopeGuard::enter(self.scope());
99 run()
100 }
101
102 #[doc(hidden)]
108 #[must_use]
109 pub fn __is_compatible_with_current(&self) -> bool {
110 match current_request_scope() {
111 Some(current) => current.same_counters(&self.scope),
112 None => true,
113 }
114 }
115
116 #[doc(hidden)]
118 #[must_use]
119 pub fn __is_current(&self) -> bool {
120 current_request_scope().is_some_and(|current| current.same_counters(&self.scope))
121 }
122
123 #[cfg(test)]
124 #[must_use]
125 pub(in crate::db) fn new_for_tests(budget: HardExecutionBudget) -> Self {
126 Self::from_budget(budget)
127 }
128
129 fn from_budget(budget: HardExecutionBudget) -> Self {
130 Self {
131 scope: RequestExecutionScope {
132 counters: Rc::new(RequestExecutionCounters {
133 budget,
134 observed: [const { Cell::new(0) };
135 DiagnosticExecutionBudgetResource::ALL.len()],
136 #[cfg(feature = "diagnostics")]
137 diagnostics: RefCell::new(None),
138 }),
139 },
140 }
141 }
142
143 pub(in crate::db) fn scope(&self) -> RequestExecutionScope {
144 self.scope.clone()
145 }
146
147 #[cfg(test)]
148 #[must_use]
149 pub(in crate::db) fn observed(&self, resource: DiagnosticExecutionBudgetResource) -> u64 {
150 self.scope.observed(resource)
151 }
152}
153
154pub(in crate::db) fn current_request_scope() -> Option<RequestExecutionScope> {
155 CURRENT_REQUEST_SCOPE.with(|current| current.borrow().clone())
156}
157
158struct CurrentRequestScopeGuard {
159 previous: Option<RequestExecutionScope>,
160}
161
162impl CurrentRequestScopeGuard {
163 fn enter(scope: RequestExecutionScope) -> Self {
164 let previous = CURRENT_REQUEST_SCOPE.with(|current| current.replace(Some(scope)));
165 Self { previous }
166 }
167}
168
169impl Drop for CurrentRequestScopeGuard {
170 fn drop(&mut self) {
171 CURRENT_REQUEST_SCOPE.with(|current| {
172 current.replace(self.previous.take());
173 });
174 }
175}
176
177#[derive(Clone)]
179pub(in crate::db) struct RequestExecutionScope {
180 counters: Rc<RequestExecutionCounters>,
181}
182
183impl RequestExecutionScope {
184 fn same_counters(&self, other: &Self) -> bool {
185 Rc::ptr_eq(&self.counters, &other.counters)
186 }
187
188 pub(in crate::db) fn charge(
189 &self,
190 context: HardExecutionContext,
191 resource: DiagnosticExecutionBudgetResource,
192 amount: u64,
193 ) -> Result<(), ExecutionBudgetExceeded> {
194 self.counters.charge(context, resource, amount)
195 }
196
197 #[cfg(feature = "diagnostics")]
198 pub(in crate::db) fn enable_diagnostics(&self) -> bool {
199 let mut diagnostics = self.counters.diagnostics.borrow_mut();
200 if diagnostics.is_some() {
201 return false;
202 }
203 *diagnostics = Some(RequestDiagnosticsState::default());
204 true
205 }
206
207 #[cfg(feature = "diagnostics")]
208 pub(in crate::db) fn diagnostics_enabled(&self) -> bool {
209 self.counters.diagnostics.borrow().is_some()
210 }
211
212 #[cfg(feature = "diagnostics")]
213 pub(in crate::db) fn diagnostics_snapshot(&self) -> Option<RequestDiagnostics> {
214 let mut snapshot = self
215 .counters
216 .diagnostics
217 .borrow()
218 .as_ref()
219 .map(RequestDiagnosticsState::snapshot)?;
220 let response_bytes = request_diagnostics_bytes_estimate(&snapshot);
221 let context = HardExecutionContext::new(
222 DiagnosticExecutionBudgetScope::Request,
223 icydb_diagnostic_code::DiagnosticExecutionLane::TrustedRead,
224 0,
225 );
226 let charged = self.counters.charge_fail_soft(
227 context,
228 DiagnosticExecutionBudgetResource::DiagnosticSteps,
229 1,
230 ) && self.counters.charge_fail_soft(
231 context,
232 DiagnosticExecutionBudgetResource::ResultBytes,
233 response_bytes,
234 );
235 if !charged {
236 self.suppress_diagnostics(1);
237 snapshot.shapes.clear();
238 snapshot.warnings.clear();
239 snapshot.suppressed_observations = snapshot.suppressed_observations.saturating_add(1);
240 }
241 Some(snapshot)
242 }
243
244 #[cfg(feature = "diagnostics")]
245 pub(in crate::db) fn record_query_plan(
246 &self,
247 evidence: RequestQueryPlanEvidence,
248 cache: QueryPlanCacheAttribution,
249 ) {
250 if !self.diagnostics_enabled() {
251 return;
252 }
253 let context = HardExecutionContext::new(
254 DiagnosticExecutionBudgetScope::Request,
255 icydb_diagnostic_code::DiagnosticExecutionLane::TrustedRead,
256 evidence.normalized_shape_fingerprint_prefix,
257 );
258 let retained_bytes = evidence.retained_bytes_estimate();
259 let diagnostic_steps = evidence.work_steps_estimate();
260 if !self.counters.charge_fail_soft(
261 context,
262 DiagnosticExecutionBudgetResource::DiagnosticSteps,
263 diagnostic_steps,
264 ) || !self.counters.charge_fail_soft(
265 context,
266 DiagnosticExecutionBudgetResource::TemporaryBytes,
267 retained_bytes,
268 ) {
269 self.suppress_diagnostics(1);
270 return;
271 }
272 if let Some(diagnostics) = self.counters.diagnostics.borrow_mut().as_mut() {
273 diagnostics.observe_plan(evidence, cache.hits, cache.misses);
274 }
275 }
276
277 #[cfg(feature = "diagnostics")]
278 pub(in crate::db) fn record_execution(
279 &self,
280 context: HardExecutionContext,
281 usage: RequestDiagnosticResourceUsage,
282 ) {
283 if !self.diagnostics_enabled() {
284 return;
285 }
286 if !self.counters.charge_fail_soft(
287 context,
288 DiagnosticExecutionBudgetResource::DiagnosticSteps,
289 1,
290 ) {
291 self.suppress_diagnostics(1);
292 return;
293 }
294 if let Some(diagnostics) = self.counters.diagnostics.borrow_mut().as_mut() {
295 diagnostics.observe_execution(context.normalized_shape_fingerprint_prefix(), usage);
296 }
297 }
298
299 #[cfg(feature = "diagnostics")]
300 pub(in crate::db) fn record_exact_key_hashes(
301 &self,
302 context: HardExecutionContext,
303 hashes: &[[u8; 16]],
304 ) {
305 if hashes.is_empty() || !self.diagnostics_enabled() {
306 return;
307 }
308 let steps = u64::try_from(hashes.len()).unwrap_or(u64::MAX);
309 let retained_bytes = steps.saturating_mul(16);
310 if !self.counters.charge_fail_soft(
311 context,
312 DiagnosticExecutionBudgetResource::DiagnosticSteps,
313 steps,
314 ) || !self.counters.charge_fail_soft(
315 context,
316 DiagnosticExecutionBudgetResource::TemporaryBytes,
317 retained_bytes,
318 ) {
319 self.suppress_diagnostics(steps);
320 return;
321 }
322 if let Some(diagnostics) = self.counters.diagnostics.borrow_mut().as_mut() {
323 diagnostics
324 .observe_exact_key_hashes(context.normalized_shape_fingerprint_prefix(), hashes);
325 }
326 }
327
328 #[cfg(feature = "diagnostics")]
329 fn suppress_diagnostics(&self, count: u64) {
330 if let Some(diagnostics) = self.counters.diagnostics.borrow_mut().as_mut() {
331 diagnostics.suppress(count);
332 }
333 }
334
335 #[cfg(test)]
336 fn observed(&self, resource: DiagnosticExecutionBudgetResource) -> u64 {
337 self.counters.observed[resource_index(resource)].get()
338 }
339}
340
341struct RequestExecutionCounters {
342 budget: HardExecutionBudget,
343 observed: [Cell<u64>; DiagnosticExecutionBudgetResource::ALL.len()],
344 #[cfg(feature = "diagnostics")]
345 diagnostics: RefCell<Option<RequestDiagnosticsState>>,
346}
347
348impl RequestExecutionCounters {
349 fn charge(
350 &self,
351 context: HardExecutionContext,
352 resource: DiagnosticExecutionBudgetResource,
353 amount: u64,
354 ) -> Result<(), ExecutionBudgetExceeded> {
355 let index = resource_index(resource);
356 let counter = &self.observed[index];
357 let current = counter.get();
358 let (observed, overflowed) = current.overflowing_add(amount);
359 let observed = if overflowed { u64::MAX } else { observed };
360 counter.set(observed);
361 let limit = self.budget.limit(resource);
362 if overflowed || observed > limit {
363 return Err(ExecutionBudgetExceeded::new(
364 resource,
365 limit,
366 observed,
367 context.with_scope(DiagnosticExecutionBudgetScope::Request),
368 ));
369 }
370
371 Ok(())
372 }
373
374 #[cfg(feature = "diagnostics")]
375 fn charge_fail_soft(
376 &self,
377 _context: HardExecutionContext,
378 resource: DiagnosticExecutionBudgetResource,
379 amount: u64,
380 ) -> bool {
381 let index = resource_index(resource);
382 let counter = &self.observed[index];
383 let current = counter.get();
384 let limit = self.budget.limit(resource);
385 let Some(observed) = current.checked_add(amount) else {
386 counter.set(limit);
387 return false;
388 };
389 if observed > limit {
390 counter.set(limit);
391 return false;
392 }
393 counter.set(observed);
394 true
395 }
396}
397
398#[cfg(feature = "diagnostics")]
399fn request_diagnostics_bytes_estimate(diagnostics: &RequestDiagnostics) -> u64 {
400 let shape_bytes = diagnostics.shapes.iter().fold(0_u64, |total, shape| {
401 total
402 .saturating_add(u64::try_from(shape.entity.len()).unwrap_or(u64::MAX))
403 .saturating_add(
404 u64::try_from(shape.selected_index.as_ref().map_or(0, String::len))
405 .unwrap_or(u64::MAX),
406 )
407 .saturating_add(
408 shape
409 .residual_fields
410 .iter()
411 .chain(shape.compound_index_candidate.iter())
412 .fold(0_u64, |bytes, field| {
413 bytes.saturating_add(u64::try_from(field.len()).unwrap_or(u64::MAX))
414 }),
415 )
416 .saturating_add(256)
417 });
418 diagnostics
419 .warnings
420 .iter()
421 .fold(shape_bytes, |total, warning| {
422 total
423 .saturating_add(u64::try_from(warning.message.len()).unwrap_or(u64::MAX))
424 .saturating_add(32)
425 })
426}
427
428#[cfg(test)]
429mod tests {
430 use super::*;
431
432 #[test]
433 fn synchronous_scope_is_installed_then_removed() {
434 assert!(current_request_scope().is_none());
435 let root = RequestExecutionRoot::new_for_tests(REQUEST_HARD_BUDGET);
436
437 root.__with_current_scope(|| {
438 assert!(current_request_scope().is_some());
439 });
440
441 assert!(current_request_scope().is_none());
442 }
443
444 #[test]
445 fn nested_entry_reuses_current_counters() {
446 let resource = DiagnosticExecutionBudgetResource::QueryExecutions;
447 let budget = REQUEST_HARD_BUDGET.with_limit_for_tests(resource, 1);
448 let root = RequestExecutionRoot::new_for_tests(budget);
449 let context = HardExecutionContext::new(
450 DiagnosticExecutionBudgetScope::Execution,
451 icydb_diagnostic_code::DiagnosticExecutionLane::PublicRead,
452 0,
453 );
454
455 root.__with_current_scope(|| {
456 let nested = RequestExecutionRoot::__new_or_current_runtime_root();
457 nested
458 .scope()
459 .charge(context, resource, 1)
460 .expect("first nested charge should fit");
461 let exhausted = root
462 .scope()
463 .charge(context, resource, 1)
464 .expect_err("parent should observe the nested charge");
465
466 assert_eq!(exhausted.scope(), DiagnosticExecutionBudgetScope::Request);
467 assert_eq!(exhausted.observed(), 2);
468 });
469 }
470
471 #[test]
472 fn explicit_root_compatibility_rejects_a_different_active_root() {
473 let first = RequestExecutionRoot::new_for_tests(REQUEST_HARD_BUDGET);
474 let second = RequestExecutionRoot::new_for_tests(REQUEST_HARD_BUDGET);
475
476 assert!(first.__is_compatible_with_current());
477 assert!(!first.__is_current());
478 first.__with_current_scope(|| {
479 assert!(first.__is_current());
480 assert!(first.__is_compatible_with_current());
481 assert!(!second.__is_compatible_with_current());
482 });
483 assert!(second.__is_compatible_with_current());
484 }
485
486 #[cfg(feature = "diagnostics")]
487 #[test]
488 fn request_diagnostic_work_is_charged_to_the_shared_root() {
489 let root = RequestExecutionRoot::new_for_tests(REQUEST_HARD_BUDGET);
490 let scope = root.scope();
491 assert!(scope.enable_diagnostics());
492 scope.record_query_plan(
493 RequestQueryPlanEvidence::bounded(
494 12,
495 "Token",
496 crate::db::RequestDiagnosticAccessPath::ByKey,
497 None,
498 Vec::new(),
499 Vec::new(),
500 vec![[1; 16]],
501 ),
502 QueryPlanCacheAttribution {
503 hits: 1,
504 ..QueryPlanCacheAttribution::default()
505 },
506 );
507
508 assert_eq!(
509 root.observed(DiagnosticExecutionBudgetResource::DiagnosticSteps),
510 2,
511 );
512 assert!(root.observed(DiagnosticExecutionBudgetResource::TemporaryBytes) >= 21);
513 }
514
515 #[cfg(feature = "diagnostics")]
516 #[test]
517 fn exhausted_diagnostic_allowance_suppresses_detail_without_an_error() {
518 let budget = REQUEST_HARD_BUDGET
519 .with_limit_for_tests(DiagnosticExecutionBudgetResource::DiagnosticSteps, 0);
520 let root = RequestExecutionRoot::new_for_tests(budget);
521 let scope = root.scope();
522 assert!(scope.enable_diagnostics());
523 scope.record_query_plan(
524 RequestQueryPlanEvidence::bounded(
525 13,
526 "Token",
527 crate::db::RequestDiagnosticAccessPath::ByKey,
528 None,
529 Vec::new(),
530 Vec::new(),
531 Vec::new(),
532 ),
533 QueryPlanCacheAttribution::default(),
534 );
535
536 let snapshot = scope
537 .diagnostics_snapshot()
538 .expect("enabled diagnostics should still return a bounded snapshot");
539 assert!(snapshot.shapes.is_empty());
540 assert!(snapshot.suppressed_observations >= 2);
541 }
542}