1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
use std::sync::Arc;
use parking_lot::Mutex;
use super::super::runtime::{ExecutionRuntime, OperatorProfileKey};
use super::super::spill::SpillManager;
use super::super::state::{
GlobalState, GlobalStateKey, LocalState, LocalStateKey, StateArenaSet, TaskId,
};
use crate::executor::streaming::plan::types::PhysicalOperatorId;
use crate::executor::streaming::slot::SlotLayout;
use linkrs_core::Arena;
/// Explicit operator lifecycle state machine.
///
/// # Transitions
/// - `New` → `Opened`: on successful `open()`
/// - `Opened` → `Exhausted`: when `advance()` returns `None`
/// - `Opened` / `Exhausted` → `Stopped`: when consumer calls `stop()` early
/// - Any → `Failed`: unrecoverable error (set during error cleanup)
/// - Any non-terminal → `Closed`: after `close()` completes (idempotent)
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum OperatorLifecycle {
/// Before `open()` has been called.
New,
/// After successful `open()`; can produce chunks via `advance()`.
Opened,
/// All data has been consumed (`advance()` returned `None`).
Exhausted,
/// Consumer signalled early termination via `stop()`.
Stopped,
/// Unrecoverable error during execution.
Failed,
/// Resources released. Terminal state.
Closed,
}
impl OperatorLifecycle {
/// Whether the operator was opened and not yet stopped/failed/closed.
/// `Exhausted` is included so the close guard works after natural exhaustion.
pub fn is_opened(self) -> bool {
matches!(self, Self::Opened | Self::Exhausted)
}
pub fn is_exhausted(self) -> bool {
matches!(self, Self::Exhausted)
}
pub fn mark_opened(&mut self) {
debug_assert!(
matches!(*self, Self::New),
"mark_opened from {:?} (expected New)",
self
);
*self = Self::Opened;
}
pub fn mark_exhausted(&mut self) {
if matches!(*self, Self::Opened) {
*self = Self::Exhausted;
}
}
pub fn mark_stopped(&mut self) {
if matches!(*self, Self::Opened | Self::Exhausted) {
*self = Self::Stopped;
}
}
pub fn mark_failed(&mut self) {
*self = Self::Failed;
}
pub fn mark_closed(&mut self) {
*self = Self::Closed;
}
}
#[derive(Debug)]
pub struct OperatorBase {
pub plan_node_id: i64,
pub runtime: Option<Arc<ExecutionRuntime>>,
pub lifecycle: OperatorLifecycle,
/// Whether this operator produces global (merged) output.
/// Local operators process one partition at a time.
pub is_global: bool,
/// Local partition that owns this operator. `None` denotes a global or
/// non-partitioned operator.
pub partition_id: Option<usize>,
/// Rows per chunk when this operator produces output.
/// Source operators use this value directly; unary/blocking operators
/// pass through whatever they receive from their child.
pub chunk_size: usize,
/// Unique physical identity used for this operator's mutable runtime
/// state. Production materialization must provide the arena-assigned ID.
pub physical_operator_id: PhysicalOperatorId,
/// Immutable output layout supplied by the physical plan. Operators use
/// this for chunks they construct themselves, including empty-result
/// paths, rather than inferring schema from data rows.
pub output_layout: Arc<SlotLayout>,
/// Set when a `reset()` call degraded to the transitional `close + open`
/// fallback for this operator (see `StreamingExecutor::reset`). Surfaced
/// in EXPLAIN as `reset:fallback` so un-resettable paths stay auditable.
pub reset_used_fallback: bool,
}
impl OperatorBase {
pub fn new(plan_node_id: i64) -> Self {
Self {
plan_node_id,
runtime: None,
lifecycle: OperatorLifecycle::New,
is_global: false,
partition_id: None,
chunk_size: 2048,
physical_operator_id: PhysicalOperatorId(plan_node_id.unsigned_abs() as usize),
output_layout: Arc::new(SlotLayout::new(Vec::new())),
reset_used_fallback: false,
}
}
/// Lock and return the partition-local operator state arena from the runtime.
///
/// Each partition gets its own arena so parallel workers don't contend.
/// Panics if no runtime is attached.
pub fn state_arena(&self) -> parking_lot::MutexGuard<'_, StateArenaSet> {
self.runtime
.as_ref()
.expect("runtime required")
.state_arena_for(self.partition_id)
.lock()
}
/// Override the tree-node identity with the arena-assigned physical
/// operator identity. This is required by production materialization.
pub fn with_physical_operator_id(mut self, operator_id: PhysicalOperatorId) -> Self {
self.physical_operator_id = operator_id;
self
}
/// Attach the output contract layout during physical-plan materialization.
pub fn with_output_layout(mut self, output_layout: Arc<SlotLayout>) -> Self {
self.output_layout = output_layout;
self
}
/// Return the [`GlobalStateKey`] for this operator's slot.
pub fn state_key(&self) -> GlobalStateKey {
GlobalStateKey(self.physical_operator_id, self.partition_id)
}
/// Take the [`GlobalState`] out of the arena (for cleanup).
pub fn take_state(&mut self) -> Option<GlobalState> {
let rt = self.runtime.as_ref()?;
let key = self.state_key();
rt.state_arena_for(self.partition_id)
.lock()
.global
.remove(&key)
}
/// Insert a [`GlobalState`] into this operator's slot in the runtime arena.
///
/// No-op when no runtime is attached (e.g. in unit tests that construct
/// executor trees directly without an [`ExecutionRuntime`]).
pub fn insert_state(&mut self, state: GlobalState) {
let Some(rt) = self.runtime.as_ref() else {
return;
};
let key = self.state_key();
rt.state_arena_for(self.partition_id)
.lock()
.global
.insert(key, state);
}
// ── Local state access (per-task) ──
/// Return the [`LocalStateKey`] for this operator + task.
pub fn local_state_key(&self, task_id: TaskId) -> LocalStateKey {
LocalStateKey(self.physical_operator_id, self.partition_id, task_id)
}
/// Insert a [`LocalState`] into the arena for a given task.
pub fn insert_local_state(&mut self, task_id: TaskId, state: LocalState) {
let Some(rt) = self.runtime.as_ref() else {
return;
};
let key = self.local_state_key(task_id);
rt.state_arena_for(self.partition_id)
.lock()
.local
.insert(key, state);
}
/// Take a [`LocalState`] out of the arena (for cleanup).
pub fn take_local_state(&mut self, task_id: TaskId) -> Option<LocalState> {
let rt = self.runtime.as_ref()?;
let key = self.local_state_key(task_id);
rt.state_arena_for(self.partition_id)
.lock()
.local
.remove(&key)
}
/// Access local state for a given task via a closure.
///
/// The closure receives `Option<&mut LocalState>` scoped within the
/// state arena lock. Returns the closure's result.
pub fn with_local_state<T, F>(&self, task_id: TaskId, f: F) -> Option<T>
where
F: FnOnce(&mut LocalState) -> T,
{
let rt = self.runtime.as_ref()?;
let key = self.local_state_key(task_id);
let mut arena = rt.state_arena_for(self.partition_id).lock();
arena.local.get_mut(&key).map(f)
}
pub fn with_chunk_size(mut self, chunk_size: usize) -> Self {
self.chunk_size = chunk_size;
self
}
pub fn with_runtime(mut self, rt: Option<Arc<ExecutionRuntime>>) -> Self {
self.runtime = rt;
self
}
pub fn with_global(mut self, is_global: bool) -> Self {
self.is_global = is_global;
self
}
pub fn with_partition(mut self, partition_id: usize) -> Self {
self.partition_id = Some(partition_id);
self
}
pub fn profile_key(&self) -> OperatorProfileKey {
OperatorProfileKey::new(self.physical_operator_id, self.partition_id)
}
pub fn ensure_not_cancelled(&self) -> Result<(), linkrs_core::error::QueryError> {
if let Some(rt) = &self.runtime {
rt.ensure_not_cancelled()
} else {
Ok(())
}
}
pub fn record_profile_rows(&self, count: u64) {
if let Some(rt) = &self.runtime {
if let Some(entry) = rt.profile().get_entry(&self.profile_key()) {
entry
.output_rows
.fetch_add(count, std::sync::atomic::Ordering::Relaxed);
entry
.advance_count
.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
}
}
}
/// Mark this operator instance as ran-to-exhaustion in the profile.
///
/// Called when `advance` returns `None`: operators that never exhaust
/// were starved (Limit / early termination) and their partial counts
/// must not train the feedback loop.
pub fn mark_profile_exhausted(&self) {
if let Some(rt) = &self.runtime {
if let Some(entry) = rt.profile().get_entry(&self.profile_key()) {
entry
.exhausted
.store(true, std::sync::atomic::Ordering::Relaxed);
}
}
}
pub fn register_resource<F>(&self, f: F)
where
F: FnOnce() + Send + 'static,
{
if let Some(rt) = &self.runtime {
rt.on_cleanup(f);
}
}
/// Convenience accessor for the spill manager from the runtime.
pub fn spill_manager(&self) -> Option<Arc<SpillManager>> {
self.runtime.as_ref().and_then(|rt| rt.get_spill_manager())
}
/// Return a reference to the bumpalo arena, if configured.
pub fn arena(&self) -> Option<&Arc<Mutex<Arena>>> {
self.runtime.as_ref().and_then(|rt| rt.arena())
}
/// Reset the lifecycle to `New` so a `close + open` sequence can run
/// again on the same operator instance (reset fallback path).
pub fn mark_new(&mut self) {
self.lifecycle = OperatorLifecycle::New;
}
}
#[cfg(test)]
mod tests {
use super::OperatorBase;
use crate::executor::streaming::plan::types::PhysicalOperatorId;
#[test]
fn state_keys_include_physical_operator_and_partition() {
let global = OperatorBase::new(10).with_physical_operator_id(PhysicalOperatorId(7));
let partition_zero = OperatorBase::new(10)
.with_physical_operator_id(PhysicalOperatorId(7))
.with_partition(0);
let partition_one = OperatorBase::new(10)
.with_physical_operator_id(PhysicalOperatorId(7))
.with_partition(1);
assert_ne!(global.state_key(), partition_zero.state_key());
assert_ne!(partition_zero.state_key(), partition_one.state_key());
assert_ne!(
partition_zero.local_state_key(0),
partition_zero.local_state_key(1)
);
}
}