Skip to main content

linkrs_query/executor/streaming/
stream.rs

1use std::sync::Arc;
2
3use super::chunk::DataChunk;
4use super::chunk::LocalChunkCollector;
5use super::engine::StreamingExecutionEngine;
6use super::runtime::ExecutionRuntime;
7use linkrs_core::error::QueryError;
8use linkrs_core::DataSet;
9
10/// Streaming result handle for pull-based chunk consumption.
11///
12/// Created by `StreamingExecutionEngine::into_stream()`. The caller
13/// pulls chunks one at a time via `next_chunk()`, enabling the API
14/// layer to stream results back without materialising the full result
15/// set.  Call `close()` or let the handle drop to clean up resources.
16///
17/// When a full materialised `DataSet` is needed, use `collect()`.
18pub struct ResultStream {
19    engine: Option<StreamingExecutionEngine>,
20    runtime: Arc<ExecutionRuntime>,
21    opened: bool,
22    exhausted: bool,
23    closed: bool,
24    /// Buffered chunks for the partitioned fallback path.
25    buffered: Vec<DataChunk>,
26    buffered_idx: usize,
27}
28
29impl ResultStream {
30    /// Wrap an engine into a streaming result.
31    ///
32    /// The engine must have a root executor registered.  The stream
33    /// takes ownership of the engine and calls `open()` on the first
34    /// `next_chunk()` call (or immediately if `open_now` is true).
35    pub fn new(engine: StreamingExecutionEngine, runtime: Arc<ExecutionRuntime>) -> Self {
36        Self {
37            engine: Some(engine),
38            runtime,
39            opened: false,
40            exhausted: false,
41            closed: false,
42            buffered: Vec::new(),
43            buffered_idx: 0,
44        }
45    }
46
47    /// Create a stream from pre-collected chunks (used for partitioned fallback).
48    ///
49    /// The chunks are served one at a time via `next_chunk()`. The engine
50    /// is retained for proper cleanup via `close_inner()`.
51    pub fn from_collected(
52        chunks: Vec<DataChunk>,
53        engine: StreamingExecutionEngine,
54        runtime: Arc<ExecutionRuntime>,
55    ) -> Self {
56        Self {
57            engine: Some(engine),
58            runtime,
59            opened: true,
60            exhausted: false,
61            closed: false,
62            buffered: chunks,
63            buffered_idx: 0,
64        }
65    }
66
67    /// Ensure the root executor is open.
68    fn ensure_opened(&mut self) -> Result<(), QueryError> {
69        if !self.opened {
70            if let Some(ref mut engine) = self.engine {
71                if let Err(error) = engine.open_root() {
72                    return self.return_execution_error(error);
73                }
74            }
75            self.opened = true;
76        }
77        Ok(())
78    }
79
80    /// Pull the next chunk of results.
81    ///
82    /// Returns `Ok(None)` when the stream is exhausted.
83    /// The runtime's cancel token is checked on each call.
84    pub fn next_chunk(&mut self) -> Result<Option<DataChunk>, QueryError> {
85        if let Err(error) = self.runtime.ensure_not_cancelled() {
86            return self.return_execution_error(error);
87        }
88
89        if self.exhausted {
90            return Ok(None);
91        }
92
93        // Serve from buffered chunks first (partitioned fallback).
94        // Move the whole chunk out instead of cloning it: schema/layout
95        // are Arc-shared, and the single move preserves the selection
96        // vector, multiplicity, column caches, reservation, and stats.
97        // A valid empty chunk is left in the slot (slots are consumed once
98        // via the monotonically advancing `buffered_idx`).
99        if self.buffered_idx < self.buffered.len() {
100            let layout = self.buffered[self.buffered_idx].get_layout();
101            let empty = DataChunk::new_with_layout(Vec::new(), layout);
102            let chunk = std::mem::replace(&mut self.buffered[self.buffered_idx], empty);
103            self.buffered_idx += 1;
104            return Ok(Some(chunk));
105        }
106
107        self.ensure_opened()?;
108
109        if let Some(ref mut engine) = self.engine {
110            let next = match engine.next_chunk_from_root() {
111                Ok(next) => next,
112                Err(error) => return self.return_execution_error(error),
113            };
114            match next {
115                Some(chunk) => Ok(Some(chunk)),
116                None => {
117                    self.exhausted = true;
118                    self.close_inner()?;
119                    Ok(None)
120                }
121            }
122        } else {
123            Ok(None)
124        }
125    }
126
127    /// Close the stream and release resources.
128    pub fn close(&mut self) -> Result<(), QueryError> {
129        if self.closed {
130            return Ok(());
131        }
132        self.exhausted = true;
133        self.close_inner()
134    }
135
136    fn close_inner(&mut self) -> Result<(), QueryError> {
137        if self.closed {
138            return Ok(());
139        }
140        let result = if let Some(ref mut engine) = self.engine {
141            let stop_error = engine.stop_root().err();
142            let close_error = engine.close_root().err();
143            match (stop_error, close_error) {
144                (Some(stop_err), Some(close_err)) => {
145                    log::warn!(
146                        "Both stop and close failed during stream teardown; \
147                         stop error: {stop_err}; close error: {close_err}"
148                    );
149                    Err(stop_err)
150                }
151                (Some(stop_err), None) | (None, Some(stop_err)) => Err(stop_err),
152                (None, None) => Ok(()),
153            }
154        } else {
155            Ok(())
156        };
157        self.runtime.profile_end();
158        self.runtime.release_resources();
159        self.runtime.reset_arena();
160        self.closed = true;
161        result
162    }
163
164    /// Preserve an execution error while guaranteeing immediate, idempotent
165    /// teardown. A cleanup failure is logged because the caller must receive
166    /// the original execution failure.
167    fn return_execution_error<T>(&mut self, error: QueryError) -> Result<T, QueryError> {
168        if let Err(close_error) = self.close_inner() {
169            log::warn!(
170                "Failed to clean up streaming result after execution error: {}",
171                close_error
172            );
173        }
174        Err(error)
175    }
176
177    /// Consume the stream and materialise all remaining chunks into a `DataSet`.
178    ///
179    /// Routes through `LocalChunkCollector`, the single terminal collection
180    /// logic shared with `convert_chunks_to_dataset`.
181    pub fn collect(mut self) -> Result<DataSet, QueryError> {
182        let spill_manager = self.runtime.get_spill_manager();
183        let mut collector: Option<LocalChunkCollector> = None;
184
185        while let Some(mut chunk) = self.next_chunk()? {
186            let collector = match collector {
187                Some(ref mut c) => c,
188                None => {
189                    let mut fresh = LocalChunkCollector::new(chunk.col_names());
190                    if let Some(ref manager) = spill_manager {
191                        fresh.attach_spill_manager(manager.clone());
192                    }
193                    collector.insert(fresh)
194                }
195            };
196            collector.push_chunk(&mut chunk)?;
197        }
198
199        let Some(mut collector) = collector else {
200            return Ok(DataSet::with_columns(Vec::new()));
201        };
202        collector.finish_spill()?;
203        let stats = self.runtime.columnar_stats();
204        let spilled_rows = collector.spilled_rows();
205        let spilled_bytes = collector.spilled_bytes();
206        let spilled_runs = collector.spilled_run_count() as u64;
207        let (all_rows, col_names) = collector.into_rows()?;
208        if spilled_rows > 0 {
209            stats.record_spill_with_runs(spilled_rows, spilled_bytes, spilled_runs);
210        }
211        Ok(DataSet::from_rows(all_rows, col_names))
212    }
213}
214
215impl Drop for ResultStream {
216    fn drop(&mut self) {
217        if !self.closed {
218            let _ = self.close_inner();
219        }
220    }
221}
222
223#[cfg(test)]
224mod tests {
225    use std::sync::atomic::{AtomicBool, Ordering};
226
227    use super::*;
228    use crate::executor::base::MemoryBudget;
229    use crate::executor::streaming::executor::StreamingExecutor;
230    use crate::executor::streaming::operators::base::OperatorBase;
231    use crate::executor::streaming::operators::source_operator::SourceOperator;
232    use crate::executor::streaming::operators::source_operator::SourceOperatorKind;
233    use crate::executor::streaming::runtime::QueryIdentity;
234    use crate::executor::streaming::slot::SlotLayout;
235    use linkrs_core::Value;
236
237    #[test]
238    fn cancellation_error_releases_resources_immediately() {
239        let runtime = Arc::new(ExecutionRuntime::new(
240            QueryIdentity::default(),
241            MemoryBudget::default_budget(),
242            None,
243            crate::executor::base::SearchContext::default(),
244        ));
245        let released = Arc::new(AtomicBool::new(false));
246        let released_for_cleanup = released.clone();
247        runtime.on_cleanup(move || {
248            released_for_cleanup.store(true, Ordering::Relaxed);
249        });
250
251        let mut engine = StreamingExecutionEngine::new();
252        engine.register_executor(
253            0,
254            StreamingExecutor::Source(
255                OperatorBase::new(1),
256                SourceOperator::new(
257                    SourceOperatorKind::ScanVertices {
258                        buffer: vec![vec![Value::BigInt(1)]],
259                        current_index: 0,
260                        col_names: vec!["id".to_string()],
261                    },
262                    Arc::new(SlotLayout::from_names(&["id".to_string()])),
263                ),
264            ),
265        );
266        engine.set_runtime(runtime.clone());
267        let mut stream = engine.into_stream().expect("create stream");
268
269        runtime.cancel();
270        assert!(stream.next_chunk().is_err());
271        assert!(released.load(Ordering::Relaxed));
272        assert!(stream.close().is_ok());
273    }
274}