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};
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,                 // query executions
22        1_024,               // planning operations
23        256,                 // plan compilations
24        250_000,             // key/index entries visited
25        250_000,             // rows visited
26        128 * 1_024 * 1_024, // stored bytes read
27        16_000_000,          // predicate/expression steps
28        16_000_000,          // nested value steps
29        128 * 1_024 * 1_024, // decoded bytes
30        128 * 1_024 * 1_024, // materialized bytes
31        250_000,             // sort entries
32        32_000_000,          // sort comparisons
33        128 * 1_024 * 1_024, // sort temporary bytes
34        100_000,             // group/distinct entries
35        128 * 1_024 * 1_024, // group/distinct state bytes
36        1_000_000,           // cursor steps
37        128 * 1_024 * 1_024, // temporary bytes
38        1_000_000,           // diagnostic steps
39        100_000,             // result rows
40        64 * 1_024 * 1_024,  // result bytes
41        4_500_000_000,       // instruction units
42    ],
43    REQUEST_FAILURE_HEADROOM,
44);
45
46thread_local! {
47    static CURRENT_REQUEST_SCOPE: RefCell<Option<RequestExecutionScope>> =
48        const { RefCell::new(None) };
49}
50
51/// Non-cloneable capability owning one request's aggregate database counters.
52///
53/// Construct this once at request entry and derive every database session used
54/// by the request from it. Sessions retain the counters, so dropping this
55/// value does not reset work already attached to a derived session.
56pub struct RequestExecutionRoot {
57    scope: RequestExecutionScope,
58}
59
60impl RequestExecutionRoot {
61    /// Mint the fixed production request profile.
62    ///
63    /// This constructor is runtime wiring for generated and guarded facade
64    /// request entry. It is intentionally not a budget-policy configuration
65    /// surface.
66    #[doc(hidden)]
67    #[must_use]
68    pub fn __new_runtime_root() -> Self {
69        Self::from_budget(REQUEST_HARD_BUDGET)
70    }
71
72    /// Reuse the active synchronous request scope or mint the production root.
73    ///
74    /// This is runtime wiring for the public scoped-entry helper. Re-entering
75    /// that helper inside one active database segment must retain the existing
76    /// counters instead of creating a budget-reset escape hatch.
77    #[doc(hidden)]
78    #[must_use]
79    pub fn __new_or_current_runtime_root() -> Self {
80        current_request_scope().map_or_else(Self::__new_runtime_root, |scope| Self { scope })
81    }
82
83    /// Make this root current only while one synchronous call tree executes.
84    ///
85    /// The previous scope is restored before this method returns, including
86    /// during host unwinding. The scope is never retained ambiently across an
87    /// async suspension point.
88    #[doc(hidden)]
89    pub fn __with_current_scope<T>(&self, run: impl FnOnce() -> T) -> T {
90        let _guard = CurrentRequestScopeGuard::enter(self.scope());
91        run()
92    }
93
94    #[cfg(test)]
95    #[must_use]
96    pub(in crate::db) fn new_for_tests(budget: HardExecutionBudget) -> Self {
97        Self::from_budget(budget)
98    }
99
100    fn from_budget(budget: HardExecutionBudget) -> Self {
101        Self {
102            scope: RequestExecutionScope {
103                counters: Rc::new(RequestExecutionCounters {
104                    budget,
105                    observed: [const { Cell::new(0) };
106                        DiagnosticExecutionBudgetResource::ALL.len()],
107                }),
108            },
109        }
110    }
111
112    pub(in crate::db) fn scope(&self) -> RequestExecutionScope {
113        self.scope.clone()
114    }
115
116    #[cfg(test)]
117    #[must_use]
118    pub(in crate::db) fn observed(&self, resource: DiagnosticExecutionBudgetResource) -> u64 {
119        self.scope.observed(resource)
120    }
121}
122
123pub(in crate::db) fn current_request_scope() -> Option<RequestExecutionScope> {
124    CURRENT_REQUEST_SCOPE.with(|current| current.borrow().clone())
125}
126
127struct CurrentRequestScopeGuard {
128    previous: Option<RequestExecutionScope>,
129}
130
131impl CurrentRequestScopeGuard {
132    fn enter(scope: RequestExecutionScope) -> Self {
133        let previous = CURRENT_REQUEST_SCOPE.with(|current| current.replace(Some(scope)));
134        Self { previous }
135    }
136}
137
138impl Drop for CurrentRequestScopeGuard {
139    fn drop(&mut self) {
140        CURRENT_REQUEST_SCOPE.with(|current| {
141            current.replace(self.previous.take());
142        });
143    }
144}
145
146/// Shared internal handle retained by every session derived from one root.
147#[derive(Clone)]
148pub(in crate::db) struct RequestExecutionScope {
149    counters: Rc<RequestExecutionCounters>,
150}
151
152impl RequestExecutionScope {
153    pub(in crate::db) fn charge(
154        &self,
155        context: HardExecutionContext,
156        resource: DiagnosticExecutionBudgetResource,
157        amount: u64,
158    ) -> Result<(), ExecutionBudgetExceeded> {
159        self.counters.charge(context, resource, amount)
160    }
161
162    #[cfg(test)]
163    fn observed(&self, resource: DiagnosticExecutionBudgetResource) -> u64 {
164        self.counters.observed[resource_index(resource)].get()
165    }
166}
167
168struct RequestExecutionCounters {
169    budget: HardExecutionBudget,
170    observed: [Cell<u64>; DiagnosticExecutionBudgetResource::ALL.len()],
171}
172
173impl RequestExecutionCounters {
174    fn charge(
175        &self,
176        context: HardExecutionContext,
177        resource: DiagnosticExecutionBudgetResource,
178        amount: u64,
179    ) -> Result<(), ExecutionBudgetExceeded> {
180        let index = resource_index(resource);
181        let counter = &self.observed[index];
182        let current = counter.get();
183        let (observed, overflowed) = current.overflowing_add(amount);
184        let observed = if overflowed { u64::MAX } else { observed };
185        counter.set(observed);
186        let limit = self.budget.limit(resource);
187        if overflowed || observed > limit {
188            return Err(ExecutionBudgetExceeded::new(
189                resource,
190                limit,
191                observed,
192                context.with_scope(DiagnosticExecutionBudgetScope::Request),
193            ));
194        }
195
196        Ok(())
197    }
198}
199
200#[cfg(test)]
201mod tests {
202    use super::*;
203
204    #[test]
205    fn synchronous_scope_is_installed_then_removed() {
206        assert!(current_request_scope().is_none());
207        let root = RequestExecutionRoot::new_for_tests(REQUEST_HARD_BUDGET);
208
209        root.__with_current_scope(|| {
210            assert!(current_request_scope().is_some());
211        });
212
213        assert!(current_request_scope().is_none());
214    }
215
216    #[test]
217    fn nested_entry_reuses_current_counters() {
218        let resource = DiagnosticExecutionBudgetResource::QueryExecutions;
219        let budget = REQUEST_HARD_BUDGET.with_limit_for_tests(resource, 1);
220        let root = RequestExecutionRoot::new_for_tests(budget);
221        let context = HardExecutionContext::new(
222            DiagnosticExecutionBudgetScope::Execution,
223            icydb_diagnostic_code::DiagnosticExecutionLane::PublicRead,
224            0,
225        );
226
227        root.__with_current_scope(|| {
228            let nested = RequestExecutionRoot::__new_or_current_runtime_root();
229            nested
230                .scope()
231                .charge(context, resource, 1)
232                .expect("first nested charge should fit");
233            let exhausted = root
234                .scope()
235                .charge(context, resource, 1)
236                .expect_err("parent should observe the nested charge");
237
238            assert_eq!(exhausted.scope(), DiagnosticExecutionBudgetScope::Request);
239            assert_eq!(exhausted.observed(), 2);
240        });
241    }
242}