1use std::mem;
18use std::sync::Arc;
19use std::time::{Duration, Instant};
20
21use bytes::Bytes;
22use common_runtime::JoinHandle;
23use datatypes::arrow::datatypes::SchemaRef;
24use datatypes::arrow::record_batch::RecordBatch;
25use parquet::arrow::ArrowWriter;
26use parquet::arrow::arrow_writer::{
27 ArrowColumnChunk, ArrowColumnWriter, ArrowRowGroupWriterFactory, compute_leaves,
28};
29use parquet::arrow::async_writer::AsyncFileWriter;
30use parquet::errors::{ParquetError, Result};
31use parquet::file::metadata::ParquetMetaData;
32use parquet::file::properties::WriterProperties;
33use parquet::file::writer::SerializedFileWriter;
34
35#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
37pub enum EncodeRuntime {
38 #[default]
40 Global,
41 Compact,
43}
44
45impl EncodeRuntime {
46 fn spawn<F, R>(self, f: F) -> JoinHandle<R>
47 where
48 F: FnOnce() -> R + Send + 'static,
49 R: Send + 'static,
50 {
51 match self {
52 EncodeRuntime::Global => common_runtime::spawn_blocking_global(f),
53 EncodeRuntime::Compact => common_runtime::spawn_blocking_compact(f),
54 }
55 }
56}
57
58type FieldWriters = (usize, Vec<(usize, ArrowColumnWriter)>);
60
61type EncodedFields = (Vec<(usize, ArrowColumnChunk)>, Vec<(usize, Duration)>);
64
65pub struct ParallelArrowWriter<W> {
74 file_writer: SerializedFileWriter<Vec<u8>>,
76 factory: Arc<ArrowRowGroupWriterFactory>,
78 arrow_schema: SchemaRef,
79 leaf_fields: Vec<usize>,
81 field_costs: Vec<Duration>,
83 async_writer: W,
84 max_row_group_rows: usize,
85 parallelism: usize,
86 runtime: EncodeRuntime,
87 buffered: Vec<RecordBatch>,
89 buffered_rows: usize,
90 encoding: Vec<JoinHandle<Result<EncodedFields>>>,
92 num_row_groups: usize,
93}
94
95impl<W: AsyncFileWriter> ParallelArrowWriter<W> {
96 pub fn supports(props: &WriterProperties) -> bool {
99 props.max_row_group_row_count().is_some() && props.max_row_group_bytes().is_none()
100 }
101
102 pub fn try_new(
104 async_writer: W,
105 arrow_schema: SchemaRef,
106 props: WriterProperties,
107 parallelism: usize,
108 runtime: EncodeRuntime,
109 ) -> Result<Self> {
110 let max_row_group_rows = match props.max_row_group_row_count() {
111 Some(rows) if Self::supports(&props) => rows.max(1),
112 _ => {
113 return Err(ParquetError::General(
114 "parallel writer requires a row-count-only row group limit".to_string(),
115 ));
116 }
117 };
118 let (file_writer, factory) =
119 ArrowWriter::try_new(Vec::new(), arrow_schema.clone(), Some(props))?
120 .into_serialized_writer()?;
121 let schema = file_writer.schema_descr();
122 let leaf_fields = (0..schema.num_columns())
123 .map(|leaf| schema.get_column_root_idx(leaf))
124 .collect();
125 let num_fields = arrow_schema.fields().len();
126 Ok(Self {
127 file_writer,
128 factory: Arc::new(factory),
129 arrow_schema,
130 leaf_fields,
131 field_costs: vec![Duration::ZERO; num_fields],
132 async_writer,
133 max_row_group_rows,
134 parallelism: parallelism.max(1),
135 runtime,
136 buffered: Vec::new(),
137 buffered_rows: 0,
138 encoding: Vec::new(),
139 num_row_groups: 0,
140 })
141 }
142
143 pub async fn write(&mut self, batch: &RecordBatch) -> Result<()> {
145 let mut offset = 0;
146 while offset < batch.num_rows() {
147 let len = (self.max_row_group_rows - self.buffered_rows).min(batch.num_rows() - offset);
148 self.buffered.push(batch.slice(offset, len));
149 self.buffered_rows += len;
150 offset += len;
151 if self.buffered_rows == self.max_row_group_rows {
152 self.encode_buffered().await?;
153 }
154 }
155 Ok(())
156 }
157
158 pub async fn flush(&mut self) -> Result<()> {
160 if self.buffered_rows > 0 {
161 self.encode_buffered().await?;
162 }
163 self.finish_encoding().await
164 }
165
166 pub async fn close(mut self) -> Result<ParquetMetaData> {
168 self.flush().await?;
169 let metadata = self.file_writer.finish()?;
170 self.write_buffered_bytes().await?;
171 self.async_writer.complete().await?;
172 Ok(metadata)
173 }
174
175 pub(crate) async fn abort(mut self) {
177 self.buffered.clear();
178 abort_encoding_tasks(&mut self.encoding).await;
179 }
180
181 #[cfg(test)]
183 pub(crate) fn set_encoding_tasks_for_test(
184 &mut self,
185 tasks: Vec<JoinHandle<Result<EncodedFields>>>,
186 ) {
187 assert!(self.encoding.is_empty());
188 self.encoding = tasks;
189 }
190
191 async fn encode_buffered(&mut self) -> Result<()> {
193 self.finish_encoding().await?;
194
195 let batches: Arc<[RecordBatch]> = mem::take(&mut self.buffered).into();
196 self.buffered_rows = 0;
197 let writers = self.factory.create_column_writers(self.num_row_groups)?;
198 self.num_row_groups += 1;
199
200 let mut fields: Vec<FieldWriters> = (0..self.arrow_schema.fields().len())
202 .map(|field| (field, Vec::new()))
203 .collect();
204 for (leaf, writer) in writers.into_iter().enumerate() {
205 fields[self.leaf_fields[leaf]].1.push((leaf, writer));
206 }
207 self.encoding = assign_fields(fields, &self.field_costs, self.parallelism)
208 .into_iter()
209 .map(|fields| {
210 let batches = batches.clone();
211 let schema = self.arrow_schema.clone();
212 self.runtime
213 .spawn(move || encode_fields(&schema, &batches, fields))
214 })
215 .collect();
216 Ok(())
217 }
218
219 async fn finish_encoding(&mut self) -> Result<()> {
221 if self.encoding.is_empty() {
222 return Ok(());
223 }
224 let mut chunks = Vec::with_capacity(self.leaf_fields.len());
225 let mut tasks = mem::take(&mut self.encoding).into_iter();
226 while let Some(task) = tasks.next() {
227 let encoded = task
228 .await
229 .map_err(|e| ParquetError::External(Box::new(e)))
230 .and_then(|encoded| encoded);
231 let (task_chunks, costs) = match encoded {
232 Ok(encoded) => encoded,
233 Err(error) => {
234 abort_encoding_tasks(tasks.as_mut_slice()).await;
235 return Err(error);
236 }
237 };
238 chunks.extend(task_chunks);
239 for (field, cost) in costs {
240 self.field_costs[field] = cost;
241 }
242 }
243 chunks.sort_unstable_by_key(|(leaf, _)| *leaf);
244 let mut row_group = self.file_writer.next_row_group()?;
245 for (_, chunk) in chunks {
246 chunk.append_to_row_group(&mut row_group)?;
247 }
248 row_group.close()?;
249 self.write_buffered_bytes().await
250 }
251
252 async fn write_buffered_bytes(&mut self) -> Result<()> {
254 let bytes = mem::take(self.file_writer.inner_mut());
255 if bytes.is_empty() {
256 return Ok(());
257 }
258 self.async_writer.write(Bytes::from(bytes)).await
259 }
260}
261
262async fn abort_encoding_tasks(tasks: &mut [JoinHandle<Result<EncodedFields>>]) {
264 for task in tasks.iter() {
265 task.abort();
266 }
267 for task in tasks {
268 let _ = task.await;
270 }
271}
272
273fn assign_fields(
277 mut fields: Vec<FieldWriters>,
278 costs: &[Duration],
279 parallelism: usize,
280) -> Vec<Vec<FieldWriters>> {
281 let num_tasks = parallelism.min(fields.len()).max(1);
282 fields.sort_by_key(|(field, _)| std::cmp::Reverse(costs[*field]));
283 let mut tasks: Vec<(Duration, Vec<FieldWriters>)> = (0..num_tasks)
284 .map(|_| (Duration::ZERO, Vec::new()))
285 .collect();
286 for field in fields {
287 let key = |(load, fields): &(Duration, Vec<FieldWriters>)| (*load, fields.len());
288 let mut lightest = 0;
289 for task in 1..tasks.len() {
290 if key(&tasks[task]) < key(&tasks[lightest]) {
291 lightest = task;
292 }
293 }
294 tasks[lightest].0 += costs[field.0];
295 tasks[lightest].1.push(field);
296 }
297 tasks
298 .into_iter()
299 .map(|(_, fields)| fields)
300 .filter(|fields| !fields.is_empty())
301 .collect()
302}
303
304fn encode_fields(
306 schema: &SchemaRef,
307 batches: &[RecordBatch],
308 fields: Vec<FieldWriters>,
309) -> Result<EncodedFields> {
310 let mut chunks = Vec::new();
311 let mut costs = Vec::with_capacity(fields.len());
312 for (field, mut writers) in fields {
313 let start = Instant::now();
314 for batch in batches {
315 let leaves = compute_leaves(schema.field(field), batch.column(field))?;
316 if leaves.len() != writers.len() {
317 return Err(ParquetError::General(format!(
318 "field {} has {} leaf arrays but {} column writers",
319 schema.field(field).name(),
320 leaves.len(),
321 writers.len()
322 )));
323 }
324 for (leaf, (_, writer)) in leaves.iter().zip(writers.iter_mut()) {
325 writer.write(leaf)?;
326 }
327 }
328 for (leaf, writer) in writers {
329 chunks.push((leaf, writer.close()?));
330 }
331 costs.push((field, start.elapsed()));
332 }
333 Ok((chunks, costs))
334}
335
336#[cfg(test)]
337mod tests {
338 use std::sync::Arc;
339 use std::sync::atomic::{AtomicBool, Ordering};
340
341 use common_runtime::runtime::{BuilderBuild, RuntimeTrait};
342 use datatypes::arrow::array::{
343 ArrayRef, DictionaryArray, Float64Array, Int32Array, Int64Array, StringArray, StructArray,
344 };
345 use datatypes::arrow::datatypes::{DataType, Field, Fields, Schema, UInt32Type};
346 use parquet::basic::{Compression, ZstdLevel};
347 use tokio::sync::oneshot;
348 use tokio::time::timeout;
349
350 use super::*;
351 use crate::compaction::memory_manager::new_compaction_memory_manager;
352
353 fn schema() -> SchemaRef {
354 let nested = Fields::from(vec![
355 Field::new("a", DataType::Int32, true),
356 Field::new("b", DataType::Utf8, true),
357 ]);
358 Arc::new(Schema::new(vec![
359 Field::new("int", DataType::Int64, false),
360 Field::new("float", DataType::Float64, true),
361 Field::new(
362 "tag",
363 DataType::Dictionary(Box::new(DataType::UInt32), Box::new(DataType::Utf8)),
364 false,
365 ),
366 Field::new("nested", DataType::Struct(nested), true),
367 ]))
368 }
369
370 fn batch(start: usize, len: usize) -> RecordBatch {
371 let rows = start..start + len;
372 let ints = Int64Array::from_iter_values(rows.clone().map(|i| i as i64 * 7));
373 let floats = Float64Array::from_iter(
374 rows.clone()
375 .map(|i| (i % 5 != 0).then_some((i % 13) as f64 / 3.0)),
376 );
377 let tags: DictionaryArray<UInt32Type> = rows
378 .clone()
379 .map(|i| format!("tag-{}", i % 3))
380 .collect::<Vec<_>>()
381 .iter()
382 .map(String::as_str)
383 .collect();
384 let nested = StructArray::from(vec![
385 (
386 Arc::new(Field::new("a", DataType::Int32, true)),
387 Arc::new(Int32Array::from_iter(
388 rows.clone().map(|i| (i % 4 != 0).then_some(i as i32)),
389 )) as ArrayRef,
390 ),
391 (
392 Arc::new(Field::new("b", DataType::Utf8, true)),
393 Arc::new(StringArray::from_iter(
394 rows.map(|i| (i % 6 != 0).then(|| format!("v{i}"))),
395 )) as ArrayRef,
396 ),
397 ]);
398 RecordBatch::try_new(
399 schema(),
400 vec![
401 Arc::new(ints),
402 Arc::new(floats),
403 Arc::new(tags),
404 Arc::new(nested),
405 ],
406 )
407 .unwrap()
408 }
409
410 fn props(row_group_rows: usize) -> WriterProperties {
411 WriterProperties::builder()
412 .set_compression(Compression::ZSTD(ZstdLevel::default()))
413 .set_max_row_group_row_count(Some(row_group_rows))
414 .set_data_page_row_count_limit(16)
415 .build()
416 }
417
418 #[derive(Clone, Copy)]
420 enum Step {
421 Write(usize),
422 Flush,
423 }
424
425 fn write_serial(steps: &[Step], props: WriterProperties) -> Vec<u8> {
426 let mut buf = Vec::new();
427 let mut writer = ArrowWriter::try_new(&mut buf, schema(), Some(props)).unwrap();
428 let mut start = 0;
429 for step in steps {
430 match *step {
431 Step::Write(len) => {
432 writer.write(&batch(start, len)).unwrap();
433 start += len;
434 }
435 Step::Flush => writer.flush().unwrap(),
436 }
437 }
438 writer.close().unwrap();
439 buf
440 }
441
442 async fn write_parallel(
443 steps: &[Step],
444 props: WriterProperties,
445 parallelism: usize,
446 ) -> Vec<u8> {
447 let mut buf = Vec::new();
448 let mut writer = ParallelArrowWriter::try_new(
449 &mut buf,
450 schema(),
451 props,
452 parallelism,
453 EncodeRuntime::Global,
454 )
455 .unwrap();
456 let mut start = 0;
457 for step in steps {
458 match *step {
459 Step::Write(len) => {
460 writer.write(&batch(start, len)).await.unwrap();
461 start += len;
462 }
463 Step::Flush => writer.flush().await.unwrap(),
464 }
465 }
466 writer.close().await.unwrap();
467 buf
468 }
469
470 #[tokio::test]
471 async fn test_parallel_writer_matches_arrow_writer() {
472 use Step::*;
473 let cases: Vec<(&str, Vec<Step>)> = vec![
474 ("empty", vec![]),
475 ("partial row group", vec![Write(10)]),
476 ("exact row groups", vec![Write(64), Write(64)]),
477 (
478 "batches across row groups",
479 vec![Write(7), Write(100), Write(3), Write(250), Write(1)],
480 ),
481 ("batch spanning many row groups", vec![Write(1000)]),
482 (
483 "explicit flushes",
484 vec![Write(10), Flush, Flush, Write(70), Flush, Write(5)],
485 ),
486 ];
487 for (name, steps) in cases {
488 let expected = write_serial(&steps, props(64));
489 for parallelism in [1, 2, 3, 16] {
490 let actual = write_parallel(&steps, props(64), parallelism).await;
491 assert!(
492 expected == actual,
493 "case {name}, parallelism {parallelism}: {} bytes != {} bytes",
494 expected.len(),
495 actual.len()
496 );
497 }
498 }
499 }
500
501 #[rstest::rstest]
502 #[case::encode_error(false)]
503 #[case::encode_panic(true)]
504 #[tokio::test]
505 async fn test_encode_failure_drains_tasks_before_releasing_budget(#[case] panics: bool) {
506 let runtime: common_runtime::Runtime = common_runtime::Builder::default()
507 .worker_threads(1)
508 .max_blocking_threads(1)
509 .build()
510 .unwrap();
511 let mut writer = ParallelArrowWriter::try_new(
512 Vec::new(),
513 schema(),
514 props(64),
515 3,
516 EncodeRuntime::Compact,
517 )
518 .unwrap();
519 let memory = new_compaction_memory_manager(1024 * 1024);
520 let budget = memory.try_acquire(memory.limit_bytes()).unwrap();
521 let batches: Arc<[RecordBatch]> = vec![batch(0, 4)].into();
522 let retained_batches = Arc::downgrade(&batches);
523
524 let failed = runtime.spawn_blocking(move || -> Result<EncodedFields> {
525 if panics {
526 panic!("encode task failed");
527 }
528 Err(ParquetError::General("encode task failed".to_string()))
529 });
530 let (started_tx, started_rx) = oneshot::channel();
531 let (release_tx, release_rx) = oneshot::channel();
532 let column = writer.factory.create_column_writers(0).unwrap().remove(0);
533 let running_batches = batches.clone();
534 let running = runtime.spawn_blocking(move || {
535 started_tx.send(()).unwrap();
536 release_rx.blocking_recv().unwrap();
537 let _encoded =
538 encode_fields(&schema(), &running_batches, vec![(0, vec![(0, column)])])?;
539 Err(ParquetError::General("later encoding failure".to_string()))
540 });
541 timeout(Duration::from_secs(10), started_rx)
543 .await
544 .unwrap()
545 .unwrap();
546 let queued_started = Arc::new(AtomicBool::new(false));
547 let queued_started_task = queued_started.clone();
548 let queued = runtime.spawn_blocking(move || {
549 queued_started_task.store(true, Ordering::SeqCst);
550 drop(batches);
551 Ok((Vec::new(), Vec::new()))
552 });
553 writer.encoding = vec![failed, running, queued];
554
555 let mut finish = Box::pin(async {
557 let _budget = budget;
558 writer.flush().await
559 });
560 assert!(futures::poll!(finish.as_mut()).is_pending());
561 assert!(memory.try_acquire(memory.limit_bytes()).is_none());
562 assert!(retained_batches.upgrade().is_some());
563
564 release_tx.send(()).unwrap();
565 let error = timeout(Duration::from_secs(10), finish)
566 .await
567 .unwrap()
568 .unwrap_err();
569 if panics {
570 let ParquetError::External(source) = error else {
571 panic!("expected the original task panic, got {error}");
572 };
573 assert!(
574 source
575 .downcast_ref::<common_runtime::JoinError>()
576 .unwrap()
577 .is_panic()
578 );
579 } else {
580 assert!(
581 matches!(error, ParquetError::General(message) if message == "encode task failed")
582 );
583 }
584 assert!(!queued_started.load(Ordering::SeqCst));
585 assert!(retained_batches.upgrade().is_none());
586 assert!(memory.try_acquire(memory.limit_bytes()).is_some());
587 assert!(writer.file_writer.flushed_row_groups().is_empty());
588 assert!(writer.async_writer.is_empty());
589 }
590
591 #[test]
592 fn test_parallel_writer_requires_row_count_limit() {
593 assert!(ParallelArrowWriter::<Vec<u8>>::supports(&props(64)));
594 let byte_limited = WriterProperties::builder()
595 .set_max_row_group_bytes(Some(1024))
596 .build();
597 assert!(!ParallelArrowWriter::<Vec<u8>>::supports(&byte_limited));
598 let unlimited = WriterProperties::builder()
599 .set_max_row_group_row_count(None)
600 .build();
601 assert!(!ParallelArrowWriter::<Vec<u8>>::supports(&unlimited));
602 assert!(
603 ParallelArrowWriter::try_new(Vec::new(), schema(), unlimited, 2, EncodeRuntime::Global)
604 .is_err()
605 );
606 }
607
608 #[test]
609 fn test_assign_fields_balances_measured_costs() {
610 let (file_writer, factory) = ArrowWriter::try_new(Vec::new(), schema(), Some(props(64)))
611 .unwrap()
612 .into_serialized_writer()
613 .unwrap();
614 let schema_descr = file_writer.schema_descr();
615 let mut fields: Vec<FieldWriters> = (0..4).map(|field| (field, Vec::new())).collect();
616 for (leaf, writer) in factory
617 .create_column_writers(0)
618 .unwrap()
619 .into_iter()
620 .enumerate()
621 {
622 fields[schema_descr.get_column_root_idx(leaf)]
623 .1
624 .push((leaf, writer));
625 }
626 assert_eq!(2, fields[3].1.len());
628
629 let costs = [10, 9, 1, 1].map(Duration::from_millis);
630 let tasks = assign_fields(fields, &costs, 2);
631 let mut loads: Vec<_> = tasks
632 .iter()
633 .map(|task| {
634 task.iter()
635 .map(|(field, _)| costs[*field])
636 .sum::<Duration>()
637 })
638 .collect();
639 loads.sort();
640 assert_eq!(
641 vec![Duration::from_millis(10), Duration::from_millis(11)],
642 loads
643 );
644 let mut assigned: Vec<_> = tasks.iter().flatten().map(|(field, _)| *field).collect();
645 assigned.sort();
646 assert_eq!(vec![0, 1, 2, 3], assigned);
647 }
648}