linkrs_query/executor/streaming/
stream_result.rs1use std::sync::{Arc, Weak};
2
3use parking_lot::Mutex;
4
5use super::chunk::{ColumnInfo, DataChunk, LocalChunkCollector, Schema};
6use super::runtime::ExecutionRuntime;
7use super::stream::ResultStream;
8use crate::executor::base::ExecutionResult;
9use linkrs_core::error::QueryError;
10use linkrs_core::DataSet;
11
12use std::sync::atomic::{AtomicBool, Ordering};
17
18type DropCallback = Arc<Mutex<Option<Box<dyn FnOnce() + Send>>>>;
19type TransactionCallback = Box<dyn FnOnce() -> Result<(), String> + Send>;
20type TransactionFinalizer = Arc<Mutex<Option<(TransactionCallback, TransactionCallback)>>>;
21
22#[derive(Clone)]
23pub struct StreamingQueryResult {
24 inner: Arc<Mutex<StreamState>>,
25 runtime: Arc<ExecutionRuntime>,
26 on_drop: DropCallback,
27 transaction_finalizer: TransactionFinalizer,
28 dropped: Arc<AtomicBool>,
29}
30
31impl Drop for StreamingQueryResult {
32 fn drop(&mut self) {
33 if Arc::strong_count(&self.inner) != 1 {
37 return;
38 }
39 if self.dropped.swap(true, Ordering::Relaxed) {
40 return;
41 }
42 if let Err(error) = self.finalize_transaction() {
43 log::error!("Streaming transaction finalization failed: {}", error);
44 }
45 if let Some(f) = self.on_drop.lock().take() {
46 f();
47 }
48 }
49}
50
51enum StreamState {
52 Streaming(ResultStream, Option<Vec<String>>),
57 Materialized {
59 rows: Vec<Vec<linkrs_core::Value>>,
60 col_names: Vec<String>,
61 exhausted: bool,
62 },
63 Exhausted,
65 Closed,
67}
68
69impl StreamingQueryResult {
70 pub fn new(stream: ResultStream, runtime: Arc<ExecutionRuntime>) -> Self {
72 Self::new_with_schema(stream, runtime, Vec::new())
73 }
74
75 pub fn new_with_schema(
78 stream: ResultStream,
79 runtime: Arc<ExecutionRuntime>,
80 col_names: Vec<String>,
81 ) -> Self {
82 Self {
83 inner: Arc::new(Mutex::new(StreamState::Streaming(stream, Some(col_names)))),
84 runtime,
85 on_drop: Arc::new(Mutex::new(None)),
86 transaction_finalizer: Arc::new(Mutex::new(None)),
87 dropped: Arc::new(AtomicBool::new(false)),
88 }
89 }
90
91 pub fn from_execution_result(result: ExecutionResult) -> Self {
96 match result {
97 ExecutionResult::DataSet { data: ds, .. } => {
98 let exhausted = ds.rows.is_empty();
99 let runtime = Arc::new(ExecutionRuntime::default_budget());
100 Self {
101 inner: Arc::new(Mutex::new(StreamState::Materialized {
102 rows: ds.rows,
103 col_names: ds.col_names,
104 exhausted,
105 })),
106 runtime,
107 on_drop: Arc::new(Mutex::new(None)),
108 transaction_finalizer: Arc::new(Mutex::new(None)),
109 dropped: Arc::new(AtomicBool::new(false)),
110 }
111 }
112 ExecutionResult::Success | ExecutionResult::Empty => {
113 let runtime = Arc::new(ExecutionRuntime::default_budget());
114 Self {
115 inner: Arc::new(Mutex::new(StreamState::Exhausted)),
116 runtime,
117 on_drop: Arc::new(Mutex::new(None)),
118 transaction_finalizer: Arc::new(Mutex::new(None)),
119 dropped: Arc::new(AtomicBool::new(false)),
120 }
121 }
122 ExecutionResult::SpaceSwitched(summary) => {
123 let row = vec![
124 linkrs_core::Value::string(summary.name.clone()),
125 linkrs_core::Value::BigInt(summary.id as i64),
126 ];
127 let col_names = vec!["space_name".to_string(), "space_id".to_string()];
128 let runtime = Arc::new(ExecutionRuntime::default_budget());
129 Self {
130 inner: Arc::new(Mutex::new(StreamState::Materialized {
131 rows: vec![row],
132 col_names,
133 exhausted: false,
134 })),
135 runtime,
136 on_drop: Arc::new(Mutex::new(None)),
137 transaction_finalizer: Arc::new(Mutex::new(None)),
138 dropped: Arc::new(AtomicBool::new(false)),
139 }
140 }
141 ExecutionResult::Error(msg) => {
142 let col_names = vec!["error".to_string()];
143 let runtime = Arc::new(ExecutionRuntime::default_budget());
144 Self {
145 inner: Arc::new(Mutex::new(StreamState::Materialized {
146 rows: vec![vec![linkrs_core::Value::string(msg)]],
147 col_names,
148 exhausted: false,
149 })),
150 runtime,
151 on_drop: Arc::new(Mutex::new(None)),
152 transaction_finalizer: Arc::new(Mutex::new(None)),
153 dropped: Arc::new(AtomicBool::new(false)),
154 }
155 }
156 ExecutionResult::ConfigUpdate {
157 module,
158 name,
159 value,
160 } => {
161 let row = vec![
162 module
163 .map(linkrs_core::Value::string)
164 .unwrap_or(linkrs_core::Value::Null(linkrs_core::NullType::Null)),
165 linkrs_core::Value::string(name),
166 value,
167 ];
168 let col_names = vec![
169 "module".to_string(),
170 "config_name".to_string(),
171 "config_value".to_string(),
172 ];
173 let runtime = Arc::new(ExecutionRuntime::default_budget());
174 Self {
175 inner: Arc::new(Mutex::new(StreamState::Materialized {
176 rows: vec![row],
177 col_names,
178 exhausted: false,
179 })),
180 runtime,
181 on_drop: Arc::new(Mutex::new(None)),
182 transaction_finalizer: Arc::new(Mutex::new(None)),
183 dropped: Arc::new(AtomicBool::new(false)),
184 }
185 }
186 ExecutionResult::ShowConfigs { module } => {
187 let row = vec![module
188 .map(linkrs_core::Value::string)
189 .unwrap_or(linkrs_core::Value::Null(linkrs_core::NullType::Null))];
190 let col_names = vec!["module".to_string()];
191 let runtime = Arc::new(ExecutionRuntime::default_budget());
192 Self {
193 inner: Arc::new(Mutex::new(StreamState::Materialized {
194 rows: vec![row],
195 col_names,
196 exhausted: false,
197 })),
198 runtime,
199 on_drop: Arc::new(Mutex::new(None)),
200 transaction_finalizer: Arc::new(Mutex::new(None)),
201 dropped: Arc::new(AtomicBool::new(false)),
202 }
203 }
204 }
205 }
206
207 pub fn next_chunk(&self) -> Result<Option<DataChunk>, QueryError> {
212 let mut guard = self.inner.lock();
213 match &mut *guard {
214 StreamState::Streaming(ref mut stream, ref mut cached) => {
215 let result = match stream.next_chunk() {
216 Ok(result) => result,
217 Err(error) => {
218 *guard = StreamState::Closed;
219 drop(guard);
220 self.finalize_transaction().map_err(QueryError::execution)?;
221 return Err(error);
222 }
223 };
224 if let Some(ref chunk) = result {
225 if cached.is_none() {
226 *cached = Some(chunk.col_names());
227 }
228 }
229 let exhausted = result.is_none();
230 if exhausted {
231 *guard = StreamState::Exhausted;
232 }
233 drop(guard);
234 if exhausted {
235 self.finalize_transaction().map_err(QueryError::execution)?;
236 }
237 Ok(result)
238 }
239 StreamState::Materialized {
240 rows,
241 col_names,
242 exhausted,
243 } => {
244 if *exhausted {
245 *guard = StreamState::Exhausted;
246 drop(guard);
247 self.finalize_transaction().map_err(QueryError::execution)?;
248 return Ok(None);
249 }
250 *exhausted = true;
251 let names = std::mem::take(col_names);
252 let data = std::mem::take(rows);
253 let columns: Vec<ColumnInfo> = names
254 .iter()
255 .map(|n| ColumnInfo {
256 name: n.clone(),
257 data_type: String::new(),
258 })
259 .collect();
260 let chunk = DataChunk::new(data, Arc::new(Schema::new(columns)));
261 Ok(Some(chunk))
262 }
263 StreamState::Exhausted | StreamState::Closed => Ok(None),
264 }
265 }
266
267 pub fn cancel(&self) {
269 self.runtime.cancel();
270 if let Err(error) = self.abort_transaction() {
271 log::error!("Streaming transaction cancellation failed: {}", error);
272 }
273 }
274
275 pub fn is_cancelled(&self) -> bool {
277 self.runtime.is_cancelled()
278 }
279
280 pub fn close(&self) -> Result<(), QueryError> {
282 let mut guard = self.inner.lock();
283 let result = match &mut *guard {
284 StreamState::Streaming(ref mut stream, _) => stream.close(),
285 _ => Ok(()),
286 };
287 *guard = StreamState::Closed;
288 drop(guard);
289 result?;
290 self.abort_transaction().map_err(QueryError::execution)
291 }
292
293 pub fn collect(&self) -> Result<DataSet, QueryError> {
298 let spill_manager = self.runtime.get_spill_manager();
299 let mut collector: Option<LocalChunkCollector> = None;
300
301 while let Some(mut chunk) = self.next_chunk()? {
302 let collector = match collector {
303 Some(ref mut c) => c,
304 None => {
305 let mut fresh = LocalChunkCollector::new(chunk.col_names());
306 if let Some(ref manager) = spill_manager {
307 fresh.attach_spill_manager(manager.clone());
308 }
309 collector.insert(fresh)
310 }
311 };
312 collector.push_chunk(&mut chunk)?;
313 }
314
315 let Some(mut collector) = collector else {
316 return Ok(DataSet::with_columns(Vec::new()));
317 };
318 collector.finish_spill()?;
319 let stats = self.runtime.columnar_stats();
320 let spilled_rows = collector.spilled_rows();
321 let spilled_bytes = collector.spilled_bytes();
322 let spilled_runs = collector.spilled_run_count() as u64;
323 let (all_rows, col_names) = collector.into_rows()?;
324 if spilled_rows > 0 {
325 stats.record_spill_with_runs(spilled_rows, spilled_bytes, spilled_runs);
326 }
327 Ok(DataSet::from_rows(all_rows, col_names))
328 }
329
330 pub fn column_names(&self) -> Option<Vec<String>> {
336 let guard = self.inner.lock();
337 match &*guard {
338 StreamState::Streaming(_, cached) => cached.clone(),
339 StreamState::Materialized { col_names, .. } => Some(col_names.clone()),
340 StreamState::Exhausted | StreamState::Closed => None,
341 }
342 }
343
344 pub fn runtime(&self) -> &ExecutionRuntime {
346 &self.runtime
347 }
348
349 pub fn runtime_downgrade(&self) -> Weak<ExecutionRuntime> {
351 Arc::downgrade(&self.runtime)
352 }
353
354 pub fn set_on_drop(&self, f: Box<dyn FnOnce() + Send>) {
359 let previous = self.on_drop.lock().take();
360 *self.on_drop.lock() = Some(Box::new(move || {
361 if let Some(previous) = previous {
362 previous();
363 }
364 f();
365 }));
366 }
367
368 pub fn set_transaction_finalizer(
376 &self,
377 commit: Box<dyn FnOnce() + Send>,
378 abort: Box<dyn FnOnce() + Send>,
379 ) {
380 self.set_transaction_finalizer_with_result(
381 Box::new(move || {
382 commit();
383 Ok(())
384 }),
385 Box::new(move || {
386 abort();
387 Ok(())
388 }),
389 );
390 }
391
392 pub fn set_transaction_finalizer_with_result(
396 &self,
397 commit: TransactionCallback,
398 abort: TransactionCallback,
399 ) {
400 *self.transaction_finalizer.lock() = Some((commit, abort));
401 }
402
403 fn finalize_transaction(&self) -> Result<(), String> {
404 let is_exhausted = matches!(*self.inner.lock(), StreamState::Exhausted);
405 let Some((commit, abort)) = self.transaction_finalizer.lock().take() else {
406 return Ok(());
407 };
408 if is_exhausted {
409 commit()
410 } else {
411 abort()
412 }
413 }
414
415 fn abort_transaction(&self) -> Result<(), String> {
416 let Some((_, abort)) = self.transaction_finalizer.lock().take() else {
417 return Ok(());
418 };
419 abort()
420 }
421
422 pub fn set_fallback_column_names(&self, names: Vec<String>) {
429 let mut guard = self.inner.lock();
430 if let StreamState::Streaming(_, ref mut cached) = &mut *guard {
431 if cached.is_none() {
432 *cached = Some(names);
433 }
434 }
435 }
436}
437
438#[cfg(test)]
439mod tests {
440 use super::*;
441 use crate::executor::base::MemoryBudget;
442 use crate::executor::streaming::engine::StreamingExecutionEngine;
443 use crate::executor::streaming::executor::StreamingExecutor;
444 use crate::executor::streaming::operators::base::OperatorBase;
445 use crate::executor::streaming::operators::source_operator::SourceOperator;
446 use crate::executor::streaming::operators::source_operator::SourceOperatorKind;
447 use crate::executor::streaming::runtime::QueryIdentity;
448 use crate::executor::streaming::slot::SlotLayout;
449 use linkrs_core::Value;
450
451 fn create_test_stream(count: usize) -> StreamingQueryResult {
452 let mut engine = StreamingExecutionEngine::new();
453 let runtime = Arc::new(ExecutionRuntime::new(
454 QueryIdentity::default(),
455 MemoryBudget::default_budget(),
456 None,
457 crate::executor::base::SearchContext::default(),
458 ));
459 engine.set_runtime(runtime.clone());
460
461 let buffer: Vec<Vec<Value>> = (0..count).map(|i| vec![Value::BigInt(i as i64)]).collect();
462
463 let scan = StreamingExecutor::Source(
464 OperatorBase::new(0),
465 SourceOperator::new(
466 SourceOperatorKind::ScanVertices {
467 buffer,
468 current_index: 0,
469 col_names: vec!["id".to_string()],
470 },
471 Arc::new(SlotLayout::from_names(&["id".to_string()])),
472 ),
473 );
474 engine.register_executor(0, scan);
475 let stream = engine.into_stream().unwrap();
476 StreamingQueryResult::new(stream, runtime)
477 }
478
479 #[test]
480 fn test_next_chunk() {
481 let result = create_test_stream(10);
482 let chunk = result.next_chunk().unwrap();
483 assert!(chunk.is_some());
484 assert_eq!(chunk.unwrap().len(), 10);
485 let done = result.next_chunk().unwrap();
486 assert!(done.is_none());
487 }
488
489 #[test]
490 fn test_cancel() {
491 let result = create_test_stream(100);
492 result.cancel();
493 assert!(result.is_cancelled());
494 assert!(result.next_chunk().is_err());
495 }
496
497 #[test]
498 fn test_collect() {
499 let result = create_test_stream(25);
500 let ds = result.collect().unwrap();
501 assert_eq!(ds.row_count(), 25);
502 }
503
504 #[test]
505 fn test_clone_shared_stream() {
506 let result = create_test_stream(10);
507 let result2 = result.clone();
508 assert!(result.next_chunk().unwrap().is_some());
511 assert!(result2.next_chunk().unwrap().is_none());
513 }
514
515 #[test]
516 fn test_from_execution_result_dataset() {
517 let ds = DataSet::from_rows(
518 vec![vec![Value::Int(1)], vec![Value::Int(2)]],
519 vec!["id".to_string()],
520 );
521 let result =
522 StreamingQueryResult::from_execution_result(ExecutionResult::DataSet { data: ds });
523 let chunk = result.next_chunk().unwrap().unwrap();
524 assert_eq!(chunk.len(), 2);
525 assert_eq!(chunk.col_names(), vec!["id"]);
526 assert!(result.next_chunk().unwrap().is_none());
527 }
528
529 #[test]
530 fn test_from_execution_result_empty() {
531 let result = StreamingQueryResult::from_execution_result(ExecutionResult::Empty);
532 assert!(result.next_chunk().unwrap().is_none());
533 }
534
535 #[test]
536 fn test_from_execution_result_error() {
537 let result =
538 StreamingQueryResult::from_execution_result(ExecutionResult::Error("oops".to_string()));
539 let chunk = result.next_chunk().unwrap().unwrap();
540 assert!(!chunk.is_empty());
541 }
542
543 #[test]
546 fn test_on_drop_callback_fires_on_drop() {
547 let result = create_test_stream(5);
548 let fired = Arc::new(AtomicBool::new(false));
549 let fired_clone = fired.clone();
550 result.set_on_drop(Box::new(move || {
551 fired_clone.store(true, Ordering::Relaxed);
552 }));
553 drop(result);
554 assert!(
555 fired.load(Ordering::Relaxed),
556 "on_drop must fire when handle is dropped"
557 );
558 }
559
560 #[test]
561 fn test_on_drop_waits_for_the_last_clone() {
562 let result = create_test_stream(5);
563 let call_count = Arc::new(AtomicBool::new(false));
564 let count_clone = call_count.clone();
565 result.set_on_drop(Box::new(move || {
566 count_clone.store(true, Ordering::Relaxed);
567 }));
568
569 let r2 = result.clone();
570 let r3 = r2.clone();
571
572 drop(r2);
574 assert!(
575 !call_count.load(Ordering::Relaxed),
576 "on_drop must wait for the last handle"
577 );
578
579 drop(r3);
580 assert!(
581 !call_count.load(Ordering::Relaxed),
582 "on_drop must wait for the last handle"
583 );
584
585 drop(result);
586 assert!(
587 call_count.load(Ordering::Relaxed),
588 "on_drop must fire when the final handle is dropped"
589 );
590 }
591
592 #[test]
593 fn test_on_drop_not_called_when_not_set() {
594 let result = create_test_stream(5);
595 drop(result);
597 }
598
599 #[test]
600 fn test_cancel_after_partial_consumption() {
601 let result = create_test_stream(50);
602 let chunk = result.next_chunk().unwrap();
603 assert!(chunk.is_some());
604 result.cancel();
606 assert!(result.is_cancelled());
607 assert!(result.next_chunk().is_err());
608 }
609
610 #[test]
611 fn test_double_cancel_is_safe() {
612 let result = create_test_stream(10);
613 result.cancel();
614 result.cancel(); assert!(result.is_cancelled());
616 }
617
618 #[test]
619 fn test_cancel_with_zero_rows_stream() {
620 let result = create_test_stream(0);
621 result.cancel();
622 assert!(result.is_cancelled());
623 assert!(result.next_chunk().is_err());
624 }
625
626 #[test]
627 fn test_from_execution_result_success_has_exhausted_stream() {
628 let result = StreamingQueryResult::from_execution_result(ExecutionResult::Success);
629 assert!(result.next_chunk().unwrap().is_none());
630 }
631
632 #[test]
633 fn test_drop_without_consuming_does_not_leak() {
634 let result = create_test_stream(10);
637 drop(result);
638 }
639}