Skip to main content

datafusion_datasource_parquet/
sink.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
18//! [`ParquetSink`] — DataFusion `DataSink` implementation that writes one
19//! or more Parquet files to an [`ObjectStore`], optionally with parallel
20//! per-column and per-row-group serialization.
21
22use std::fmt;
23use std::fmt::Debug;
24use std::sync::Arc;
25
26use arrow::array::RecordBatch;
27use arrow::datatypes::{Schema, SchemaRef};
28use async_trait::async_trait;
29use datafusion_common::config::TableParquetOptions;
30use datafusion_common::{DataFusionError, HashMap, Result, internal_datafusion_err};
31use datafusion_common_runtime::{JoinSet, SpawnedTask};
32use datafusion_datasource::display::FileGroupDisplay;
33use datafusion_datasource::file_compression_type::FileCompressionType;
34use datafusion_datasource::file_sink_config::{FileSink, FileSinkConfig};
35use datafusion_datasource::sink::DataSink;
36#[cfg(feature = "proto")]
37use datafusion_datasource::sink::DataSinkExec;
38use datafusion_datasource::write::demux::DemuxedStreamReceiver;
39use datafusion_datasource::write::{
40    ObjectWriterBuilder, SharedBuffer, get_writer_schema,
41};
42use datafusion_execution::memory_pool::{MemoryConsumer, MemoryPool, MemoryReservation};
43use datafusion_execution::runtime_env::RuntimeEnv;
44use datafusion_execution::{SendableRecordBatchStream, TaskContext};
45#[cfg(feature = "proto")]
46use datafusion_physical_plan::ExecutionPlan;
47use datafusion_physical_plan::metrics::{
48    ElapsedComputeFutureExt, ExecutionPlanMetricsSet, MetricBuilder, MetricCategory,
49    MetricsSet, Time,
50};
51use datafusion_physical_plan::{DisplayAs, DisplayFormatType};
52use object_store::ObjectStore;
53use object_store::buffered::BufWriter;
54use object_store::path::Path;
55use parquet::arrow::arrow_writer::{
56    ArrowColumnChunk, ArrowColumnWriter, ArrowLeafColumn, ArrowRowGroupWriterFactory,
57    ArrowWriterOptions, compute_leaves,
58};
59use parquet::arrow::{ArrowWriter, AsyncArrowWriter};
60#[cfg(feature = "parquet_encryption")]
61use parquet::encryption::encrypt::FileEncryptionProperties;
62use parquet::file::metadata::{ParquetMetaData, SortingColumn};
63use parquet::file::properties::{
64    DEFAULT_MAX_ROW_GROUP_ROW_COUNT, WriterProperties, WriterPropertiesBuilder,
65};
66use parquet::file::writer::SerializedFileWriter;
67use tokio::io::{AsyncWrite, AsyncWriteExt};
68use tokio::sync::mpsc::{self, Receiver, Sender};
69
70/// Initial writing buffer size. Note this is just a size hint for efficiency. It
71/// will grow beyond the set value if needed.
72const INITIAL_BUFFER_BYTES: usize = 1048576;
73
74/// When writing parquet files in parallel, if the buffered Parquet data exceeds
75/// this size, it is flushed to object store
76const BUFFER_FLUSH_BYTES: usize = 1024000;
77
78/// Implements [`DataSink`] for writing to a parquet file.
79pub struct ParquetSink {
80    /// Config options for writing data
81    config: FileSinkConfig,
82    /// Underlying parquet options
83    parquet_options: TableParquetOptions,
84    /// File metadata from successfully produced parquet files. The Mutex is only used
85    /// to allow inserting to HashMap from behind borrowed reference in DataSink::write_all.
86    written: Arc<parking_lot::Mutex<HashMap<Path, ParquetMetaData>>>,
87    /// Optional sorting columns to write to Parquet metadata
88    sorting_columns: Option<Vec<SortingColumn>>,
89    /// Metrics for tracking write operations
90    metrics: ExecutionPlanMetricsSet,
91}
92
93impl Debug for ParquetSink {
94    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
95        f.debug_struct("ParquetSink").finish()
96    }
97}
98
99impl DisplayAs for ParquetSink {
100    fn fmt_as(&self, t: DisplayFormatType, f: &mut fmt::Formatter<'_>) -> fmt::Result {
101        match t {
102            DisplayFormatType::Default | DisplayFormatType::Verbose => {
103                write!(f, "ParquetSink(file_groups=",)?;
104                FileGroupDisplay(&self.config.file_group).fmt_as(t, f)?;
105                write!(f, ")")
106            }
107            DisplayFormatType::TreeRender => {
108                // TODO: collect info
109                write!(f, "")
110            }
111        }
112    }
113}
114
115impl ParquetSink {
116    /// Create from config.
117    pub fn new(config: FileSinkConfig, parquet_options: TableParquetOptions) -> Self {
118        Self {
119            config,
120            parquet_options,
121            written: Default::default(),
122            sorting_columns: None,
123            metrics: ExecutionPlanMetricsSet::new(),
124        }
125    }
126
127    /// Set sorting columns for the Parquet file metadata.
128    pub fn with_sorting_columns(
129        mut self,
130        sorting_columns: Option<Vec<SortingColumn>>,
131    ) -> Self {
132        self.sorting_columns = sorting_columns;
133        self
134    }
135
136    /// Retrieve the file metadata for the written files, keyed to the path
137    /// which may be partitioned (in the case of hive style partitioning).
138    pub fn written(&self) -> HashMap<Path, ParquetMetaData> {
139        self.written.lock().clone()
140    }
141
142    /// Create writer properties based upon configuration settings,
143    /// including partitioning and the inclusion of arrow schema metadata.
144    async fn create_writer_props(
145        &self,
146        runtime: &Arc<RuntimeEnv>,
147        path: &Path,
148    ) -> Result<WriterProperties> {
149        let schema = self.config.output_schema();
150
151        // TODO: avoid this clone in follow up PR, where the writer properties & schema
152        // are calculated once on `ParquetSink::new`
153        let mut parquet_opts = self.parquet_options.clone();
154        if !self.parquet_options.global.skip_arrow_metadata {
155            parquet_opts.arrow_schema(schema);
156        }
157
158        let mut builder = WriterPropertiesBuilder::try_from(&parquet_opts)?;
159
160        // Set sorting columns if configured
161        if let Some(ref sorting_columns) = self.sorting_columns {
162            builder = builder.set_sorting_columns(Some(sorting_columns.clone()));
163        }
164
165        builder = set_writer_encryption_properties(
166            builder,
167            runtime,
168            parquet_opts,
169            schema,
170            path,
171        )
172        .await?;
173        Ok(builder.build())
174    }
175
176    /// Creates an AsyncArrowWriter which serializes a parquet file to an ObjectStore
177    /// AsyncArrowWriters are used when individual parquet file serialization is not parallelized
178    fn create_async_arrow_writer(
179        &self,
180        location: &Path,
181        object_store: Arc<dyn ObjectStore>,
182        context: &Arc<TaskContext>,
183        parquet_props: WriterProperties,
184    ) -> Result<AsyncArrowWriter<BufWriter>> {
185        let buf_writer = BufWriter::with_capacity(
186            object_store,
187            location.clone(),
188            context
189                .session_config()
190                .options()
191                .execution
192                .objectstore_writer_buffer_size,
193        );
194        let options = ArrowWriterOptions::new()
195            .with_properties(parquet_props)
196            .with_skip_arrow_metadata(self.parquet_options.global.skip_arrow_metadata);
197
198        let writer = AsyncArrowWriter::try_new_with_options(
199            buf_writer,
200            get_writer_schema(&self.config),
201            options,
202        )?;
203        Ok(writer)
204    }
205
206    /// Parquet options
207    pub fn parquet_options(&self) -> &TableParquetOptions {
208        &self.parquet_options
209    }
210}
211
212#[cfg(feature = "parquet_encryption")]
213async fn set_writer_encryption_properties(
214    builder: WriterPropertiesBuilder,
215    runtime: &Arc<RuntimeEnv>,
216    parquet_opts: TableParquetOptions,
217    schema: &Arc<Schema>,
218    path: &Path,
219) -> Result<WriterPropertiesBuilder> {
220    if let Some(file_encryption_properties) = parquet_opts.crypto.file_encryption {
221        // Encryption properties have been specified directly
222        return Ok(builder.with_file_encryption_properties(Arc::new(
223            FileEncryptionProperties::try_from(file_encryption_properties)?,
224        )));
225    } else if let Some(encryption_factory_id) = &parquet_opts.crypto.factory_id.as_ref() {
226        // Encryption properties will be generated by an encryption factory
227        let encryption_factory =
228            runtime.parquet_encryption_factory(encryption_factory_id)?;
229        let file_encryption_properties = encryption_factory
230            .get_file_encryption_properties(
231                &parquet_opts.crypto.factory_options,
232                schema,
233                path,
234            )
235            .await?;
236        if let Some(file_encryption_properties) = file_encryption_properties {
237            return Ok(
238                builder.with_file_encryption_properties(file_encryption_properties)
239            );
240        }
241    }
242    Ok(builder)
243}
244
245#[cfg(not(feature = "parquet_encryption"))]
246#[expect(clippy::unused_async)]
247async fn set_writer_encryption_properties(
248    builder: WriterPropertiesBuilder,
249    _runtime: &Arc<RuntimeEnv>,
250    _parquet_opts: TableParquetOptions,
251    _schema: &Arc<Schema>,
252    _path: &Path,
253) -> Result<WriterPropertiesBuilder> {
254    Ok(builder)
255}
256
257#[async_trait]
258impl FileSink for ParquetSink {
259    fn config(&self) -> &FileSinkConfig {
260        &self.config
261    }
262
263    async fn spawn_writer_tasks_and_join(
264        &self,
265        context: &Arc<TaskContext>,
266        demux_task: SpawnedTask<Result<()>>,
267        mut file_stream_rx: DemuxedStreamReceiver,
268        object_store: Arc<dyn ObjectStore>,
269    ) -> Result<u64> {
270        let rows_written_counter = MetricBuilder::new(&self.metrics)
271            .with_category(MetricCategory::Rows)
272            .global_counter("rows_written");
273        // Note: bytes_written is the sum of compressed row group sizes, which
274        // may differ slightly from the actual on-disk file size (excludes footer,
275        // page indexes, and other Parquet metadata overhead).
276        let bytes_written_counter = MetricBuilder::new(&self.metrics)
277            .with_category(MetricCategory::Bytes)
278            .global_counter("bytes_written");
279        let elapsed_compute = MetricBuilder::new(&self.metrics).elapsed_compute(0);
280
281        let parquet_opts = &self.parquet_options;
282
283        let mut file_write_tasks: JoinSet<
284            std::result::Result<(Path, ParquetMetaData), DataFusionError>,
285        > = JoinSet::new();
286
287        let runtime = context.runtime_env();
288        let parallel_options = ParallelParquetWriterOptions {
289            max_parallel_row_groups: parquet_opts
290                .global
291                .maximum_parallel_row_group_writers,
292            max_buffered_record_batches_per_stream: parquet_opts
293                .global
294                .maximum_buffered_record_batches_per_stream,
295        };
296
297        while let Some((path, mut rx)) = file_stream_rx.recv().await {
298            let parquet_props = self.create_writer_props(&runtime, &path).await?;
299            // CDC requires the sequential writer: the chunker state lives in ArrowWriter
300            // and persists across row groups. The parallel path bypasses ArrowWriter entirely.
301            if !parquet_opts.global.allow_single_file_parallelism
302                || parquet_opts.global.content_defined_chunking.enabled
303            {
304                let mut writer = self.create_async_arrow_writer(
305                    &path,
306                    Arc::clone(&object_store),
307                    context,
308                    parquet_props.clone(),
309                )?;
310                let reservation = MemoryConsumer::new(format!("ParquetSink[{path}]"))
311                    .register(context.memory_pool());
312                file_write_tasks.spawn(
313                    async move {
314                        while let Some(batch) = rx.recv().await {
315                            writer.write(&batch).await?;
316                            reservation.try_resize(writer.memory_size())?;
317                        }
318                        let parquet_meta_data = writer
319                            .close()
320                            .await
321                            .map_err(|e| DataFusionError::ParquetError(Box::new(e)))?;
322                        Ok((path, parquet_meta_data))
323                    }
324                    .with_elapsed_compute(elapsed_compute.clone()),
325                );
326            } else {
327                let writer = ObjectWriterBuilder::new(
328                    // Parquet files as a whole are never compressed, since they
329                    // manage compressed blocks themselves.
330                    FileCompressionType::UNCOMPRESSED,
331                    &path,
332                    Arc::clone(&object_store),
333                )
334                .with_buffer_size(Some(
335                    context
336                        .session_config()
337                        .options()
338                        .execution
339                        .objectstore_writer_buffer_size,
340                ))
341                .build()?;
342                let ctx = ParquetFileWriteContext {
343                    schema: get_writer_schema(&self.config),
344                    props: Arc::new(parquet_props),
345                    skip_arrow_metadata: self.parquet_options.global.skip_arrow_metadata,
346                    parallel_options: Arc::new(parallel_options.clone()),
347                    pool: Arc::clone(context.memory_pool()),
348                };
349                let encoding_time = elapsed_compute.clone();
350                file_write_tasks.spawn(async move {
351                    let parquet_meta_data = output_single_parquet_file_parallelized(
352                        writer,
353                        rx,
354                        ctx,
355                        encoding_time,
356                    )
357                    .await?;
358                    Ok((path, parquet_meta_data))
359                });
360            }
361        }
362
363        while let Some(result) = file_write_tasks.join_next().await {
364            match result {
365                Ok(r) => {
366                    let (path, parquet_meta_data) = r?;
367                    let file_rows = parquet_meta_data.file_metadata().num_rows() as usize;
368                    let file_bytes: usize = parquet_meta_data
369                        .row_groups()
370                        .iter()
371                        .map(|rg| rg.compressed_size() as usize)
372                        .sum();
373                    rows_written_counter.add(file_rows);
374                    bytes_written_counter.add(file_bytes);
375                    let mut written_files = self.written.lock();
376                    written_files
377                        .try_insert(path.clone(), parquet_meta_data)
378                        .map_err(|e| internal_datafusion_err!("duplicate entry detected for partitioned file {path}: {e}"))?;
379                    drop(written_files);
380                }
381                Err(e) => {
382                    if e.is_panic() {
383                        std::panic::resume_unwind(e.into_panic());
384                    } else {
385                        unreachable!();
386                    }
387                }
388            }
389        }
390
391        demux_task
392            .join_unwind()
393            .await
394            .map_err(|e| DataFusionError::ExecutionJoin(Box::new(e)))??;
395
396        Ok(rows_written_counter.value() as u64)
397    }
398}
399
400#[async_trait]
401impl DataSink for ParquetSink {
402    fn metrics(&self) -> Option<MetricsSet> {
403        Some(self.metrics.clone_inner())
404    }
405
406    fn schema(&self) -> &SchemaRef {
407        self.config.output_schema()
408    }
409
410    async fn write_all(
411        &self,
412        data: SendableRecordBatchStream,
413        context: &Arc<TaskContext>,
414    ) -> Result<u64> {
415        FileSink::write_all(self, data, context).await
416    }
417
418    #[cfg(feature = "proto")]
419    fn try_to_proto(
420        &self,
421        exec: &DataSinkExec,
422        ctx: &datafusion_physical_plan::proto::ExecutionPlanEncodeCtx<'_>,
423    ) -> Result<Option<datafusion_proto_models::protobuf::PhysicalPlanNode>> {
424        use datafusion_proto_models::protobuf;
425        use protobuf::physical_plan_node::PhysicalPlanType;
426
427        let input = ctx.encode_child(exec.input())?;
428        let sort_order = exec.encode_sort_order(ctx)?;
429        let sink = protobuf::ParquetSink::try_from(self)?;
430        let node = protobuf::ParquetSinkExecNode {
431            input: Some(Box::new(input)),
432            sink: Some(sink),
433            sink_schema: Some(exec.schema().as_ref().try_into()?),
434            sort_order,
435        };
436        Ok(Some(protobuf::PhysicalPlanNode {
437            physical_plan_type: Some(PhysicalPlanType::ParquetSink(Box::new(node))),
438        }))
439    }
440}
441
442#[cfg(feature = "proto")]
443impl TryFrom<&ParquetSink> for datafusion_proto_models::protobuf::ParquetSink {
444    type Error = DataFusionError;
445
446    fn try_from(value: &ParquetSink) -> Result<Self> {
447        Ok(Self {
448            config: Some(value.config().try_into()?),
449            parquet_options: Some(value.parquet_options().try_into()?),
450        })
451    }
452}
453
454#[cfg(feature = "proto")]
455impl TryFrom<&datafusion_proto_models::protobuf::ParquetSink> for ParquetSink {
456    type Error = DataFusionError;
457
458    fn try_from(value: &datafusion_proto_models::protobuf::ParquetSink) -> Result<Self> {
459        let config =
460            FileSinkConfig::try_from(value.config.as_ref().ok_or_else(|| {
461                datafusion_common::internal_datafusion_err!(
462                    "ParquetSink is missing required field 'config'"
463                )
464            })?)?;
465        let parquet_options = value
466            .parquet_options
467            .as_ref()
468            .ok_or_else(|| {
469                datafusion_common::internal_datafusion_err!(
470                    "ParquetSink is missing required field 'parquet_options'"
471                )
472            })?
473            .try_into()?;
474
475        Ok(Self::new(config, parquet_options))
476    }
477}
478
479#[cfg(feature = "proto")]
480impl ParquetSink {
481    /// Reconstructs a [`DataSinkExec`] containing a `ParquetSink` from protobuf.
482    pub fn try_from_proto(
483        node: &datafusion_proto_models::protobuf::PhysicalPlanNode,
484        ctx: &datafusion_physical_plan::proto::ExecutionPlanDecodeCtx<'_>,
485    ) -> Result<Arc<dyn ExecutionPlan>> {
486        use datafusion_proto_models::protobuf;
487
488        let sink_node = datafusion_physical_plan::expect_plan_variant!(
489            node,
490            protobuf::physical_plan_node::PhysicalPlanType::ParquetSink,
491            "ParquetSink",
492        );
493        let input = ctx.decode_required_child(
494            sink_node.input.as_deref(),
495            "ParquetSinkExecNode",
496            "input",
497        )?;
498        let proto_sink = sink_node.sink.as_ref().ok_or_else(|| {
499            datafusion_common::internal_datafusion_err!(
500                "ParquetSinkExecNode is missing required field 'sink'"
501            )
502        })?;
503        let data_sink = ParquetSink::try_from(proto_sink)?;
504        let sort_order = DataSinkExec::decode_sort_order(
505            sink_node.sort_order.as_ref(),
506            ctx,
507            input.schema().as_ref(),
508        )?;
509
510        Ok(Arc::new(DataSinkExec::new(
511            input,
512            Arc::new(data_sink),
513            sort_order,
514        )))
515    }
516}
517
518/// Consumes a stream of [ArrowLeafColumn] via a channel and serializes them using an [ArrowColumnWriter]
519/// Once the channel is exhausted, returns the ArrowColumnWriter.
520async fn column_serializer_task(
521    mut rx: Receiver<ArrowLeafColumn>,
522    mut writer: ArrowColumnWriter,
523    reservation: MemoryReservation,
524    encoding_time: Time,
525) -> Result<(ArrowColumnWriter, MemoryReservation)> {
526    while let Some(col) = rx.recv().await {
527        let _timer = encoding_time.timer();
528        writer.write(&col)?;
529        reservation.try_resize(writer.memory_size())?;
530    }
531    Ok((writer, reservation))
532}
533
534type ColumnWriterTask = SpawnedTask<Result<(ArrowColumnWriter, MemoryReservation)>>;
535type ColSender = Sender<ArrowLeafColumn>;
536
537/// Spawns a parallel serialization task for each column
538/// Returns join handles for each columns serialization task along with a send channel
539/// to send arrow arrays to each serialization task.
540fn spawn_column_parallel_row_group_writer(
541    col_writers: Vec<ArrowColumnWriter>,
542    max_buffer_size: usize,
543    pool: &Arc<dyn MemoryPool>,
544    encoding_time: &Time,
545) -> Result<(Vec<ColumnWriterTask>, Vec<ColSender>)> {
546    let num_columns = col_writers.len();
547
548    let mut col_writer_tasks = Vec::with_capacity(num_columns);
549    let mut col_array_channels = Vec::with_capacity(num_columns);
550    for writer in col_writers.into_iter() {
551        // Buffer size of this channel limits the number of arrays queued up for column level serialization
552        let (send_array, receive_array) =
553            mpsc::channel::<ArrowLeafColumn>(max_buffer_size);
554        col_array_channels.push(send_array);
555
556        let reservation =
557            MemoryConsumer::new("ParquetSink(ArrowColumnWriter)").register(pool);
558        let task = SpawnedTask::spawn(column_serializer_task(
559            receive_array,
560            writer,
561            reservation,
562            encoding_time.clone(),
563        ));
564        col_writer_tasks.push(task);
565    }
566
567    Ok((col_writer_tasks, col_array_channels))
568}
569
570/// Settings related to writing parquet files in parallel
571#[derive(Clone)]
572struct ParallelParquetWriterOptions {
573    max_parallel_row_groups: usize,
574    max_buffered_record_batches_per_stream: usize,
575}
576
577/// Write configuration inputs shared across all parallel tasks that encode a
578/// single Parquet file. These values are invariant for the duration of one file
579/// write and do not change per row-group or per column.
580///
581/// Separating these from per-call parameters (`object_store_writer`, `data`,
582/// `encoding_time`) keeps the deep parallel call chain below the argument-count
583/// limit without mixing configuration with runtime state.
584#[derive(Clone)]
585struct ParquetFileWriteContext {
586    schema: Arc<Schema>,
587    props: Arc<WriterProperties>,
588    skip_arrow_metadata: bool,
589    parallel_options: Arc<ParallelParquetWriterOptions>,
590    pool: Arc<dyn MemoryPool>,
591}
592
593/// This is the return type of calling [ArrowColumnWriter].close() on each column
594/// i.e. the Vec of encoded columns which can be appended to a row group
595type RBStreamSerializeResult = Result<(Vec<ArrowColumnChunk>, MemoryReservation, usize)>;
596
597/// Sends the ArrowArrays in passed [RecordBatch] through the channels to their respective
598/// parallel column serializers.
599async fn send_arrays_to_col_writers(
600    col_array_channels: &[ColSender],
601    rb: &RecordBatch,
602    schema: Arc<Schema>,
603) -> Result<()> {
604    // Each leaf column has its own channel, increment next_channel for each leaf column sent.
605    let mut next_channel = 0;
606    for (array, field) in rb.columns().iter().zip(schema.fields()) {
607        for c in compute_leaves(field, array)? {
608            // Do not surface error from closed channel (means something
609            // else hit an error, and the plan is shutting down).
610            if col_array_channels[next_channel].send(c).await.is_err() {
611                return Ok(());
612            }
613
614            next_channel += 1;
615        }
616    }
617
618    Ok(())
619}
620
621/// Spawns a tokio task which joins the parallel column writer tasks,
622/// and finalizes the row group
623fn spawn_rg_join_and_finalize_task(
624    column_writer_tasks: Vec<ColumnWriterTask>,
625    rg_rows: usize,
626    pool: &Arc<dyn MemoryPool>,
627    encoding_time: Time,
628) -> SpawnedTask<RBStreamSerializeResult> {
629    let rg_reservation =
630        MemoryConsumer::new("ParquetSink(SerializedRowGroupWriter)").register(pool);
631
632    SpawnedTask::spawn(async move {
633        let num_cols = column_writer_tasks.len();
634        let mut finalized_rg = Vec::with_capacity(num_cols);
635        for task in column_writer_tasks.into_iter() {
636            let (writer, _col_reservation) = task
637                .join_unwind()
638                .await
639                .map_err(|e| DataFusionError::ExecutionJoin(Box::new(e)))??;
640            let encoded_size = writer.get_estimated_total_bytes();
641            rg_reservation.grow(encoded_size);
642            let _timer = encoding_time.timer();
643            finalized_rg.push(writer.close()?);
644        }
645
646        Ok((finalized_rg, rg_reservation, rg_rows))
647    })
648}
649
650/// This task coordinates the serialization of a parquet file in parallel.
651/// As the query produces RecordBatches, these are written to a RowGroup
652/// via parallel [ArrowColumnWriter] tasks. Once the desired max rows per
653/// row group is reached, the parallel tasks are joined on another separate task
654/// and sent to a concatenation task. This task immediately continues to work
655/// on the next row group in parallel. So, parquet serialization is parallelized
656/// across both columns and row_groups, with a theoretical max number of parallel tasks
657/// given by n_columns * num_row_groups.
658fn spawn_parquet_parallel_serialization_task(
659    row_group_writer_factory: ArrowRowGroupWriterFactory,
660    mut data: Receiver<RecordBatch>,
661    serialize_tx: Sender<SpawnedTask<RBStreamSerializeResult>>,
662    ctx: ParquetFileWriteContext,
663    encoding_time: Time,
664) -> SpawnedTask<Result<(), DataFusionError>> {
665    SpawnedTask::spawn(async move {
666        let max_buffer_rb = ctx.parallel_options.max_buffered_record_batches_per_stream;
667        let max_row_group_rows = ctx
668            .props
669            .max_row_group_row_count()
670            .unwrap_or(DEFAULT_MAX_ROW_GROUP_ROW_COUNT);
671        let mut row_group_index = 0;
672        let col_writers =
673            row_group_writer_factory.create_column_writers(row_group_index)?;
674        let (mut column_writer_handles, mut col_array_channels) =
675            spawn_column_parallel_row_group_writer(
676                col_writers,
677                max_buffer_rb,
678                &ctx.pool,
679                &encoding_time,
680            )?;
681        let mut current_rg_rows = 0;
682
683        while let Some(mut rb) = data.recv().await {
684            // This loop allows the "else" block to repeatedly split the RecordBatch to handle the case
685            // when max_row_group_rows < execution.batch_size as an alternative to a recursive async
686            // function.
687            loop {
688                if current_rg_rows + rb.num_rows() < max_row_group_rows {
689                    send_arrays_to_col_writers(
690                        &col_array_channels,
691                        &rb,
692                        Arc::clone(&ctx.schema),
693                    )
694                    .await?;
695                    current_rg_rows += rb.num_rows();
696                    break;
697                } else {
698                    let rows_left = max_row_group_rows - current_rg_rows;
699                    let a = rb.slice(0, rows_left);
700                    send_arrays_to_col_writers(
701                        &col_array_channels,
702                        &a,
703                        Arc::clone(&ctx.schema),
704                    )
705                    .await?;
706
707                    // Signal the parallel column writers that the RowGroup is done, join and finalize RowGroup
708                    // on a separate task, so that we can immediately start on the next RG before waiting
709                    // for the current one to finish.
710                    drop(col_array_channels);
711                    let finalize_rg_task = spawn_rg_join_and_finalize_task(
712                        column_writer_handles,
713                        max_row_group_rows,
714                        &ctx.pool,
715                        encoding_time.clone(),
716                    );
717
718                    // Do not surface error from closed channel (means something
719                    // else hit an error, and the plan is shutting down).
720                    if serialize_tx.send(finalize_rg_task).await.is_err() {
721                        return Ok(());
722                    }
723
724                    current_rg_rows = 0;
725                    rb = rb.slice(rows_left, rb.num_rows() - rows_left);
726
727                    row_group_index += 1;
728                    let col_writers = row_group_writer_factory
729                        .create_column_writers(row_group_index)?;
730                    (column_writer_handles, col_array_channels) =
731                        spawn_column_parallel_row_group_writer(
732                            col_writers,
733                            max_buffer_rb,
734                            &ctx.pool,
735                            &encoding_time,
736                        )?;
737                }
738            }
739        }
740
741        drop(col_array_channels);
742        // Handle leftover rows as final rowgroup, which may be smaller than max_row_group_rows
743        if current_rg_rows > 0 {
744            let finalize_rg_task = spawn_rg_join_and_finalize_task(
745                column_writer_handles,
746                current_rg_rows,
747                &ctx.pool,
748                encoding_time.clone(),
749            );
750
751            // Do not surface error from closed channel (means something
752            // else hit an error, and the plan is shutting down).
753            if serialize_tx.send(finalize_rg_task).await.is_err() {
754                return Ok(());
755            }
756        }
757
758        Ok(())
759    })
760}
761
762/// Consume RowGroups serialized by other parallel tasks and concatenate them in
763/// to the final parquet file, while flushing finalized bytes to an [ObjectStore]
764async fn concatenate_parallel_row_groups(
765    mut parquet_writer: SerializedFileWriter<SharedBuffer>,
766    merged_buff: SharedBuffer,
767    mut serialize_rx: Receiver<SpawnedTask<RBStreamSerializeResult>>,
768    mut object_store_writer: Box<dyn AsyncWrite + Send + Unpin>,
769    pool: Arc<dyn MemoryPool>,
770) -> Result<ParquetMetaData> {
771    let file_reservation =
772        MemoryConsumer::new("ParquetSink(SerializedFileWriter)").register(&pool);
773
774    while let Some(task) = serialize_rx.recv().await {
775        let result = task.join_unwind().await;
776        let (serialized_columns, rg_reservation, _cnt) =
777            result.map_err(|e| DataFusionError::ExecutionJoin(Box::new(e)))??;
778
779        let mut rg_out = parquet_writer.next_row_group()?;
780        for chunk in serialized_columns {
781            chunk.append_to_row_group(&mut rg_out)?;
782            rg_reservation.free();
783
784            let mut buff_to_flush = merged_buff.buffer.try_lock().unwrap();
785            file_reservation.try_resize(buff_to_flush.len())?;
786
787            if buff_to_flush.len() > BUFFER_FLUSH_BYTES {
788                object_store_writer
789                    .write_all(buff_to_flush.as_slice())
790                    .await?;
791                buff_to_flush.clear();
792                file_reservation.try_resize(buff_to_flush.len())?; // will set to zero
793            }
794        }
795        rg_out.close()?;
796    }
797
798    let parquet_meta_data = parquet_writer.close()?;
799    let final_buff = merged_buff.buffer.try_lock().unwrap();
800
801    object_store_writer.write_all(final_buff.as_slice()).await?;
802    object_store_writer.shutdown().await?;
803    file_reservation.free();
804
805    Ok(parquet_meta_data)
806}
807
808/// Parallelizes the serialization of a single parquet file, by first serializing N
809/// independent RecordBatch streams in parallel to RowGroups in memory. Another
810/// task then stitches these independent RowGroups together and streams this large
811/// single parquet file to an ObjectStore in multiple parts.
812async fn output_single_parquet_file_parallelized(
813    object_store_writer: Box<dyn AsyncWrite + Send + Unpin>,
814    data: Receiver<RecordBatch>,
815    ctx: ParquetFileWriteContext,
816    encoding_time: Time,
817) -> Result<ParquetMetaData> {
818    let max_rowgroups = ctx.parallel_options.max_parallel_row_groups;
819    // Buffer size of this channel limits maximum number of RowGroups being worked on in parallel
820    let (serialize_tx, serialize_rx) =
821        mpsc::channel::<SpawnedTask<RBStreamSerializeResult>>(max_rowgroups);
822
823    let merged_buff = SharedBuffer::new(INITIAL_BUFFER_BYTES);
824    let options = ArrowWriterOptions::new()
825        .with_properties((*ctx.props).clone())
826        .with_skip_arrow_metadata(ctx.skip_arrow_metadata);
827    let writer = ArrowWriter::try_new_with_options(
828        merged_buff.clone(),
829        Arc::clone(&ctx.schema),
830        options,
831    )?;
832    let (writer, row_group_writer_factory) = writer.into_serialized_writer()?;
833
834    let pool = Arc::clone(&ctx.pool);
835    let launch_serialization_task = spawn_parquet_parallel_serialization_task(
836        row_group_writer_factory,
837        data,
838        serialize_tx,
839        ctx,
840        encoding_time,
841    );
842    let parquet_meta_data = concatenate_parallel_row_groups(
843        writer,
844        merged_buff,
845        serialize_rx,
846        object_store_writer,
847        pool,
848    )
849    .await?;
850
851    launch_serialization_task
852        .join_unwind()
853        .await
854        .map_err(|e| DataFusionError::ExecutionJoin(Box::new(e)))??;
855    Ok(parquet_meta_data)
856}