icydb_core/db/session/
request.rs1use 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, 1_024, 256, 250_000, 250_000, 128 * 1_024 * 1_024, 16_000_000, 16_000_000, 128 * 1_024 * 1_024, 128 * 1_024 * 1_024, 250_000, 32_000_000, 128 * 1_024 * 1_024, 100_000, 128 * 1_024 * 1_024, 1_000_000, 128 * 1_024 * 1_024, 1_000_000, 100_000, 64 * 1_024 * 1_024, 4_500_000_000, ],
43 REQUEST_FAILURE_HEADROOM,
44);
45
46thread_local! {
47 static CURRENT_REQUEST_SCOPE: RefCell<Option<RequestExecutionScope>> =
48 const { RefCell::new(None) };
49}
50
51pub struct RequestExecutionRoot {
57 scope: RequestExecutionScope,
58}
59
60impl RequestExecutionRoot {
61 #[doc(hidden)]
67 #[must_use]
68 pub fn __new_runtime_root() -> Self {
69 Self::from_budget(REQUEST_HARD_BUDGET)
70 }
71
72 #[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 #[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#[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}