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
14pub const DEFAULT_IMAGE_SEARCH_LIMIT: usize = 10_000_000;
16
17#[derive(Clone, Debug)]
19pub struct ExecutionContext {
20 pub(super) memory: Arc<MemoryAccount>,
21 cancellation: CancellationToken,
22 scratch: ScratchPolicy,
23 temp_storage: TempStoragePolicy,
24 disk: Option<Arc<DiskAccount>>,
25 executor: Arc<SharedPool>,
26 worker_budget: usize,
27 image_search_limit: usize,
28 pub(crate) admission: Arc<Admission>,
29}
30
31impl ExecutionContext {
32 #[must_use]
34 pub fn builder() -> ExecutionContextBuilder {
35 ExecutionContextBuilder::default()
36 }
37
38 #[must_use]
45 pub const fn image_search_limit(&self) -> usize {
46 self.image_search_limit
47 }
48
49 #[must_use]
51 pub fn memory_budget(&self) -> MemoryBudget {
52 self.memory.budget()
53 }
54
55 #[must_use]
57 pub const fn worker_budget(&self) -> usize {
58 self.worker_budget
59 }
60
61 pub(crate) fn executor(&self) -> &SharedPool {
62 &self.executor
63 }
64
65 #[must_use]
67 pub const fn cancellation(&self) -> &CancellationToken {
68 &self.cancellation
69 }
70
71 #[must_use]
73 pub const fn scratch_policy(&self) -> ScratchPolicy {
74 self.scratch
75 }
76
77 #[must_use]
79 pub const fn temp_storage_policy(&self) -> &TempStoragePolicy {
80 &self.temp_storage
81 }
82
83 pub fn create_spill_file(&self, key: &str) -> Result<SpillFile, SpillError> {
93 let Some(account) = &self.disk else {
94 return Err(SpillError::Disabled);
95 };
96 SpillFile::create(Arc::clone(account), key)
97 }
98
99 #[must_use]
101 pub fn spill_bytes(&self) -> u64 {
102 match &self.disk {
103 Some(account) => account.used_bytes(),
104 None => 0,
105 }
106 }
107
108 #[must_use]
110 pub fn total_spilled_bytes(&self) -> u64 {
111 match &self.disk {
112 Some(account) => account.spilled_bytes(),
113 None => 0,
114 }
115 }
116
117 #[must_use]
119 pub fn reserved_bytes(&self) -> usize {
120 self.memory.reserved_bytes()
121 }
122
123 #[must_use]
125 pub fn peak_reserved_bytes(&self) -> usize {
126 self.memory.peak_reserved_bytes()
127 }
128
129 #[must_use]
131 pub fn live_batches(&self) -> usize {
132 self.memory.live_batches()
133 }
134
135 #[must_use]
137 pub fn peak_live_batches(&self) -> usize {
138 self.memory.peak_live_batches()
139 }
140
141 pub fn try_reserve(&self, bytes: usize) -> Result<MemoryReservation, MemoryBudgetError> {
148 MemoryAccount::reserve(&self.memory, bytes)
149 }
150}
151
152impl Default for ExecutionContext {
153 fn default() -> Self {
154 Self::builder().build_infallible()
155 }
156}
157
158#[derive(Clone, Debug, Default)]
160pub struct ExecutionContextBuilder {
161 memory_budget: MemoryBudget,
162 cancellation: CancellationToken,
163 scratch: ScratchPolicy,
164 temp_storage: TempStoragePolicy,
165 worker_budget: Option<usize>,
166 image_search_limit: Option<usize>,
167}
168
169impl ExecutionContextBuilder {
170 #[must_use]
172 pub const fn memory_budget(mut self, budget: MemoryBudget) -> Self {
173 self.memory_budget = budget;
174 self
175 }
176
177 #[must_use]
179 pub fn cancellation(mut self, token: CancellationToken) -> Self {
180 self.cancellation = token;
181 self
182 }
183
184 #[must_use]
186 pub const fn scratch_policy(mut self, policy: ScratchPolicy) -> Self {
187 self.scratch = policy;
188 self
189 }
190
191 #[must_use]
193 pub fn temp_storage_policy(mut self, policy: TempStoragePolicy) -> Self {
194 self.temp_storage = policy;
195 self
196 }
197
198 #[must_use]
200 pub const fn worker_budget(mut self, workers: usize) -> Self {
201 self.worker_budget = Some(workers);
202 self
203 }
204
205 #[must_use]
207 pub const fn image_search_limit(mut self, images: usize) -> Self {
208 self.image_search_limit = Some(images);
209 self
210 }
211
212 pub fn build(self) -> Result<ExecutionContext, ContextError> {
219 if self.worker_budget == Some(0) {
220 return Err(ContextError::ZeroWorkers);
221 }
222 if self.scratch.max_bytes() > self.memory_budget.bytes() {
223 return Err(ContextError::ScratchExceedsMemory {
224 scratch: self.scratch.max_bytes(),
225 memory: self.memory_budget.bytes(),
226 });
227 }
228 if matches!(
229 self.temp_storage,
230 TempStoragePolicy::Directory { max_bytes: 0, .. }
231 ) {
232 return Err(ContextError::EmptyTempStorage);
233 }
234 Ok(self.finish())
235 }
236
237 fn build_infallible(self) -> ExecutionContext {
238 self.finish()
239 }
240
241 fn finish(self) -> ExecutionContext {
242 let disk = DiskAccount::from_policy(&self.temp_storage);
243 let executor = SharedPool::global();
244 let worker_budget = self.worker_budget.map_or_else(
245 || executor.capacity(),
246 |workers| workers.min(executor.capacity()),
247 );
248 ExecutionContext {
249 memory: Arc::new(MemoryAccount::new(self.memory_budget)),
250 cancellation: self.cancellation,
251 scratch: self.scratch,
252 temp_storage: self.temp_storage,
253 disk,
254 executor,
255 worker_budget,
256 image_search_limit: match self.image_search_limit {
257 Some(limit) => limit,
258 None => DEFAULT_IMAGE_SEARCH_LIMIT,
259 },
260 admission: Arc::new(Admission::new(worker_budget.saturating_mul(2))),
261 }
262 }
263}
264
265#[derive(Clone, Copy, Debug, PartialEq, Eq)]
267pub enum ContextError {
268 ScratchExceedsMemory {
270 scratch: usize,
272 memory: usize,
274 },
275 EmptyTempStorage,
277 ZeroWorkers,
279}
280
281impl fmt::Display for ContextError {
282 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
283 match self {
284 Self::ScratchExceedsMemory { scratch, memory } => write!(
285 formatter,
286 "scratch policy retains {scratch} bytes but memory budget is {memory}"
287 ),
288 Self::EmptyTempStorage => {
289 formatter.write_str("enabled temporary storage must have non-zero capacity")
290 }
291 Self::ZeroWorkers => formatter.write_str("execution worker budget must be non-zero"),
292 }
293 }
294}
295
296impl std::error::Error for ContextError {}
297
298#[cfg(test)]
299#[path = "context_tests.rs"]
300mod tests;
301
302crate::diagnostic_from!(ContextError, |_error| crate::Code::E6102);