Skip to main content

icydb_core/db/session/
request.rs

1//! Module: db::session::request
2//! Responsibility: one monotonic aggregate execution scope per request entry.
3//! Does not own: caller authorization, per-execution limits, or physical charging sites.
4//! Boundary: request roots issue shared scope handles that every derived session retains.
5
6use 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,                 // query executions
30        1_024,               // planning operations
31        256,                 // plan compilations
32        250_000,             // key/index entries visited
33        250_000,             // rows visited
34        128 * 1_024 * 1_024, // stored bytes read
35        16_000_000,          // predicate/expression steps
36        16_000_000,          // nested value steps
37        128 * 1_024 * 1_024, // decoded bytes
38        128 * 1_024 * 1_024, // materialized bytes
39        250_000,             // sort entries
40        32_000_000,          // sort comparisons
41        128 * 1_024 * 1_024, // sort temporary bytes
42        100_000,             // group/distinct entries
43        128 * 1_024 * 1_024, // group/distinct state bytes
44        1_000_000,           // cursor steps
45        128 * 1_024 * 1_024, // temporary bytes
46        1_000_000,           // diagnostic steps
47        100_000,             // result rows
48        64 * 1_024 * 1_024,  // result bytes
49        4_500_000_000,       // instruction units
50    ],
51    REQUEST_FAILURE_HEADROOM,
52);
53
54thread_local! {
55    static CURRENT_REQUEST_SCOPE: RefCell<Option<RequestExecutionScope>> =
56        const { RefCell::new(None) };
57}
58
59/// Non-cloneable capability owning one request's aggregate database counters.
60///
61/// Construct this once at request entry and derive every database session used
62/// by the request from it. Sessions retain the counters, so dropping this
63/// value does not reset work already attached to a derived session.
64pub struct RequestExecutionRoot {
65    scope: RequestExecutionScope,
66}
67
68impl RequestExecutionRoot {
69    /// Mint the fixed production request profile.
70    ///
71    /// This constructor is runtime wiring for generated and guarded facade
72    /// request entry. It is intentionally not a budget-policy configuration
73    /// surface.
74    #[doc(hidden)]
75    #[must_use]
76    pub fn __new_runtime_root() -> Self {
77        Self::from_budget(REQUEST_HARD_BUDGET)
78    }
79
80    /// Reuse the active synchronous request scope or mint the production root.
81    ///
82    /// This is runtime wiring for the public scoped-entry helper. Re-entering
83    /// that helper inside one active database segment must retain the existing
84    /// counters instead of creating a budget-reset escape hatch.
85    #[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    /// Make this root current only while one synchronous call tree executes.
92    ///
93    /// The previous scope is restored before this method returns, including
94    /// during host unwinding. The scope is never retained ambiently across an
95    /// async suspension point.
96    #[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    /// Whether no root is active or this root owns the active counters.
103    ///
104    /// Generated facade wiring uses this before accepting an explicit root.
105    /// A different active root would reset aggregate accounting inside a
106    /// request and must fail closed.
107    #[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    /// Whether this root owns the counters currently installed for this poll.
117    #[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/// Shared internal handle retained by every session derived from one root.
178#[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}