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    /// Whether no root is active or this root owns the active counters.
95    ///
96    /// Generated facade wiring uses this before accepting an explicit root.
97    /// A different active root would reset aggregate accounting inside a
98    /// request and must fail closed.
99    #[doc(hidden)]
100    #[must_use]
101    pub fn __is_compatible_with_current(&self) -> bool {
102        match current_request_scope() {
103            Some(current) => current.same_counters(&self.scope),
104            None => true,
105        }
106    }
107
108    /// Whether this root owns the counters currently installed for this poll.
109    #[doc(hidden)]
110    #[must_use]
111    pub fn __is_current(&self) -> bool {
112        current_request_scope().is_some_and(|current| current.same_counters(&self.scope))
113    }
114
115    #[cfg(test)]
116    #[must_use]
117    pub(in crate::db) fn new_for_tests(budget: HardExecutionBudget) -> Self {
118        Self::from_budget(budget)
119    }
120
121    fn from_budget(budget: HardExecutionBudget) -> Self {
122        Self {
123            scope: RequestExecutionScope {
124                counters: Rc::new(RequestExecutionCounters {
125                    budget,
126                    observed: [const { Cell::new(0) };
127                        DiagnosticExecutionBudgetResource::ALL.len()],
128                }),
129            },
130        }
131    }
132
133    pub(in crate::db) fn scope(&self) -> RequestExecutionScope {
134        self.scope.clone()
135    }
136
137    #[cfg(test)]
138    #[must_use]
139    pub(in crate::db) fn observed(&self, resource: DiagnosticExecutionBudgetResource) -> u64 {
140        self.scope.observed(resource)
141    }
142}
143
144pub(in crate::db) fn current_request_scope() -> Option<RequestExecutionScope> {
145    CURRENT_REQUEST_SCOPE.with(|current| current.borrow().clone())
146}
147
148struct CurrentRequestScopeGuard {
149    previous: Option<RequestExecutionScope>,
150}
151
152impl CurrentRequestScopeGuard {
153    fn enter(scope: RequestExecutionScope) -> Self {
154        let previous = CURRENT_REQUEST_SCOPE.with(|current| current.replace(Some(scope)));
155        Self { previous }
156    }
157}
158
159impl Drop for CurrentRequestScopeGuard {
160    fn drop(&mut self) {
161        CURRENT_REQUEST_SCOPE.with(|current| {
162            current.replace(self.previous.take());
163        });
164    }
165}
166
167/// Shared internal handle retained by every session derived from one root.
168#[derive(Clone)]
169pub(in crate::db) struct RequestExecutionScope {
170    counters: Rc<RequestExecutionCounters>,
171}
172
173impl RequestExecutionScope {
174    fn same_counters(&self, other: &Self) -> bool {
175        Rc::ptr_eq(&self.counters, &other.counters)
176    }
177
178    pub(in crate::db) fn charge(
179        &self,
180        context: HardExecutionContext,
181        resource: DiagnosticExecutionBudgetResource,
182        amount: u64,
183    ) -> Result<(), ExecutionBudgetExceeded> {
184        self.counters.charge(context, resource, amount)
185    }
186
187    #[cfg(test)]
188    fn observed(&self, resource: DiagnosticExecutionBudgetResource) -> u64 {
189        self.counters.observed[resource_index(resource)].get()
190    }
191}
192
193struct RequestExecutionCounters {
194    budget: HardExecutionBudget,
195    observed: [Cell<u64>; DiagnosticExecutionBudgetResource::ALL.len()],
196}
197
198impl RequestExecutionCounters {
199    fn charge(
200        &self,
201        context: HardExecutionContext,
202        resource: DiagnosticExecutionBudgetResource,
203        amount: u64,
204    ) -> Result<(), ExecutionBudgetExceeded> {
205        let index = resource_index(resource);
206        let counter = &self.observed[index];
207        let current = counter.get();
208        let (observed, overflowed) = current.overflowing_add(amount);
209        let observed = if overflowed { u64::MAX } else { observed };
210        counter.set(observed);
211        let limit = self.budget.limit(resource);
212        if overflowed || observed > limit {
213            return Err(ExecutionBudgetExceeded::new(
214                resource,
215                limit,
216                observed,
217                context.with_scope(DiagnosticExecutionBudgetScope::Request),
218            ));
219        }
220
221        Ok(())
222    }
223}
224
225#[cfg(test)]
226mod tests {
227    use super::*;
228
229    #[test]
230    fn synchronous_scope_is_installed_then_removed() {
231        assert!(current_request_scope().is_none());
232        let root = RequestExecutionRoot::new_for_tests(REQUEST_HARD_BUDGET);
233
234        root.__with_current_scope(|| {
235            assert!(current_request_scope().is_some());
236        });
237
238        assert!(current_request_scope().is_none());
239    }
240
241    #[test]
242    fn nested_entry_reuses_current_counters() {
243        let resource = DiagnosticExecutionBudgetResource::QueryExecutions;
244        let budget = REQUEST_HARD_BUDGET.with_limit_for_tests(resource, 1);
245        let root = RequestExecutionRoot::new_for_tests(budget);
246        let context = HardExecutionContext::new(
247            DiagnosticExecutionBudgetScope::Execution,
248            icydb_diagnostic_code::DiagnosticExecutionLane::PublicRead,
249            0,
250        );
251
252        root.__with_current_scope(|| {
253            let nested = RequestExecutionRoot::__new_or_current_runtime_root();
254            nested
255                .scope()
256                .charge(context, resource, 1)
257                .expect("first nested charge should fit");
258            let exhausted = root
259                .scope()
260                .charge(context, resource, 1)
261                .expect_err("parent should observe the nested charge");
262
263            assert_eq!(exhausted.scope(), DiagnosticExecutionBudgetScope::Request);
264            assert_eq!(exhausted.observed(), 2);
265        });
266    }
267
268    #[test]
269    fn explicit_root_compatibility_rejects_a_different_active_root() {
270        let first = RequestExecutionRoot::new_for_tests(REQUEST_HARD_BUDGET);
271        let second = RequestExecutionRoot::new_for_tests(REQUEST_HARD_BUDGET);
272
273        assert!(first.__is_compatible_with_current());
274        assert!(!first.__is_current());
275        first.__with_current_scope(|| {
276            assert!(first.__is_current());
277            assert!(first.__is_compatible_with_current());
278            assert!(!second.__is_compatible_with_current());
279        });
280        assert!(second.__is_compatible_with_current());
281    }
282}