Skip to main content

mito2/sst/parquet/
writer.rs

1// Copyright 2023 Greptime Team
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Parquet writer.
16
17use std::collections::HashMap;
18use std::future::Future;
19use std::mem;
20use std::pin::Pin;
21use std::sync::Arc;
22use std::sync::atomic::{AtomicUsize, Ordering};
23use std::task::{Context, Poll};
24use std::time::Instant;
25
26use bytes::Bytes;
27use common_telemetry::debug;
28use common_time::Timestamp;
29use datatypes::arrow::array::{
30    ArrayRef, BinaryArray, TimestampMicrosecondArray, TimestampMillisecondArray,
31    TimestampNanosecondArray, TimestampSecondArray, UInt32Array,
32};
33use datatypes::arrow::compute::{max, min};
34use datatypes::arrow::datatypes::{DataType, SchemaRef, TimeUnit};
35use datatypes::arrow::record_batch::RecordBatch;
36use datatypes::extension::json::is_json2_extension_type;
37use object_store::{FuturesAsyncWriter, ObjectStore};
38use parquet::arrow::AsyncArrowWriter;
39use parquet::arrow::async_writer::AsyncFileWriter;
40use parquet::basic::{Compression, Encoding, ZstdLevel};
41use parquet::file::metadata::{FileMetaData, KeyValue, ParquetMetaData};
42use parquet::file::properties::WriterProperties;
43use smallvec::smallvec;
44use snafu::{OptionExt, ResultExt, ensure};
45use store_api::metadata::RegionMetadataRef;
46use store_api::storage::{FileId, SequenceNumber};
47use tokio::io::AsyncWrite;
48use tokio_util::compat::{Compat, FuturesAsyncWriteCompatExt};
49
50use crate::access_layer::{FilePathProvider, Metrics, SstInfoArray, TempFileCleaner};
51use crate::config::{IndexBuildMode, IndexConfig};
52use crate::error::{
53    InvalidMetadataSnafu, InvalidRecordBatchSnafu, OpenDalSnafu, Result, UnexpectedSnafu,
54    WriteParquetSnafu,
55};
56use crate::read::FlatSource;
57use crate::sst::file::RegionFileId;
58use crate::sst::index::{IndexOutput, Indexer, IndexerBuilder};
59use crate::sst::parquet::flat_format::{
60    FlatWriteFormat, primary_key_column_index, time_index_column_index,
61};
62use crate::sst::parquet::format::{PrimaryKeyArray, PrimaryKeyWriteFormat};
63use crate::sst::parquet::parallel_writer::ParallelArrowWriter;
64use crate::sst::parquet::{
65    COLUMN_INDEX_TRUNCATE_LENGTH, PARQUET_METADATA_KEY, SstInfo, WriteOptions,
66    apply_column_compression, apply_float_field_encoding, apply_internal_column_encoding,
67};
68use crate::sst::{
69    DEFAULT_WRITE_CONCURRENCY, FlatSchemaOptions, SeriesEstimator, maybe_wrap_schema,
70};
71
72/// Converts a flat RecordBatch for writing to parquet.
73enum FlatBatchConverter {
74    /// Write as-is in flat format.
75    Flat(FlatWriteFormat),
76    /// Convert flat batch to primary-key format by stripping tag columns.
77    PrimaryKey {
78        format: PrimaryKeyWriteFormat,
79        num_fields: usize,
80    },
81}
82
83impl FlatBatchConverter {
84    fn convert_batch(&self, batch: &RecordBatch) -> Result<RecordBatch> {
85        match self {
86            FlatBatchConverter::Flat(f) => f.convert_batch(batch),
87            FlatBatchConverter::PrimaryKey { format, num_fields } => {
88                format.convert_flat_batch(batch, *num_fields)
89            }
90        }
91    }
92}
93
94/// Result of splitting a batch at the next series boundary.
95enum SeriesBoundarySplit {
96    /// The whole batch belongs to the current series and stays in the current file.
97    Continue(RecordBatch),
98    /// The batch crosses a series boundary.
99    Split {
100        /// Remaining rows of the current series, appended to the current file.
101        current_file_tail: Option<RecordBatch>,
102        /// Rows of the following series that start the next file.
103        next_file_head: RecordBatch,
104    },
105}
106
107/// Returns the dictionary keys and values of the encoded `__primary_key` column.
108fn encoded_primary_keys(batch: &RecordBatch) -> Result<(&UInt32Array, &BinaryArray)> {
109    let column = batch.column(primary_key_column_index(batch.num_columns()));
110    let primary_keys = column
111        .as_any()
112        .downcast_ref::<PrimaryKeyArray>()
113        .with_context(|| InvalidRecordBatchSnafu {
114            reason: format!(
115                "expected dictionary primary key column, got {:?}",
116                column.data_type()
117            ),
118        })?;
119    let values = primary_keys
120        .values()
121        .as_any()
122        .downcast_ref::<BinaryArray>()
123        .with_context(|| InvalidRecordBatchSnafu {
124            reason: format!(
125                "expected binary primary key values, got {:?}",
126                primary_keys.values().data_type()
127            ),
128        })?;
129    Ok((primary_keys.keys(), values))
130}
131
132/// Splits `batch` at the first row whose encoded primary key differs from
133/// `current_primary_key`, the last primary key written to the current file.
134///
135/// The writer finishes an oversized file at such a series boundary so that the
136/// primary key ranges of output files never overlap, which allows pickers like
137/// TWCS to detect overlapping files by their time and primary key ranges. A
138/// series is never split in the middle: if no row starts a new series, the whole
139/// batch stays in the current file even if it already exceeds the size limit.
140fn split_at_next_series(
141    batch: RecordBatch,
142    current_primary_key: &[u8],
143) -> Result<SeriesBoundarySplit> {
144    let (keys, values) = encoded_primary_keys(&batch)?;
145    let Some(offset) = (0..batch.num_rows())
146        .find(|&row| values.value(keys.value(row) as usize) != current_primary_key)
147    else {
148        return Ok(SeriesBoundarySplit::Continue(batch));
149    };
150
151    let next_file_head = batch.slice(offset, batch.num_rows() - offset);
152    let current_file_tail = (offset > 0).then(|| batch.slice(0, offset));
153    Ok(SeriesBoundarySplit::Split {
154        current_file_tail,
155        next_file_head,
156    })
157}
158
159/// Arrow writer of the current SST file.
160enum SstArrowWriter<W> {
161    /// Encodes on the writing task.
162    Serial(AsyncArrowWriter<W>),
163    /// Encodes the columns of each row group on blocking tasks.
164    Parallel(ParallelArrowWriter<W>),
165}
166
167impl<W: AsyncFileWriter> SstArrowWriter<W> {
168    async fn write(&mut self, batch: &RecordBatch) -> parquet::errors::Result<()> {
169        match self {
170            SstArrowWriter::Serial(writer) => writer.write(batch).await,
171            SstArrowWriter::Parallel(writer) => writer.write(batch).await,
172        }
173    }
174
175    async fn flush(&mut self) -> parquet::errors::Result<()> {
176        match self {
177            SstArrowWriter::Serial(writer) => writer.flush().await,
178            SstArrowWriter::Parallel(writer) => writer.flush().await,
179        }
180    }
181
182    async fn close(self) -> parquet::errors::Result<ParquetMetaData> {
183        match self {
184            SstArrowWriter::Serial(writer) => writer.close().await,
185            SstArrowWriter::Parallel(writer) => writer.close().await,
186        }
187    }
188
189    async fn abort(self) {
190        if let SstArrowWriter::Parallel(writer) = self {
191            writer.abort().await;
192        }
193    }
194}
195
196/// Optional limits used by multi-output compaction. A batch may temporarily
197/// exceed the working threshold before its check; callers reserve that workspace.
198#[derive(Clone, Copy)]
199pub(crate) struct SstWriteLimits {
200    pub(crate) working_bytes: usize,
201    pub(crate) metadata_bytes: usize,
202}
203
204/// Tracks retained footer memory across rolled files as well as the current file.
205#[derive(Default)]
206struct WriteMemory {
207    limits: Option<SstWriteLimits>,
208    completed: usize,
209    current: usize,
210    row_groups: usize,
211}
212
213impl WriteMemory {
214    fn check(&self, bytes: usize) -> Result<()> {
215        if let Some(limits) = self.limits {
216            ensure!(
217                bytes <= limits.metadata_bytes,
218                crate::error::SstWriteMemoryExceededSnafu {
219                    kind: "metadata",
220                    bytes,
221                    limit: limits.metadata_bytes,
222                }
223            );
224        }
225        Ok(())
226    }
227}
228
229/// Parquet SST writer.
230pub struct ParquetWriter<'a, F: WriterFactory, I: IndexerBuilder, P: FilePathProvider> {
231    /// Path provider that creates SST and index file paths according to file id.
232    path_provider: P,
233    writer: Option<SstArrowWriter<SizeAwareWriter<F::Writer>>>,
234    /// Current active file id.
235    current_file: FileId,
236    writer_factory: F,
237    /// Region metadata of the source and the target SST.
238    metadata: RegionMetadataRef,
239    /// Global index config.
240    index_config: IndexConfig,
241    /// Indexer build that can create indexer for multiple files.
242    indexer_builder: I,
243    /// Current active indexer.
244    current_indexer: Option<Indexer>,
245    bytes_written: Arc<AtomicUsize>,
246    /// Cleaner to remove temp files on failure.
247    file_cleaner: Option<TempFileCleaner>,
248    /// Write metrics
249    metrics: &'a mut Metrics,
250    write_guard: Option<Arc<dyn SstWriteGuard>>,
251    memory: WriteMemory,
252}
253
254/// Registers durable ownership before either an SST or its index is published to storage.
255#[async_trait::async_trait]
256pub trait SstWriteGuard: Send + Sync {
257    async fn before_create(&self, file_id: RegionFileId) -> Result<()>;
258}
259
260/// Protection and resource limits travel together for table-compaction output.
261pub(crate) struct SstWriteContext {
262    pub(crate) guard: Arc<dyn SstWriteGuard>,
263    pub(crate) limits: SstWriteLimits,
264}
265
266pub trait WriterFactory {
267    type Writer: AsyncWrite + Send + Unpin;
268    fn create(
269        &mut self,
270        file_path: &str,
271        write_buffer_size: usize,
272    ) -> impl Future<Output = Result<Self::Writer>>;
273}
274
275pub struct ObjectStoreWriterFactory {
276    object_store: ObjectStore,
277    concurrency: usize,
278}
279
280impl WriterFactory for ObjectStoreWriterFactory {
281    type Writer = Compat<FuturesAsyncWriter>;
282
283    async fn create(&mut self, file_path: &str, write_buffer_size: usize) -> Result<Self::Writer> {
284        self.object_store
285            .writer_with(file_path)
286            .chunk(write_buffer_size)
287            .concurrent(self.concurrency)
288            .await
289            .map(|v| v.into_futures_async_write().compat_write())
290            .context(OpenDalSnafu)
291    }
292}
293
294impl<'a, I, P> ParquetWriter<'a, ObjectStoreWriterFactory, I, P>
295where
296    P: FilePathProvider,
297    I: IndexerBuilder,
298{
299    pub(crate) fn with_write_concurrency(mut self, concurrency: usize) -> Self {
300        self.writer_factory.concurrency = concurrency;
301        self
302    }
303
304    pub async fn new_with_object_store(
305        object_store: ObjectStore,
306        metadata: RegionMetadataRef,
307        index_config: IndexConfig,
308        indexer_builder: I,
309        path_provider: P,
310        metrics: &'a mut Metrics,
311    ) -> ParquetWriter<'a, ObjectStoreWriterFactory, I, P> {
312        ParquetWriter::new(
313            ObjectStoreWriterFactory {
314                object_store,
315                concurrency: DEFAULT_WRITE_CONCURRENCY,
316            },
317            metadata,
318            index_config,
319            indexer_builder,
320            path_provider,
321            metrics,
322        )
323        .await
324    }
325
326    pub(crate) fn with_file_cleaner(mut self, cleaner: TempFileCleaner) -> Self {
327        self.file_cleaner = Some(cleaner);
328        self
329    }
330}
331
332impl<'a, F, I, P> ParquetWriter<'a, F, I, P>
333where
334    F: WriterFactory,
335    I: IndexerBuilder,
336    P: FilePathProvider,
337{
338    /// Creates a new parquet SST writer.
339    pub async fn new(
340        factory: F,
341        metadata: RegionMetadataRef,
342        index_config: IndexConfig,
343        indexer_builder: I,
344        path_provider: P,
345        metrics: &'a mut Metrics,
346    ) -> ParquetWriter<'a, F, I, P> {
347        let init_file = FileId::random();
348
349        ParquetWriter {
350            path_provider,
351            writer: None,
352            current_file: init_file,
353            writer_factory: factory,
354            metadata,
355            index_config,
356            indexer_builder,
357            // Build lazily with the actual row-group size, after the write guard.
358            current_indexer: None,
359            memory: WriteMemory::default(),
360            bytes_written: Arc::new(AtomicUsize::new(0)),
361            file_cleaner: None,
362            metrics,
363            write_guard: None,
364        }
365    }
366
367    pub(crate) fn with_write_guard(mut self, guard: Option<Arc<dyn SstWriteGuard>>) -> Self {
368        self.write_guard = guard;
369        self
370    }
371
372    pub(crate) fn with_memory_limits(mut self, limits: Option<SstWriteLimits>) -> Self {
373        self.memory.limits = limits;
374        self
375    }
376
377    /// Checks encoding state without flushing a partial row group: readers and
378    /// index row IDs require uniform row-group lengths except for the final one.
379    /// Only new footer entries are measured, avoiding quadratic footer walks.
380    async fn check_memory(&mut self) -> Result<()> {
381        let Some(limits) = self.memory.limits else {
382            return Ok(());
383        };
384        if let Some(SstArrowWriter::Serial(writer)) = &mut self.writer {
385            ensure!(
386                writer.memory_size() <= limits.working_bytes,
387                crate::error::SstWriteMemoryExceededSnafu {
388                    kind: "encoder",
389                    bytes: writer.memory_size(),
390                    limit: limits.working_bytes,
391                }
392            );
393            for group in &writer.flushed_row_groups()[self.memory.row_groups..] {
394                let file = FileMetaData::new(1, 0, None, None, group.schema_descr_ptr(), None);
395                // Reuse Parquet's heap estimator; counting the schema per group
396                // is conservative, including variable-length min/max statistics.
397                let metadata = ParquetMetaData::new(file, vec![group.clone()]);
398                self.memory.current = self.memory.current.saturating_add(metadata.memory_size());
399            }
400            self.memory.row_groups = writer.flushed_row_groups().len();
401        }
402        self.memory
403            .check(self.memory.completed.saturating_add(self.memory.current))
404    }
405
406    /// Finishes current SST file and index file.
407    async fn finish_current_file(
408        &mut self,
409        ssts: &mut SstInfoArray,
410        stats: &mut SourceStats,
411    ) -> Result<()> {
412        // maybe_init_writer will re-create a new file.
413        if let Some(mut current_writer) = mem::take(&mut self.writer) {
414            let mut stats = mem::take(stats);
415            // At least one row has been written.
416            assert!(stats.num_rows > 0);
417
418            debug!(
419                "Finishing current file {}, file size: {}, num rows: {}",
420                self.current_file,
421                self.bytes_written.load(Ordering::Relaxed),
422                stats.num_rows
423            );
424
425            // Finish indexer and writer.
426            // safety: writer and index can only be both present or not.
427            let mut index_output = IndexOutput::default();
428            match self.index_config.build_mode {
429                IndexBuildMode::Sync => {
430                    index_output = self.current_indexer.as_mut().unwrap().finish().await;
431                }
432                IndexBuildMode::Async => {
433                    debug!(
434                        "Index for file {} will be built asynchronously later",
435                        self.current_file
436                    );
437                }
438            }
439            current_writer.flush().await.context(WriteParquetSnafu)?;
440
441            let parquet_metadata = current_writer.close().await.context(WriteParquetSnafu)?;
442            let file_size = self.bytes_written.load(Ordering::Relaxed) as u64;
443            if self.memory.limits.is_some() {
444                self.memory.completed = self.memory.completed.saturating_add(
445                    parquet_metadata.memory_size().saturating_mul(2)
446                        + std::mem::size_of::<SstInfo>(),
447                );
448                self.memory.check(self.memory.completed)?;
449                self.memory.current = 0;
450                self.memory.row_groups = 0;
451            }
452
453            // Safety: num rows > 0 so we must have min/max.
454            let time_range = stats.time_range.unwrap();
455
456            let max_row_group_uncompressed_size: u64 = parquet_metadata
457                .row_groups()
458                .iter()
459                .map(|rg| {
460                    rg.columns()
461                        .iter()
462                        .map(|c| c.uncompressed_size() as u64)
463                        .sum::<u64>()
464                })
465                .max()
466                .unwrap_or(0);
467            let num_series = stats.series_estimator.finish();
468            ssts.push(SstInfo {
469                file_id: self.current_file,
470                time_range,
471                file_size,
472                max_row_group_uncompressed_size,
473                num_rows: stats.num_rows,
474                num_row_groups: parquet_metadata.num_row_groups() as u64,
475                file_metadata: Some(Arc::new(parquet_metadata)),
476                index_metadata: index_output,
477                num_series,
478            });
479            self.current_file = FileId::random();
480            self.bytes_written.store(0, Ordering::Relaxed)
481        };
482
483        Ok(())
484    }
485
486    /// Iterates FlatSource and writes all RecordBatch in flat format to Parquet file.
487    ///
488    /// The source must yield batches globally sorted by the encoded primary key.
489    /// `opts.max_file_size` is a soft limit for regions with a primary key: the
490    /// current file is finished at the next series boundary after exceeding the
491    /// limit (see [split_at_next_series]), so a series larger than the limit
492    /// stays in one file. Regions without a primary key are split at batch
493    /// boundaries.
494    ///
495    /// Returns the [SstInfo] if the SST is written.
496    pub async fn write_all_flat(
497        &mut self,
498        source: FlatSource,
499        override_sequence: Option<SequenceNumber>,
500        opts: &WriteOptions,
501    ) -> Result<SstInfoArray> {
502        let mut options = FlatSchemaOptions::from_encoding(self.metadata.primary_key_encoding);
503
504        if source.schema().fields().iter().any(is_json2_extension_type) {
505            options.concretized_json_types = source
506                .schema()
507                .fields()
508                .iter()
509                .filter(|&field| is_json2_extension_type(field))
510                .map(|field| (field.name().clone(), field.data_type().clone()))
511                .collect::<HashMap<_, _>>();
512        }
513
514        let converter = FlatBatchConverter::Flat(
515            FlatWriteFormat::new(self.metadata.clone(), &options)
516                .with_override_sequence(override_sequence),
517        );
518        let res = self.write_all_flat_inner(source, &converter, opts).await;
519        if res.is_err() {
520            self.abort_current_file().await;
521        }
522        res
523    }
524
525    /// Iterates FlatSource and writes all RecordBatch in primary-key format to Parquet file.
526    ///
527    /// The source must yield batches globally sorted by the encoded primary key.
528    /// See [Self::write_all_flat] for the `opts.max_file_size` semantics.
529    ///
530    /// Returns the [SstInfo] if the SST is written.
531    pub async fn write_all_flat_as_primary_key(
532        &mut self,
533        source: FlatSource,
534        override_sequence: Option<SequenceNumber>,
535        opts: &WriteOptions,
536    ) -> Result<SstInfoArray> {
537        let num_fields = self.metadata.field_columns().count();
538        let converter = FlatBatchConverter::PrimaryKey {
539            format: PrimaryKeyWriteFormat::new(self.metadata.clone())
540                .with_override_sequence(override_sequence),
541            num_fields,
542        };
543        let res = self.write_all_flat_inner(source, &converter, opts).await;
544        if res.is_err() {
545            self.abort_current_file().await;
546        }
547        res
548    }
549
550    async fn write_all_flat_inner(
551        &mut self,
552        mut source: FlatSource,
553        converter: &FlatBatchConverter,
554        opts: &WriteOptions,
555    ) -> Result<SstInfoArray> {
556        let mut results = smallvec![];
557        let mut stats = SourceStats::default();
558
559        loop {
560            let start = Instant::now();
561            let batch = match source.next_batch().await {
562                Ok(Some(batch)) => batch,
563                Ok(None) => break,
564                Err(e) => {
565                    self.abort_current_indexer().await;
566                    return Err(e);
567                }
568            };
569            self.metrics.iter_source += start.elapsed();
570
571            if self.metadata.primary_key.is_empty() {
572                self.append_flat_batch(&batch, converter, opts, &mut stats)
573                    .await?;
574                if self.exceeds_max_file_size(opts) {
575                    self.finish_current_file(&mut results, &mut stats).await?;
576                }
577            } else if self.exceeds_max_file_size(opts)
578                && let Some(current_primary_key) = stats.last_primary_key.as_deref()
579            {
580                let series_split = match split_at_next_series(batch, current_primary_key) {
581                    Ok(series_split) => series_split,
582                    Err(e) => {
583                        self.abort_current_indexer().await;
584                        return Err(e);
585                    }
586                };
587                match series_split {
588                    SeriesBoundarySplit::Continue(batch) => {
589                        self.append_flat_batch(&batch, converter, opts, &mut stats)
590                            .await?;
591                    }
592                    SeriesBoundarySplit::Split {
593                        current_file_tail,
594                        next_file_head,
595                    } => {
596                        if let Some(tail) = current_file_tail {
597                            self.append_flat_batch(&tail, converter, opts, &mut stats)
598                                .await?;
599                        }
600                        self.finish_current_file(&mut results, &mut stats).await?;
601                        self.append_flat_batch(&next_file_head, converter, opts, &mut stats)
602                            .await?;
603                    }
604                }
605            } else {
606                self.append_flat_batch(&batch, converter, opts, &mut stats)
607                    .await?;
608            }
609        }
610
611        self.finish_current_file(&mut results, &mut stats).await?;
612
613        // object_store.write will make sure all bytes are written or an error is raised.
614        Ok(results)
615    }
616
617    async fn append_flat_batch(
618        &mut self,
619        batch: &RecordBatch,
620        converter: &FlatBatchConverter,
621        opts: &WriteOptions,
622        stats: &mut SourceStats,
623    ) -> Result<()> {
624        let result = async {
625            let arrow_batch = converter.convert_batch(batch)?;
626            let start = Instant::now();
627            self.maybe_init_writer(arrow_batch.schema_ref(), opts)
628                .await?
629                .write(&arrow_batch)
630                .await
631                .context(WriteParquetSnafu)?;
632            self.metrics.write_batch += start.elapsed();
633
634            stats.update_flat(batch)?;
635            if matches!(self.index_config.build_mode, IndexBuildMode::Sync) {
636                let start = Instant::now();
637                // safety: self.current_indexer must be set when first batch has been written.
638                self.current_indexer
639                    .as_mut()
640                    .unwrap()
641                    .update_flat(batch)
642                    .await;
643                self.metrics.update_index += start.elapsed();
644            }
645            Ok(())
646        }
647        .await;
648
649        let result = match result {
650            Ok(()) => self.check_memory().await,
651            Err(error) => Err(error),
652        };
653        if result.is_err() {
654            self.abort_current_indexer().await;
655        }
656        result
657    }
658
659    fn exceeds_max_file_size(&self, opts: &WriteOptions) -> bool {
660        opts.max_file_size
661            .is_some_and(|max_size| self.bytes_written.load(Ordering::Relaxed) >= max_size)
662    }
663
664    /// Joins encoders before dropping the writer and deleting its failed output.
665    async fn abort_current_file(&mut self) {
666        if let Some(writer) = self.writer.take() {
667            writer.abort().await;
668        }
669        if let Some(cleaner) = &self.file_cleaner {
670            cleaner.clean_by_file_id(self.current_file).await;
671        }
672    }
673
674    async fn abort_current_indexer(&mut self) {
675        if let Some(indexer) = &mut self.current_indexer {
676            indexer.abort().await;
677        }
678    }
679
680    async fn maybe_init_writer(
681        &mut self,
682        schema: &SchemaRef,
683        opts: &WriteOptions,
684    ) -> Result<&mut SstArrowWriter<SizeAwareWriter<F::Writer>>> {
685        if let Some(ref mut w) = self.writer {
686            Ok(w)
687        } else {
688            let json = self.metadata.to_json().context(InvalidMetadataSnafu)?;
689            let key_value_meta = KeyValue::new(PARQUET_METADATA_KEY.to_string(), json);
690
691            // TODO(yingwen): Find and set proper column encoding for internal columns: op type and tsid.
692            let props_builder = WriterProperties::builder()
693                .set_key_value_metadata(Some(vec![key_value_meta]))
694                .set_compression(Compression::ZSTD(ZstdLevel::default()))
695                .set_encoding(Encoding::PLAIN)
696                .set_max_row_group_row_count(Some(opts.row_group_size))
697                .set_column_index_truncate_length(COLUMN_INDEX_TRUNCATE_LENGTH)
698                .set_statistics_truncate_length(None);
699            let props_builder = apply_internal_column_encoding(props_builder, &self.metadata);
700            let props_builder = apply_float_field_encoding(
701                props_builder,
702                &self.metadata,
703                opts.float_field_encoding,
704            );
705            let props_builder = apply_column_compression(props_builder, &self.metadata)?;
706            let writer_props = props_builder.build();
707
708            let sst_file_path = self.path_provider.build_sst_file_path(RegionFileId::new(
709                self.metadata.region_id,
710                self.current_file,
711            ));
712            if let Some(guard) = &self.write_guard {
713                guard
714                    .before_create(RegionFileId::new(
715                        self.metadata.region_id,
716                        self.current_file,
717                    ))
718                    .await?;
719            }
720            let writer = SizeAwareWriter::new(
721                self.writer_factory
722                    .create(&sst_file_path, opts.write_buffer_size.as_bytes() as usize)
723                    .await?,
724                self.bytes_written.clone(),
725            );
726            let schema = maybe_wrap_schema(schema)?;
727            let arrow_writer = if self.memory.limits.is_none()
728                && opts.encode_parallelism > 1
729                && ParallelArrowWriter::<SizeAwareWriter<F::Writer>>::supports(&writer_props)
730            {
731                SstArrowWriter::Parallel(
732                    ParallelArrowWriter::try_new(
733                        writer,
734                        schema,
735                        writer_props,
736                        opts.encode_parallelism,
737                        opts.encode_runtime,
738                    )
739                    .context(WriteParquetSnafu)?,
740                )
741            } else {
742                SstArrowWriter::Serial(
743                    AsyncArrowWriter::try_new(writer, schema, Some(writer_props))
744                        .context(WriteParquetSnafu)?,
745                )
746            };
747            self.writer = Some(arrow_writer);
748
749            let indexer = self
750                .indexer_builder
751                .build(
752                    RegionFileId::new(self.metadata.region_id, self.current_file),
753                    0,
754                    Some(opts.row_group_size),
755                )
756                .await;
757            self.current_indexer = Some(indexer);
758
759            // safety: self.writer is assigned above
760            Ok(self.writer.as_mut().unwrap())
761        }
762    }
763}
764
765#[derive(Default)]
766struct SourceStats {
767    /// Number of rows fetched.
768    num_rows: usize,
769    /// Time range of fetched batches.
770    time_range: Option<(Timestamp, Timestamp)>,
771    /// Last primary key written to the current file.
772    last_primary_key: Option<Bytes>,
773    /// Series estimator for computing num_series.
774    series_estimator: SeriesEstimator,
775}
776
777impl SourceStats {
778    fn update_flat(&mut self, record_batch: &RecordBatch) -> Result<()> {
779        if record_batch.num_rows() == 0 {
780            return Ok(());
781        }
782
783        self.num_rows += record_batch.num_rows();
784        self.series_estimator.update_flat(record_batch);
785        let (keys, values) = encoded_primary_keys(record_batch)?;
786        let key = keys.value(record_batch.num_rows() - 1);
787        self.last_primary_key = Some(Bytes::copy_from_slice(values.value(key as usize)));
788
789        // Get the timestamp column by index
790        let time_index_col_idx = time_index_column_index(record_batch.num_columns());
791        let timestamp_array = record_batch.column(time_index_col_idx);
792
793        if let Some((min_in_batch, max_in_batch)) = timestamp_range_from_array(timestamp_array)? {
794            if let Some(time_range) = &mut self.time_range {
795                time_range.0 = time_range.0.min(min_in_batch);
796                time_range.1 = time_range.1.max(max_in_batch);
797            } else {
798                self.time_range = Some((min_in_batch, max_in_batch));
799            }
800        }
801
802        Ok(())
803    }
804}
805
806/// Gets min and max timestamp from an timestamp array.
807fn timestamp_range_from_array(
808    timestamp_array: &ArrayRef,
809) -> Result<Option<(Timestamp, Timestamp)>> {
810    let (min_ts, max_ts) = match timestamp_array.data_type() {
811        DataType::Timestamp(TimeUnit::Second, _) => {
812            let array = timestamp_array
813                .as_any()
814                .downcast_ref::<TimestampSecondArray>()
815                .unwrap();
816            let min_val = min(array).map(Timestamp::new_second);
817            let max_val = max(array).map(Timestamp::new_second);
818            (min_val, max_val)
819        }
820        DataType::Timestamp(TimeUnit::Millisecond, _) => {
821            let array = timestamp_array
822                .as_any()
823                .downcast_ref::<TimestampMillisecondArray>()
824                .unwrap();
825            let min_val = min(array).map(Timestamp::new_millisecond);
826            let max_val = max(array).map(Timestamp::new_millisecond);
827            (min_val, max_val)
828        }
829        DataType::Timestamp(TimeUnit::Microsecond, _) => {
830            let array = timestamp_array
831                .as_any()
832                .downcast_ref::<TimestampMicrosecondArray>()
833                .unwrap();
834            let min_val = min(array).map(Timestamp::new_microsecond);
835            let max_val = max(array).map(Timestamp::new_microsecond);
836            (min_val, max_val)
837        }
838        DataType::Timestamp(TimeUnit::Nanosecond, _) => {
839            let array = timestamp_array
840                .as_any()
841                .downcast_ref::<TimestampNanosecondArray>()
842                .unwrap();
843            let min_val = min(array).map(Timestamp::new_nanosecond);
844            let max_val = max(array).map(Timestamp::new_nanosecond);
845            (min_val, max_val)
846        }
847        _ => {
848            return UnexpectedSnafu {
849                reason: format!(
850                    "Unexpected data type of time index: {:?}",
851                    timestamp_array.data_type()
852                ),
853            }
854            .fail();
855        }
856    };
857
858    // If min timestamp exists, max timestamp should also exist.
859    Ok(min_ts.zip(max_ts))
860}
861
862/// Workaround for [AsyncArrowWriter] does not provide a method to
863/// get total bytes written after close.
864struct SizeAwareWriter<W> {
865    inner: W,
866    size: Arc<AtomicUsize>,
867}
868
869impl<W> SizeAwareWriter<W> {
870    fn new(inner: W, size: Arc<AtomicUsize>) -> Self {
871        Self {
872            inner,
873            size: size.clone(),
874        }
875    }
876}
877
878impl<W> AsyncWrite for SizeAwareWriter<W>
879where
880    W: AsyncWrite + Unpin,
881{
882    fn poll_write(
883        mut self: Pin<&mut Self>,
884        cx: &mut Context<'_>,
885        buf: &[u8],
886    ) -> Poll<std::result::Result<usize, std::io::Error>> {
887        let this = self.as_mut().get_mut();
888
889        match Pin::new(&mut this.inner).poll_write(cx, buf) {
890            Poll::Ready(Ok(bytes_written)) => {
891                this.size.fetch_add(bytes_written, Ordering::Relaxed);
892                Poll::Ready(Ok(bytes_written))
893            }
894            other => other,
895        }
896    }
897
898    fn poll_flush(
899        mut self: Pin<&mut Self>,
900        cx: &mut Context<'_>,
901    ) -> Poll<std::result::Result<(), std::io::Error>> {
902        Pin::new(&mut self.inner).poll_flush(cx)
903    }
904
905    fn poll_shutdown(
906        mut self: Pin<&mut Self>,
907        cx: &mut Context<'_>,
908    ) -> Poll<std::result::Result<(), std::io::Error>> {
909        Pin::new(&mut self.inner).poll_shutdown(cx)
910    }
911}
912
913#[cfg(test)]
914mod tests {
915    use std::sync::atomic::AtomicBool;
916    use std::time::Duration;
917
918    use common_runtime::runtime::{BuilderBuild, RuntimeTrait};
919    use parquet::arrow::ArrowWriter;
920    use parquet::errors::ParquetError;
921    use store_api::region_request::PathType;
922    use tokio::sync::oneshot;
923    use tokio::time::timeout;
924
925    use super::*;
926    use crate::access_layer::{RegionFilePathFactory, WriteType};
927    use crate::compaction::memory_manager::new_compaction_memory_manager;
928    use crate::error::Error;
929    use crate::sst::FormatType;
930    use crate::sst::parquet::tests::NoopIndexBuilder;
931    use crate::test_util::TestEnv;
932    use crate::test_util::sst_util::{new_record_batch_by_range, sst_region_metadata};
933
934    #[rstest::rstest]
935    #[case::flat(FormatType::Flat)]
936    #[case::primary_key(FormatType::PrimaryKey)]
937    #[tokio::test]
938    async fn test_source_error_waits_for_encoding_cleanup(#[case] format: FormatType) {
939        let mut env = TestEnv::new().await;
940        let metadata = Arc::new(sst_region_metadata());
941        let mut metrics = Metrics::new(WriteType::Compaction);
942        let mut writer = ParquetWriter::new_with_object_store(
943            env.init_object_store_manager(),
944            metadata.clone(),
945            IndexConfig::default(),
946            NoopIndexBuilder,
947            RegionFilePathFactory::new("".to_string(), PathType::Bare),
948            &mut metrics,
949        )
950        .await;
951        let opts = WriteOptions {
952            row_group_size: 8,
953            encode_parallelism: 2,
954            ..Default::default()
955        };
956        let batch = new_record_batch_by_range(&["a", "b"], 0, 4);
957        let retained_field = Arc::downgrade(batch.column(2));
958        let converter = match format {
959            FormatType::Flat => FlatBatchConverter::Flat(FlatWriteFormat::new(
960                metadata.clone(),
961                &FlatSchemaOptions::default(),
962            )),
963            FormatType::PrimaryKey => FlatBatchConverter::PrimaryKey {
964                format: PrimaryKeyWriteFormat::new(metadata.clone()),
965                num_fields: metadata.field_columns().count(),
966            },
967        };
968        let encoding_batch = converter.convert_batch(&batch).unwrap();
969        let encoding_schema = encoding_batch.schema();
970        let (_, factory) = ArrowWriter::try_new(Vec::new(), encoding_schema.clone(), None)
971            .unwrap()
972            .into_serialized_writer()
973            .unwrap();
974        let columns = factory.create_column_writers(0).unwrap();
975        let runtime: common_runtime::Runtime = common_runtime::Builder::default()
976            .worker_threads(1)
977            .max_blocking_threads(1)
978            .build()
979            .unwrap();
980        let (started_tx, started_rx) = oneshot::channel();
981        let (release_tx, release_rx) = oneshot::channel();
982        let queued_batch = encoding_batch.clone();
983        let running = runtime.spawn_blocking(move || {
984            started_tx.send(()).unwrap();
985            release_rx.blocking_recv().unwrap();
986            drop((encoding_batch, columns));
987            Err(ParquetError::General(
988                "cleanup encoding failure".to_string(),
989            ))
990        });
991        timeout(Duration::from_secs(10), started_rx)
992            .await
993            .unwrap()
994            .unwrap();
995        let queued_started = Arc::new(AtomicBool::new(false));
996        let queued_started_task = queued_started.clone();
997        let queued = runtime.spawn_blocking(move || {
998            queued_started_task.store(true, Ordering::SeqCst);
999            drop(queued_batch);
1000            Ok((Vec::new(), Vec::new()))
1001        });
1002        let SstArrowWriter::Parallel(parallel) = writer
1003            .maybe_init_writer(&encoding_schema, &opts)
1004            .await
1005            .unwrap()
1006        else {
1007            panic!("expected parallel encoding");
1008        };
1009        // Controlled real tasks represent the previous row group in flight.
1010        parallel.set_encoding_tasks_for_test(vec![running, queued]);
1011        let source = FlatSource::new_stream(
1012            batch.schema(),
1013            Box::pin(futures::stream::iter(vec![
1014                Ok(batch),
1015                InvalidRecordBatchSnafu {
1016                    reason: "source read failed",
1017                }
1018                .fail(),
1019            ])),
1020        );
1021        let memory = new_compaction_memory_manager(1024 * 1024);
1022        let budget = memory.try_acquire(memory.limit_bytes()).unwrap();
1023        let mut write = Box::pin(async {
1024            let _budget = budget;
1025            match format {
1026                FormatType::Flat => writer.write_all_flat(source, None, &opts).await,
1027                FormatType::PrimaryKey => {
1028                    writer
1029                        .write_all_flat_as_primary_key(source, None, &opts)
1030                        .await
1031                }
1032            }
1033        });
1034        // The next row group is buffered, then the source fails. No codec timing
1035        // or I/O wait can make the old indexer-only cleanup pass this assertion.
1036        assert!(futures::poll!(write.as_mut()).is_pending());
1037        assert!(memory.try_acquire(memory.limit_bytes()).is_none());
1038        assert!(retained_field.upgrade().is_some());
1039        release_tx.send(()).unwrap();
1040        let error = timeout(Duration::from_secs(10), write)
1041            .await
1042            .unwrap()
1043            .unwrap_err();
1044        assert!(
1045            matches!(error, Error::InvalidRecordBatch { reason, .. } if reason == "source read failed")
1046        );
1047        assert!(!queued_started.load(Ordering::SeqCst));
1048        assert!(retained_field.upgrade().is_none());
1049        assert!(memory.try_acquire(memory.limit_bytes()).is_some());
1050        assert!(writer.writer.is_none());
1051        assert_eq!(0, writer.bytes_written.load(Ordering::Relaxed));
1052    }
1053}