This is an automated email from the ASF dual-hosted git repository.

andygrove pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/datafusion-comet.git


The following commit(s) were added to refs/heads/main by this push:
     new ff41f9648e refactor(shuffle): Introduce PartitionWriter interface to 
decouple shuffle partitioning from storage (#4779)
ff41f9648e is described below

commit ff41f9648efc252e015164ac1dd7561133aee29e
Author: Zhen Wang <[email protected]>
AuthorDate: Fri Jul 10 04:18:46 2026 +0800

    refactor(shuffle): Introduce PartitionWriter interface to decouple shuffle 
partitioning from storage (#4779)
---
 native/shuffle/src/partitioners/empty_schema.rs    |  71 ++---
 native/shuffle/src/partitioners/mod.rs             |   1 -
 native/shuffle/src/partitioners/multi_partition.rs | 150 ++---------
 .../src/partitioners/partitioned_batch_iterator.rs |  23 +-
 .../shuffle/src/partitioners/single_partition.rs   |  83 ++----
 native/shuffle/src/shuffle_writer.rs               |  81 +++---
 .../src/writers/local/local_partition_writer.rs    | 296 +++++++++++++++++++++
 native/shuffle/src/writers/{ => local}/mod.rs      |   9 +-
 native/shuffle/src/writers/{ => local}/spill.rs    |  96 ++++---
 native/shuffle/src/writers/mod.rs                  |   6 +-
 native/shuffle/src/writers/partition_writer.rs     |  71 +++++
 11 files changed, 544 insertions(+), 343 deletions(-)

diff --git a/native/shuffle/src/partitioners/empty_schema.rs 
b/native/shuffle/src/partitioners/empty_schema.rs
index 45decfec05..2901b82e65 100644
--- a/native/shuffle/src/partitioners/empty_schema.rs
+++ b/native/shuffle/src/partitioners/empty_schema.rs
@@ -17,47 +17,38 @@
 
 use crate::metrics::ShufflePartitionerMetrics;
 use crate::partitioners::ShufflePartitioner;
-use crate::ShuffleBlockWriter;
+use crate::writers::PartitionWriter;
 use arrow::array::RecordBatch;
 use arrow::datatypes::SchemaRef;
-use datafusion::common::DataFusionError;
-use std::fs::OpenOptions;
-use std::io::{BufWriter, Seek, Write};
+use std::iter;
 use tokio::time::Instant;
 
 /// A partitioner for zero-column schemas (e.g. queries where ColumnPruning 
removes all columns).
 /// This handles shuffles for operations like COUNT(*) that produce 
empty-schema record batches
 /// but contain a valid row count. Accumulates the total row count and writes 
a single
 /// zero-column IPC batch to partition 0. All other partitions get empty 
entries in the index file.
-pub(crate) struct EmptySchemaShufflePartitioner {
-    output_data_file: String,
-    output_index_file: String,
+pub(crate) struct EmptySchemaShufflePartitioner<T: PartitionWriter> {
+    partition_writer: T,
     schema: SchemaRef,
-    shuffle_block_writer: ShuffleBlockWriter,
     num_output_partitions: usize,
     total_rows: usize,
     metrics: ShufflePartitionerMetrics,
 }
 
-impl EmptySchemaShufflePartitioner {
+impl<T: PartitionWriter> EmptySchemaShufflePartitioner<T> {
     pub(crate) fn try_new(
-        output_data_file: String,
-        output_index_file: String,
+        partition_writer: T,
         schema: SchemaRef,
         num_output_partitions: usize,
         metrics: ShufflePartitionerMetrics,
-        codec: crate::CompressionCodec,
     ) -> datafusion::common::Result<Self> {
         debug_assert!(
             schema.fields().is_empty(),
             "EmptySchemaShufflePartitioner requires a zero-column schema"
         );
-        let shuffle_block_writer = 
ShuffleBlockWriter::try_new(schema.as_ref(), codec)?;
         Ok(Self {
-            output_data_file,
-            output_index_file,
+            partition_writer,
             schema,
-            shuffle_block_writer,
             num_output_partitions,
             total_rows: 0,
             metrics,
@@ -66,7 +57,7 @@ impl EmptySchemaShufflePartitioner {
 }
 
 #[async_trait::async_trait]
-impl ShufflePartitioner for EmptySchemaShufflePartitioner {
+impl<T: PartitionWriter> ShufflePartitioner for 
EmptySchemaShufflePartitioner<T> {
     async fn insert_batch(&mut self, batch: RecordBatch) -> 
datafusion::common::Result<()> {
         let start_time = Instant::now();
         let num_rows = batch.num_rows();
@@ -85,46 +76,24 @@ impl ShufflePartitioner for EmptySchemaShufflePartitioner {
     fn shuffle_write(&mut self) -> datafusion::common::Result<()> {
         let start_time = Instant::now();
 
-        let output_data = OpenOptions::new()
-            .write(true)
-            .create(true)
-            .truncate(true)
-            .open(&self.output_data_file)
-            .map_err(|e| DataFusionError::Execution(format!("shuffle write 
error: {e:?}")))?;
-        let mut output_data = BufWriter::new(output_data);
-
         // Write a single zero-column batch with the accumulated row count to 
partition 0
-        if self.total_rows > 0 {
-            let batch = RecordBatch::try_new_with_options(
+        let batch_opt = if self.total_rows > 0 {
+            Some(Ok(RecordBatch::try_new_with_options(
                 self.schema.clone(),
                 vec![],
                 
&arrow::array::RecordBatchOptions::new().with_row_count(Some(self.total_rows)),
-            )?;
-            self.shuffle_block_writer.write_batch(
-                &batch,
-                &mut output_data,
-                &self.metrics.encode_time,
-            )?;
-        }
-
-        let mut write_timer = self.metrics.write_time.timer();
-        output_data.flush()?;
-        let data_file_length = output_data.stream_position()?;
+            )?))
+        } else {
+            None
+        };
 
-        // Write index file: partition 0 spans [0, data_file_length), all 
others are empty
-        let index_file = OpenOptions::new()
-            .write(true)
-            .create(true)
-            .truncate(true)
-            .open(&self.output_index_file)
-            .map_err(|e| DataFusionError::Execution(format!("shuffle write 
error: {e:?}")))?;
-        let mut index_writer = BufWriter::new(index_file);
-        index_writer.write_all(&0i64.to_le_bytes())?;
-        for _ in 0..self.num_output_partitions {
-            index_writer.write_all(&(data_file_length as i64).to_le_bytes())?;
+        self.partition_writer
+            .finish_partition(0, &mut batch_opt.into_iter(), &self.metrics)?;
+        for pid in 1..self.num_output_partitions {
+            self.partition_writer
+                .finish_partition(pid, &mut iter::empty(), &self.metrics)?;
         }
-        index_writer.flush()?;
-        write_timer.stop();
+        self.partition_writer.finish_all(&self.metrics)?;
 
         self.metrics
             .baseline
diff --git a/native/shuffle/src/partitioners/mod.rs 
b/native/shuffle/src/partitioners/mod.rs
index a0bc652b4b..8f4239a820 100644
--- a/native/shuffle/src/partitioners/mod.rs
+++ b/native/shuffle/src/partitioners/mod.rs
@@ -23,6 +23,5 @@ mod traits;
 
 pub(crate) use empty_schema::EmptySchemaShufflePartitioner;
 pub(crate) use multi_partition::MultiPartitionShuffleRepartitioner;
-pub(crate) use partitioned_batch_iterator::PartitionedBatchIterator;
 pub(crate) use single_partition::SinglePartitionShufflePartitioner;
 pub(crate) use traits::ShufflePartitioner;
diff --git a/native/shuffle/src/partitioners/multi_partition.rs 
b/native/shuffle/src/partitioners/multi_partition.rs
index efdbb970aa..be2d977e83 100644
--- a/native/shuffle/src/partitioners/multi_partition.rs
+++ b/native/shuffle/src/partitioners/multi_partition.rs
@@ -16,26 +16,20 @@
 // under the License.
 
 use crate::metrics::ShufflePartitionerMetrics;
-use crate::partitioners::partitioned_batch_iterator::{
-    PartitionedBatchIterator, PartitionedBatchesProducer,
-};
+use 
crate::partitioners::partitioned_batch_iterator::PartitionedBatchesProducer;
 use crate::partitioners::ShufflePartitioner;
-use crate::writers::{BufBatchWriter, PartitionWriter};
-use crate::{comet_partitioning, CometPartitioning, CompressionCodec, 
ShuffleBlockWriter};
+use crate::writers::PartitionWriter;
+use crate::{comet_partitioning, CometPartitioning};
 use arrow::array::{Array, ArrayData, ArrayRef, RecordBatch};
-use arrow::datatypes::SchemaRef;
 use datafusion::common::utils::proxy::VecAllocExt;
 use datafusion::common::{DataFusionError, HashSet};
 use datafusion::execution::memory_pool::{MemoryConsumer, MemoryReservation};
 use datafusion::execution::runtime_env::RuntimeEnv;
-use datafusion::physical_plan::metrics::Time;
 use datafusion_comet_common::tracing::{with_trace, with_trace_async};
 use datafusion_comet_spark_expr::murmur3::create_murmur3_hashes;
 use itertools::Itertools;
 use std::fmt;
 use std::fmt::{Debug, Formatter};
-use std::fs::{File, OpenOptions};
-use std::io::{BufWriter, Seek, Write};
 use std::sync::Arc;
 use tokio::time::Instant;
 
@@ -105,16 +99,12 @@ impl ScratchSpace {
 }
 
 /// A partitioner that uses a hash function to partition data into multiple 
partitions
-pub(crate) struct MultiPartitionShuffleRepartitioner {
-    output_data_file: String,
-    output_index_file: String,
+pub(crate) struct MultiPartitionShuffleRepartitioner<T: PartitionWriter> {
     buffered_batches: Vec<RecordBatch>,
     partition_indices: Vec<Vec<(u32, u32)>>,
-    partition_writers: Vec<PartitionWriter>,
-    shuffle_block_writer: ShuffleBlockWriter,
+    partition_writer: T,
     /// Partitioning scheme to use
     partitioning: CometPartitioning,
-    runtime: Arc<RuntimeEnv>,
     metrics: ShufflePartitionerMetrics,
     /// Reused scratch space for computing partition indices
     scratch: ScratchSpace,
@@ -123,8 +113,6 @@ pub(crate) struct MultiPartitionShuffleRepartitioner {
     /// Reservation for repartitioning
     reservation: MemoryReservation,
     tracing_enabled: bool,
-    /// Size of the write buffer in bytes
-    write_buffer_size: usize,
     /// Start addresses (as `usize`, since raw pointers are not `Send`) of the 
backing buffers
     /// currently pinned by `buffered_batches`, so the spill reservation 
charges each distinct
     /// allocation once rather than once per slice that references it. Cleared 
whenever the
@@ -176,20 +164,16 @@ fn count_new_buffers(batch: &RecordBatch, seen: &mut 
HashSet<usize>) -> usize {
     total
 }
 
-impl MultiPartitionShuffleRepartitioner {
+impl<T: PartitionWriter> MultiPartitionShuffleRepartitioner<T> {
     #[allow(clippy::too_many_arguments)]
     pub(crate) fn try_new(
         partition: usize,
-        output_data_file: String,
-        output_index_file: String,
-        schema: SchemaRef,
+        partition_writer: T,
         partitioning: CometPartitioning,
         metrics: ShufflePartitionerMetrics,
         runtime: Arc<RuntimeEnv>,
         batch_size: usize,
-        codec: CompressionCodec,
         tracing_enabled: bool,
-        write_buffer_size: usize,
     ) -> datafusion::common::Result<Self> {
         let num_output_partitions = partitioning.partition_count();
         assert_ne!(
@@ -214,31 +198,20 @@ impl MultiPartitionShuffleRepartitioner {
             partition_starts: vec![0; num_output_partitions + 1],
         };
 
-        let shuffle_block_writer = 
ShuffleBlockWriter::try_new(schema.as_ref(), codec.clone())?;
-
-        let partition_writers = (0..num_output_partitions)
-            .map(|_| PartitionWriter::try_new(shuffle_block_writer.clone()))
-            .collect::<datafusion::common::Result<Vec<_>>>()?;
-
         let reservation = 
MemoryConsumer::new(format!("ShuffleRepartitioner[{partition}]"))
             .with_can_spill(true)
             .register(&runtime.memory_pool);
 
         Ok(Self {
-            output_data_file,
-            output_index_file,
             buffered_batches: vec![],
             partition_indices: vec![vec![]; num_output_partitions],
-            partition_writers,
-            shuffle_block_writer,
+            partition_writer,
             partitioning,
-            runtime,
             metrics,
             scratch,
             batch_size,
             reservation,
             tracing_enabled,
-            write_buffer_size,
             pinned_buffers: HashSet::new(),
         })
     }
@@ -485,31 +458,6 @@ impl MultiPartitionShuffleRepartitioner {
         Ok(())
     }
 
-    #[allow(clippy::too_many_arguments)]
-    fn shuffle_write_partition(
-        partition_iter: &mut PartitionedBatchIterator,
-        shuffle_block_writer: &mut ShuffleBlockWriter,
-        output_data: &mut BufWriter<File>,
-        interleave_time: &Time,
-        encode_time: &Time,
-        write_time: &Time,
-        write_buffer_size: usize,
-        batch_size: usize,
-    ) -> datafusion::common::Result<()> {
-        let mut buf_batch_writer = BufBatchWriter::new(
-            shuffle_block_writer,
-            output_data,
-            write_buffer_size,
-            batch_size,
-        );
-        while let Some(batch) = partition_iter.next(interleave_time) {
-            let batch = batch?;
-            buf_batch_writer.write(&batch, encode_time, write_time)?;
-        }
-        buf_batch_writer.flush(encode_time, write_time)?;
-        Ok(())
-    }
-
     fn used(&self) -> usize {
         self.reservation.size()
     }
@@ -553,38 +501,32 @@ impl MultiPartitionShuffleRepartitioner {
         }
 
         with_trace("shuffle_spill", self.tracing_enabled, || {
-            let num_output_partitions = self.partition_writers.len();
+            let num_output_partitions = self.partition_indices.len();
             let mut partitioned_batches = self.partitioned_batches();
-            let mut spilled_bytes = 0;
 
             for partition_id in 0..num_output_partitions {
-                let partition_writer = &mut 
self.partition_writers[partition_id];
-                let mut iter = partitioned_batches.produce(partition_id);
-                spilled_bytes += partition_writer.spill(
-                    &mut iter,
-                    &self.runtime,
+                self.partition_writer.write(
+                    partition_id,
+                    &mut partitioned_batches.produce(partition_id, 
&self.metrics.interleave_time),
                     &self.metrics,
-                    self.write_buffer_size,
-                    self.batch_size,
                 )?;
             }
 
             self.reservation.free();
             self.pinned_buffers.clear();
             self.metrics.spill_count.add(1);
-            self.metrics.spilled_bytes.add(spilled_bytes);
             Ok(())
         })
     }
 
     #[cfg(test)]
-    pub(crate) fn partition_writers(&self) -> &[PartitionWriter] {
-        &self.partition_writers
+    pub(crate) fn partition_writer(&self) -> &T {
+        &self.partition_writer
     }
 }
 
 #[async_trait::async_trait]
-impl ShufflePartitioner for MultiPartitionShuffleRepartitioner {
+impl<T: PartitionWriter> ShufflePartitioner for 
MultiPartitionShuffleRepartitioner<T> {
     /// Shuffles rows in input batch into corresponding partition buffer.
     /// This function will slice input batch according to configured batch 
size and then
     /// shuffle rows into corresponding partition buffer.
@@ -616,78 +558,28 @@ impl ShufflePartitioner for 
MultiPartitionShuffleRepartitioner {
             let mut partitioned_batches = self.partitioned_batches();
             self.pinned_buffers.clear();
             let num_output_partitions = self.partition_indices.len();
-            let mut offsets = vec![0; num_output_partitions + 1];
-
-            let data_file = self.output_data_file.clone();
-            let index_file = self.output_index_file.clone();
-
-            let output_data = OpenOptions::new()
-                .write(true)
-                .create(true)
-                .truncate(true)
-                .open(data_file)
-                .map_err(|e| DataFusionError::Execution(format!("shuffle write 
error: {e:?}")))?;
-
-            let mut output_data = 
BufWriter::with_capacity(self.write_buffer_size, output_data);
 
             #[allow(clippy::needless_range_loop)]
             for i in 0..num_output_partitions {
-                offsets[i] = output_data.stream_position()?;
-
-                // if we wrote a spill file for this partition then copy the
-                // contents into the shuffle file
-                if let Some(spill_path) = self.partition_writers[i].path() {
-                    // Use raw File handle (not BufReader) so that 
std::io::copy
-                    // can use copy_file_range/sendfile for zero-copy on Linux.
-                    let mut spill_file = File::open(spill_path)?;
-                    let mut write_timer = self.metrics.write_time.timer();
-                    std::io::copy(&mut spill_file, &mut output_data)?;
-                    write_timer.stop();
-                }
-
-                // Write in memory batches to output data file
-                let mut partition_iter = partitioned_batches.produce(i);
-                Self::shuffle_write_partition(
-                    &mut partition_iter,
-                    &mut self.shuffle_block_writer,
-                    &mut output_data,
-                    &self.metrics.interleave_time,
-                    &self.metrics.encode_time,
-                    &self.metrics.write_time,
-                    self.write_buffer_size,
-                    self.batch_size,
+                self.partition_writer.finish_partition(
+                    i,
+                    &mut partitioned_batches.produce(i, 
&self.metrics.interleave_time),
+                    &self.metrics,
                 )?;
             }
 
-            let mut write_timer = self.metrics.write_time.timer();
-            output_data.flush()?;
-            write_timer.stop();
-
-            // add one extra offset at last to ease partition length 
computation
-            offsets[num_output_partitions] = output_data.stream_position()?;
-
-            let mut write_timer = self.metrics.write_time.timer();
-            let mut output_index =
-                BufWriter::new(File::create(index_file).map_err(|e| {
-                    DataFusionError::Execution(format!("shuffle write error: 
{e:?}"))
-                })?);
-            for offset in offsets {
-                output_index.write_all(&(offset as i64).to_le_bytes()[..])?;
-            }
-            output_index.flush()?;
-            write_timer.stop();
+            self.partition_writer.finish_all(&self.metrics)?;
 
             self.metrics
                 .baseline
                 .elapsed_compute()
                 .add_duration(start_time.elapsed());
-
             Ok(())
         })
     }
 }
 
-impl Debug for MultiPartitionShuffleRepartitioner {
+impl<T: PartitionWriter> Debug for MultiPartitionShuffleRepartitioner<T> {
     fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
         f.debug_struct("ShuffleRepartitioner")
             .field("memory_used", &self.used())
diff --git a/native/shuffle/src/partitioners/partitioned_batch_iterator.rs 
b/native/shuffle/src/partitioners/partitioned_batch_iterator.rs
index c7f1781866..f124d98ff2 100644
--- a/native/shuffle/src/partitioners/partitioned_batch_iterator.rs
+++ b/native/shuffle/src/partitioners/partitioned_batch_iterator.rs
@@ -42,11 +42,16 @@ impl PartitionedBatchesProducer {
         }
     }
 
-    pub(super) fn produce(&mut self, partition_id: usize) -> 
PartitionedBatchIterator<'_> {
+    pub(super) fn produce<'a>(
+        &'a mut self,
+        partition_id: usize,
+        interleave_time: &'a Time,
+    ) -> PartitionedBatchIterator<'a> {
         PartitionedBatchIterator::new(
             &self.partition_indices[partition_id],
             &self.buffered_batches,
             self.batch_size,
+            interleave_time,
         )
     }
 }
@@ -57,6 +62,7 @@ pub(crate) struct PartitionedBatchIterator<'a> {
     batch_size: usize,
     indices: Vec<(usize, usize)>,
     pos: usize,
+    interleave_time: &'a Time,
 }
 
 impl<'a> PartitionedBatchIterator<'a> {
@@ -64,6 +70,7 @@ impl<'a> PartitionedBatchIterator<'a> {
         indices: &'a [(u32, u32)],
         buffered_batches: &'a [RecordBatch],
         batch_size: usize,
+        interleave_time: &'a Time,
     ) -> Self {
         if indices.is_empty() {
             // Avoid unnecessary allocations when the partition is empty
@@ -72,6 +79,7 @@ impl<'a> PartitionedBatchIterator<'a> {
                 batch_size,
                 indices: vec![],
                 pos: 0,
+                interleave_time,
             };
         }
         let record_batches = buffered_batches.iter().collect::<Vec<_>>();
@@ -84,21 +92,22 @@ impl<'a> PartitionedBatchIterator<'a> {
             batch_size,
             indices: current_indices,
             pos: 0,
+            interleave_time,
         }
     }
+}
+
+impl Iterator for PartitionedBatchIterator<'_> {
+    type Item = datafusion::common::Result<RecordBatch>;
 
-    /// Returns the next shuffled batch, recording the gather cost into 
`interleave_time`.
-    pub(crate) fn next(
-        &mut self,
-        interleave_time: &Time,
-    ) -> Option<datafusion::common::Result<RecordBatch>> {
+    fn next(&mut self) -> Option<Self::Item> {
         if self.pos >= self.indices.len() {
             return None;
         }
 
         let indices_end = std::cmp::min(self.pos + self.batch_size, 
self.indices.len());
         let indices = &self.indices[self.pos..indices_end];
-        let mut timer = interleave_time.timer();
+        let mut timer = self.interleave_time.timer();
         let result = interleave_record_batch(&self.record_batches, indices);
         timer.stop();
         match result {
diff --git a/native/shuffle/src/partitioners/single_partition.rs 
b/native/shuffle/src/partitioners/single_partition.rs
index 5801ef613b..4a2f6f64a9 100644
--- a/native/shuffle/src/partitioners/single_partition.rs
+++ b/native/shuffle/src/partitioners/single_partition.rs
@@ -17,20 +17,15 @@
 
 use crate::metrics::ShufflePartitionerMetrics;
 use crate::partitioners::ShufflePartitioner;
-use crate::writers::BufBatchWriter;
-use crate::{CompressionCodec, ShuffleBlockWriter};
+use crate::writers::PartitionWriter;
 use arrow::array::RecordBatch;
-use arrow::datatypes::SchemaRef;
 use datafusion::common::DataFusionError;
-use std::fs::{File, OpenOptions};
-use std::io::{BufWriter, Write};
+use std::iter;
 use tokio::time::Instant;
 
 /// A partitioner that writes all shuffle data to a single file and a single 
index file
-pub(crate) struct SinglePartitionShufflePartitioner {
-    // output_data_file: File,
-    output_data_writer: BufBatchWriter<ShuffleBlockWriter, File>,
-    output_index_path: String,
+pub(crate) struct SinglePartitionShufflePartitioner<T: PartitionWriter> {
+    partition_writer: T,
     /// Batches that are smaller than the batch size and to be concatenated
     buffered_batches: Vec<RecordBatch>,
     /// Number of rows in the concatenating batches
@@ -41,34 +36,14 @@ pub(crate) struct SinglePartitionShufflePartitioner {
     batch_size: usize,
 }
 
-impl SinglePartitionShufflePartitioner {
+impl<T: PartitionWriter> SinglePartitionShufflePartitioner<T> {
     pub(crate) fn try_new(
-        output_data_path: String,
-        output_index_path: String,
-        schema: SchemaRef,
+        partition_writer: T,
         metrics: ShufflePartitionerMetrics,
         batch_size: usize,
-        codec: CompressionCodec,
-        write_buffer_size: usize,
     ) -> datafusion::common::Result<Self> {
-        let shuffle_block_writer = 
ShuffleBlockWriter::try_new(schema.as_ref(), codec.clone())?;
-
-        let output_data_file = OpenOptions::new()
-            .write(true)
-            .create(true)
-            .truncate(true)
-            .open(output_data_path)?;
-
-        let output_data_writer = BufBatchWriter::new(
-            shuffle_block_writer,
-            output_data_file,
-            write_buffer_size,
-            batch_size,
-        );
-
         Ok(Self {
-            output_data_writer,
-            output_index_path,
+            partition_writer,
             buffered_batches: vec![],
             num_buffered_rows: 0,
             metrics,
@@ -109,7 +84,7 @@ impl SinglePartitionShufflePartitioner {
 }
 
 #[async_trait::async_trait]
-impl ShufflePartitioner for SinglePartitionShufflePartitioner {
+impl<T: PartitionWriter> ShufflePartitioner for 
SinglePartitionShufflePartitioner<T> {
     async fn insert_batch(&mut self, batch: RecordBatch) -> 
datafusion::common::Result<()> {
         let start_time = Instant::now();
         let num_rows = batch.num_rows();
@@ -123,20 +98,14 @@ impl ShufflePartitioner for 
SinglePartitionShufflePartitioner {
 
                 // Write the concatenated buffered batch
                 if let Some(batch) = concatenated_batch {
-                    self.output_data_writer.write(
-                        &batch,
-                        &self.metrics.encode_time,
-                        &self.metrics.write_time,
-                    )?;
+                    self.partition_writer
+                        .write(0, &mut iter::once(Ok(batch)), &self.metrics)?;
                 }
 
                 if num_rows >= self.batch_size {
                     // Write the new batch
-                    self.output_data_writer.write(
-                        &batch,
-                        &self.metrics.encode_time,
-                        &self.metrics.write_time,
-                    )?;
+                    self.partition_writer
+                        .write(0, &mut iter::once(Ok(batch)), &self.metrics)?;
                 } else {
                     // Add the new batch to the buffer
                     self.add_buffered_batch(batch);
@@ -160,28 +129,14 @@ impl ShufflePartitioner for 
SinglePartitionShufflePartitioner {
 
         // Write the concatenated buffered batch
         if let Some(batch) = concatenated_batch {
-            self.output_data_writer.write(
-                &batch,
-                &self.metrics.encode_time,
-                &self.metrics.write_time,
-            )?;
-        }
-        self.output_data_writer
-            .flush(&self.metrics.encode_time, &self.metrics.write_time)?;
-
-        // Write index file. It should only contain 2 entries: 0 and the total 
number of bytes written
-        let index_file = OpenOptions::new()
-            .write(true)
-            .create(true)
-            .truncate(true)
-            .open(self.output_index_path.clone())
-            .map_err(|e| DataFusionError::Execution(format!("shuffle write 
error: {e:?}")))?;
-        let mut index_buf_writer = BufWriter::new(index_file);
-        let data_file_length = 
self.output_data_writer.writer_stream_position()?;
-        for offset in [0, data_file_length] {
-            index_buf_writer.write_all(&(offset as i64).to_le_bytes()[..])?;
+            self.partition_writer
+                .write(0, &mut iter::once(Ok(batch)), &self.metrics)?;
         }
-        index_buf_writer.flush()?;
+
+        self.partition_writer
+            .finish_partition(0, &mut iter::empty(), &self.metrics)?;
+
+        self.partition_writer.finish_all(&self.metrics)?;
 
         self.metrics
             .baseline
diff --git a/native/shuffle/src/shuffle_writer.rs 
b/native/shuffle/src/shuffle_writer.rs
index 756e753b3c..de6989e52a 100644
--- a/native/shuffle/src/shuffle_writer.rs
+++ b/native/shuffle/src/shuffle_writer.rs
@@ -22,7 +22,8 @@ use crate::partitioners::{
     EmptySchemaShufflePartitioner, MultiPartitionShuffleRepartitioner, 
ShufflePartitioner,
     SinglePartitionShufflePartitioner,
 };
-use crate::{CometPartitioning, CompressionCodec};
+use crate::writers::LocalPartitionWriter;
+use crate::{CometPartitioning, CompressionCodec, ShuffleBlockWriter};
 use async_trait::async_trait;
 use datafusion::common::exec_datafusion_err;
 use datafusion::physical_expr::{EquivalenceProperties, Partitioning};
@@ -202,39 +203,40 @@ async fn external_shuffle(
 ) -> Result<SendableRecordBatchStream> {
     let schema = input.schema();
 
+    let shuffle_block_writer = ShuffleBlockWriter::try_new(schema.as_ref(), 
codec.clone())?;
+    let local_partition_writer = LocalPartitionWriter::try_new(
+        output_data_file,
+        output_index_file,
+        shuffle_block_writer,
+        partitioning.partition_count(),
+        context.session_config().batch_size(),
+        write_buffer_size,
+        context.runtime_env(),
+    )?;
+
     let mut repartitioner: Box<dyn ShufflePartitioner> = match &partitioning {
         _ if schema.fields().is_empty() => {
             log::debug!("found empty schema, overriding {partitioning:?} 
partitioning with EmptySchemaShufflePartitioner");
             Box::new(EmptySchemaShufflePartitioner::try_new(
-                output_data_file,
-                output_index_file,
+                local_partition_writer,
                 Arc::clone(&schema),
                 partitioning.partition_count(),
                 metrics,
-                codec,
             )?)
         }
         any if any.partition_count() == 1 => 
Box::new(SinglePartitionShufflePartitioner::try_new(
-            output_data_file,
-            output_index_file,
-            Arc::clone(&schema),
+            local_partition_writer,
             metrics,
             context.session_config().batch_size(),
-            codec,
-            write_buffer_size,
         )?),
         _ => Box::new(MultiPartitionShuffleRepartitioner::try_new(
             partition,
-            output_data_file,
-            output_index_file,
-            Arc::clone(&schema),
+            local_partition_writer,
             partitioning,
             metrics,
             context.runtime_env(),
             context.session_config().batch_size(),
-            codec,
             tracing_enabled,
-            write_buffer_size,
         )?),
     };
 
@@ -346,38 +348,47 @@ mod test {
         let num_partitions = 2;
         let runtime_env = create_runtime(memory_limit);
         let metrics_set = ExecutionPlanMetricsSet::new();
-        let mut repartitioner = MultiPartitionShuffleRepartitioner::try_new(
-            0,
+        let shuffle_block_writer =
+            ShuffleBlockWriter::try_new(batch.schema().as_ref(), 
CompressionCodec::Lz4Frame)
+                .unwrap();
+        let local_partition_writer = LocalPartitionWriter::try_new(
             "/tmp/data.out".to_string(),
             "/tmp/index.out".to_string(),
-            batch.schema(),
+            shuffle_block_writer,
+            num_partitions,
+            1024,
+            1024 * 1024, // write_buffer_size: 1MB default
+            Arc::clone(&runtime_env),
+        )
+        .unwrap();
+        let mut repartitioner = MultiPartitionShuffleRepartitioner::try_new(
+            0,
+            local_partition_writer,
             CometPartitioning::Hash(vec![Arc::new(Column::new("a", 0))], 
num_partitions),
             ShufflePartitionerMetrics::new(&metrics_set, 0),
             runtime_env,
             1024,
-            CompressionCodec::Lz4Frame,
             false,
-            1024 * 1024, // write_buffer_size: 1MB default
         )
         .unwrap();
 
         repartitioner.insert_batch(batch.clone()).await.unwrap();
 
         {
-            let partition_writers = repartitioner.partition_writers();
-            assert_eq!(partition_writers.len(), 2);
+            let spill_writers = 
repartitioner.partition_writer().get_spill_writers();
+            assert_eq!(spill_writers.len(), 2);
 
-            assert!(!partition_writers[0].has_spill_file());
-            assert!(!partition_writers[1].has_spill_file());
+            assert!(!spill_writers[0].has_spill_file());
+            assert!(!spill_writers[1].has_spill_file());
         }
 
         repartitioner.spill().unwrap();
 
         // after spill, there should be spill files
         {
-            let partition_writers = repartitioner.partition_writers();
-            assert!(partition_writers[0].has_spill_file());
-            assert!(partition_writers[1].has_spill_file());
+            let spill_writers = 
repartitioner.partition_writer().get_spill_writers();
+            assert!(spill_writers[0].has_spill_file());
+            assert!(spill_writers[1].has_spill_file());
         }
 
         // insert another batch after spilling
@@ -409,18 +420,26 @@ mod test {
         let data_size = metrics.data_size.clone();
         let spill_count = metrics.spill_count.clone();
         let dir = tempfile::tempdir().unwrap();
-        let mut repartitioner = MultiPartitionShuffleRepartitioner::try_new(
-            0,
+        let shuffle_block_writer =
+            ShuffleBlockWriter::try_new(schema.as_ref(), 
CompressionCodec::Lz4Frame).unwrap();
+        let local_partition_writer = LocalPartitionWriter::try_new(
             dir.path().join("data.out").to_str().unwrap().to_string(),
             dir.path().join("index.out").to_str().unwrap().to_string(),
-            backing.schema(),
+            shuffle_block_writer,
+            num_partitions,
+            1024,
+            1024 * 1024, // write_buffer_size: 1MB default
+            Arc::clone(&runtime_env),
+        )
+        .unwrap();
+        let mut repartitioner = MultiPartitionShuffleRepartitioner::try_new(
+            0,
+            local_partition_writer,
             CometPartitioning::Hash(vec![Arc::new(Column::new("a", 0))], 
num_partitions),
             metrics,
             runtime_env,
             batch_size,
-            CompressionCodec::Lz4Frame,
             false,
-            1024 * 1024,
         )
         .unwrap();
 
diff --git a/native/shuffle/src/writers/local/local_partition_writer.rs 
b/native/shuffle/src/writers/local/local_partition_writer.rs
new file mode 100644
index 0000000000..3a9a6484db
--- /dev/null
+++ b/native/shuffle/src/writers/local/local_partition_writer.rs
@@ -0,0 +1,296 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+use crate::metrics::ShufflePartitionerMetrics;
+use crate::writers::local::spill::SpillWriter;
+use crate::writers::partition_writer::PartitionWriter;
+use crate::writers::BufBatchWriter;
+use crate::ShuffleBlockWriter;
+use arrow::array::RecordBatch;
+use datafusion::common::DataFusionError;
+use datafusion::execution::runtime_env::RuntimeEnv;
+use std::fs::{File, OpenOptions};
+use std::io::{BufWriter, Seek, Write};
+use std::sync::Arc;
+
+/// Output target for the shuffle data file.
+///
+/// The two shuffle modes drive the writer differently:
+///
+/// * Single-partition shuffles stream every batch through a single long-lived
+///   [`BufBatchWriter`]. Keeping it alive across `write` calls preserves
+///   cross-batch coalescing in the internal `BatchCoalescer` and limits
+///   flushing (which also finalizes partially coalesced batches) to
+///   [`PartitionWriter::finish_all`].
+/// * Multi-partition shuffles finalize one partition at a time in
+///   [`PartitionWriter::finish_partition`], each with its own short-lived
+///   `BufBatchWriter`, so coalescing intentionally does not cross partition
+///   boundaries. They hold the raw output writer and block writer directly.
+enum DataOutput {
+    /// Single-partition output: one long-lived writer streams all batches.
+    Single(BufBatchWriter<ShuffleBlockWriter, File>),
+    /// Multi-partition output: batches are staged per partition and merged 
into
+    /// `output_writer` one partition at a time during `finish_partition`.
+    Multi {
+        output_writer: BufWriter<File>,
+        shuffle_block_writer: ShuffleBlockWriter,
+        /// One spill file per output partition, buffered until 
`finish_partition`
+        /// merges them into the shuffle output.
+        spill_writers: Vec<SpillWriter>,
+        /// Runtime used to allocate the temporary spill files.
+        runtime: Arc<RuntimeEnv>,
+    },
+}
+
+/// Local file-based [`PartitionWriter`] implementation.
+///
+/// Writes shuffle output to a single data file plus an index file recording 
the
+/// byte offset where each partition begins. See [`DataOutput`] for how the
+/// single- and multi-partition modes differ.
+pub(crate) struct LocalPartitionWriter {
+    output_index_file: String,
+    data_output: DataOutput,
+    /// Start offset of each partition in the data file, plus a trailing entry
+    /// with the total length so partition sizes are simple offset differences.
+    /// Has `num_output_partitions + 1` elements.
+    offsets: Vec<u64>,
+    batch_size: usize,
+    write_buffer_size: usize,
+    num_output_partitions: usize,
+    /// Id of the last partition passed to `finish_partition`, used to assert
+    /// partitions are finalized in ascending order. `-1` before any call.
+    last_finish_pid: i32,
+}
+
+impl LocalPartitionWriter {
+    pub(crate) fn try_new(
+        output_data_file: String,
+        output_index_file: String,
+        shuffle_block_writer: ShuffleBlockWriter,
+        num_output_partitions: usize,
+        batch_size: usize,
+        write_buffer_size: usize,
+        runtime: Arc<RuntimeEnv>,
+    ) -> datafusion::common::Result<Self> {
+        let output_file = OpenOptions::new()
+            .write(true)
+            .create(true)
+            .truncate(true)
+            .open(output_data_file.clone())
+            .map_err(|e| DataFusionError::Execution(format!("shuffle write 
error: {e:?}")))?;
+
+        let data_output = if num_output_partitions == 1 {
+            DataOutput::Single(BufBatchWriter::new(
+                shuffle_block_writer,
+                output_file,
+                write_buffer_size,
+                batch_size,
+            ))
+        } else {
+            let output_writer = BufWriter::with_capacity(write_buffer_size, 
output_file);
+            let spill_writers = (0..num_output_partitions)
+                .map(|_| {
+                    SpillWriter::try_new(
+                        shuffle_block_writer.clone(),
+                        write_buffer_size,
+                        batch_size,
+                    )
+                })
+                .collect::<datafusion::common::Result<Vec<_>>>()?;
+            DataOutput::Multi {
+                output_writer,
+                shuffle_block_writer,
+                spill_writers,
+                runtime,
+            }
+        };
+        Ok(Self {
+            output_index_file,
+            data_output,
+            offsets: vec![0u64; num_output_partitions + 1],
+            batch_size,
+            write_buffer_size,
+            num_output_partitions,
+            last_finish_pid: -1,
+        })
+    }
+
+    #[cfg(test)]
+    pub(crate) fn get_spill_writers(&self) -> &Vec<SpillWriter> {
+        match &self.data_output {
+            DataOutput::Multi { spill_writers, .. } => spill_writers,
+            DataOutput::Single(_) => panic!("single-partition output has no 
spill writers"),
+        }
+    }
+}
+
+impl PartitionWriter for LocalPartitionWriter {
+    fn write<I>(
+        &mut self,
+        pid: usize,
+        iter: &mut I,
+        metrics: &ShufflePartitionerMetrics,
+    ) -> datafusion::common::Result<()>
+    where
+        I: Iterator<Item = datafusion::common::Result<RecordBatch>>,
+    {
+        match &mut self.data_output {
+            DataOutput::Single(writer) => {
+                if pid != 0 {
+                    return Err(DataFusionError::Execution(
+                        "LocalPartitionWriter single-partition output only 
supports partition 0."
+                            .to_string(),
+                    ));
+                }
+
+                // Stream batches through the long-lived writer so small 
batches keep
+                // coalescing across calls. Do not flush here: flushing also 
finalizes any
+                // partially coalesced batch, which would defeat cross-call 
coalescing and
+                // increase flush frequency. The single-partition writer is 
flushed once, in
+                // `finish_all`.
+                for batch in iter.by_ref() {
+                    let batch = batch?;
+                    writer.write(&batch, &metrics.encode_time, 
&metrics.write_time)?;
+                }
+            }
+            DataOutput::Multi {
+                spill_writers,
+                runtime,
+                ..
+            } => {
+                // Multi-partition output buffers each partition's batches 
into its own
+                // spill file. `finish_partition` later merges the spill files 
(and any
+                // remaining in-memory batches) into the shuffle output in 
partition order.
+                spill_writers[pid].write(iter, runtime, metrics)?;
+            }
+        }
+
+        Ok(())
+    }
+
+    fn finish_partition<I>(
+        &mut self,
+        pid: usize,
+        iter: &mut I,
+        metrics: &ShufflePartitionerMetrics,
+    ) -> datafusion::common::Result<()>
+    where
+        I: Iterator<Item = datafusion::common::Result<RecordBatch>>,
+    {
+        if pid as i32 - self.last_finish_pid != 1 {
+            return Err(DataFusionError::Execution(
+                "LocalPartitionWriter::finish_partition must be called in 
order.".to_string(),
+            ));
+        }
+        self.last_finish_pid = pid as i32;
+
+        let write_buffer_size = self.write_buffer_size;
+        let batch_size = self.batch_size;
+
+        match &mut self.data_output {
+            DataOutput::Single(writer) => {
+                // Single-partition data was already streamed via `write`, 
starting at
+                // offset 0 (already recorded in `self.offsets[0]`). Stream 
any trailing
+                // batches (normally none) without flushing; the long-lived 
writer is
+                // flushed once in `finish_all`.
+                for batch in iter.by_ref() {
+                    let batch = batch?;
+                    writer.write(&batch, &metrics.encode_time, 
&metrics.write_time)?;
+                }
+            }
+            DataOutput::Multi {
+                output_writer,
+                shuffle_block_writer,
+                spill_writers,
+                ..
+            } => {
+                self.offsets[pid] = output_writer.stream_position()?;
+
+                // if we wrote a spill file for this partition then copy the
+                // contents into the shuffle file
+                if let Some(writer) = spill_writers.get(pid) {
+                    if let Some(spill_path) = writer.path() {
+                        // Use raw File handle (not BufReader) so that 
std::io::copy
+                        // can use copy_file_range/sendfile for zero-copy on 
Linux.
+                        let mut spill_file = File::open(spill_path)?;
+                        let mut write_timer = metrics.write_time.timer();
+                        std::io::copy(&mut spill_file, output_writer)?;
+                        write_timer.stop();
+                    }
+                }
+
+                // Write in memory batches to output data file. Each partition 
uses its
+                // own writer so coalescing does not cross partition 
boundaries.
+                let mut buf_batch_writer = BufBatchWriter::new(
+                    shuffle_block_writer,
+                    output_writer,
+                    write_buffer_size,
+                    batch_size,
+                );
+                for batch in iter.by_ref() {
+                    let batch = batch?;
+                    buf_batch_writer.write(&batch, &metrics.encode_time, 
&metrics.write_time)?;
+                }
+                buf_batch_writer.flush(&metrics.encode_time, 
&metrics.write_time)?;
+            }
+        }
+        Ok(())
+    }
+
+    fn finish_all(
+        &mut self,
+        metrics: &ShufflePartitionerMetrics,
+    ) -> datafusion::common::Result<()> {
+        // Flush the data output and capture the final position. For the
+        // single-partition writer this also finalizes the last coalesced 
batch.
+        let final_offset = match &mut self.data_output {
+            DataOutput::Single(writer) => {
+                writer.flush(&metrics.encode_time, &metrics.write_time)?;
+                writer.writer_stream_position()?
+            }
+            DataOutput::Multi { output_writer, .. } => {
+                let mut write_timer = metrics.write_time.timer();
+                output_writer.flush()?;
+                let pos = output_writer.stream_position()?;
+                write_timer.stop();
+                pos
+            }
+        };
+
+        // add one extra offset at last to ease partition length computation
+        self.offsets[self.num_output_partitions] = final_offset;
+
+        let mut write_timer = metrics.write_time.timer();
+        let mut output_index = BufWriter::new(
+            File::create(self.output_index_file.clone())
+                .map_err(|e| DataFusionError::Execution(format!("shuffle write 
error: {e:?}")))?,
+        );
+
+        for offset in &self.offsets {
+            let offset_i64 = i64::try_from(*offset).map_err(|_| {
+                DataFusionError::Execution(format!(
+                    "shuffle write error: offset overflow ({offset})"
+                ))
+            })?;
+            output_index.write_all(&offset_i64.to_le_bytes())?;
+        }
+        output_index.flush()?;
+        write_timer.stop();
+
+        Ok(())
+    }
+}
diff --git a/native/shuffle/src/writers/mod.rs 
b/native/shuffle/src/writers/local/mod.rs
similarity index 76%
copy from native/shuffle/src/writers/mod.rs
copy to native/shuffle/src/writers/local/mod.rs
index 75caf9f3a3..e76bfba21a 100644
--- a/native/shuffle/src/writers/mod.rs
+++ b/native/shuffle/src/writers/local/mod.rs
@@ -15,12 +15,5 @@
 // specific language governing permissions and limitations
 // under the License.
 
-mod buf_batch_writer;
-mod checksum;
-mod shuffle_block_writer;
+pub(crate) mod local_partition_writer;
 mod spill;
-
-pub(crate) use buf_batch_writer::BufBatchWriter;
-pub(crate) use checksum::Checksum;
-pub use shuffle_block_writer::{CompressionCodec, ShuffleBlockWriter};
-pub(crate) use spill::PartitionWriter;
diff --git a/native/shuffle/src/writers/spill.rs 
b/native/shuffle/src/writers/local/spill.rs
similarity index 77%
rename from native/shuffle/src/writers/spill.rs
rename to native/shuffle/src/writers/local/spill.rs
index 624a45befe..450b617421 100644
--- a/native/shuffle/src/writers/spill.rs
+++ b/native/shuffle/src/writers/local/spill.rs
@@ -15,87 +15,60 @@
 // specific language governing permissions and limitations
 // under the License.
 
-use super::ShuffleBlockWriter;
 use crate::metrics::ShufflePartitionerMetrics;
-use crate::partitioners::PartitionedBatchIterator;
-use crate::writers::buf_batch_writer::BufBatchWriter;
+use crate::writers::BufBatchWriter;
+use crate::ShuffleBlockWriter;
+use arrow::record_batch::RecordBatch;
 use datafusion::common::DataFusionError;
 use datafusion::execution::disk_manager::RefCountedTempFile;
 use datafusion::execution::runtime_env::RuntimeEnv;
 use std::fs::{File, OpenOptions};
 
-/// A temporary disk file for spilling a partition's intermediate shuffle data.
 struct SpillFile {
     temp_file: RefCountedTempFile,
     file: File,
 }
 
-/// Manages encoding and optional disk spilling for a single shuffle partition.
-pub(crate) struct PartitionWriter {
-    /// Spill file for intermediate shuffle output for this partition. Each 
spill event
-    /// will append to this file and the contents will be copied to the 
shuffle file at
-    /// the end of processing.
-    spill_file: Option<SpillFile>,
-    /// Writer that performs encoding and compression
+pub(crate) struct SpillWriter {
     shuffle_block_writer: ShuffleBlockWriter,
+    write_buffer_size: usize,
+    batch_size: usize,
+    spill_file: Option<SpillFile>,
 }
 
-impl PartitionWriter {
+impl SpillWriter {
     pub(crate) fn try_new(
         shuffle_block_writer: ShuffleBlockWriter,
+        write_buffer_size: usize,
+        batch_size: usize,
     ) -> datafusion::common::Result<Self> {
         Ok(Self {
-            spill_file: None,
             shuffle_block_writer,
+            write_buffer_size,
+            batch_size,
+            spill_file: None,
         })
     }
 
-    fn ensure_spill_file_created(
+    pub(crate) fn write<I: Iterator<Item = 
datafusion::common::Result<RecordBatch>>>(
         &mut self,
-        runtime: &RuntimeEnv,
-    ) -> datafusion::common::Result<()> {
-        if self.spill_file.is_none() {
-            // Spill file is not yet created, create it
-            let spill_file = runtime
-                .disk_manager
-                .create_tmp_file("shuffle writer spill")?;
-            let spill_data = OpenOptions::new()
-                .write(true)
-                .create(true)
-                .truncate(true)
-                .open(spill_file.path())
-                .map_err(|e| {
-                    DataFusionError::Execution(format!("Error occurred while 
spilling {e}"))
-                })?;
-            self.spill_file = Some(SpillFile {
-                temp_file: spill_file,
-                file: spill_data,
-            });
-        }
-        Ok(())
-    }
-
-    pub(crate) fn spill(
-        &mut self,
-        iter: &mut PartitionedBatchIterator,
+        iter: &mut I,
         runtime: &RuntimeEnv,
         metrics: &ShufflePartitionerMetrics,
-        write_buffer_size: usize,
-        batch_size: usize,
-    ) -> datafusion::common::Result<usize> {
-        if let Some(batch) = iter.next(&metrics.interleave_time) {
+    ) -> datafusion::common::Result<()> {
+        if let Some(batch) = iter.next() {
             self.ensure_spill_file_created(runtime)?;
 
             let total_bytes_written = {
                 let mut buf_batch_writer = BufBatchWriter::new(
                     &mut self.shuffle_block_writer,
                     &mut self.spill_file.as_mut().unwrap().file,
-                    write_buffer_size,
-                    batch_size,
+                    self.write_buffer_size,
+                    self.batch_size,
                 );
                 let mut bytes_written =
                     buf_batch_writer.write(&batch?, &metrics.encode_time, 
&metrics.write_time)?;
-                while let Some(batch) = iter.next(&metrics.interleave_time) {
+                for batch in iter.by_ref() {
                     let batch = batch?;
                     bytes_written += buf_batch_writer.write(
                         &batch,
@@ -106,11 +79,34 @@ impl PartitionWriter {
                 buf_batch_writer.flush(&metrics.encode_time, 
&metrics.write_time)?;
                 bytes_written
             };
+            metrics.spilled_bytes.add(total_bytes_written);
+        }
+        Ok(())
+    }
 
-            Ok(total_bytes_written)
-        } else {
-            Ok(0)
+    fn ensure_spill_file_created(
+        &mut self,
+        runtime: &RuntimeEnv,
+    ) -> datafusion::common::Result<()> {
+        if self.spill_file.is_none() {
+            // Spill file is not yet created, create it
+            let spill_file = runtime
+                .disk_manager
+                .create_tmp_file("shuffle writer spill")?;
+            let spill_data = OpenOptions::new()
+                .write(true)
+                .create(true)
+                .truncate(true)
+                .open(spill_file.path())
+                .map_err(|e| {
+                    DataFusionError::Execution(format!("Error occurred while 
spilling {e}"))
+                })?;
+            self.spill_file = Some(SpillFile {
+                temp_file: spill_file,
+                file: spill_data,
+            });
         }
+        Ok(())
     }
 
     pub(crate) fn path(&self) -> Option<&std::path::Path> {
diff --git a/native/shuffle/src/writers/mod.rs 
b/native/shuffle/src/writers/mod.rs
index 75caf9f3a3..6d330fd12a 100644
--- a/native/shuffle/src/writers/mod.rs
+++ b/native/shuffle/src/writers/mod.rs
@@ -17,10 +17,12 @@
 
 mod buf_batch_writer;
 mod checksum;
+mod local;
+mod partition_writer;
 mod shuffle_block_writer;
-mod spill;
 
 pub(crate) use buf_batch_writer::BufBatchWriter;
 pub(crate) use checksum::Checksum;
+pub(crate) use local::local_partition_writer::LocalPartitionWriter;
+pub(crate) use partition_writer::PartitionWriter;
 pub use shuffle_block_writer::{CompressionCodec, ShuffleBlockWriter};
-pub(crate) use spill::PartitionWriter;
diff --git a/native/shuffle/src/writers/partition_writer.rs 
b/native/shuffle/src/writers/partition_writer.rs
new file mode 100644
index 0000000000..25b0e598df
--- /dev/null
+++ b/native/shuffle/src/writers/partition_writer.rs
@@ -0,0 +1,71 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+use crate::metrics::ShufflePartitionerMetrics;
+use arrow::record_batch::RecordBatch;
+
+/// Storage backend abstraction for shuffle partition output.
+///
+/// Decouples partitioning from storage: partitioners only produce partitioned
+/// `RecordBatch` streams, while implementations of this trait own how those
+/// batches are stored and finalized. [`LocalPartitionWriter`] implements the
+/// local file behavior; other backends (e.g. a remote shuffle writer) can be
+/// added without changing the partitioners.
+///
+/// A partitioner drives a writer as: any number of
+/// [`write`](PartitionWriter::write) calls to stage batches, then one
+/// [`finish_partition`](PartitionWriter::finish_partition) per partition in
+/// ascending id order, then a single 
[`finish_all`](PartitionWriter::finish_all).
+///
+/// [`LocalPartitionWriter`]: 
crate::writers::local::local_partition_writer::LocalPartitionWriter
+pub(crate) trait PartitionWriter: Send + Sync {
+    /// Stages the batches from `iter` for partition `pid` without finalizing 
it.
+    ///
+    /// Used to stream single-partition output and to stage multi-partition
+    /// spilled batches. A partition may be written multiple times and in any
+    /// order; staged data is only guaranteed visible after
+    /// [`finish_partition`](PartitionWriter::finish_partition).
+    fn write<I>(
+        &mut self,
+        pid: usize,
+        iter: &mut I,
+        metrics: &ShufflePartitionerMetrics,
+    ) -> datafusion::common::Result<()>
+    where
+        I: Iterator<Item = datafusion::common::Result<RecordBatch>>;
+
+    /// Finalizes partition `pid`, writing any remaining batches from `iter` 
and
+    /// combining them with data previously staged via
+    /// [`write`](PartitionWriter::write).
+    ///
+    /// Must be called exactly once per partition, in ascending id order, so 
the
+    /// writer can lay partitions out contiguously and record their offsets.
+    fn finish_partition<I>(
+        &mut self,
+        pid: usize,
+        iter: &mut I,
+        metrics: &ShufflePartitionerMetrics,
+    ) -> datafusion::common::Result<()>
+    where
+        I: Iterator<Item = datafusion::common::Result<RecordBatch>>;
+
+    /// Completes the shuffle write, flushing output and emitting the partition
+    /// index. Called exactly once, after the last
+    /// [`finish_partition`](PartitionWriter::finish_partition).
+    fn finish_all(&mut self, metrics: &ShufflePartitionerMetrics)
+        -> datafusion::common::Result<()>;
+}


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to