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, READ_HARD_BUDGET,
13    resource_index,
14};
15use icydb_diagnostic_code::{DiagnosticExecutionBudgetResource, DiagnosticExecutionBudgetScope};
16
17// Request aggregation shares physical ceilings with reads; only request-level
18// execution and compilation admission differ. Each profile retains its counters.
19const REQUEST_HARD_BUDGET: HardExecutionBudget = READ_HARD_BUDGET
20    .with_limit(DiagnosticExecutionBudgetResource::QueryExecutions, 256)
21    .with_limit(DiagnosticExecutionBudgetResource::PlanningSteps, 1_024)
22    .with_limit(DiagnosticExecutionBudgetResource::PlanCompilations, 256);
23
24thread_local! {
25    static CURRENT_REQUEST_SCOPE: RefCell<Option<RequestExecutionScope>> =
26        const { RefCell::new(None) };
27}
28
29/// Read-only snapshot of the aggregate database budget for one request.
30///
31/// Values use each resource's native units and include retained charges from
32/// failed attempts. This snapshot neither charges nor reserves work. It is
33/// frozen at capture and reports request capacity, not per-execution capacity.
34/// `InstructionUnits` covers charged database spans, not total endpoint work
35/// or the IC's remaining instruction allowance.
36#[derive(Clone, Debug, Eq, PartialEq)]
37pub struct RequestBudgetSnapshot {
38    observed: [u64; DiagnosticExecutionBudgetResource::ALL.len()],
39    limits: [u64; DiagnosticExecutionBudgetResource::ALL.len()],
40}
41
42impl RequestBudgetSnapshot {
43    /// Return cumulative charged work, which may exceed the limit after failure.
44    #[must_use]
45    pub const fn observed(&self, resource: DiagnosticExecutionBudgetResource) -> u64 {
46        self.observed[resource_index(resource)]
47    }
48
49    /// Return the request owner's fixed ceiling for this resource.
50    #[must_use]
51    pub const fn limit(&self, resource: DiagnosticExecutionBudgetResource) -> u64 {
52        self.limits[resource_index(resource)]
53    }
54
55    /// Return uncharged capacity, saturating at zero after exhaustion.
56    ///
57    /// Capacity does not guarantee that the next application item fits: its
58    /// costs, other resources and individual execution limits still apply.
59    #[must_use]
60    pub const fn remaining(&self, resource: DiagnosticExecutionBudgetResource) -> u64 {
61        self.limit(resource).saturating_sub(self.observed(resource))
62    }
63}
64
65/// Non-cloneable capability owning one request's aggregate database counters.
66///
67/// Construct this once at request entry and derive every database session used
68/// by the request from it. Sessions retain the counters, so dropping this
69/// value does not reset work already attached to a derived session.
70pub struct RequestExecutionRoot {
71    scope: RequestExecutionScope,
72}
73
74impl RequestExecutionRoot {
75    /// Snapshot this root's retained aggregate request counters without charging work.
76    #[must_use]
77    pub fn request_budget(&self) -> RequestBudgetSnapshot {
78        self.scope.request_budget()
79    }
80
81    /// Mint the fixed production request profile.
82    ///
83    /// This constructor is runtime wiring for generated and guarded facade
84    /// request entry. It is intentionally not a budget-policy configuration
85    /// surface.
86    #[doc(hidden)]
87    #[must_use]
88    pub fn __new_runtime_root() -> Self {
89        Self::from_budget(REQUEST_HARD_BUDGET)
90    }
91
92    /// Reuse the active synchronous request scope or mint the production root.
93    ///
94    /// This is runtime wiring for the public scoped-entry helper. Re-entering
95    /// that helper inside one active database segment must retain the existing
96    /// counters instead of creating a budget-reset escape hatch.
97    #[doc(hidden)]
98    #[must_use]
99    pub fn __new_or_current_runtime_root() -> Self {
100        current_request_scope().map_or_else(Self::__new_runtime_root, |scope| Self { scope })
101    }
102
103    /// Make this root current only while one synchronous call tree executes.
104    ///
105    /// The previous scope is restored before this method returns, including
106    /// during host unwinding. The scope is never retained ambiently across an
107    /// async suspension point.
108    #[doc(hidden)]
109    pub fn __with_current_scope<T>(&self, run: impl FnOnce() -> T) -> T {
110        let _guard = CurrentRequestScopeGuard::enter(self.scope());
111        run()
112    }
113
114    /// Whether no root is active or this root owns the active counters.
115    ///
116    /// Generated facade wiring uses this before accepting an explicit root.
117    /// A different active root would reset aggregate accounting inside a
118    /// request and must fail closed.
119    #[doc(hidden)]
120    #[must_use]
121    pub fn __is_compatible_with_current(&self) -> bool {
122        match current_request_scope() {
123            Some(current) => current.same_counters(&self.scope),
124            None => true,
125        }
126    }
127
128    /// Whether this root owns the counters currently installed for this poll.
129    #[doc(hidden)]
130    #[must_use]
131    pub fn __is_current(&self) -> bool {
132        current_request_scope().is_some_and(|current| current.same_counters(&self.scope))
133    }
134
135    #[cfg(test)]
136    #[must_use]
137    pub(in crate::db) fn new_for_tests(budget: HardExecutionBudget) -> Self {
138        Self::from_budget(budget)
139    }
140
141    fn from_budget(budget: HardExecutionBudget) -> Self {
142        Self {
143            scope: RequestExecutionScope {
144                counters: Rc::new(RequestExecutionCounters {
145                    budget,
146                    observed: [const { Cell::new(0) };
147                        DiagnosticExecutionBudgetResource::ALL.len()],
148                }),
149            },
150        }
151    }
152
153    pub(in crate::db) fn scope(&self) -> RequestExecutionScope {
154        self.scope.clone()
155    }
156
157    #[cfg(test)]
158    #[must_use]
159    pub(in crate::db) fn observed(&self, resource: DiagnosticExecutionBudgetResource) -> u64 {
160        self.scope.observed(resource)
161    }
162}
163
164pub(in crate::db) fn current_request_scope() -> Option<RequestExecutionScope> {
165    CURRENT_REQUEST_SCOPE.with(|current| current.borrow().clone())
166}
167
168struct CurrentRequestScopeGuard {
169    previous: Option<RequestExecutionScope>,
170}
171
172impl CurrentRequestScopeGuard {
173    fn enter(scope: RequestExecutionScope) -> Self {
174        let previous = CURRENT_REQUEST_SCOPE.with(|current| current.replace(Some(scope)));
175        Self { previous }
176    }
177}
178
179impl Drop for CurrentRequestScopeGuard {
180    fn drop(&mut self) {
181        CURRENT_REQUEST_SCOPE.with(|current| {
182            current.replace(self.previous.take());
183        });
184    }
185}
186
187/// Shared internal handle retained by every session derived from one root.
188#[derive(Clone)]
189pub(in crate::db) struct RequestExecutionScope {
190    counters: Rc<RequestExecutionCounters>,
191}
192
193impl RequestExecutionScope {
194    /// Project the retained owner, independently of the currently active root.
195    pub(in crate::db) fn request_budget(&self) -> RequestBudgetSnapshot {
196        RequestBudgetSnapshot {
197            observed: DiagnosticExecutionBudgetResource::ALL
198                .map(|resource| self.counters.observed[resource_index(resource)].get()),
199            limits: DiagnosticExecutionBudgetResource::ALL
200                .map(|resource| self.counters.budget.limit(resource)),
201        }
202    }
203
204    fn same_counters(&self, other: &Self) -> bool {
205        Rc::ptr_eq(&self.counters, &other.counters)
206    }
207
208    pub(in crate::db) fn charge(
209        &self,
210        context: HardExecutionContext,
211        resource: DiagnosticExecutionBudgetResource,
212        amount: u64,
213    ) -> Result<(), ExecutionBudgetExceeded> {
214        self.counters.charge(context, resource, amount)
215    }
216
217    /// Return how many complete equal-cost units remain across every named
218    /// request resource without mutating request accounting.
219    pub(in crate::db) fn remaining_budget_units(
220        &self,
221        per_unit: &[(DiagnosticExecutionBudgetResource, u64)],
222    ) -> u64 {
223        self.counters
224            .budget
225            .remaining_budget_units(per_unit, |resource| {
226                self.counters.observed[resource_index(resource)].get()
227            })
228    }
229
230    /// Preflight one fixed resource bundle without retaining a rejected or
231    /// speculative charge in the request scope.
232    pub(in crate::db) fn can_charge_budget_bundle(
233        &self,
234        charges: &[(DiagnosticExecutionBudgetResource, u64)],
235    ) -> bool {
236        self.counters
237            .budget
238            .can_charge_budget_bundle(charges, |resource| {
239                self.counters.observed[resource_index(resource)].get()
240            })
241    }
242
243    /// Commit one complete bundle only when every request resource fits.
244    /// `false` leaves every request counter unchanged.
245    pub(in crate::db) fn try_commit_budget_bundle(
246        &self,
247        charges: &[(DiagnosticExecutionBudgetResource, u64)],
248    ) -> bool {
249        if !self.can_charge_budget_bundle(charges) {
250            return false;
251        }
252        for (resource, amount) in charges {
253            let counter = &self.counters.observed[resource_index(*resource)];
254            counter.set(counter.get().saturating_add(*amount));
255        }
256
257        true
258    }
259
260    #[cfg(test)]
261    fn observed(&self, resource: DiagnosticExecutionBudgetResource) -> u64 {
262        self.counters.observed[resource_index(resource)].get()
263    }
264}
265
266struct RequestExecutionCounters {
267    budget: HardExecutionBudget,
268    observed: [Cell<u64>; DiagnosticExecutionBudgetResource::ALL.len()],
269}
270
271impl RequestExecutionCounters {
272    fn charge(
273        &self,
274        context: HardExecutionContext,
275        resource: DiagnosticExecutionBudgetResource,
276        amount: u64,
277    ) -> Result<(), ExecutionBudgetExceeded> {
278        let index = resource_index(resource);
279        let counter = &self.observed[index];
280        let current = counter.get();
281        let (observed, overflowed) = current.overflowing_add(amount);
282        let observed = if overflowed { u64::MAX } else { observed };
283        counter.set(observed);
284        let limit = self.budget.limit(resource);
285        if overflowed || observed > limit {
286            return Err(ExecutionBudgetExceeded::new(
287                resource,
288                limit,
289                observed,
290                context.with_scope(DiagnosticExecutionBudgetScope::Request),
291            ));
292        }
293
294        Ok(())
295    }
296}
297
298#[cfg(test)]
299mod tests {
300    use super::*;
301
302    #[test]
303    fn request_budget_snapshots_cover_every_resource_without_mutating_the_owner() {
304        let root = RequestExecutionRoot::__new_runtime_root();
305        let initial = root.request_budget();
306        let context = HardExecutionContext::new(
307            DiagnosticExecutionBudgetScope::Execution,
308            icydb_diagnostic_code::DiagnosticExecutionLane::PublicRead,
309            0,
310        );
311        for resource in DiagnosticExecutionBudgetResource::ALL {
312            assert_eq!(initial.observed(resource), 0);
313            assert_eq!(initial.limit(resource), REQUEST_HARD_BUDGET.limit(resource));
314            assert_eq!(initial.remaining(resource), initial.limit(resource));
315            root.scope()
316                .charge(context, resource, resource.raw())
317                .unwrap();
318        }
319        let charged = root.request_budget();
320        assert_eq!(root.request_budget(), charged);
321        for resource in DiagnosticExecutionBudgetResource::ALL {
322            assert_eq!(charged.observed(resource), resource.raw());
323            assert_eq!(
324                charged.remaining(resource),
325                charged.limit(resource) - resource.raw()
326            );
327            assert_eq!(initial.observed(resource), 0, "snapshots are frozen");
328        }
329        root.__with_current_scope(|| {
330            let nested = RequestExecutionRoot::__new_or_current_runtime_root();
331            assert_eq!(nested.request_budget(), charged);
332        });
333        let isolated = RequestExecutionRoot::__new_runtime_root();
334        isolated.__with_current_scope(|| {
335            assert_eq!(root.request_budget(), charged);
336            assert_eq!(isolated.request_budget(), initial);
337        });
338    }
339
340    #[test]
341    fn request_budget_headroom_saturates_after_failed_and_overflowing_charges() {
342        let context = HardExecutionContext::new(
343            DiagnosticExecutionBudgetScope::Execution,
344            icydb_diagnostic_code::DiagnosticExecutionLane::PublicRead,
345            0,
346        );
347        for resource in DiagnosticExecutionBudgetResource::ALL {
348            let root = RequestExecutionRoot::new_for_tests(
349                REQUEST_HARD_BUDGET.with_limit_for_tests(resource, 2),
350            );
351            let scope = root.scope();
352            scope.charge(context, resource, 2).unwrap();
353            assert_eq!(root.request_budget().remaining(resource), 0);
354            let failure = scope.charge(context, resource, 1).unwrap_err();
355            assert_eq!(failure.scope(), DiagnosticExecutionBudgetScope::Request);
356            assert_eq!(failure.observed(), 3);
357            assert_eq!(root.request_budget().observed(resource), 3);
358            assert_eq!(root.request_budget().remaining(resource), 0);
359            let overflow = scope.charge(context, resource, u64::MAX).unwrap_err();
360            assert_eq!(overflow.observed(), u64::MAX);
361            assert_eq!(root.request_budget().observed(resource), u64::MAX);
362            assert_eq!(root.request_budget().remaining(resource), 0);
363        }
364    }
365
366    #[test]
367    fn synchronous_scope_is_installed_then_removed() {
368        assert!(current_request_scope().is_none());
369        let root = RequestExecutionRoot::new_for_tests(REQUEST_HARD_BUDGET);
370
371        root.__with_current_scope(|| {
372            assert!(current_request_scope().is_some());
373        });
374
375        assert!(current_request_scope().is_none());
376    }
377
378    #[test]
379    fn nested_entry_reuses_current_counters() {
380        let resource = DiagnosticExecutionBudgetResource::QueryExecutions;
381        let budget = REQUEST_HARD_BUDGET.with_limit_for_tests(resource, 1);
382        let root = RequestExecutionRoot::new_for_tests(budget);
383        let context = HardExecutionContext::new(
384            DiagnosticExecutionBudgetScope::Execution,
385            icydb_diagnostic_code::DiagnosticExecutionLane::PublicRead,
386            0,
387        );
388
389        root.__with_current_scope(|| {
390            let nested = RequestExecutionRoot::__new_or_current_runtime_root();
391            nested
392                .scope()
393                .charge(context, resource, 1)
394                .expect("first nested charge should fit");
395            let exhausted = root
396                .scope()
397                .charge(context, resource, 1)
398                .expect_err("parent should observe the nested charge");
399
400            assert_eq!(exhausted.scope(), DiagnosticExecutionBudgetScope::Request);
401            assert_eq!(exhausted.observed(), 2);
402        });
403    }
404
405    #[test]
406    fn budget_bundle_preflight_is_non_mutating_and_uses_remaining_capacity() {
407        let entries = DiagnosticExecutionBudgetResource::GroupDistinctEntries;
408        let bytes = DiagnosticExecutionBudgetResource::GroupDistinctStateBytes;
409        let budget = REQUEST_HARD_BUDGET
410            .with_limit_for_tests(entries, 5)
411            .with_limit_for_tests(bytes, 60);
412        let root = RequestExecutionRoot::new_for_tests(budget);
413        let scope = root.scope();
414        let context = HardExecutionContext::new(
415            DiagnosticExecutionBudgetScope::Execution,
416            icydb_diagnostic_code::DiagnosticExecutionLane::PublicRead,
417            0,
418        );
419        scope
420            .charge(context, entries, 2)
421            .expect("initial request charge should fit");
422        scope
423            .charge(context, bytes, 20)
424            .expect("initial request charge should fit");
425
426        assert_eq!(
427            scope.remaining_budget_units(&[(entries, 1), (bytes, 10)]),
428            3
429        );
430        assert!(scope.can_charge_budget_bundle(&[(entries, 3), (bytes, 30)]));
431        assert!(!scope.can_charge_budget_bundle(&[(entries, 4), (bytes, 30)]));
432        assert_eq!(
433            root.observed(entries),
434            2,
435            "preflight must not charge entries"
436        );
437        assert_eq!(root.observed(bytes), 20, "preflight must not charge bytes");
438        assert!(!scope.try_commit_budget_bundle(&[(entries, 4), (bytes, 30)]));
439        assert_eq!(root.observed(entries), 2);
440        assert_eq!(root.observed(bytes), 20);
441        assert!(scope.try_commit_budget_bundle(&[(entries, 3), (bytes, 30)]));
442        assert_eq!(root.observed(entries), 5);
443        assert_eq!(root.observed(bytes), 50);
444    }
445
446    #[test]
447    fn repeated_budget_bundle_resources_use_total_remaining_capacity() {
448        let context = HardExecutionContext::new(
449            DiagnosticExecutionBudgetScope::Execution,
450            icydb_diagnostic_code::DiagnosticExecutionLane::PublicRead,
451            0,
452        );
453        for resource in DiagnosticExecutionBudgetResource::ALL {
454            let root = RequestExecutionRoot::new_for_tests(HardExecutionBudget::uniform_for_tests(
455                10,
456                READ_HARD_BUDGET.failure_headroom(),
457            ));
458            let scope = root.scope();
459            scope.charge(context, resource, 2).unwrap();
460            let before = root.request_budget();
461            let per_unit = [(resource, 2), (resource, 0), (resource, 2)];
462            assert_eq!(scope.remaining_budget_units(&per_unit), 2);
463            assert_eq!(root.request_budget(), before);
464
465            let rejected = [(resource, 5), (resource, 4)];
466            assert!(!scope.can_charge_budget_bundle(&rejected));
467            assert!(!scope.try_commit_budget_bundle(&rejected));
468            assert_eq!(root.request_budget(), before);
469
470            let admitted = [(resource, 5), (resource, 0), (resource, 3)];
471            assert!(scope.can_charge_budget_bundle(&admitted));
472            assert_eq!(root.request_budget(), before);
473            assert!(scope.try_commit_budget_bundle(&admitted));
474            assert_eq!(root.observed(resource), 10);
475            assert_eq!(scope.remaining_budget_units(&per_unit), 0);
476        }
477    }
478
479    #[test]
480    fn explicit_root_compatibility_rejects_a_different_active_root() {
481        let first = RequestExecutionRoot::new_for_tests(REQUEST_HARD_BUDGET);
482        let second = RequestExecutionRoot::new_for_tests(REQUEST_HARD_BUDGET);
483
484        assert!(first.__is_compatible_with_current());
485        assert!(!first.__is_current());
486        first.__with_current_scope(|| {
487            assert!(first.__is_current());
488            assert!(first.__is_compatible_with_current());
489            assert!(!second.__is_compatible_with_current());
490        });
491        assert!(second.__is_compatible_with_current());
492    }
493}