Skip to main content

icydb_core/db/resource/
request.rs

1//! Module: db::resource::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::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,                 // 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        100_000,             // result rows
39        64 * 1_024 * 1_024,  // result bytes
40        4_500_000_000,       // instruction units
41    ],
42    REQUEST_FAILURE_HEADROOM,
43);
44
45thread_local! {
46    static CURRENT_REQUEST_SCOPE: RefCell<Option<RequestExecutionScope>> =
47        const { RefCell::new(None) };
48}
49
50/// Read-only snapshot of the aggregate database budget for one request.
51///
52/// Values use each resource's native units and include retained charges from
53/// failed attempts. This snapshot neither charges nor reserves work. It is
54/// frozen at capture and reports request capacity, not per-execution capacity.
55/// `InstructionUnits` covers charged database spans, not total endpoint work
56/// or the IC's remaining instruction allowance.
57#[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    /// Return cumulative charged work, which may exceed the limit after failure.
65    #[must_use]
66    pub const fn observed(&self, resource: DiagnosticExecutionBudgetResource) -> u64 {
67        self.observed[resource_index(resource)]
68    }
69
70    /// Return the request owner's fixed ceiling for this resource.
71    #[must_use]
72    pub const fn limit(&self, resource: DiagnosticExecutionBudgetResource) -> u64 {
73        self.limits[resource_index(resource)]
74    }
75
76    /// Return uncharged capacity, saturating at zero after exhaustion.
77    ///
78    /// Capacity does not guarantee that the next application item fits: its
79    /// costs, other resources and individual execution limits still apply.
80    #[must_use]
81    pub const fn remaining(&self, resource: DiagnosticExecutionBudgetResource) -> u64 {
82        self.limit(resource).saturating_sub(self.observed(resource))
83    }
84}
85
86/// Non-cloneable capability owning one request's aggregate database counters.
87///
88/// Construct this once at request entry and derive every database session used
89/// by the request from it. Sessions retain the counters, so dropping this
90/// value does not reset work already attached to a derived session.
91pub struct RequestExecutionRoot {
92    scope: RequestExecutionScope,
93}
94
95impl RequestExecutionRoot {
96    /// Snapshot this root's retained aggregate request counters without charging work.
97    #[must_use]
98    pub fn request_budget(&self) -> RequestBudgetSnapshot {
99        self.scope.request_budget()
100    }
101
102    /// Mint the fixed production request profile.
103    ///
104    /// This constructor is runtime wiring for generated and guarded facade
105    /// request entry. It is intentionally not a budget-policy configuration
106    /// surface.
107    #[doc(hidden)]
108    #[must_use]
109    pub fn __new_runtime_root() -> Self {
110        Self::from_budget(REQUEST_HARD_BUDGET)
111    }
112
113    /// Reuse the active synchronous request scope or mint the production root.
114    ///
115    /// This is runtime wiring for the public scoped-entry helper. Re-entering
116    /// that helper inside one active database segment must retain the existing
117    /// counters instead of creating a budget-reset escape hatch.
118    #[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    /// Make this root current only while one synchronous call tree executes.
125    ///
126    /// The previous scope is restored before this method returns, including
127    /// during host unwinding. The scope is never retained ambiently across an
128    /// async suspension point.
129    #[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    /// Whether no root is active or this root owns the active counters.
136    ///
137    /// Generated facade wiring uses this before accepting an explicit root.
138    /// A different active root would reset aggregate accounting inside a
139    /// request and must fail closed.
140    #[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    /// Whether this root owns the counters currently installed for this poll.
150    #[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/// Shared internal handle retained by every session derived from one root.
209#[derive(Clone)]
210pub(in crate::db) struct RequestExecutionScope {
211    counters: Rc<RequestExecutionCounters>,
212}
213
214impl RequestExecutionScope {
215    /// Project the retained owner, independently of the currently active root.
216    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    /// Return how many complete equal-cost units remain across every named
239    /// request resource without mutating request accounting.
240    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    /// Preflight one fixed resource bundle without retaining a rejected or
252    /// speculative charge in the request scope.
253    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    /// Commit one complete bundle only when every request resource fits.
265    /// `false` leaves every request counter unchanged.
266    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}