Skip to main content

lance_datafusion/
spill.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: Copyright The Lance Authors
3
4use std::{
5    io::{BufReader, BufWriter},
6    path::PathBuf,
7    sync::{Arc, Mutex},
8};
9
10use arrow::ipc::{reader::StreamReader, writer::StreamWriter};
11use arrow_array::RecordBatch;
12use arrow_schema::{ArrowError, Schema, SchemaRef};
13use datafusion::{
14    catalog::{TableProvider, streaming::StreamingTable},
15    execution::{SendableRecordBatchStream, TaskContext},
16    physical_plan::{stream::RecordBatchStreamAdapter, streaming::PartitionStream},
17};
18use datafusion_common::DataFusionError;
19use futures::StreamExt;
20use lance_arrow::memory::MemoryAccumulator;
21use lance_core::error::LanceOptionExt;
22use lance_core::utils::tempfile::TempDir;
23
24/// Start a spill of Arrow data to a file that can be read later multiple times.
25///
26/// Up to `memory_limit` bytes of data can be buffered in memory before a spill
27/// is created. If the memory limit is never reached before [`SpillSender::finish()`]
28/// is called, then the data will simply be kept in memory and no spill will be
29/// created.
30///
31/// `path` is the path to the file that may be created. It should not already
32/// exist. It is the responsibility of the caller to delete the file after it is
33/// no longer needed.
34///
35/// The [`SpillSender`] allows you to write batches to the spill.
36///
37/// The [`SpillReceiver`] can open a [`SendableRecordBatchStream`] that reads
38/// batches from the spill. This can be opened before, during, or after batches
39/// have been written to the spill.
40///
41/// Once [`SpillSender`] is dropped, the temporary file is deleted. This will
42/// cause the [`SpillReceiver`] to return an error if it is still open.
43pub fn create_replay_spill(
44    path: std::path::PathBuf,
45    schema: Arc<Schema>,
46    memory_limit: usize,
47) -> (SpillSender, SpillReceiver) {
48    let initial_status = WriteStatus::default();
49    let (status_sender, status_receiver) = tokio::sync::watch::channel(initial_status);
50    let sender = SpillSender {
51        memory_limit,
52        path: path.clone(),
53        schema: schema.clone(),
54        state: SpillState::default(),
55        status_sender,
56    };
57
58    let receiver = SpillReceiver {
59        status_receiver,
60        path,
61        schema,
62    };
63
64    (sender, receiver)
65}
66
67/// Wrap a one-shot [`SendableRecordBatchStream`] in a re-scannable [`TableProvider`].
68///
69/// The source is drained in the background into a replayable spill. Two properties
70/// keep this cheap for the common case:
71///
72/// - **Memory-first.** Up to `memory_limit` bytes are buffered in memory; the spill
73///   only touches disk once that budget is exceeded. A source that fits under the
74///   limit never hits the filesystem.
75/// - **Streaming replay.** A scan can start consuming batches as soon as they land,
76///   before the source has finished draining — the first reader is not blocked
77///   waiting for the whole source to buffer.
78///
79/// Each scan of the returned provider replays the full source, which is what makes a
80/// one-shot stream usable in the write retry loop.
81///
82/// The provider reports no statistics — the source size is not known until it has
83/// been fully drained — so callers that need source statistics (e.g. to drive join
84/// ordering) should prefer a materialized or file-backed provider instead.
85///
86/// # Examples
87///
88/// ```
89/// # use std::sync::Arc;
90/// # use arrow_array::{Int32Array, RecordBatch};
91/// # use arrow_schema::{DataType, Field, Schema};
92/// # use datafusion::execution::SendableRecordBatchStream;
93/// # use datafusion::physical_plan::stream::RecordBatchStreamAdapter;
94/// # use futures::TryStreamExt;
95/// # use lance_datafusion::exec::provider_to_stream;
96/// # use lance_datafusion::spill::spilling_table_provider;
97/// # #[tokio::main]
98/// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
99/// let schema = Arc::new(Schema::new(vec![Field::new("a", DataType::Int32, false)]));
100/// let batch =
101///     RecordBatch::try_new(schema.clone(), vec![Arc::new(Int32Array::from(vec![1, 2, 3]))])?;
102/// // A one-shot stream can only be consumed once.
103/// let source: SendableRecordBatchStream = Box::pin(RecordBatchStreamAdapter::new(
104///     schema.clone(),
105///     futures::stream::iter(vec![Ok(batch)]),
106/// ));
107///
108/// // Wrapping it makes it re-scannable: each scan replays the full source.
109/// let provider = spilling_table_provider(source, 100 * 1024 * 1024).await?;
110/// let first: Vec<RecordBatch> = provider_to_stream(provider.clone()).await?.try_collect().await?;
111/// let second: Vec<RecordBatch> = provider_to_stream(provider).await?.try_collect().await?;
112/// assert_eq!(first.iter().map(|b| b.num_rows()).sum::<usize>(), 3);
113/// assert_eq!(second.iter().map(|b| b.num_rows()).sum::<usize>(), 3);
114/// # Ok(())
115/// # }
116/// ```
117pub async fn spilling_table_provider(
118    mut source: SendableRecordBatchStream,
119    memory_limit: usize,
120) -> Result<Arc<dyn TableProvider>, DataFusionError> {
121    let schema = source.schema();
122    let tmp_dir = tokio::task::spawn_blocking(TempDir::try_new)
123        .await
124        .map_err(|e| DataFusionError::Execution(format!("Failed to spawn temp dir task: {e}")))?
125        .map_err(|e| DataFusionError::Execution(format!("Failed to create temp dir: {e}")))?;
126    let tmp_path = tmp_dir.std_path().join("spill.arrows");
127    let (mut sender, receiver) = create_replay_spill(tmp_path, schema.clone(), memory_limit);
128
129    // Drain the one-shot source into the spill once, in the background. The spill
130    // tees to memory/disk so the first reader can consume batches as they arrive
131    // while later readers replay the complete source.
132    let drain_handle = tokio::task::spawn(async move {
133        let mut errored = false;
134        while let Some(res) = source.next().await {
135            match res {
136                Ok(batch) => {
137                    if let Err(e) = sender.write(batch).await {
138                        sender.send_error(e);
139                        errored = true;
140                        break;
141                    }
142                }
143                Err(e) => {
144                    sender.send_error(e);
145                    errored = true;
146                    break;
147                }
148            }
149        }
150        // Only finish on a clean drain. Calling finish() after an error would
151        // overwrite the original (replayable) error with a generic one, losing
152        // the source error's type (e.g. an external error from user code).
153        if !errored && let Err(err) = sender.finish().await {
154            sender.send_error(err);
155        }
156        sender
157    });
158
159    let partition = Arc::new(SpillPartition {
160        schema: schema.clone(),
161        receiver,
162        _tmp_dir: Arc::new(tmp_dir),
163        _drain_handle: Arc::new(drain_handle),
164    });
165    Ok(Arc::new(StreamingTable::try_new(schema, vec![partition])?))
166}
167
168/// A [`PartitionStream`] backed by a replayable spill.
169///
170/// Each call to [`PartitionStream::execute`] opens a fresh stream over the spill,
171/// so the partition can be scanned repeatedly. The spill file and the background
172/// task draining the source are kept alive for as long as this partition exists.
173struct SpillPartition {
174    schema: SchemaRef,
175    receiver: SpillReceiver,
176    // The spilled data lives in this temp dir; dropping it deletes the spill file.
177    _tmp_dir: Arc<TempDir>,
178    // Keeps the background drain task (which owns the `SpillSender`) alive. The
179    // `SpillSender` must outlive the readers or they error out, so we hold the
180    // handle rather than detaching it.
181    _drain_handle: Arc<tokio::task::JoinHandle<SpillSender>>,
182}
183
184impl std::fmt::Debug for SpillPartition {
185    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
186        f.debug_struct("SpillPartition")
187            .field("schema", &self.schema)
188            .finish()
189    }
190}
191
192impl PartitionStream for SpillPartition {
193    fn schema(&self) -> &SchemaRef {
194        &self.schema
195    }
196
197    fn execute(&self, _ctx: Arc<TaskContext>) -> SendableRecordBatchStream {
198        self.receiver.read()
199    }
200}
201
202#[derive(Clone)]
203pub struct SpillReceiver {
204    status_receiver: tokio::sync::watch::Receiver<WriteStatus>,
205    path: PathBuf,
206    schema: Arc<Schema>,
207}
208
209impl SpillReceiver {
210    /// Returns a stream of batches from the spill. The stream will emit
211    /// batches as they are written to the spill. If the spill has already
212    /// been finished, the stream will emit all batches in the spill.
213    ///
214    /// The stream will not complete until [`SpillSender::finish()`] is called.
215    ///
216    /// If the spill has been dropped, an error will be returned.
217    pub fn read(&self) -> SendableRecordBatchStream {
218        let rx = self.status_receiver.clone();
219        let reader = SpillReader::new(rx, self.path.clone());
220
221        let stream = futures::stream::try_unfold(reader, move |mut reader| async move {
222            match reader.read().await {
223                Ok(None) => Ok(None),
224                Ok(Some(batch)) => Ok(Some((batch, reader))),
225                Err(err) => Err(err),
226            }
227        });
228
229        Box::pin(RecordBatchStreamAdapter::new(self.schema.clone(), stream))
230    }
231}
232
233struct SpillReader {
234    pub batches_read: usize,
235    receiver: tokio::sync::watch::Receiver<WriteStatus>,
236    state: SpillReaderState,
237}
238
239enum SpillReaderState {
240    Buffered { spill_path: PathBuf },
241    Reader { reader: AsyncStreamReader },
242}
243
244impl SpillReader {
245    fn new(receiver: tokio::sync::watch::Receiver<WriteStatus>, spill_path: PathBuf) -> Self {
246        Self {
247            batches_read: 0,
248            receiver,
249            state: SpillReaderState::Buffered { spill_path },
250        }
251    }
252
253    async fn wait_for_more_data(&mut self) -> Result<Option<Arc<[RecordBatch]>>, DataFusionError> {
254        let status = self
255            .receiver
256            .wait_for(|status| {
257                status.error.is_some()
258                    || status.finished
259                    || status.batches_written() > self.batches_read
260            })
261            .await
262            .map_err(|_| {
263                DataFusionError::Execution(
264                    "Spill has been dropped before reader has finish.".into(),
265                )
266            })?;
267
268        if let Some(error) = &status.error {
269            let mut guard = error.lock().ok().expect_ok()?;
270            return Err(DataFusionError::from(&mut (*guard)));
271        }
272
273        if let DataLocation::Buffered { batches } = &status.data_location {
274            Ok(Some(batches.clone()))
275        } else {
276            Ok(None)
277        }
278    }
279
280    async fn get_reader(&mut self) -> Result<&AsyncStreamReader, ArrowError> {
281        if let SpillReaderState::Buffered { spill_path } = &self.state {
282            let reader = AsyncStreamReader::open(spill_path.clone()).await?;
283            // Skip batches we've already read before the writer started spilling.
284            // The read batches were spilled to the file for the benefit of
285            // future readers, as the spill is replay-able.
286            for _ in 0..self.batches_read {
287                reader.read().await?;
288            }
289            self.state = SpillReaderState::Reader { reader };
290        }
291
292        if let SpillReaderState::Reader { reader } = &mut self.state {
293            Ok(reader)
294        } else {
295            unreachable!()
296        }
297    }
298
299    async fn read(&mut self) -> Result<Option<RecordBatch>, DataFusionError> {
300        let maybe_data = self.wait_for_more_data().await?;
301
302        if let Some(batches) = maybe_data {
303            if self.batches_read < batches.len() {
304                let batch = batches[self.batches_read].clone();
305                self.batches_read += 1;
306                Ok(Some(batch))
307            } else {
308                Ok(None)
309            }
310        } else {
311            let reader = self.get_reader().await?;
312            let batch = reader.read().await?;
313            if batch.is_some() {
314                self.batches_read += 1;
315            }
316            Ok(batch)
317        }
318    }
319}
320
321/// The sender side of the spill. This is used to write batches to the spill.
322///
323/// Note: this must be kept alive until after the readers are done reading the
324/// spill. Otherwise, they will return an error.
325pub struct SpillSender {
326    memory_limit: usize,
327    schema: Arc<Schema>,
328    path: PathBuf,
329    state: SpillState,
330    status_sender: tokio::sync::watch::Sender<WriteStatus>,
331}
332
333enum SpillState {
334    Buffering {
335        batches: Vec<RecordBatch>,
336        memory_accumulator: MemoryAccumulator,
337    },
338    Spilling {
339        writer: AsyncStreamWriter,
340        batches_written: usize,
341    },
342    Finished {
343        batches: Option<Arc<[RecordBatch]>>,
344        batches_written: usize,
345    },
346    Errored {
347        error: Arc<Mutex<SpillError>>,
348    },
349}
350
351impl Default for SpillState {
352    fn default() -> Self {
353        Self::Buffering {
354            batches: Vec::new(),
355            memory_accumulator: MemoryAccumulator::default(),
356        }
357    }
358}
359
360#[derive(Clone, Debug, Default)]
361struct WriteStatus {
362    error: Option<Arc<Mutex<SpillError>>>,
363    finished: bool,
364    data_location: DataLocation,
365}
366
367impl WriteStatus {
368    fn batches_written(&self) -> usize {
369        match &self.data_location {
370            DataLocation::Buffered { batches } => batches.len(),
371            DataLocation::Spilled {
372                batches_written, ..
373            } => *batches_written,
374        }
375    }
376}
377
378#[derive(Clone, Debug)]
379enum DataLocation {
380    Buffered { batches: Arc<[RecordBatch]> },
381    Spilled { batches_written: usize },
382}
383
384impl Default for DataLocation {
385    fn default() -> Self {
386        Self::Buffered {
387            batches: Arc::new([]),
388        }
389    }
390}
391
392/// A DataFusion error that be be emitted multiple times. We provide the
393/// Original error first, and subsequent conversions provide a copy with a
394/// string representation of the original error.
395#[derive(Debug)]
396enum SpillError {
397    Original(DataFusionError),
398    Copy(DataFusionError),
399}
400
401impl From<DataFusionError> for SpillError {
402    fn from(err: DataFusionError) -> Self {
403        Self::Original(err)
404    }
405}
406
407impl From<&mut SpillError> for DataFusionError {
408    fn from(err: &mut SpillError) -> Self {
409        match err {
410            SpillError::Original(inner) => {
411                let copy = Self::Execution(inner.to_string());
412                let original = std::mem::replace(err, SpillError::Copy(copy));
413                if let SpillError::Original(inner) = original {
414                    inner
415                } else {
416                    unreachable!()
417                }
418            }
419            SpillError::Copy(Self::Execution(message)) => Self::Execution(message.clone()),
420            _ => unreachable!(),
421        }
422    }
423}
424
425impl From<&SpillState> for WriteStatus {
426    fn from(state: &SpillState) -> Self {
427        match state {
428            SpillState::Buffering { batches, .. } => Self {
429                finished: false,
430                data_location: DataLocation::Buffered {
431                    batches: batches.clone().into(),
432                },
433                error: None,
434            },
435            SpillState::Spilling {
436                batches_written, ..
437            } => Self {
438                finished: false,
439                data_location: DataLocation::Spilled {
440                    batches_written: *batches_written,
441                },
442                error: None,
443            },
444            SpillState::Finished {
445                batches_written,
446                batches,
447            } => {
448                let data_location = if let Some(batches) = batches {
449                    DataLocation::Buffered {
450                        batches: batches.clone(),
451                    }
452                } else {
453                    DataLocation::Spilled {
454                        batches_written: *batches_written,
455                    }
456                };
457                Self {
458                    finished: true,
459                    data_location,
460                    error: None,
461                }
462            }
463            SpillState::Errored { error } => Self {
464                finished: true,
465                data_location: DataLocation::default(), // Doesn't matter.
466                error: Some(error.clone()),
467            },
468        }
469    }
470}
471
472impl SpillSender {
473    /// Write a batch to the spill.  
474    ///  
475    /// If there is room in the `memory_limit` then the batch is queued.  
476    /// If `memory_limit` is first encountered then all queued batches, and this one,  
477    /// will be written to disk as part of this call.  
478    /// If we are already spilling then the batch will be written to disk as part of this  
479    /// call.
480    pub async fn write(&mut self, batch: RecordBatch) -> Result<(), DataFusionError> {
481        if let SpillState::Finished { .. } = self.state {
482            return Err(DataFusionError::Execution(
483                "Spill has already been finished".to_string(),
484            ));
485        }
486
487        if let SpillState::Errored { .. } = &self.state {
488            return Err(DataFusionError::Execution(
489                "Spill has sent an error".to_string(),
490            ));
491        }
492
493        let (writer, batches_written) = match &mut self.state {
494            SpillState::Buffering {
495                batches,
496                memory_accumulator,
497            } => {
498                memory_accumulator.record_batch(&batch);
499
500                if memory_accumulator.total() > self.memory_limit {
501                    let writer =
502                        AsyncStreamWriter::open(self.path.clone(), self.schema.clone()).await?;
503                    let batches_written = batches.len();
504                    for batch in batches.drain(..) {
505                        writer.write(batch).await?;
506                    }
507                    self.state = SpillState::Spilling {
508                        writer,
509                        batches_written,
510                    };
511                    if let SpillState::Spilling {
512                        writer,
513                        batches_written,
514                    } = &mut self.state
515                    {
516                        (writer, batches_written)
517                    } else {
518                        unreachable!()
519                    }
520                } else {
521                    batches.push(batch);
522                    self.status_sender
523                        .send_replace(WriteStatus::from(&self.state));
524                    return Ok(());
525                }
526            }
527            SpillState::Spilling {
528                writer,
529                batches_written,
530            } => (writer, batches_written),
531            _ => unreachable!(),
532        };
533
534        writer.write(batch).await?;
535        *batches_written += 1;
536        self.status_sender
537            .send_replace(WriteStatus::from(&self.state));
538
539        Ok(())
540    }
541
542    /// Send an error to the spill. This will be sent to all readers of the
543    /// spill.
544    pub fn send_error(&mut self, err: DataFusionError) {
545        let error = Arc::new(Mutex::new(err.into()));
546        self.state = SpillState::Errored { error };
547        self.status_sender
548            .send_replace(WriteStatus::from(&self.state));
549    }
550
551    /// Complete the spill write. This will finalize the Arrow IPC stream file.
552    /// The file will remain available for reading until the spill is dropped.
553    pub async fn finish(&mut self) -> Result<(), DataFusionError> {
554        // We create a temporary state to get an owned copy of current state.
555        // Since we hold an exclusive reference to `self`, no one should be
556        // able to see this temporary state.
557        let tmp_state = SpillState::Finished {
558            batches_written: 0,
559            batches: None,
560        };
561        match std::mem::replace(&mut self.state, tmp_state) {
562            SpillState::Buffering { batches, .. } => {
563                let batches_written = batches.len();
564                self.state = SpillState::Finished {
565                    batches_written,
566                    batches: Some(batches.into()),
567                };
568                self.status_sender
569                    .send_replace(WriteStatus::from(&self.state));
570            }
571            SpillState::Spilling {
572                writer,
573                batches_written,
574            } => {
575                writer.finish().await?;
576                self.state = SpillState::Finished {
577                    batches_written,
578                    batches: None,
579                };
580                self.status_sender
581                    .send_replace(WriteStatus::from(&self.state));
582            }
583            SpillState::Finished { .. } => {
584                return Err(DataFusionError::Execution(
585                    "Spill has already been finished".to_string(),
586                ));
587            }
588            SpillState::Errored { .. } => {
589                return Err(DataFusionError::Execution(
590                    "Spill has sent an error".to_string(),
591                ));
592            }
593        };
594
595        Ok(())
596    }
597}
598
599/// An async wrapper around [`StreamWriter`]. Each call uses [`tokio::task::spawn_blocking`]
600/// to spawn a blocking task to write the batch.
601struct AsyncStreamWriter {
602    writer: Arc<Mutex<StreamWriter<BufWriter<std::fs::File>>>>,
603}
604
605impl AsyncStreamWriter {
606    pub async fn open(path: PathBuf, schema: Arc<Schema>) -> Result<Self, ArrowError> {
607        let writer = tokio::task::spawn_blocking(move || {
608            let file = std::fs::File::create(&path).map_err(ArrowError::from)?;
609            let writer = BufWriter::new(file);
610            StreamWriter::try_new(writer, &schema)
611        })
612        .await
613        .unwrap()?;
614        let writer = Arc::new(Mutex::new(writer));
615        Ok(Self { writer })
616    }
617
618    pub async fn write(&self, batch: RecordBatch) -> Result<(), ArrowError> {
619        let writer = self.writer.clone();
620        tokio::task::spawn_blocking(move || {
621            let mut writer = writer.lock().unwrap();
622            writer.write(&batch)?;
623            writer.flush()
624        })
625        .await
626        .unwrap()
627    }
628
629    pub async fn finish(self) -> Result<(), ArrowError> {
630        let writer = self.writer.clone();
631        tokio::task::spawn_blocking(move || {
632            let mut writer = writer.lock().unwrap();
633            writer.finish()
634        })
635        .await
636        .unwrap()
637    }
638}
639
640struct AsyncStreamReader {
641    reader: Arc<Mutex<StreamReader<BufReader<std::fs::File>>>>,
642}
643
644impl AsyncStreamReader {
645    pub async fn open(path: PathBuf) -> Result<Self, ArrowError> {
646        let reader = tokio::task::spawn_blocking(move || {
647            let file = std::fs::File::open(&path).map_err(ArrowError::from)?;
648            let reader = BufReader::new(file);
649            StreamReader::try_new(reader, None)
650        })
651        .await
652        .unwrap()?;
653        let reader = Arc::new(Mutex::new(reader));
654        Ok(Self { reader })
655    }
656
657    pub async fn read(&self) -> Result<Option<RecordBatch>, ArrowError> {
658        let reader = self.reader.clone();
659        tokio::task::spawn_blocking(move || {
660            let mut reader = reader.lock().unwrap();
661            reader.next()
662        })
663        .await
664        .unwrap()
665        .transpose()
666    }
667}
668
669#[cfg(test)]
670mod tests {
671    use arrow_array::Int32Array;
672    use arrow_schema::{DataType, Field};
673    use futures::{StreamExt, TryStreamExt, poll};
674    use lance_core::utils::tempfile::{TempStdFile, TempStdPath};
675
676    use super::*;
677
678    #[tokio::test]
679    async fn test_spill() {
680        let schema = Arc::new(Schema::new(vec![Field::new("a", DataType::Int32, false)]));
681        let batches = [
682            RecordBatch::try_new(
683                schema.clone(),
684                vec![Arc::new(Int32Array::from(vec![1, 2, 3]))],
685            )
686            .unwrap(),
687            RecordBatch::try_new(
688                schema.clone(),
689                vec![Arc::new(Int32Array::from(vec![4, 5, 6]))],
690            )
691            .unwrap(),
692        ];
693
694        // Create a stream
695        let path = TempStdFile::default();
696        let (mut spill, receiver) = create_replay_spill(path.to_owned(), schema.clone(), 0);
697
698        // We can open a reader prior to writing any data. No batches will be ready.
699        let mut stream_before = receiver.read();
700        let mut stream_before_next = stream_before.next();
701        let poll_res = poll!(&mut stream_before_next);
702        assert!(poll_res.is_pending());
703
704        // If we write a batch, the existing reader can now receive it.
705        spill.write(batches[0].clone()).await.unwrap();
706        let stream_before_batch1 = stream_before_next
707            .await
708            .expect("Expected a batch")
709            .expect("Expected no error");
710        assert_eq!(&stream_before_batch1, &batches[0]);
711        let mut stream_before_next = stream_before.next();
712        let poll_res = poll!(&mut stream_before_next);
713        assert!(poll_res.is_pending());
714
715        // We can also open a ready while the spill is being written to. We can
716        // retrieve batches written so far immediately.
717        let mut stream_during = receiver.read();
718        let stream_during_batch1 = stream_during
719            .next()
720            .await
721            .expect("Expected a batch")
722            .expect("Expected no error");
723        assert_eq!(&stream_during_batch1, &batches[0]);
724        let mut stream_during_next = stream_during.next();
725        let poll_res = poll!(&mut stream_during_next);
726        assert!(poll_res.is_pending());
727
728        // Once we finish the spill, readers can get remaining batches and will
729        // reach the end of the stream.
730        spill.write(batches[1].clone()).await.unwrap();
731        spill.finish().await.unwrap();
732
733        let stream_before_batch2 = stream_before_next
734            .await
735            .expect("Expected a batch")
736            .expect("Expected no error");
737        assert_eq!(&stream_before_batch2, &batches[1]);
738        assert!(stream_before.next().await.is_none());
739
740        let stream_during_batch2 = stream_during_next
741            .await
742            .expect("Expected a batch")
743            .expect("Expected no error");
744        assert_eq!(&stream_during_batch2, &batches[1]);
745        assert!(stream_during.next().await.is_none());
746
747        // Can also start a reader after finishing.
748        let stream_after = receiver.read();
749        let stream_after_batches = stream_after.try_collect::<Vec<_>>().await.unwrap();
750        assert_eq!(&stream_after_batches, &batches);
751
752        std::fs::remove_file(path).unwrap();
753    }
754
755    #[tokio::test]
756    async fn test_spill_error() {
757        // Create a spill
758        let schema = Arc::new(Schema::new(vec![Field::new("a", DataType::Int32, false)]));
759        let path = TempStdFile::default();
760        let (mut spill, receiver) =
761            create_replay_spill(path.as_ref().to_owned(), schema.clone(), 0);
762        let batch = RecordBatch::try_new(
763            schema.clone(),
764            vec![Arc::new(Int32Array::from(vec![1, 2, 3]))],
765        )
766        .unwrap();
767
768        spill.write(batch.clone()).await.unwrap();
769
770        let mut stream = receiver.read();
771        let stream_batch = stream
772            .next()
773            .await
774            .expect("Expected a batch")
775            .expect("Expected no error");
776        assert_eq!(&stream_batch, &batch);
777
778        spill.send_error(DataFusionError::ResourcesExhausted("🥱".into()));
779        let stream_error = stream
780            .next()
781            .await
782            .expect("Expected an error")
783            .expect_err("Expected an error");
784        assert!(matches!(
785            stream_error,
786            DataFusionError::ResourcesExhausted(message) if message == "🥱"
787        ));
788
789        // If we try to write after sending an error, it should return an error.
790        let err = spill.write(batch).await;
791        assert!(matches!(
792            err,
793            Err(DataFusionError::Execution(message)) if message == "Spill has sent an error"
794        ));
795
796        // If we try to finish after sending an error, it should return an error.
797        let err = spill.finish().await;
798        assert!(matches!(
799            err,
800            Err(DataFusionError::Execution(message)) if message == "Spill has sent an error"
801        ));
802
803        // If we try to read after sending an error, it should return an error.
804        let mut stream = receiver.read();
805        let stream_error = stream
806            .next()
807            .await
808            .expect("Expected an error")
809            .expect_err("Expected an error");
810        assert!(matches!(
811            stream_error,
812            DataFusionError::Execution(message) if message.contains("🥱")
813        ));
814
815        std::fs::remove_file(path).unwrap();
816    }
817
818    #[tokio::test]
819    async fn test_spill_buffered() {
820        let schema = Arc::new(Schema::new(vec![Field::new("a", DataType::Int32, false)]));
821        let path = TempStdPath::default();
822        let memory_limit = 1024 * 1024; // 1 MiB
823        let (mut spill, receiver) = create_replay_spill(path.clone(), schema.clone(), memory_limit);
824
825        // 0.5 MB batch
826        let batch = RecordBatch::try_new(
827            schema.clone(),
828            vec![Arc::new(Int32Array::from(vec![1; (512 * 1024) / 4]))],
829        )
830        .unwrap();
831        spill.write(batch.clone()).await.unwrap();
832        assert!(!std::fs::exists(&path).unwrap());
833
834        spill.finish().await.unwrap();
835        assert!(!std::fs::exists(&path).unwrap());
836
837        let mut stream = receiver.read();
838        let stream_batch = stream
839            .next()
840            .await
841            .expect("Expected a batch")
842            .expect("Expected no error");
843        assert_eq!(&stream_batch, &batch);
844
845        assert!(!std::fs::exists(&path).unwrap());
846    }
847
848    #[tokio::test]
849    async fn test_spill_buffered_transition() {
850        // Starts as buffered, then spills, then finished.
851        let schema = Arc::new(Schema::new(vec![Field::new("a", DataType::Int32, false)]));
852        let path = TempStdPath::default();
853        let memory_limit = 1024 * 1024; // 1 MiB
854        let (mut spill, receiver) = create_replay_spill(path.clone(), schema.clone(), memory_limit);
855
856        // 0.7 MB batch
857        let batch = RecordBatch::try_new(
858            schema.clone(),
859            vec![Arc::new(Int32Array::from(vec![1; (768 * 1024) / 4]))],
860        )
861        .unwrap();
862        spill.write(batch.clone()).await.unwrap();
863        assert!(!std::fs::exists(&path).unwrap());
864
865        let mut stream = receiver.read();
866        let stream_batch = stream
867            .next()
868            .await
869            .expect("Expected a batch")
870            .expect("Expected no error");
871        assert_eq!(&stream_batch, &batch);
872        assert!(!std::fs::exists(&path).unwrap());
873
874        // 0.5 MB batch
875        let batch = RecordBatch::try_new(
876            schema.clone(),
877            vec![Arc::new(Int32Array::from(vec![1; (512 * 1024) / 4]))],
878        )
879        .unwrap();
880        spill.write(batch.clone()).await.unwrap();
881        assert!(std::fs::exists(&path).unwrap());
882
883        let stream_batch = stream
884            .next()
885            .await
886            .expect("Expected a batch")
887            .expect("Expected no error");
888        assert_eq!(&stream_batch, &batch);
889        assert!(std::fs::exists(&path).unwrap());
890
891        spill.finish().await.unwrap();
892
893        assert!(stream.next().await.is_none());
894
895        std::fs::remove_file(path).unwrap();
896    }
897}