Skip to main content

datafusion_physical_plan/spill/
spill_pool.rs

1// Licensed to the Apache Software Foundation (ASF) under one
2// or more contributor license agreements.  See the NOTICE file
3// distributed with this work for additional information
4// regarding copyright ownership.  The ASF licenses this file
5// to you under the Apache License, Version 2.0 (the
6// "License"); you may not use this file except in compliance
7// with the License.  You may obtain a copy of the License at
8//
9//   http://www.apache.org/licenses/LICENSE-2.0
10//
11// Unless required by applicable law or agreed to in writing,
12// software distributed under the License is distributed on an
13// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14// KIND, either express or implied.  See the License for the
15// specific language governing permissions and limitations
16// under the License.
17
18use futures::{Stream, StreamExt};
19use std::collections::VecDeque;
20use std::mem;
21use std::sync::Arc;
22use std::task::Waker;
23
24use parking_lot::Mutex;
25
26use arrow::datatypes::SchemaRef;
27use arrow::record_batch::RecordBatch;
28use datafusion_common::Result;
29use datafusion_execution::{RecordBatchStream, SendableRecordBatchStream, SpillFile};
30
31use super::in_progress_spill_file::InProgressSpillFile;
32use super::spill_manager::SpillManager;
33
34/// Shared state between the writer and readers of a spill pool.
35/// This contains the queue of files and coordination state.
36///
37/// # Locking Design
38///
39/// This struct uses **fine-grained locking** with nested `Arc<Mutex<>>`:
40/// - `SpillPoolShared` is wrapped in `Arc<Mutex<>>` (outer lock)
41/// - Each `ActiveSpillFileShared` is wrapped in `Arc<Mutex<>>` (inner lock)
42///
43/// This enables:
44/// 1. **Short critical sections**: The outer lock is held only for queue operations
45/// 2. **I/O outside locks**: Disk I/O happens while holding only the file-specific lock
46/// 3. **Concurrent operations**: Reader can access the queue while writer does I/O
47///
48/// **Lock ordering discipline**: Never hold both locks simultaneously to prevent deadlock.
49/// Always: acquire outer lock → release outer lock → acquire inner lock (if needed).
50struct SpillPoolShared {
51    /// Queue of ALL files (including the current write files if any exist).
52    /// Readers always read from the front of this queue (FIFO).
53    /// Each file has its own lock to enable concurrent reader/writer access.
54    files: VecDeque<Arc<Mutex<ActiveSpillFileShared>>>,
55    /// SpillManager for creating files and tracking metrics
56    spill_manager: Arc<SpillManager>,
57    /// Pool-level waker to notify when new files are available (single reader)
58    waker: Option<Waker>,
59    /// FIFO queue of open write files. The queue may contain multiple items when multiple
60    /// writers concurrently write to the pool.
61    /// Each write file has its own lock to allow I/O without blocking queue access.
62    open_write_files: VecDeque<Arc<Mutex<ActiveSpillFileShared>>>,
63    /// Number of `SpillPoolWriter` instances that have not been dropped yet. As long as this value
64    /// is greater than zero, readers should assume batches may still be pushed. This prevents
65    /// premature EOF signaling.
66    remaining_writer_count: usize,
67}
68
69impl SpillPoolShared {
70    /// Creates a new shared pool state
71    fn new(spill_manager: Arc<SpillManager>) -> Self {
72        Self {
73            files: VecDeque::new(),
74            spill_manager,
75            waker: None,
76            open_write_files: VecDeque::new(),
77            remaining_writer_count: 1,
78        }
79    }
80
81    /// Registers a waker to be notified when new data is available (pool-level)
82    fn register_waker(&mut self, waker: Waker) {
83        self.waker = Some(waker);
84    }
85
86    /// Wakes the pool-level reader
87    fn wake(&mut self) {
88        if let Some(waker) = self.waker.take() {
89            waker.wake();
90        }
91    }
92}
93
94/// Writer for a spill pool that can be cloned to produce additional writers.
95///
96/// Created by [`mpsc_channel`]. See that function for architecture diagrams and usage
97/// examples.
98pub struct SpillPoolWriter {
99    /// The underlying shared writer. Kept private and never cloned, so this pool always has
100    /// exactly one writer.
101    inner: SpillPoolSink,
102}
103
104impl SpillPoolWriter {
105    /// Spills a batch to the pool, rotating files when necessary.
106    ///
107    /// See [`mpsc_channel`] for the rotation semantics.
108    ///
109    /// # Errors
110    ///
111    /// Returns an error if disk I/O fails or disk quota is exceeded.
112    pub fn push_batch(&self, batch: &RecordBatch) -> Result<()> {
113        self.inner.push_batch(batch)
114    }
115}
116
117impl SpillPoolWriter {
118    /// Returns a new sink that can be used to spill batches to the pool.
119    ///
120    /// As an alternative to this function, it is also possible to clone the writer. The benefit
121    /// of this method is that the output type matches the type used by [`spsc_channel`]. This
122    /// enables cost-free abstraction for producers over SPSC and MPSC channels.
123    pub fn new_sink(&self) -> SpillPoolSink {
124        // Increment `remaining_writer_count`. The corresponding decrement is done in the `Drop`
125        // implementation of `SpillPoolWriter`.
126        self.inner.shared.lock().remaining_writer_count += 1;
127        SpillPoolSink {
128            max_file_size_bytes: self.inner.max_file_size_bytes,
129            shared: Arc::clone(&self.inner.shared),
130        }
131    }
132}
133
134impl Clone for SpillPoolWriter {
135    fn clone(&self) -> Self {
136        Self {
137            inner: self.new_sink(),
138        }
139    }
140}
141
142impl Drop for SpillPoolSink {
143    fn drop(&mut self) {
144        let mut shared = self.shared.lock();
145
146        shared.remaining_writer_count -= 1;
147        let is_last_writer = shared.remaining_writer_count == 0;
148
149        if !is_last_writer {
150            // Other writer clones are still active; do not finalize or
151            // signal EOF to readers.
152            return;
153        }
154
155        // Finalize any spill files that were not finished yet
156        if !shared.open_write_files.is_empty() {
157            let files = mem::take(&mut shared.open_write_files);
158            drop(shared);
159
160            for file in files {
161                let mut file_shared = file.lock();
162
163                // Finish the current writer if it exists
164                if let Some(mut writer) = file_shared.writer.take() {
165                    // Ignore errors on drop - we're in destructor
166                    let _ = writer.finish();
167                }
168
169                // Mark as finished so readers know not to wait for more data
170                file_shared.writer_finished = true;
171
172                // Wake reader waiting on this file (it's now finished)
173                file_shared.wake();
174                drop(file_shared);
175            }
176
177            shared = self.shared.lock();
178        }
179
180        // Wake pool-level readers
181        shared.wake();
182    }
183}
184
185/// Single writer for a spill pool that cannot be cloned.
186///
187/// Created by [`spsc_channel`] and [`SpillPoolWriter::new_sink`].
188pub struct SpillPoolSink {
189    /// Maximum size in bytes before rotating to a new file.
190    /// Typically set from configuration `datafusion.execution.max_spill_file_size_bytes`.
191    max_file_size_bytes: usize,
192    /// Shared state with readers (includes current_write_file for coordination)
193    shared: Arc<Mutex<SpillPoolShared>>,
194}
195
196impl SpillPoolSink {
197    /// Spills a batch to the pool, rotating files when necessary.
198    ///
199    /// See [`spsc_channel`] for overall architecture and examples.
200    ///
201    /// # Errors
202    ///
203    /// Returns an error if disk I/O fails or disk quota is exceeded.
204    pub fn push_batch(&self, batch: &RecordBatch) -> Result<()> {
205        if batch.num_rows() == 0 {
206            // Skip empty batches
207            return Ok(());
208        }
209
210        let batch_size = batch.get_array_memory_size();
211
212        // Fine-grained locking: Lock shared state briefly for queue access
213        let mut shared = self.shared.lock();
214
215        // Create new file if there is none available to append to
216        let write_file = if !shared.open_write_files.is_empty() {
217            shared.open_write_files.pop_front().unwrap()
218        } else {
219            let spill_manager = Arc::clone(&shared.spill_manager);
220            // Release shared lock before disk I/O (fine-grained locking)
221            drop(shared);
222
223            let writer = spill_manager.create_in_progress_file("SpillPool")?;
224            // Clone the file so readers can access it immediately
225            let file = Arc::clone(writer.file().expect(
226                "InProgressSpillFile should always have a file when it is first created",
227            ));
228
229            let file_shared = Arc::new(Mutex::new(ActiveSpillFileShared {
230                writer: Some(writer),
231                file: Some(file), // Set immediately so readers can access it
232                batches_written: 0,
233                estimated_size: 0,
234                writer_finished: false,
235                waker: None,
236            }));
237
238            // Re-acquire lock and push to shared queue
239            shared = self.shared.lock();
240            shared.files.push_back(Arc::clone(&file_shared));
241            shared.wake(); // Wake readers waiting for new files
242            file_shared
243        };
244
245        // Release shared lock before file I/O (fine-grained locking)
246        // This allows readers to access the queue while we do disk I/O
247        drop(shared);
248
249        // Write batch to current file - lock only the specific file
250        let mut file_shared = write_file.lock();
251
252        // Append the batch
253        if let Some(ref mut writer) = file_shared.writer {
254            writer.append_batch(batch)?;
255            // make sure we flush the writer for readers
256            writer.flush()?;
257            file_shared.batches_written += 1;
258            file_shared.estimated_size += batch_size;
259        }
260
261        // Wake reader waiting on this specific file
262        file_shared.wake();
263
264        let max_file_size_reached = file_shared.estimated_size > self.max_file_size_bytes;
265
266        if max_file_size_reached {
267            // Finish the IPC writer
268            if let Some(mut writer) = file_shared.writer.take() {
269                writer.finish()?;
270            }
271            // Mark as finished so readers know not to wait for more data
272            file_shared.writer_finished = true;
273            // Wake reader waiting on this file (it's now finished)
274            file_shared.wake();
275
276            // Don't place `write_file` back in the `open_write_files` queue so we don't
277            // try writing to it again
278        } else {
279            // Release file lock
280            drop(file_shared);
281            // Put back the current file for further writing
282            let mut shared = self.shared.lock();
283            shared.open_write_files.push_back(write_file);
284        }
285
286        Ok(())
287    }
288}
289
290/// Creates a paired writer and reader for a spill pool with SPSC (single-producer,
291/// single-consumer) semantics and strict FIFO ordering.
292///
293/// If you need a spill pool that supports several producers, use [`mpsc_channel`] instead.
294///
295/// The reader can start reading immediately after the writer appends a batch
296/// to the spill file, without waiting for the file to be sealed, while the writer continues to
297/// write more data.
298///
299/// Internally this coordinates rotating spill files based on size limits, and
300/// handles asynchronous notification between the writer and reader using wakers.
301/// This ensures that we manage disk usage efficiently while allowing concurrent
302/// I/O between the writer and reader.
303///
304/// # Data Flow Overview
305///
306/// 1. Writer write batch `B0` to F1
307/// 2. Writer write batch `B1` to F1, notices the size limit exceeded, finishes F1.
308/// 3. Reader read `B0` from F1
309/// 4. Reader read `B1`, no more batch to read -> wait on the waker
310/// 5. Writer write batch `B2` to a new file `F2`, wake up the waiting reader.
311/// 6. Reader read `B2` from F2.
312/// 7. Repeat until writer is dropped.
313///
314/// # Architecture
315///
316/// ```text
317/// ┌─────────────────────────────────────────────────────────────────────────┐
318/// │                            SpillPool                                    │
319/// │                                                                         │
320/// │  Writer Side              Shared State              Reader Side         │
321/// │  ───────────              ────────────              ───────────         │
322/// │                                                                         │
323/// │  SpillPoolSink      ┌────────────────────┐    RecordBatchStream         │
324/// │       │             │  VecDeque<File>    │          │                   │
325/// │       │             │  ┌────┐┌────┐      │          │                   │
326/// │  push_batch()       │  │ F1 ││ F2 │ ...  │      next().await            │
327/// │       │             │  └────┘└────┘      │          │                   │
328/// │       ▼             │                    │          ▼                   │
329/// │  ┌─────────┐        │                    │    ┌──────────┐              │
330/// │  │Current  │───────▶│ Coordination:      │◀───│ Current  │              │
331/// │  │Write    │        │ - Wakers           │    │ Read     │              │
332/// │  │File     │        │ - Batch counts     │    │ File     │              │
333/// │  └─────────┘        │ - Writer status    │    └──────────┘              │
334/// │       │             └────────────────────┘           │                  │
335/// │       │                                              │                  │
336/// │  Size > limit?                                Read all batches?         │
337/// │       │                                              │                  │
338/// │       ▼                                              ▼                  │
339/// │  Rotate to new file                            Pop from queue           │
340/// └─────────────────────────────────────────────────────────────────────────┘
341///
342/// Writer produces → Shared queue → Reader consumes
343/// ```
344///
345/// # File State Machine
346///
347/// Each file in the pool coordinates between writer and reader:
348///
349/// ```text
350///                Writer View              Reader View
351///                ───────────              ───────────
352///
353/// Created        writer: Some(..)         batches_read: 0
354///                batches_written: 0       (waiting for data)
355///                       │
356///                       ▼
357/// Writing        append_batch()           Can read if:
358///                batches_written++        batches_read < batches_written
359///                wake readers
360///                       │                        │
361///                       │                        ▼
362///                ┌──────┴──────┐          poll_next() → batch
363///                │             │          batches_read++
364///                ▼             ▼
365///          Size > limit?  More data?
366///                │             │
367///                │             └─▶ Yes ──▶ Continue writing
368///                ▼
369///          finish()                   Reader catches up:
370///          writer_finished = true     batches_read == batches_written
371///          wake readers                       │
372///                │                            ▼
373///                └─────────────────────▶ Returns Poll::Ready(None)
374///                                       File complete, pop from queue
375/// ```
376///
377/// # Arguments
378///
379/// * `max_file_size_bytes` - Maximum size per file before rotation. When a file
380///   exceeds this size, the writer automatically rotates to a new file.
381/// * `spill_manager` - Manager for file creation and metrics tracking
382///
383/// # Returns
384///
385/// A tuple of `(SpillPoolSink, SendableRecordBatchStream)` that share the same
386/// underlying pool. The reader is returned as a stream for immediate use with
387/// async stream combinators.
388///
389/// # Example
390///
391/// ```
392/// use std::sync::Arc;
393/// use arrow::array::{ArrayRef, Int32Array};
394/// use arrow::datatypes::{DataType, Field, Schema};
395/// use arrow::record_batch::RecordBatch;
396/// use datafusion_execution::runtime_env::RuntimeEnv;
397/// use futures::StreamExt;
398///
399/// # use datafusion_physical_plan::spill::spill_pool;
400/// # use datafusion_physical_plan::spill::SpillManager; // Re-exported for doctests
401/// # use datafusion_physical_plan::metrics::{ExecutionPlanMetricsSet, SpillMetrics};
402/// #
403/// # #[tokio::main]
404/// # async fn main() -> datafusion_common::Result<()> {
405/// # // Setup for the example (typically comes from TaskContext in production)
406/// # let env = Arc::new(RuntimeEnv::default());
407/// # let metrics = SpillMetrics::new(&ExecutionPlanMetricsSet::new(), 0);
408/// # let schema = Arc::new(Schema::new(vec![Field::new("a", DataType::Int32, false)]));
409/// # let spill_manager = Arc::new(SpillManager::new(env, metrics, schema.clone()));
410/// #
411/// // Create channel with 1MB file size limit
412/// let (writer, mut reader) = spill_pool::spsc_channel(1024 * 1024, spill_manager);
413///
414/// // Spawn writer and reader concurrently; writer wakes reader via wakers
415/// let writer_task = tokio::spawn(async move {
416///     for i in 0..5 {
417///         let array: ArrayRef = Arc::new(Int32Array::from(vec![i; 100]));
418///         let batch = RecordBatch::try_new(schema.clone(), vec![array]).unwrap();
419///         writer.push_batch(&batch)?;
420///     }
421///     // Explicitly drop writer to finalize the spill file and wake the reader
422///     drop(writer);
423///     datafusion_common::Result::<()>::Ok(())
424/// });
425///
426/// let reader_task = tokio::spawn(async move {
427///     let mut batches_read = 0;
428///     while let Some(result) = reader.next().await {
429///         let _batch = result?;
430///         batches_read += 1;
431///     }
432///     datafusion_common::Result::<usize>::Ok(batches_read)
433/// });
434///
435/// let (writer_res, reader_res) = tokio::join!(writer_task, reader_task);
436/// writer_res
437///     .map_err(|e| datafusion_common::DataFusionError::Execution(e.to_string()))??;
438/// let batches_read = reader_res
439///     .map_err(|e| datafusion_common::DataFusionError::Execution(e.to_string()))??;
440///
441/// assert_eq!(batches_read, 5);
442/// # Ok(())
443/// # }
444/// ```
445///
446/// # Why rotate files?
447///
448/// File rotation ensures we don't end up with unreferenced disk usage.
449/// If we used a single file for all spilled data, we would end up with
450/// unreferenced data at the beginning of the file that has already been read
451/// by readers but we can't delete because you can't truncate from the start of a file.
452///
453/// Consider the case of a query like `SELECT * FROM large_table WHERE false`.
454/// Obviously this query produces no output rows, but if we had a spilling operator
455/// in the middle of this query between the scan and the filter it would see the entire
456/// `large_table` flow through it and thus would spill all of that data to disk.
457/// So we'd end up using up to `size(large_table)` bytes of disk space.
458/// If instead we use file rotation, and as long as the readers can keep up with the writer,
459/// then we can ensure that once a file is fully read by all readers it can be deleted,
460/// thus bounding the maximum disk usage to roughly `max_file_size_bytes`.
461pub fn spsc_channel(
462    max_file_size_bytes: usize,
463    spill_manager: Arc<SpillManager>,
464) -> (SpillPoolSink, SendableRecordBatchStream) {
465    let schema = Arc::clone(spill_manager.schema());
466    let shared = Arc::new(Mutex::new(SpillPoolShared::new(spill_manager)));
467
468    let writer = SpillPoolSink {
469        max_file_size_bytes,
470        shared: Arc::clone(&shared),
471    };
472
473    let reader = SpillPoolReader::new(shared, schema);
474
475    (writer, Box::pin(reader))
476}
477
478/// Alias for [`mpsc_channel`].
479#[deprecated(note = "Use mpsc_channel instead")]
480pub fn channel(
481    max_file_size_bytes: usize,
482    spill_manager: Arc<SpillManager>,
483) -> (SpillPoolWriter, SendableRecordBatchStream) {
484    mpsc_channel(max_file_size_bytes, spill_manager)
485}
486
487/// Creates a paired writer and reader for a spill pool with MPSC (multi-producer,
488/// single-consumer) semantics. See [`spsc_channel`] for the general architecture description
489/// of the spill pool.
490///
491/// Additional writers can be created by cloning the returned [`SpillPoolWriter`].
492///
493/// In contrast to [`spsc_channel`], this implementation provides no guarantees regarding
494/// the read order of the returned [`SendableRecordBatchStream`].
495///
496/// If you need strict end-to-end FIFO (a single writer whose batches are read back in exact
497/// write order), use [`spsc_channel`] instead.
498///
499/// # File Management
500///
501/// The shared channel uses the same size-based rotation trigger as the [single producer channel](spsc_channel).
502/// All writers share the same pool of write files and coordinate file rotation. The number of open
503/// files is kept as small as possible. When more writes occur concurrently than there are open write
504/// files an additional file will be opened to write to. This prevents multiple writers from blocking
505/// each other.
506///
507/// When the last writer clone is dropped, it finalizes any remaining open write files so that all
508/// written data can be accessed by the reader.
509///
510/// # Returns
511///
512/// A tuple of `(SpillPoolWriter, SendableRecordBatchStream)` that share the same
513/// underlying pool. The reader is returned as a stream for immediate use with
514/// async stream combinators. The writer can be cloned to create additional writers.
515pub fn mpsc_channel(
516    max_file_size_bytes: usize,
517    spill_manager: Arc<SpillManager>,
518) -> (SpillPoolWriter, SendableRecordBatchStream) {
519    let (inner, reader) = spsc_channel(max_file_size_bytes, spill_manager);
520    (SpillPoolWriter { inner }, reader)
521}
522
523/// Shared state between writer and readers for an active spill file.
524/// Protected by a Mutex to coordinate between concurrent readers and the writer.
525struct ActiveSpillFileShared {
526    /// Writer handle - taken (set to None) when finish() is called
527    writer: Option<InProgressSpillFile>,
528    /// The spill file, set when the writer finishes.
529    /// Taken by the reader when creating a stream (the file stays open via file handles).
530    file: Option<Arc<dyn SpillFile>>,
531    /// Total number of batches written to this file
532    batches_written: usize,
533    /// Estimated size in bytes of data written to this file
534    estimated_size: usize,
535    /// Whether the writer has finished writing to this file
536    writer_finished: bool,
537    /// Waker for reader waiting on this specific file (SPSC: only one reader)
538    waker: Option<Waker>,
539}
540
541impl ActiveSpillFileShared {
542    /// Registers a waker to be notified when new data is written to this file
543    fn register_waker(&mut self, waker: Waker) {
544        self.waker = Some(waker);
545    }
546
547    /// Wakes the reader waiting on this file
548    fn wake(&mut self) {
549        if let Some(waker) = self.waker.take() {
550            waker.wake();
551        }
552    }
553}
554
555/// Reader state for a SpillPoolFile (owned by individual SpillPoolFile instances).
556/// This is kept separate from the shared state to avoid holding locks during I/O.
557struct SpillPoolFileReader {
558    /// The actual stream reading from disk
559    stream: SendableRecordBatchStream,
560    /// Number of batches this reader has consumed
561    batches_read: usize,
562}
563
564struct SpillPoolFile {
565    /// Shared coordination state (contains writer and batch counts)
566    shared: Arc<Mutex<ActiveSpillFileShared>>,
567    /// Reader state (lazy-initialized, owned by this SpillPoolFile)
568    reader: Option<SpillPoolFileReader>,
569    /// Spill manager for creating readers
570    spill_manager: Arc<SpillManager>,
571}
572
573impl Stream for SpillPoolFile {
574    type Item = Result<RecordBatch>;
575
576    fn poll_next(
577        mut self: std::pin::Pin<&mut Self>,
578        cx: &mut std::task::Context<'_>,
579    ) -> std::task::Poll<Option<Self::Item>> {
580        use std::task::Poll;
581
582        // Step 1: Lock shared state and check coordination
583        let (should_read, file) = {
584            let mut shared = self.shared.lock();
585
586            // Determine if we can read
587            let batches_read = self.reader.as_ref().map_or(0, |r| r.batches_read);
588
589            if batches_read < shared.batches_written {
590                // More data available to read - take the file if we don't have a reader yet
591                let file = if self.reader.is_none() {
592                    shared.file.take()
593                } else {
594                    None
595                };
596                (true, file)
597            } else if shared.writer_finished {
598                // No more data and writer is done - EOF
599                return Poll::Ready(None);
600            } else {
601                // Caught up to writer, but writer still active - register waker and wait
602                shared.register_waker(cx.waker().clone());
603                return Poll::Pending;
604            }
605        }; // Lock released here
606
607        // Step 2: Lazy-create reader stream if needed
608        if self.reader.is_none() && should_read {
609            if let Some(file) = file {
610                // we want this unbuffered because files are actively being written to
611                match self
612                    .spill_manager
613                    .read_spill_as_stream_unbuffered(file, None)
614                {
615                    Ok(stream) => {
616                        self.reader = Some(SpillPoolFileReader {
617                            stream,
618                            batches_read: 0,
619                        });
620                    }
621                    Err(e) => return Poll::Ready(Some(Err(e))),
622                }
623            } else {
624                // File not available yet (writer hasn't finished or already taken)
625                // Register waker and wait for file to be ready
626                let mut shared = self.shared.lock();
627                shared.register_waker(cx.waker().clone());
628                return Poll::Pending;
629            }
630        }
631
632        // Step 3: Poll the reader stream (no lock held)
633        if let Some(reader) = &mut self.reader {
634            match reader.stream.poll_next_unpin(cx) {
635                Poll::Ready(Some(Ok(batch))) => {
636                    // Successfully read a batch - increment counter
637                    reader.batches_read += 1;
638                    Poll::Ready(Some(Ok(batch)))
639                }
640                Poll::Ready(Some(Err(e))) => Poll::Ready(Some(Err(e))),
641                Poll::Ready(None) => {
642                    // Stream exhausted unexpectedly
643                    // This shouldn't happen if coordination is correct, but handle gracefully
644                    Poll::Ready(None)
645                }
646                Poll::Pending => Poll::Pending,
647            }
648        } else {
649            // Should not reach here, but handle gracefully
650            Poll::Ready(None)
651        }
652    }
653}
654
655/// A stream that reads from a SpillPool. The reader guarantees FIFO order if a single writer is used.
656///
657/// Created by [`spsc_channel`]. See that function for architecture diagrams and usage examples.
658///
659/// The stream automatically handles file rotation and reads from completed files.
660/// When no data is available, it returns `Poll::Pending` and registers a waker to
661/// be notified when the writer produces more data.
662///
663/// # Infinite Stream Semantics
664///
665/// This stream never returns `None` (`Poll::Ready(None)`) on its own - it will keep
666/// waiting for the writer to produce more data. The stream ends only when:
667/// - The reader is dropped
668/// - The writer is dropped AND all queued data has been consumed
669///
670/// This makes it suitable for continuous streaming scenarios where the writer may
671/// produce data intermittently.
672pub struct SpillPoolReader {
673    /// Shared reference to the spill pool
674    shared: Arc<Mutex<SpillPoolShared>>,
675    /// Current SpillPoolFile we're reading from
676    current_file: Option<SpillPoolFile>,
677    /// Schema of the spilled data
678    schema: SchemaRef,
679}
680
681impl SpillPoolReader {
682    /// Creates a new reader from shared pool state.
683    ///
684    /// This is private - use the [`spsc_channel`] function to create a reader/writer pair.
685    ///
686    /// # Arguments
687    ///
688    /// * `shared` - Shared reference to the pool state
689    fn new(shared: Arc<Mutex<SpillPoolShared>>, schema: SchemaRef) -> Self {
690        Self {
691            shared,
692            current_file: None,
693            schema,
694        }
695    }
696}
697
698impl Stream for SpillPoolReader {
699    type Item = Result<RecordBatch>;
700
701    fn poll_next(
702        mut self: std::pin::Pin<&mut Self>,
703        cx: &mut std::task::Context<'_>,
704    ) -> std::task::Poll<Option<Self::Item>> {
705        use std::task::Poll;
706
707        loop {
708            // If we have a current file, try to read from it
709            if let Some(ref mut file) = self.current_file {
710                match file.poll_next_unpin(cx) {
711                    Poll::Ready(Some(Ok(batch))) => {
712                        // Got a batch, return it
713                        return Poll::Ready(Some(Ok(batch)));
714                    }
715                    Poll::Ready(Some(Err(e))) => {
716                        // Error reading batch
717                        return Poll::Ready(Some(Err(e)));
718                    }
719                    Poll::Ready(None) => {
720                        // Current file stream exhausted
721                        // Check if this file is marked as writer_finished
722                        let writer_finished = { file.shared.lock().writer_finished };
723
724                        if writer_finished {
725                            // File is complete, pop it from the queue and move to next
726                            let mut shared = self.shared.lock();
727                            shared.files.pop_front();
728                            drop(shared); // Release lock
729
730                            // Clear current file and continue loop to get next file
731                            self.current_file = None;
732                            continue;
733                        } else {
734                            // Stream exhausted but writer not finished - unexpected
735                            // This shouldn't happen with proper coordination
736                            return Poll::Ready(None);
737                        }
738                    }
739                    Poll::Pending => {
740                        // File not ready yet (waiting for writer)
741                        // Register waker so we get notified when writer adds more batches
742                        let mut shared = self.shared.lock();
743                        shared.register_waker(cx.waker().clone());
744                        return Poll::Pending;
745                    }
746                }
747            }
748
749            // No current file, need to get the next one
750            let mut shared = self.shared.lock();
751
752            // Peek at the front of the queue (don't pop yet)
753            if let Some(file_shared) = shared.files.front() {
754                // Create a SpillPoolFile from the shared state
755                let spill_manager = Arc::clone(&shared.spill_manager);
756                let file_shared = Arc::clone(file_shared);
757                drop(shared); // Release lock before creating SpillPoolFile
758
759                self.current_file = Some(SpillPoolFile {
760                    shared: file_shared,
761                    reader: None,
762                    spill_manager,
763                });
764
765                // Continue loop to poll the new file
766                continue;
767            }
768
769            // No files in queue - check if writer is done
770            if shared.remaining_writer_count == 0 {
771                // Writer is done and no more files will be added - EOF
772                return Poll::Ready(None);
773            }
774
775            // Writer still active, register waker that will get notified when new files are added
776            shared.register_waker(cx.waker().clone());
777            return Poll::Pending;
778        }
779    }
780}
781
782impl RecordBatchStream for SpillPoolReader {
783    fn schema(&self) -> SchemaRef {
784        Arc::clone(&self.schema)
785    }
786}
787
788#[cfg(test)]
789mod tests {
790    use super::*;
791    use crate::metrics::{ExecutionPlanMetricsSet, SpillMetrics};
792    use arrow::array::{ArrayRef, Int32Array};
793    use arrow::datatypes::{DataType, Field, Schema};
794    use datafusion_common_runtime::{JoinSet, SpawnedTask};
795    use datafusion_execution::runtime_env::RuntimeEnv;
796
797    fn create_test_schema() -> SchemaRef {
798        Arc::new(Schema::new(vec![Field::new("a", DataType::Int32, false)]))
799    }
800
801    fn create_test_batch(start: i32, count: usize) -> RecordBatch {
802        let schema = create_test_schema();
803        let a: ArrayRef = Arc::new(Int32Array::from(
804            (start..start + count as i32).collect::<Vec<_>>(),
805        ));
806        RecordBatch::try_new(schema, vec![a]).unwrap()
807    }
808
809    fn create_spill_channel(
810        max_file_size: usize,
811    ) -> (SpillPoolSink, SendableRecordBatchStream) {
812        let env = Arc::new(RuntimeEnv::default());
813        let metrics = SpillMetrics::new(&ExecutionPlanMetricsSet::new(), 0);
814        let schema = create_test_schema();
815        let spill_manager = Arc::new(SpillManager::new(env, metrics, schema));
816
817        spsc_channel(max_file_size, spill_manager)
818    }
819
820    fn create_shared_spill_channel(
821        max_file_size: usize,
822    ) -> (SpillPoolWriter, SendableRecordBatchStream) {
823        let env = Arc::new(RuntimeEnv::default());
824        let metrics = SpillMetrics::new(&ExecutionPlanMetricsSet::new(), 0);
825        let schema = create_test_schema();
826        let spill_manager = Arc::new(SpillManager::new(env, metrics, schema));
827
828        mpsc_channel(max_file_size, spill_manager)
829    }
830
831    fn create_spill_channel_with_metrics(
832        max_file_size: usize,
833    ) -> (SpillPoolSink, SendableRecordBatchStream, SpillMetrics) {
834        let env = Arc::new(RuntimeEnv::default());
835        let metrics = SpillMetrics::new(&ExecutionPlanMetricsSet::new(), 0);
836        let schema = create_test_schema();
837        let spill_manager = Arc::new(SpillManager::new(env, metrics.clone(), schema));
838
839        let (writer, reader) = spsc_channel(max_file_size, spill_manager);
840        (writer, reader, metrics)
841    }
842
843    #[tokio::test]
844    async fn test_basic_write_and_read() -> Result<()> {
845        let (writer, mut reader) = create_spill_channel(1024 * 1024);
846
847        // Write one batch
848        let batch1 = create_test_batch(0, 10);
849        writer.push_batch(&batch1)?;
850
851        // Read the batch
852        let result = reader.next().await.unwrap()?;
853        assert_eq!(result.num_rows(), 10);
854
855        // Write another batch
856        let batch2 = create_test_batch(10, 5);
857        writer.push_batch(&batch2)?;
858        // Read the second batch
859        let result = reader.next().await.unwrap()?;
860        assert_eq!(result.num_rows(), 5);
861
862        Ok(())
863    }
864
865    #[tokio::test]
866    async fn test_single_batch_write_read() -> Result<()> {
867        let (writer, mut reader) = create_spill_channel(1024 * 1024);
868
869        // Write one batch
870        let batch = create_test_batch(0, 5);
871        writer.push_batch(&batch)?;
872
873        // Read it back
874        let result = reader.next().await.unwrap()?;
875        assert_eq!(result.num_rows(), 5);
876
877        // Verify the actual data
878        let col = result
879            .column(0)
880            .as_any()
881            .downcast_ref::<Int32Array>()
882            .unwrap();
883        assert_eq!(col.value(0), 0);
884        assert_eq!(col.value(4), 4);
885
886        Ok(())
887    }
888
889    #[tokio::test]
890    async fn test_multiple_batches_sequential() -> Result<()> {
891        let (writer, mut reader) = create_spill_channel(1024 * 1024);
892
893        // Write multiple batches
894        for i in 0..5 {
895            let batch = create_test_batch(i * 10, 10);
896            writer.push_batch(&batch)?;
897        }
898
899        // Read all batches and verify FIFO order
900        for i in 0..5 {
901            let result = reader.next().await.unwrap()?;
902            assert_eq!(result.num_rows(), 10);
903
904            let col = result
905                .column(0)
906                .as_any()
907                .downcast_ref::<Int32Array>()
908                .unwrap();
909            assert_eq!(col.value(0), i * 10, "Batch {i} not in FIFO order");
910        }
911
912        Ok(())
913    }
914
915    #[tokio::test]
916    async fn test_empty_writer() -> Result<()> {
917        let (_writer, reader) = create_spill_channel(1024 * 1024);
918
919        // Reader should pend since no batches were written
920        let mut reader = reader;
921        let result =
922            tokio::time::timeout(std::time::Duration::from_millis(100), reader.next())
923                .await;
924
925        assert!(result.is_err(), "Reader should timeout on empty writer");
926
927        Ok(())
928    }
929
930    #[tokio::test]
931    async fn test_empty_batch_skipping() -> Result<()> {
932        let (writer, mut reader) = create_spill_channel(1024 * 1024);
933
934        // Write empty batch
935        let empty_batch = create_test_batch(0, 0);
936        writer.push_batch(&empty_batch)?;
937
938        // Write non-empty batch
939        let batch = create_test_batch(0, 5);
940        writer.push_batch(&batch)?;
941
942        // Should only read the non-empty batch
943        let result = reader.next().await.unwrap()?;
944        assert_eq!(result.num_rows(), 5);
945
946        Ok(())
947    }
948
949    #[tokio::test]
950    async fn test_rotation_triggered_by_size() -> Result<()> {
951        // Set a small max_file_size to trigger rotation after one batch
952        let batch1 = create_test_batch(0, 10);
953        let batch_size = batch1.get_array_memory_size() + 1;
954
955        let (writer, mut reader, metrics) = create_spill_channel_with_metrics(batch_size);
956
957        // Write first batch (should fit in first file)
958        writer.push_batch(&batch1)?;
959
960        // Check metrics after first batch - file created but not finalized yet
961        assert_eq!(
962            metrics.spill_file_count.value(),
963            1,
964            "Should have created 1 file after first batch"
965        );
966        assert_eq!(
967            metrics.spilled_bytes.value(),
968            320,
969            "Spilled bytes should reflect data written (header + 1 batch)"
970        );
971        assert_eq!(
972            metrics.spilled_rows.value(),
973            10,
974            "Should have spilled 10 rows from first batch"
975        );
976
977        // Write second batch (should trigger rotation - finalize first file)
978        let batch2 = create_test_batch(10, 10);
979        assert!(
980            batch2.get_array_memory_size() <= batch_size,
981            "batch2 size {} exceeds limit {batch_size}",
982            batch2.get_array_memory_size(),
983        );
984        assert!(
985            batch1.get_array_memory_size() + batch2.get_array_memory_size() > batch_size,
986            "Combined size {} does not exceed limit to trigger rotation",
987            batch1.get_array_memory_size() + batch2.get_array_memory_size()
988        );
989        writer.push_batch(&batch2)?;
990
991        // Check metrics after rotation - first file finalized, but second file not created yet
992        // (new file created lazily on next push_batch call)
993        assert_eq!(
994            metrics.spill_file_count.value(),
995            1,
996            "Should still have 1 file (second file not created until next write)"
997        );
998        assert!(
999            metrics.spilled_bytes.value() > 0,
1000            "Spilled bytes should be > 0 after first file finalized (got {})",
1001            metrics.spilled_bytes.value()
1002        );
1003        assert_eq!(
1004            metrics.spilled_rows.value(),
1005            20,
1006            "Should have spilled 20 total rows (10 + 10)"
1007        );
1008
1009        // Write a third batch to confirm rotation occurred (creates second file)
1010        let batch3 = create_test_batch(20, 5);
1011        writer.push_batch(&batch3)?;
1012
1013        // Now check that second file was created
1014        assert_eq!(
1015            metrics.spill_file_count.value(),
1016            2,
1017            "Should have created 2 files after writing to new file"
1018        );
1019        assert_eq!(
1020            metrics.spilled_rows.value(),
1021            25,
1022            "Should have spilled 25 total rows (10 + 10 + 5)"
1023        );
1024
1025        // Read all three batches
1026        let result1 = reader.next().await.unwrap()?;
1027        assert_eq!(result1.num_rows(), 10);
1028
1029        let result2 = reader.next().await.unwrap()?;
1030        assert_eq!(result2.num_rows(), 10);
1031
1032        let result3 = reader.next().await.unwrap()?;
1033        assert_eq!(result3.num_rows(), 5);
1034
1035        Ok(())
1036    }
1037
1038    #[tokio::test]
1039    async fn test_multiple_rotations() -> Result<()> {
1040        let batches = (0..10)
1041            .map(|i| create_test_batch(i * 10, 10))
1042            .collect::<Vec<_>>();
1043
1044        let batch_size = batches[0].get_array_memory_size() * 2 + 1;
1045
1046        // Very small max_file_size to force frequent rotations
1047        let (writer, mut reader, metrics) = create_spill_channel_with_metrics(batch_size);
1048
1049        // Write many batches to cause multiple rotations
1050        for i in 0..10 {
1051            let batch = create_test_batch(i * 10, 10);
1052            writer.push_batch(&batch)?;
1053        }
1054
1055        // Check metrics after all writes - should have multiple files due to rotations
1056        // With batch_size = 2 * one_batch + 1, each file fits ~2 batches before rotating
1057        // 10 batches should create multiple files (exact count depends on rotation timing)
1058        let file_count = metrics.spill_file_count.value();
1059        assert!(
1060            file_count >= 4,
1061            "Should have created at least 4 files with multiple rotations (got {file_count})"
1062        );
1063        assert!(
1064            metrics.spilled_bytes.value() > 0,
1065            "Spilled bytes should be > 0 after rotations (got {})",
1066            metrics.spilled_bytes.value()
1067        );
1068        assert_eq!(
1069            metrics.spilled_rows.value(),
1070            100,
1071            "Should have spilled 100 total rows (10 batches * 10 rows)"
1072        );
1073
1074        // Read all batches and verify order
1075        for i in 0..10 {
1076            let result = reader.next().await.unwrap()?;
1077            assert_eq!(result.num_rows(), 10);
1078
1079            let col = result
1080                .column(0)
1081                .as_any()
1082                .downcast_ref::<Int32Array>()
1083                .unwrap();
1084            assert_eq!(
1085                col.value(0),
1086                i * 10,
1087                "Batch {i} not in correct order after rotations"
1088            );
1089        }
1090
1091        Ok(())
1092    }
1093
1094    #[tokio::test]
1095    async fn test_single_batch_larger_than_limit() -> Result<()> {
1096        // Very small limit
1097        let (writer, mut reader, metrics) = create_spill_channel_with_metrics(100);
1098
1099        // Write a batch that exceeds the limit
1100        let large_batch = create_test_batch(0, 100);
1101        writer.push_batch(&large_batch)?;
1102
1103        // Check metrics after large batch - should trigger rotation immediately
1104        assert_eq!(
1105            metrics.spill_file_count.value(),
1106            1,
1107            "Should have created 1 file for large batch"
1108        );
1109        assert_eq!(
1110            metrics.spilled_rows.value(),
1111            100,
1112            "Should have spilled 100 rows from large batch"
1113        );
1114
1115        // Should still write and read successfully
1116        let result = reader.next().await.unwrap()?;
1117        assert_eq!(result.num_rows(), 100);
1118
1119        // Next batch should go to a new file
1120        let batch2 = create_test_batch(100, 10);
1121        writer.push_batch(&batch2)?;
1122
1123        // Check metrics after second batch - should have rotated to a new file
1124        assert_eq!(
1125            metrics.spill_file_count.value(),
1126            2,
1127            "Should have created 2 files after rotation"
1128        );
1129        assert_eq!(
1130            metrics.spilled_rows.value(),
1131            110,
1132            "Should have spilled 110 total rows (100 + 10)"
1133        );
1134
1135        let result2 = reader.next().await.unwrap()?;
1136        assert_eq!(result2.num_rows(), 10);
1137
1138        Ok(())
1139    }
1140
1141    #[tokio::test]
1142    async fn test_very_small_max_file_size() -> Result<()> {
1143        // Test with just 1 byte max (extreme case)
1144        let (writer, mut reader) = create_spill_channel(1);
1145
1146        // Any batch will exceed this limit
1147        let batch = create_test_batch(0, 5);
1148        writer.push_batch(&batch)?;
1149
1150        // Should still work
1151        let result = reader.next().await.unwrap()?;
1152        assert_eq!(result.num_rows(), 5);
1153
1154        Ok(())
1155    }
1156
1157    #[tokio::test]
1158    async fn test_exact_size_boundary() -> Result<()> {
1159        // Create a batch and measure its approximate size
1160        let batch = create_test_batch(0, 10);
1161        let batch_size = batch.get_array_memory_size();
1162
1163        // Set max_file_size to exactly the batch size
1164        let (writer, mut reader, metrics) = create_spill_channel_with_metrics(batch_size);
1165
1166        // Write first batch (exactly at the size limit)
1167        writer.push_batch(&batch)?;
1168
1169        // Check metrics after first batch - should NOT rotate yet (size == limit, not >)
1170        assert_eq!(
1171            metrics.spill_file_count.value(),
1172            1,
1173            "Should have created 1 file after first batch at exact boundary"
1174        );
1175        assert_eq!(
1176            metrics.spilled_rows.value(),
1177            10,
1178            "Should have spilled 10 rows from first batch"
1179        );
1180
1181        // Write second batch (exceeds the limit, should trigger rotation)
1182        let batch2 = create_test_batch(10, 10);
1183        writer.push_batch(&batch2)?;
1184
1185        // Check metrics after second batch - rotation triggered, first file finalized
1186        // Note: second file not created yet (lazy creation on next write)
1187        assert_eq!(
1188            metrics.spill_file_count.value(),
1189            1,
1190            "Should still have 1 file after rotation (second file created lazily)"
1191        );
1192        assert_eq!(
1193            metrics.spilled_rows.value(),
1194            20,
1195            "Should have spilled 20 total rows (10 + 10)"
1196        );
1197        // Verify first file was finalized by checking spilled_bytes
1198        assert!(
1199            metrics.spilled_bytes.value() > 0,
1200            "Spilled bytes should be > 0 after file finalization (got {})",
1201            metrics.spilled_bytes.value()
1202        );
1203
1204        // Both should be readable
1205        let result1 = reader.next().await.unwrap()?;
1206        assert_eq!(result1.num_rows(), 10);
1207
1208        let result2 = reader.next().await.unwrap()?;
1209        assert_eq!(result2.num_rows(), 10);
1210
1211        // Spill another batch, now we should see the second file created
1212        let batch3 = create_test_batch(20, 5);
1213        writer.push_batch(&batch3)?;
1214        assert_eq!(
1215            metrics.spill_file_count.value(),
1216            2,
1217            "Should have created 2 files after writing to new file"
1218        );
1219
1220        Ok(())
1221    }
1222
1223    #[tokio::test]
1224    async fn test_concurrent_reader_writer() -> Result<()> {
1225        let (writer, mut reader) = create_spill_channel(1024 * 1024);
1226
1227        // Spawn writer task
1228        let writer_handle = SpawnedTask::spawn(async move {
1229            for i in 0..10 {
1230                let batch = create_test_batch(i * 10, 10);
1231                writer.push_batch(&batch).unwrap();
1232                // Small delay to simulate real concurrent work
1233                tokio::time::sleep(std::time::Duration::from_millis(5)).await;
1234            }
1235        });
1236
1237        // Reader task (runs concurrently)
1238        let reader_handle = SpawnedTask::spawn(async move {
1239            let mut count = 0;
1240            for i in 0..10 {
1241                let result = reader.next().await.unwrap().unwrap();
1242                assert_eq!(result.num_rows(), 10);
1243
1244                let col = result
1245                    .column(0)
1246                    .as_any()
1247                    .downcast_ref::<Int32Array>()
1248                    .unwrap();
1249                assert_eq!(col.value(0), i * 10);
1250                count += 1;
1251            }
1252            count
1253        });
1254
1255        // Wait for both to complete
1256        writer_handle.await.unwrap();
1257        let batches_read = reader_handle.await.unwrap();
1258        assert_eq!(batches_read, 10);
1259
1260        Ok(())
1261    }
1262
1263    #[tokio::test(flavor = "multi_thread", worker_threads = 10)]
1264    async fn test_concurrent_writers() -> Result<()> {
1265        let (writer, mut reader) = create_shared_spill_channel(1024 * 1024);
1266
1267        // Spawn writer tasks
1268        let mut writer_join_set = JoinSet::new();
1269        for w in 0..10 {
1270            let writer = writer.clone();
1271            writer_join_set.spawn(async move {
1272                for b in 0..10 {
1273                    let batch = create_test_batch((w * 100) + (b * 10), 10);
1274                    writer.push_batch(&batch).unwrap();
1275                }
1276            });
1277        }
1278        drop(writer);
1279
1280        // Reader task (runs concurrently)
1281        let reader_handle = SpawnedTask::spawn(async move {
1282            let mut batch_order = vec![];
1283            loop {
1284                match reader.next().await {
1285                    None => break,
1286                    Some(batch) => {
1287                        let batch = batch.unwrap();
1288
1289                        assert_eq!(batch.num_rows(), 10);
1290
1291                        let col = batch
1292                            .column(0)
1293                            .as_any()
1294                            .downcast_ref::<Int32Array>()
1295                            .unwrap();
1296                        batch_order.push(col.value(0) / 10);
1297                    }
1298                }
1299            }
1300            batch_order
1301        });
1302
1303        // Wait for both to complete
1304        writer_join_set.join_all().await;
1305        let mut batch_order = reader_handle.await.unwrap();
1306
1307        // When used with multiple writers, order is not guaranteed
1308        batch_order.sort();
1309        assert_eq!(batch_order, (0i32..100i32).collect::<Vec<_>>());
1310
1311        Ok(())
1312    }
1313
1314    #[tokio::test]
1315    async fn test_reader_catches_up_to_writer() -> Result<()> {
1316        let (writer, mut reader) = create_spill_channel(1024 * 1024);
1317
1318        let (reader_waiting_tx, reader_waiting_rx) = tokio::sync::oneshot::channel();
1319        let (first_read_done_tx, first_read_done_rx) = tokio::sync::oneshot::channel();
1320
1321        #[derive(Clone, Copy, Debug, PartialEq, Eq)]
1322        enum ReadWriteEvent {
1323            ReadStart,
1324            Read(usize),
1325            Write(usize),
1326        }
1327
1328        let events = Arc::new(Mutex::new(vec![]));
1329        // Start reader first (will pend)
1330        let reader_events = Arc::clone(&events);
1331        let reader_handle = SpawnedTask::spawn(async move {
1332            reader_events.lock().push(ReadWriteEvent::ReadStart);
1333            reader_waiting_tx
1334                .send(())
1335                .expect("reader_waiting channel closed unexpectedly");
1336            let result = reader.next().await.unwrap().unwrap();
1337            reader_events
1338                .lock()
1339                .push(ReadWriteEvent::Read(result.num_rows()));
1340            first_read_done_tx
1341                .send(())
1342                .expect("first_read_done channel closed unexpectedly");
1343            let result = reader.next().await.unwrap().unwrap();
1344            reader_events
1345                .lock()
1346                .push(ReadWriteEvent::Read(result.num_rows()));
1347        });
1348
1349        // Wait until the reader is pending on the first batch
1350        reader_waiting_rx
1351            .await
1352            .expect("reader should signal when waiting");
1353
1354        // Now write a batch (should wake the reader)
1355        let batch = create_test_batch(0, 5);
1356        events.lock().push(ReadWriteEvent::Write(batch.num_rows()));
1357        writer.push_batch(&batch)?;
1358
1359        // Wait for the reader to finish the first read before allowing the
1360        // second write. This ensures deterministic ordering of events:
1361        // 1. The reader starts and pends on the first `next()`
1362        // 2. The first write wakes the reader
1363        // 3. The reader processes the first batch and signals completion
1364        // 4. The second write is issued, ensuring consistent event ordering
1365        first_read_done_rx
1366            .await
1367            .expect("reader should signal when first read completes");
1368
1369        // Write another batch
1370        let batch = create_test_batch(5, 10);
1371        events.lock().push(ReadWriteEvent::Write(batch.num_rows()));
1372        writer.push_batch(&batch)?;
1373
1374        // Reader should complete
1375        reader_handle.await.unwrap();
1376        let events = events.lock().clone();
1377        assert_eq!(
1378            events,
1379            vec![
1380                ReadWriteEvent::ReadStart,
1381                ReadWriteEvent::Write(5),
1382                ReadWriteEvent::Read(5),
1383                ReadWriteEvent::Write(10),
1384                ReadWriteEvent::Read(10)
1385            ]
1386        );
1387
1388        Ok(())
1389    }
1390
1391    #[tokio::test]
1392    async fn test_reader_starts_after_writer_finishes() -> Result<()> {
1393        let (writer, reader) = create_spill_channel(128);
1394
1395        // Writer writes all data
1396        for i in 0..5 {
1397            let batch = create_test_batch(i * 10, 10);
1398            writer.push_batch(&batch)?;
1399        }
1400
1401        drop(writer);
1402
1403        // Now start reader
1404        let mut reader = reader;
1405        let mut count = 0;
1406        for i in 0..5 {
1407            let result = reader.next().await.unwrap()?;
1408            assert_eq!(result.num_rows(), 10);
1409
1410            let col = result
1411                .column(0)
1412                .as_any()
1413                .downcast_ref::<Int32Array>()
1414                .unwrap();
1415            assert_eq!(col.value(0), i * 10);
1416            count += 1;
1417        }
1418
1419        assert_eq!(count, 5, "Should read all batches after writer finishes");
1420
1421        Ok(())
1422    }
1423
1424    #[tokio::test]
1425    async fn test_writer_drop_finalizes_file() -> Result<()> {
1426        let env = Arc::new(RuntimeEnv::default());
1427        let metrics = SpillMetrics::new(&ExecutionPlanMetricsSet::new(), 0);
1428        let schema = create_test_schema();
1429        let spill_manager =
1430            Arc::new(SpillManager::new(Arc::clone(&env), metrics.clone(), schema));
1431
1432        let (writer, mut reader) = spsc_channel(1024 * 1024, spill_manager);
1433
1434        // Write some batches
1435        for i in 0..5 {
1436            let batch = create_test_batch(i * 10, 10);
1437            writer.push_batch(&batch)?;
1438        }
1439
1440        // Check metrics before drop - spilled_bytes already reflects written data
1441        let spilled_bytes_before = metrics.spilled_bytes.value();
1442        assert_eq!(
1443            spilled_bytes_before, 1088,
1444            "Spilled bytes should reflect data written (header + 5 batches)"
1445        );
1446
1447        // Explicitly drop the writer - this should finalize the current file
1448        drop(writer);
1449
1450        // Check metrics after drop - spilled_bytes should be > 0 now
1451        let spilled_bytes_after = metrics.spilled_bytes.value();
1452        assert!(
1453            spilled_bytes_after > 0,
1454            "Spilled bytes should be > 0 after writer is dropped (got {spilled_bytes_after})"
1455        );
1456
1457        // Verify reader can still read all batches
1458        let mut count = 0;
1459        for i in 0..5 {
1460            let result = reader.next().await.unwrap()?;
1461            assert_eq!(result.num_rows(), 10);
1462
1463            let col = result
1464                .column(0)
1465                .as_any()
1466                .downcast_ref::<Int32Array>()
1467                .unwrap();
1468            assert_eq!(col.value(0), i * 10);
1469            count += 1;
1470        }
1471
1472        assert_eq!(count, 5, "Should read all batches after writer is dropped");
1473
1474        Ok(())
1475    }
1476
1477    /// Verifies that the reader stays alive as long as any writer clone exists.
1478    ///
1479    /// `SpillPoolWriter` is `Clone`, and in non-preserve-order repartitioning
1480    /// mode multiple input partition tasks share clones of the same writer.
1481    /// The reader must not see EOF until **all** clones have been dropped,
1482    /// even if the queue is temporarily empty between writes from different
1483    /// clones.
1484    ///
1485    /// The test sequence is:
1486    ///
1487    /// 1. writer1 writes a batch, then is dropped.
1488    /// 2. The reader consumes that batch (queue is now empty).
1489    /// 3. writer2 (still alive) writes a batch.
1490    /// 4. The reader must see that batch.
1491    /// 5. EOF is only signalled after writer2 is also dropped.
1492    #[tokio::test]
1493    async fn test_clone_drop_does_not_signal_eof_prematurely() -> Result<()> {
1494        let (writer1, mut reader) = create_shared_spill_channel(1024 * 1024);
1495        let writer2 = writer1.clone();
1496
1497        // Synchronization: tell writer2 when it may proceed.
1498        let (proceed_tx, proceed_rx) = tokio::sync::oneshot::channel::<()>();
1499
1500        // Spawn writer2 — it waits for the signal before writing.
1501        let writer2_handle = SpawnedTask::spawn(async move {
1502            proceed_rx.await.unwrap();
1503            writer2.push_batch(&create_test_batch(10, 10)).unwrap();
1504            // writer2 is dropped here (last clone → true EOF)
1505        });
1506
1507        // Writer1 writes one batch, then drops.
1508        writer1.push_batch(&create_test_batch(0, 10))?;
1509        drop(writer1);
1510
1511        // Read writer1's batch.
1512        let batch1 = reader.next().await.unwrap()?;
1513        assert_eq!(batch1.num_rows(), 10);
1514        let col = batch1
1515            .column(0)
1516            .as_any()
1517            .downcast_ref::<Int32Array>()
1518            .unwrap();
1519        assert_eq!(col.value(0), 0);
1520
1521        // Signal writer2 to write its batch. It will execute when the
1522        // current task yields (i.e. when reader.next() returns Pending).
1523        proceed_tx.send(()).unwrap();
1524
1525        // The reader should wait (Pending) for writer2's data, not EOF.
1526        let batch2 =
1527            tokio::time::timeout(std::time::Duration::from_secs(5), reader.next())
1528                .await
1529                .expect("Reader timed out — should not hang");
1530
1531        assert!(
1532            batch2.is_some(),
1533            "Reader must not return EOF while a writer clone is still alive"
1534        );
1535        let batch2 = batch2.unwrap()?;
1536        assert_eq!(batch2.num_rows(), 10);
1537        let col = batch2
1538            .column(0)
1539            .as_any()
1540            .downcast_ref::<Int32Array>()
1541            .unwrap();
1542        assert_eq!(col.value(0), 10);
1543
1544        writer2_handle.await.unwrap();
1545
1546        // All writers dropped — reader should see real EOF now.
1547        assert!(reader.next().await.is_none());
1548
1549        Ok(())
1550    }
1551
1552    #[tokio::test]
1553    async fn test_disk_usage_decreases_as_files_consumed() -> Result<()> {
1554        use datafusion_execution::runtime_env::RuntimeEnvBuilder;
1555
1556        // Test configuration
1557        const NUM_BATCHES: usize = 3;
1558        const ROWS_PER_BATCH: usize = 100;
1559
1560        // Step 1: Create a test batch and measure its size
1561        let batch = create_test_batch(0, ROWS_PER_BATCH);
1562        let batch_size = batch.get_array_memory_size();
1563
1564        // Step 2: Configure file rotation to approximately 1 batch per file
1565        // Create a custom RuntimeEnv so we can access the DiskManager
1566        let runtime = Arc::new(RuntimeEnvBuilder::default().build()?);
1567        let disk_manager = Arc::clone(&runtime.disk_manager);
1568
1569        let metrics = SpillMetrics::new(&ExecutionPlanMetricsSet::new(), 0);
1570        let schema = create_test_schema();
1571        let spill_manager = Arc::new(SpillManager::new(runtime, metrics.clone(), schema));
1572
1573        let (writer, mut reader) = spsc_channel(batch_size - 1, spill_manager);
1574
1575        // Step 3: Write NUM_BATCHES batches to create approximately NUM_BATCHES files
1576        for i in 0..NUM_BATCHES {
1577            let start = (i * ROWS_PER_BATCH) as i32;
1578            writer.push_batch(&create_test_batch(start, ROWS_PER_BATCH))?;
1579        }
1580
1581        // Check how many files were created (should be at least a few due to file rotation)
1582        let file_count = metrics.spill_file_count.value();
1583        assert_eq!(
1584            file_count, NUM_BATCHES,
1585            "Expected at {NUM_BATCHES} files with rotation, got {file_count}"
1586        );
1587
1588        // Step 4: Verify initial disk usage reflects all files
1589        let initial_disk_usage = disk_manager.used_disk_space();
1590        assert!(
1591            initial_disk_usage > 0,
1592            "Expected disk usage > 0 after writing batches, got {initial_disk_usage}"
1593        );
1594
1595        // Step 5: Read NUM_BATCHES - 1 batches (all but 1)
1596        // As each file is fully consumed, it should be dropped and disk usage should decrease
1597        for i in 0..(NUM_BATCHES - 1) {
1598            let result = reader.next().await.unwrap()?;
1599            assert_eq!(result.num_rows(), ROWS_PER_BATCH);
1600
1601            let col = result
1602                .column(0)
1603                .as_any()
1604                .downcast_ref::<Int32Array>()
1605                .unwrap();
1606            assert_eq!(col.value(0), (i * ROWS_PER_BATCH) as i32);
1607        }
1608
1609        // Step 6: Verify disk usage decreased but is not zero (at least 1 batch remains)
1610        let partial_disk_usage = disk_manager.used_disk_space();
1611        assert!(
1612            partial_disk_usage > 0
1613                && partial_disk_usage < (batch_size * NUM_BATCHES * 2) as u64,
1614            "Disk usage should be > 0 with remaining batches"
1615        );
1616        assert!(
1617            partial_disk_usage < initial_disk_usage,
1618            "Disk usage should have decreased after reading most batches: initial={initial_disk_usage}, partial={partial_disk_usage}"
1619        );
1620
1621        // Step 7: Read the final batch
1622        let result = reader.next().await.unwrap()?;
1623        assert_eq!(result.num_rows(), ROWS_PER_BATCH);
1624
1625        // Step 8: Drop writer first to signal no more data will be written
1626        // The reader has infinite stream semantics and will wait for the writer
1627        // to be dropped before returning None
1628        drop(writer);
1629
1630        // Verify we've read all batches - now the reader should return None
1631        assert!(
1632            reader.next().await.is_none(),
1633            "Should have no more batches to read"
1634        );
1635
1636        // Step 9: Drop reader to release all references
1637        drop(reader);
1638
1639        // Step 10: Verify complete cleanup - disk usage should be 0
1640        let final_disk_usage = disk_manager.used_disk_space();
1641        assert_eq!(
1642            final_disk_usage, 0,
1643            "Disk usage should be 0 after all files dropped, got {final_disk_usage}"
1644        );
1645
1646        Ok(())
1647    }
1648}