molframe_core/execution/
context.rs1use super::admission::Admission;
4use super::memory::MemoryAccount;
5use super::spill::DiskAccount;
6use super::{
7 CancellationToken, MemoryBudget, MemoryBudgetError, MemoryReservation, ScratchPolicy,
8 SpillError, SpillFile, TempStoragePolicy,
9};
10use crate::parallel::SharedPool;
11use std::fmt;
12use std::sync::Arc;
13
14#[derive(Clone, Debug)]
16pub struct ExecutionContext {
17 pub(super) memory: Arc<MemoryAccount>,
18 cancellation: CancellationToken,
19 scratch: ScratchPolicy,
20 temp_storage: TempStoragePolicy,
21 disk: Option<Arc<DiskAccount>>,
22 executor: Arc<SharedPool>,
23 worker_budget: usize,
24 pub(crate) admission: Arc<Admission>,
25}
26
27impl ExecutionContext {
28 #[must_use]
30 pub fn builder() -> ExecutionContextBuilder {
31 ExecutionContextBuilder::default()
32 }
33
34 #[must_use]
36 pub fn memory_budget(&self) -> MemoryBudget {
37 self.memory.budget()
38 }
39
40 #[must_use]
42 pub const fn worker_budget(&self) -> usize {
43 self.worker_budget
44 }
45
46 pub(crate) fn executor(&self) -> &SharedPool {
47 &self.executor
48 }
49
50 #[must_use]
52 pub const fn cancellation(&self) -> &CancellationToken {
53 &self.cancellation
54 }
55
56 #[must_use]
58 pub const fn scratch_policy(&self) -> ScratchPolicy {
59 self.scratch
60 }
61
62 #[must_use]
64 pub const fn temp_storage_policy(&self) -> &TempStoragePolicy {
65 &self.temp_storage
66 }
67
68 pub fn create_spill_file(&self, key: &str) -> Result<SpillFile, SpillError> {
78 let Some(account) = &self.disk else {
79 return Err(SpillError::Disabled);
80 };
81 SpillFile::create(Arc::clone(account), key)
82 }
83
84 #[must_use]
86 pub fn spill_bytes(&self) -> u64 {
87 match &self.disk {
88 Some(account) => account.used_bytes(),
89 None => 0,
90 }
91 }
92
93 #[must_use]
95 pub fn total_spilled_bytes(&self) -> u64 {
96 match &self.disk {
97 Some(account) => account.spilled_bytes(),
98 None => 0,
99 }
100 }
101
102 #[must_use]
104 pub fn reserved_bytes(&self) -> usize {
105 self.memory.reserved_bytes()
106 }
107
108 #[must_use]
110 pub fn peak_reserved_bytes(&self) -> usize {
111 self.memory.peak_reserved_bytes()
112 }
113
114 #[must_use]
116 pub fn live_batches(&self) -> usize {
117 self.memory.live_batches()
118 }
119
120 #[must_use]
122 pub fn peak_live_batches(&self) -> usize {
123 self.memory.peak_live_batches()
124 }
125
126 pub fn try_reserve(&self, bytes: usize) -> Result<MemoryReservation, MemoryBudgetError> {
133 MemoryAccount::reserve(&self.memory, bytes)
134 }
135}
136
137impl Default for ExecutionContext {
138 fn default() -> Self {
139 Self::builder().build_infallible()
140 }
141}
142
143#[derive(Clone, Debug, Default)]
145pub struct ExecutionContextBuilder {
146 memory_budget: MemoryBudget,
147 cancellation: CancellationToken,
148 scratch: ScratchPolicy,
149 temp_storage: TempStoragePolicy,
150 worker_budget: Option<usize>,
151}
152
153impl ExecutionContextBuilder {
154 #[must_use]
156 pub const fn memory_budget(mut self, budget: MemoryBudget) -> Self {
157 self.memory_budget = budget;
158 self
159 }
160
161 #[must_use]
163 pub fn cancellation(mut self, token: CancellationToken) -> Self {
164 self.cancellation = token;
165 self
166 }
167
168 #[must_use]
170 pub const fn scratch_policy(mut self, policy: ScratchPolicy) -> Self {
171 self.scratch = policy;
172 self
173 }
174
175 #[must_use]
177 pub fn temp_storage_policy(mut self, policy: TempStoragePolicy) -> Self {
178 self.temp_storage = policy;
179 self
180 }
181
182 #[must_use]
184 pub const fn worker_budget(mut self, workers: usize) -> Self {
185 self.worker_budget = Some(workers);
186 self
187 }
188
189 pub fn build(self) -> Result<ExecutionContext, ContextError> {
196 if self.worker_budget == Some(0) {
197 return Err(ContextError::ZeroWorkers);
198 }
199 if self.scratch.max_bytes() > self.memory_budget.bytes() {
200 return Err(ContextError::ScratchExceedsMemory {
201 scratch: self.scratch.max_bytes(),
202 memory: self.memory_budget.bytes(),
203 });
204 }
205 if matches!(
206 self.temp_storage,
207 TempStoragePolicy::Directory { max_bytes: 0, .. }
208 ) {
209 return Err(ContextError::EmptyTempStorage);
210 }
211 Ok(self.finish())
212 }
213
214 fn build_infallible(self) -> ExecutionContext {
215 self.finish()
216 }
217
218 fn finish(self) -> ExecutionContext {
219 let disk = DiskAccount::from_policy(&self.temp_storage);
220 let executor = SharedPool::global();
221 let worker_budget = self.worker_budget.map_or_else(
222 || executor.capacity(),
223 |workers| workers.min(executor.capacity()),
224 );
225 ExecutionContext {
226 memory: Arc::new(MemoryAccount::new(self.memory_budget)),
227 cancellation: self.cancellation,
228 scratch: self.scratch,
229 temp_storage: self.temp_storage,
230 disk,
231 executor,
232 worker_budget,
233 admission: Arc::new(Admission::new(worker_budget.saturating_mul(2))),
234 }
235 }
236}
237
238#[derive(Clone, Copy, Debug, PartialEq, Eq)]
240pub enum ContextError {
241 ScratchExceedsMemory {
243 scratch: usize,
245 memory: usize,
247 },
248 EmptyTempStorage,
250 ZeroWorkers,
252}
253
254impl fmt::Display for ContextError {
255 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
256 match self {
257 Self::ScratchExceedsMemory { scratch, memory } => write!(
258 formatter,
259 "scratch policy retains {scratch} bytes but memory budget is {memory}"
260 ),
261 Self::EmptyTempStorage => {
262 formatter.write_str("enabled temporary storage must have non-zero capacity")
263 }
264 Self::ZeroWorkers => formatter.write_str("execution worker budget must be non-zero"),
265 }
266 }
267}
268
269impl std::error::Error for ContextError {}
270
271#[cfg(test)]
272#[path = "context_tests.rs"]
273mod tests;