linkrs_query/executor/streaming/runtime.rs
1//! Per-query execution runtime.
2//!
3//! Split by concern:
4//! - [`columnar_stats`] — columnar fast-path counters and PROFILE snapshots
5//! - [`profile`] — per-operator profiling ([`OperatorProfile`],
6//! [`ProfileBoard`], [`ProfileCollector`])
7//! - [`resources`] — cleanup ownership ([`ResourceOwner`]) and the
8//! [`QueryFinishGuard`] RAII lifecycle guard
9//! - this module — [`ExecutionRuntime`], the per-query composition root
10
11pub mod columnar_stats;
12pub mod profile;
13pub mod resources;
14
15pub use columnar_stats::{
16 ColumnarStats, ColumnarStatsSnapshot, D1_EVAL_THRESHOLD, D1_TYPED_RATE_THRESHOLD,
17 SELECTION_BOUNDARY_OPS,
18};
19pub use profile::{
20 OperatorProfile, OperatorProfileKey, ProfileBoard, ProfileCollector, ProfileEntry,
21};
22pub use resources::{QueryFinishGuard, ResourceOwner};
23
24use std::collections::HashMap;
25use std::sync::atomic::{AtomicI64, AtomicU64, AtomicUsize, Ordering};
26use std::sync::Arc;
27use std::time::Instant;
28
29use parking_lot::Mutex;
30
31use parking_lot::RwLock;
32
33use super::query_registry::{CancelToken, QueryId, QueryRegistry};
34use super::spill::SpillManager;
35use super::state::StateArenaSet;
36use super::transaction_scope::{CancelReason, SessionTransactionController, TransactionScope};
37use crate::executor::base::MemoryBudget;
38use crate::executor::streaming::pool::TaskScheduler;
39use crate::optimizer::stats::feedback::history::QueryFeedbackHistory;
40use crate::query_manager::QueryManager;
41use crate::storage::QueryStorage;
42use linkrs_core::error::QueryError;
43use linkrs_core::Arena;
44use linkrs_core::Value;
45
46/// Query identity information
47#[derive(Debug, Clone, Default)]
48pub struct QueryIdentity {
49 pub query_id: u64,
50 pub session_id: Option<String>,
51 pub space_name: Option<String>,
52}
53
54/// Per-query execution runtime shared across all operators.
55///
56/// Centralises cancellation, memory tracking, profiling, resource
57/// lifecycle, and query-registration so that operators do not each
58/// carry ad-hoc context.
59///
60/// unified cancellation via [`CancelToken`]
61/// has been removed; operators check [`CancelToken::is_cancelled`].
62#[derive(Debug)]
63pub struct ExecutionRuntime {
64 /// Query identity (behind Mutex for write-once from the API layer).
65 query_id: parking_lot::Mutex<QueryIdentity>,
66 /// typed cancellation token with reason tracking (single source).
67 /// Behind a `Mutex` because the token is adopted after the runtime is
68 /// shared with the executor tree (`Arc::get_mut` is unusable once the
69 /// tree holds clones); `&self` mutation keeps the registry-None path
70 /// wired without `&mut` access.
71 cancel_token_v2: parking_lot::Mutex<CancelToken>,
72 /// Optional deadline; the query is cancelled after this instant.
73 deadline: Option<Instant>,
74 /// Per-query memory budget for blocking operators.
75 pub memory_budget: MemoryBudget,
76 /// Profile board with atomic counters for lock-free hot-path recording.
77 profile: Arc<ProfileBoard>,
78 /// Resource owner for cleanup of cursors, temp files, etc.
79 resource_owner: Arc<Mutex<ResourceOwner>>,
80 /// Optional reference to the global QueryManager for KILL QUERY, finish
81 /// tracking, and progress forwarding. Behind a `Mutex` for interior
82 /// mutability so the assembly can attach it after the runtime is shared
83 /// with the executor tree (same pattern as `query_registry`).
84 query_manager: parking_lot::Mutex<Option<Arc<QueryManager>>>,
85 /// Row cadence for query-progress notifications. Zero disables emission.
86 /// Checked with a single relaxed atomic load on the row-recording path,
87 /// so the default zero keeps the hot path allocation- and lock-free.
88 progress_rows_interval: AtomicU64,
89 /// Highest processed-row watermark already reported to progress
90 /// observers. Prevents duplicate notifications when several chunks land
91 /// inside the same interval bucket.
92 progress_last_emitted: AtomicU64,
93 /// Session id reported in progress notifications. Set by the assembly
94 /// that attaches the `QueryManager`; defaults to zero.
95 progress_session_id: AtomicI64,
96 /// Query id (in the `QueryManager` id space) reported in progress
97 /// notifications. Negative means unset and suppresses emission.
98 progress_query_id: AtomicI64,
99 /// Session-level transaction controller for transaction commands.
100 /// Behind a RwLock for interior mutability (set after runtime is shared).
101 session_controller: parking_lot::RwLock<Option<Arc<SessionTransactionController>>>,
102 /// Transaction scope for this execution (set by bindings).
103 transaction_scope: Option<TransactionScope>,
104 /// Optional reference to the [`QueryRegistry`] for KILL QUERY.
105 /// Behind a `Mutex` for the same interior-mutability reason as the
106 /// cancel token (the registry is attached after the executor tree clones
107 /// the runtime `Arc`).
108 query_registry: parking_lot::Mutex<Option<Arc<QueryRegistry>>>,
109 /// Query ID allocated by the registry.
110 registry_query_id: parking_lot::Mutex<Option<QueryId>>,
111 /// Engine-level shared scheduler for dynamic partition execution.
112 /// When set, all queries share the same worker pool instead of creating
113 /// per-query threads. Falls back to serial if neither this nor the
114 /// per-query `worker_pool` is set.
115 /// Behind a `parking_lot::Mutex` because it's written via `&self` (internal
116 /// mutability pattern used throughout [`ExecutionRuntime`]).
117 shared_scheduler: parking_lot::Mutex<Option<Arc<super::pool::SharedScheduler>>>,
118 /// Query-level morsel worker pool for dynamic partition execution
119 /// Kept for backward compatibility.
120 /// Created when `max_workers > 1` and no `shared_scheduler` is set;
121 /// `None` means serial fallback.
122 /// Behind a Mutex so the engine can set the pool after construction.
123 pub worker_pool: Arc<parking_lot::Mutex<Option<Arc<dyn TaskScheduler>>>>,
124 /// Per-partition output channel capacity for parallel exchange/gather.
125 pub max_buffered_chunks: AtomicUsize,
126 /// Spill manager for offloading operator data to disk.
127 pub spill_manager: Arc<parking_lot::Mutex<Option<Arc<SpillManager>>>>,
128 /// Storage client for this query execution.
129 /// Moved here from OperatorSpec so that the physical plan tree is
130 /// truly immutable and cacheable without sharing storage handles.
131 pub storage: Option<Arc<RwLock<dyn QueryStorage>>>,
132 pub search: crate::executor::base::SearchContext,
133 /// Per-partition operator state arenas.
134 ///
135 /// Indexed by `partition_id` so parallel workers do not contend on a
136 /// single lock. `state_arenas[0]` serves global / non-partitioned
137 /// operators.
138 ///
139 /// Operators create/read/update their typed state during
140 /// `open()` / `next()` / `close()`, indexed by [`PhysicalOperatorId`]
141 /// stored in [`OperatorBase::physical_operator_id`](super::operators::base::OperatorBase).
142 pub state_arenas: Vec<Mutex<StateArenaSet>>,
143
144 /// Runtime parameter name→value map, bound at materialization time.
145 /// Operators read this to resolve `Expression::Parameter` references.
146 pub parameter_values: Option<Arc<HashMap<String, Value>>>,
147
148 /// Runtime session variable name→value snapshot, bound at materialization
149 /// time. Operators read this to resolve `Expression::SessionVariable`
150 /// references.
151 pub session_variable_values: Option<Arc<HashMap<String, Value>>>,
152
153 /// Per-query bumpalo arena for executor temporary allocations.
154 pub arena: Option<Arc<Mutex<Arena>>>,
155 /// Columnar fast-path hit/miss counters shared with produced chunks (T5).
156 columnar_stats: Arc<ColumnarStats>,
157 /// Cross-query adaptive policy for the typed columnar chunk layout.
158 ///
159 /// Injected by the materializer from the query bindings (owned by the
160 /// optimizer engine). The policy is read at chunk-production time by
161 /// the scan gates; when the runtime finishes, the per-query columnar
162 /// stats are merged back into the policy so later queries can adapt.
163 columnar_policy: Option<Arc<super::chunk::ColumnarPolicy>>,
164 /// Per-query override for the shared columnar policy decision.
165 ///
166 /// Defaults to inherit; internal callers may pin one query without
167 /// touching the shared counters.
168 columnar_override: super::chunk::QueryColumnarOverride,
169 /// Shared query feedback history for collecting execution statistics.
170 ///
171 /// Injected by the materializer from the query bindings; when set, the
172 /// execution instance records estimated-vs-actual operator feedback here
173 /// after execution completes (stats feedback loop).
174 pub feedback_history: Option<Arc<QueryFeedbackHistory>>,
175 /// Macro catalog manager (user-defined macros), shared engine-wide.
176 ///
177 /// Injected by the materializer from the query bindings; read by DDL
178 /// operators executing `CREATE/DROP MACRO` and `SHOW MACROS`.
179 pub macro_manager: Option<Arc<linkrs_core::metadata::MacroManager>>,
180 /// Type-alias catalog manager (user-defined types), shared engine-wide.
181 ///
182 /// Injected by the materializer from the query bindings; read by DDL
183 /// operators executing `CREATE/DROP TYPE`.
184 pub type_alias_manager: Option<Arc<linkrs_core::metadata::TypeAliasManager>>,
185 /// Sequence catalog manager (named sequences), shared engine-wide.
186 ///
187 /// Injected by the materializer from the query bindings; read by DDL
188 /// operators executing `CREATE/ALTER/DROP SEQUENCE` and by the
189 /// `curr_val` / `next_val` expression functions.
190 pub sequence_manager: Option<Arc<linkrs_core::metadata::SequenceManager>>,
191 /// Working tables of enclosing recursive-CTE fixpoints, keyed by the
192 /// mangled CTE tag (`crate::cte::mangle_cte_name`).
193 ///
194 /// Written by the fixpoint operator before each step iteration and
195 /// cleared afterwards; read by CTE scan sources. Serial execution only
196 /// (fixpoint fragments are never partitioned).
197 pub cte_tables:
198 Arc<parking_lot::Mutex<std::collections::HashMap<String, Vec<Vec<linkrs_core::Value>>>>>,
199}
200
201impl ExecutionRuntime {
202 /// Create a new execution runtime with the given query identity, memory budget,
203 /// and optional storage client.
204 pub fn new(
205 query_id: QueryIdentity,
206 memory_budget: MemoryBudget,
207 storage: Option<Arc<RwLock<dyn QueryStorage>>>,
208 search: crate::executor::base::SearchContext,
209 ) -> Self {
210 Self {
211 query_id: parking_lot::Mutex::new(query_id),
212 cancel_token_v2: parking_lot::Mutex::new(CancelToken::new()),
213 deadline: None,
214 memory_budget,
215 profile: Arc::new(ProfileBoard::new()),
216 resource_owner: Arc::new(Mutex::new(ResourceOwner::new())),
217 query_manager: parking_lot::Mutex::new(None),
218 progress_rows_interval: AtomicU64::new(0),
219 progress_last_emitted: AtomicU64::new(0),
220 progress_session_id: AtomicI64::new(0),
221 progress_query_id: AtomicI64::new(-1),
222 session_controller: parking_lot::RwLock::new(None),
223 transaction_scope: None,
224 query_registry: parking_lot::Mutex::new(None),
225 registry_query_id: parking_lot::Mutex::new(None),
226 shared_scheduler: parking_lot::Mutex::new(None),
227 worker_pool: Arc::new(parking_lot::Mutex::new(None)),
228 max_buffered_chunks: AtomicUsize::new(10),
229 spill_manager: Arc::new(parking_lot::Mutex::new(None)),
230 storage,
231 search,
232 state_arenas: vec![Mutex::new(StateArenaSet::new())],
233 parameter_values: None,
234 session_variable_values: None,
235 arena: Some(Arc::new(Mutex::new(Arena::new()))),
236 columnar_stats: Arc::new(ColumnarStats::new()),
237 columnar_policy: None,
238 columnar_override: super::chunk::QueryColumnarOverride::inherit(),
239 feedback_history: None,
240 macro_manager: None,
241 type_alias_manager: None,
242 sequence_manager: None,
243 cte_tables: Arc::new(parking_lot::Mutex::new(std::collections::HashMap::new())),
244 }
245 }
246
247 /// Create a runtime with default settings (query_id = 0, default memory budget, no storage).
248 pub fn default_budget() -> Self {
249 Self::new(
250 QueryIdentity::default(),
251 MemoryBudget::default_budget(),
252 None,
253 crate::executor::base::SearchContext::default(),
254 )
255 }
256
257 // ── Query identity ──
258
259 pub fn query_id(&self) -> QueryIdentity {
260 self.query_id.lock().clone()
261 }
262
263 /// Override the query ID number after construction.
264 ///
265 /// The factory initialises `query_id.query_id` to 0; the API layer assigns the
266 /// real server-side ID before the handle is returned to the caller.
267 pub fn assign_query_id(&self, id: u64) {
268 self.query_id.lock().query_id = id;
269 }
270
271 #[cfg(feature = "fulltext")]
272 pub fn set_fulltext_manager(
273 &mut self,
274 manager: Option<Arc<linkrs_fulltext::manager::FulltextIndexManager>>,
275 ) {
276 self.search.fulltext_manager = manager;
277 }
278
279 #[cfg(feature = "vector")]
280 pub fn set_vector_coordinator(
281 &mut self,
282 coordinator: Option<Arc<linkrs_sync::VectorSyncCoordinator>>,
283 ) {
284 self.search.vector_coordinator = coordinator;
285 }
286
287 /// Attach a QueryManager so that KILL QUERY, finish tracking, and progress
288 /// forwarding work.
289 ///
290 /// Uses interior mutability (`&self`) because the runtime is shared with
291 /// the executor tree before the assembly attaches the manager, so `&mut`
292 /// access is unavailable at that point.
293 pub fn set_query_manager(&self, qm: Arc<QueryManager>) {
294 *self.query_manager.lock() = Some(qm);
295 }
296
297 /// Register this query with the attached QueryManager and return a
298 /// [`QueryFinishGuard`] that marks it finished on drop.
299 ///
300 /// Returns `None` when no QueryManager is attached (non-fatal).
301 pub fn finish_guard(&self) -> Option<QueryFinishGuard> {
302 let qm = self.query_manager.lock().as_ref()?.clone();
303 let id = self.query_id();
304 Some(QueryFinishGuard::new(qm, id.query_id as i64))
305 }
306
307 // ── QueryRegistry integration ──
308
309 /// Attach a [`QueryRegistry`] and the allocated [`QueryId`].
310 ///
311 /// Interior mutability via `Mutex`: the runtime is shared with the
312 /// executor tree before registration, so `&mut` access is not available.
313 pub fn set_query_registry(&self, registry: Arc<QueryRegistry>, qid: QueryId) {
314 *self.query_registry.lock() = Some(registry);
315 *self.registry_query_id.lock() = Some(qid);
316 }
317
318 /// Return the registry-allocated query ID, if set.
319 pub fn registry_query_id(&self) -> Option<QueryId> {
320 *self.registry_query_id.lock()
321 }
322
323 /// Set the session-level transaction controller for transaction commands.
324 pub fn set_session_controller(&self, ctrl: Arc<SessionTransactionController>) {
325 *self.session_controller.write() = Some(ctrl);
326 }
327
328 /// Return the session-level transaction controller, if set.
329 pub fn session_controller(&self) -> Option<Arc<SessionTransactionController>> {
330 self.session_controller.read().clone()
331 }
332
333 /// Set the parameter name→value map for this execution instance.
334 pub fn set_parameter_values(&mut self, values: Arc<HashMap<String, Value>>) {
335 self.parameter_values = Some(values);
336 }
337
338 /// Return the parameter name→value map, if bound.
339 pub fn parameter_values(&self) -> Option<Arc<HashMap<String, Value>>> {
340 self.parameter_values.clone()
341 }
342
343 /// Set the session variable snapshot for this execution instance.
344 pub fn set_session_variable_values(&mut self, values: Arc<HashMap<String, Value>>) {
345 self.session_variable_values = Some(values);
346 }
347
348 /// Return the session variable snapshot, if bound.
349 pub fn session_variable_values(&self) -> Option<Arc<HashMap<String, Value>>> {
350 self.session_variable_values.clone()
351 }
352
353 /// Set the macro catalog manager for this execution instance.
354 pub fn set_macro_manager(&mut self, manager: Option<Arc<linkrs_core::metadata::MacroManager>>) {
355 self.macro_manager = manager;
356 }
357
358 /// Return the macro catalog manager, if bound.
359 pub fn macro_manager(&self) -> Option<Arc<linkrs_core::metadata::MacroManager>> {
360 self.macro_manager.clone()
361 }
362
363 /// Set the type-alias catalog manager for this execution instance.
364 pub fn set_type_alias_manager(
365 &mut self,
366 manager: Option<Arc<linkrs_core::metadata::TypeAliasManager>>,
367 ) {
368 self.type_alias_manager = manager;
369 }
370
371 /// Return the type-alias catalog manager, if bound.
372 pub fn type_alias_manager(&self) -> Option<Arc<linkrs_core::metadata::TypeAliasManager>> {
373 self.type_alias_manager.clone()
374 }
375
376 /// Set the sequence catalog manager for this execution instance.
377 pub fn set_sequence_manager(
378 &mut self,
379 manager: Option<Arc<linkrs_core::metadata::SequenceManager>>,
380 ) {
381 self.sequence_manager = manager;
382 }
383
384 /// Return the sequence catalog manager, if bound.
385 pub fn sequence_manager(&self) -> Option<Arc<linkrs_core::metadata::SequenceManager>> {
386 self.sequence_manager.clone()
387 }
388
389 /// Publish a recursive-CTE working table for the duration of one step
390 /// iteration. Overwrites any previous table under the same mangled tag.
391 pub fn set_cte_table(&self, mangled_tag: &str, rows: Vec<Vec<linkrs_core::Value>>) {
392 self.cte_tables.lock().insert(mangled_tag.to_string(), rows);
393 }
394
395 /// Read the current working table published under a mangled CTE tag.
396 pub fn cte_table(&self, mangled_tag: &str) -> Option<Vec<Vec<linkrs_core::Value>>> {
397 self.cte_tables.lock().get(mangled_tag).cloned()
398 }
399
400 /// Remove the working table published under a mangled CTE tag.
401 pub fn clear_cte_table(&self, mangled_tag: &str) {
402 self.cte_tables.lock().remove(mangled_tag);
403 }
404
405 /// Set the transaction scope for this execution.
406 pub fn set_transaction_scope(&mut self, scope: TransactionScope) {
407 self.transaction_scope = Some(scope);
408 }
409
410 /// Return the current transaction scope, if any.
411 pub fn transaction_scope(&self) -> Option<&TransactionScope> {
412 self.transaction_scope.as_ref()
413 }
414
415 /// Return the typed [`CancelToken`] for cooperative cancellation.
416 pub fn cancel_token_v2(&self) -> CancelToken {
417 self.cancel_token_v2.lock().clone()
418 }
419
420 /// Propagate a write-conflict failure of the current transaction.
421 ///
422 /// Called by write operators when a storage access fails with a
423 /// conflict-classified error (write-write conflict, rollback-only). The
424 /// session controller is marked rollback-only so a later COMMIT is
425 /// rejected, and the shared cancellation token is cancelled with the
426 /// typed [`CancelReason::TransactionConflict`] reason — terminating any
427 /// pipeline stages of the same transaction that have not started yet.
428 pub fn note_transaction_conflict(&self) {
429 if let Some(controller) = self.session_controller() {
430 controller.mark_rollback_only();
431 }
432 self.cancel_with_reason(CancelReason::TransactionConflict);
433 }
434
435 /// Adopt an externally-owned cancellation token.
436 ///
437 /// Called at instantiation so the runtime, the query registry, and the
438 /// request-scoped [`QueryContext`] share one token (single source for
439 /// KILL QUERY / cancellation). Interior mutability via `Mutex` because
440 /// the executor tree holds an `Arc` clone of the runtime by the time
441 /// instantiation wires the token.
442 pub fn set_cancel_token(&self, token: CancelToken) {
443 *self.cancel_token_v2.lock() = token;
444 }
445
446 // ── Cancellation ──
447
448 /// Shared cancellation token for cooperative checks by operators and I/O.
449 pub fn cancel_token(&self) -> CancelToken {
450 self.cancel_token_v2.lock().clone()
451 }
452
453 /// Check whether the query has been cancelled (token or deadline).
454 pub fn is_cancelled(&self) -> bool {
455 self.cancel_token_v2.lock().is_cancelled()
456 || self.deadline.is_some_and(|d| Instant::now() >= d)
457 }
458
459 /// Return an error if the query has been cancelled.
460 pub fn ensure_not_cancelled(&self) -> Result<(), QueryError> {
461 if self.is_cancelled() {
462 let reason = self
463 .cancel_token_v2
464 .lock()
465 .reason()
466 .map(|r| r.to_string())
467 .unwrap_or_else(|| "Query cancelled".to_string());
468 Err(QueryError::execution(reason))
469 } else {
470 Ok(())
471 }
472 }
473
474 /// Cancel this query with a typed reason.
475 ///
476 /// Sets the [`CancelToken`], marks the query as Killed in the
477 /// attached QueryManager, and cancels the registry entry (if configured).
478 pub fn cancel_with_reason(&self, reason: CancelReason) {
479 self.cancel_token_v2.lock().cancel(reason.clone());
480 if let Some(qm) = self.query_manager.lock().as_ref() {
481 let id = self.query_id();
482 let _ = qm.kill_query(id.query_id as i64);
483 }
484 if let (Some(ref reg), Some(qid)) = (
485 &*self.query_registry.lock(),
486 self.registry_query_id.lock().as_ref().copied(),
487 ) {
488 reg.cancel(qid, reason);
489 }
490 }
491
492 /// Legacy cancel (no typed reason). Delegates to [`cancel_with_reason`]
493 /// with [`CancelReason::UserKill`].
494 pub fn cancel(&self) {
495 self.cancel_with_reason(CancelReason::UserKill);
496 }
497
498 /// Enable deadline-based cancellation.
499 pub fn cancel_on_deadline(&self) {
500 if let Some(deadline) = self.deadline {
501 if Instant::now() >= deadline {
502 self.cancel_with_reason(CancelReason::Deadline);
503 }
504 }
505 }
506
507 /// Set or clear a deadline.
508 pub fn set_deadline(&mut self, deadline: Option<Instant>) {
509 self.deadline = deadline;
510 }
511
512 // ── Profile ──
513
514 pub fn profile(&self) -> &Arc<ProfileBoard> {
515 &self.profile
516 }
517
518 /// Register an operator profile entry (called during `open()`).
519 pub fn register_operator(&self, op_profile: &OperatorProfile) -> Arc<ProfileEntry> {
520 self.profile.register_operator(op_profile)
521 }
522
523 /// Return a [`StateArenaSet`] mutex for the given partition.
524 ///
525 /// Non-partitioned operators (`partition_id == None`) always map to
526 /// arena 0. Partitioned operators map to `(partition_id + 1)`, with
527 /// fallback to the last arena when the partition count was under-estimated.
528 pub fn state_arena_for(&self, partition_id: Option<usize>) -> &Mutex<StateArenaSet> {
529 let idx = partition_id.map(|p| p + 1).unwrap_or(0);
530 let capped = idx.min(self.state_arenas.len().saturating_sub(1));
531 &self.state_arenas[capped]
532 }
533
534 /// Set the number of partition arenas (must be ≥ 1).
535 pub fn set_partition_count(&mut self, count: usize) {
536 let count = count.max(1);
537 self.state_arenas
538 .resize_with(count, || Mutex::new(StateArenaSet::new()));
539 }
540
541 /// Record that execution has started (profile timing).
542 pub fn profile_start(&self) {
543 self.profile.record_start();
544 }
545
546 /// Record that execution has ended.
547 pub fn profile_end(&self) {
548 self.profile.record_end();
549 }
550
551 /// Add rows to the profile counter.
552 pub fn profile_add_rows(&self, count: u64) {
553 let previous = self.profile.total_rows.fetch_add(count, Ordering::Relaxed);
554 self.maybe_emit_progress(previous, previous.saturating_add(count));
555 }
556
557 /// Row cadence for query-progress notifications (`0` disables).
558 ///
559 /// Set alongside `set_query_manager` by the assembly that owns both the
560 /// executor and the `QueryManager`. Zero by default: unconfigured
561 /// runtimes never emit and pay a single relaxed atomic load per batch.
562 pub fn set_progress_rows_interval(&self, rows_interval: u64) {
563 self.progress_rows_interval
564 .store(rows_interval, Ordering::Relaxed);
565 }
566
567 /// Identity reported in progress notifications, in the `QueryManager`
568 /// id space. A negative query id suppresses emission until the assembly
569 /// provides the real mapping between the two query identity schemes.
570 pub fn set_progress_identity(&self, session_id: i64, query_id: i64) {
571 self.progress_session_id
572 .store(session_id, Ordering::Relaxed);
573 self.progress_query_id.store(query_id, Ordering::Relaxed);
574 self.progress_last_emitted.store(0, Ordering::Relaxed);
575 }
576
577 /// Forward a row watermark to the attached `QueryManager` when it
578 /// crosses an unreported interval bucket. Cheap no-op unless a manager
579 /// is attached, an interval is configured, and the identity is set.
580 fn maybe_emit_progress(&self, previous_total: u64, new_total: u64) {
581 let interval = self.progress_rows_interval.load(Ordering::Relaxed);
582 if interval == 0 {
583 return;
584 }
585 let query_id = self.progress_query_id.load(Ordering::Relaxed);
586 if query_id < 0 {
587 return;
588 }
589 // Clone out of the guard so the lock is released before any callback
590 // runs (progress observers must never be invoked under the lock).
591 let query_manager = match self.query_manager.lock().as_ref() {
592 Some(query_manager) => Arc::clone(query_manager),
593 None => return,
594 };
595 if query_manager.progress_callback_count() == 0 {
596 return;
597 }
598 if new_total / interval == previous_total / interval {
599 return;
600 }
601 let session_id = self.progress_session_id.load(Ordering::Relaxed);
602 // Monotonic guard: concurrent workers may cross buckets out of
603 // order; only the highest watermark is reported.
604 let last = self.progress_last_emitted.load(Ordering::Relaxed);
605 if new_total > last
606 && self
607 .progress_last_emitted
608 .compare_exchange(last, new_total, Ordering::Relaxed, Ordering::Relaxed)
609 .is_ok()
610 {
611 query_manager.emit_progress(session_id, query_id, new_total);
612 }
613 }
614
615 // ── Resource ownership ──
616
617 pub fn resource_owner(&self) -> &Arc<Mutex<ResourceOwner>> {
618 &self.resource_owner
619 }
620
621 /// Register a cleanup callback.
622 pub fn on_cleanup<F>(&self, f: F)
623 where
624 F: FnOnce() + Send + 'static,
625 {
626 self.resource_owner.lock().add(Box::new(f));
627 }
628
629 /// Release all owned resources.
630 pub fn release_resources(&self) {
631 self.resource_owner.lock().release_all();
632 }
633
634 /// Set the morsel worker pool for this query.
635 ///
636 /// When a shared scheduler is available via
637 /// [`set_shared_scheduler`](Self::set_shared_scheduler), this call is
638 /// ignored — the shared scheduler's pool is used instead. Legacy support:
639 /// creates a per-query pool when no shared scheduler is configured.
640 pub fn set_worker_pool(&self, pool: Option<super::pool::MorselWorkerPool>) {
641 if self.shared_scheduler.lock().is_some() {
642 return;
643 }
644 *self.worker_pool.lock() = pool.map(|p| Arc::new(p) as Arc<dyn TaskScheduler>);
645 }
646
647 /// Set the engine-level shared scheduler for this query.
648 ///
649 /// When set, all parallel execution uses the shared worker pool instead
650 /// of per-query threads. The scheduler's `Arc<dyn TaskScheduler>` is
651 /// injected into `worker_pool` so existing consumer code paths
652 /// (Exchange / Gather operators) continue to work unchanged.
653 ///
654 /// Takes priority over any per-query pool that may have been set.
655 pub fn set_shared_scheduler(&self, scheduler: Option<Arc<super::pool::SharedScheduler>>) {
656 *self.shared_scheduler.lock() = scheduler.clone();
657 if let Some(ref ss) = scheduler {
658 ss.apply_to_runtime(self);
659 }
660 }
661
662 /// Raw injection — set the worker pool from an `Arc<dyn TaskScheduler>`.
663 /// Used internally by [`SharedScheduler::apply_to_runtime`].
664 pub(crate) fn set_shared_scheduler_raw(&self, pool: Option<Arc<dyn TaskScheduler>>) {
665 *self.worker_pool.lock() = pool;
666 }
667
668 /// Return the shared scheduler, if set.
669 pub fn get_shared_scheduler(&self) -> Option<Arc<super::pool::SharedScheduler>> {
670 self.shared_scheduler.lock().clone()
671 }
672
673 /// Return the effective worker pool — either from the shared scheduler,
674 /// or from the per-query pool, or `None` for serial fallback.
675 pub fn effective_worker_pool(&self) -> Option<Arc<dyn TaskScheduler>> {
676 self.worker_pool.lock().clone()
677 }
678
679 /// Set the spill manager for this query execution.
680 pub fn set_spill_manager(&self, manager: Option<Arc<SpillManager>>) {
681 if let Some(ref m) = manager {
682 m.register_cleanup(self);
683 }
684 *self.spill_manager.lock() = manager;
685 }
686
687 /// Access the spill manager.
688 pub fn get_spill_manager(&self) -> Option<Arc<SpillManager>> {
689 self.spill_manager.lock().clone()
690 }
691
692 /// Return a reference to the bumpalo arena, if configured.
693 pub fn arena(&self) -> Option<&Arc<Mutex<Arena>>> {
694 self.arena.as_ref()
695 }
696
697 /// Return the columnar fast-path counters shared with produced chunks.
698 pub fn columnar_stats(&self) -> Arc<ColumnarStats> {
699 Arc::clone(&self.columnar_stats)
700 }
701
702 /// Return the shared columnar layout policy, if injected.
703 pub fn columnar_policy(&self) -> Option<Arc<super::chunk::ColumnarPolicy>> {
704 self.columnar_policy.clone()
705 }
706
707 /// Inject the shared columnar layout policy (owned by the optimizer
708 /// engine; set by the materializer from the query bindings).
709 pub fn set_columnar_policy(&mut self, policy: Option<Arc<super::chunk::ColumnarPolicy>>) {
710 self.columnar_policy = policy;
711 }
712
713 /// Per-query columnar override for this runtime.
714 pub fn columnar_override(&self) -> super::chunk::QueryColumnarOverride {
715 self.columnar_override
716 }
717
718 /// Pin the columnar decision for this query only.
719 pub fn set_columnar_override(&mut self, query_override: super::chunk::QueryColumnarOverride) {
720 self.columnar_override = query_override;
721 }
722
723 /// Merge this query's columnar hit/miss counts into the shared policy.
724 ///
725 /// Called once when the query finishes (materialized, streaming, and
726 /// discard paths) so the adaptive gate learns across queries.
727 pub fn flush_columnar_stats_to_policy(&self) {
728 if let Some(policy) = &self.columnar_policy {
729 let snapshot = ColumnarStatsSnapshot::from_stats(&self.columnar_stats);
730 // B path hits also represent typed benefit; feed them into the
731 // policy together with the A path hits. Wasted builds stay out:
732 // they measure built-but-unconsumed layouts, not hits/misses.
733 let effective_hits = snapshot.columnar_hits + snapshot.columnar_b_path_hits;
734 policy.merge(effective_hits, snapshot.columnar_misses);
735 }
736 }
737
738 /// Reset the bumpalo arena, freeing all temporary allocations.
739 pub fn reset_arena(&self) {
740 if let Some(arena) = &self.arena {
741 arena.lock().reset();
742 }
743 }
744
745 /// Set the per-partition output channel capacity for parallel operators.
746 pub fn set_max_buffered_chunks(&self, chunks: usize) {
747 self.max_buffered_chunks
748 .store(chunks.max(1), Ordering::Relaxed);
749 }
750}
751
752#[cfg(test)]
753mod tests {
754 use super::*;
755
756 #[test]
757 fn test_runtime_default_budget() {
758 let rt = ExecutionRuntime::default_budget();
759 assert!(!rt.is_cancelled());
760 assert_eq!(rt.query_id().query_id, 0);
761 }
762
763 #[test]
764 fn test_cancel_token() {
765 let rt = ExecutionRuntime::default_budget();
766 assert!(!rt.is_cancelled());
767 rt.cancel();
768 assert!(rt.is_cancelled());
769 assert!(rt.ensure_not_cancelled().is_err());
770 }
771
772 #[test]
773 fn test_deadline() {
774 let mut rt = ExecutionRuntime::default_budget();
775 rt.set_deadline(Some(Instant::now()));
776 assert!(rt.is_cancelled());
777 }
778
779 #[test]
780 fn test_profile_add_rows() {
781 let rt = ExecutionRuntime::default_budget();
782 rt.profile_add_rows(10);
783 rt.profile_add_rows(20);
784 assert_eq!(
785 rt.profile()
786 .total_rows
787 .load(std::sync::atomic::Ordering::Relaxed),
788 30
789 );
790 }
791}