Skip to content

fix: honor max_row_group_bytes in the parallel Parquet writer - #25041

Open
Satyr09 wants to merge 3 commits into
apache:mainfrom
Satyr09:daipayan/parallel-max-row-group-bytes-barrier-corrected
Open

Satyr09 wants to merge 3 commits into
apache:mainfrom
Satyr09:daipayan/parallel-max-row-group-bytes-barrier-corrected

Conversation

@Satyr09

@Satyr09 Satyr09 commented Sep 7, 2026 •

Copy link
Copy Markdown
Contributor

Which issue does this PR close?

Closes #22982.

Rationale for this change

max_row_group_bytes is ignored by the parallel Parquet writer, which is enabled by default. This change lets users apply the byte target while keeping parallel column encoding.

What changes are included in this PR?

When a byte target is configured, column workers report their estimated encoded sizes. The writer waits for each input slice to finish before choosing the next row-group boundary. With the option unset, no feedback channels are created.

The work is split into three commits, in merge order:

  1. Return column-worker errors promptly, addressing Parallel Parquet writes delay reporting column-worker failures #25992.
  2. Group tasks and channels by row group, with no behavior change.
  3. Byte-target support in this PR.

The prerequisites are on separate fork branches. Until they merge into Apache main, GitHub's full diff includes them. Review just the byte-target change.

What is the testing strategy for this PR?

Public DataFrame tests write multiple files, compare serial and parallel row-group layouts, and read back every column. They cover nested data, growing and shrinking string widths, both size limits, and memory cleanup after column and storage failures. SQL tests cover the configuration through COPY.

Each stage passed formatting, strict full-workspace Clippy, and extended workspace tests on Linux. The feature also passed its SQL and documentation checks, ten focused public regressions, and all 255 Parquet integration tests. Validation logs are available on the fork; the final refactor suite passed in a follow-up run.

The existing SQL benchmark workloads passed all 108 output readback checks. Byte-enabled layouts matched serial writing, and disabled-option layouts matched upstream. Timings varied between rounds, so these are preliminary measurements. Peak tracked memory for changing-width strings remained higher: 38.7 MiB versus 14.4 MiB on upstream.

Are there any user-facing changes?

The byte target now applies to parallel writing. It is a best-effort estimate, can reduce write throughput, and is not a hard memory limit. The default remains unset.

@github-actions github-actions Bot added documentation Improvements or additions to documentation sqllogictest SQL Logic Tests (.slt) common Related to common crate datasource Changes to the datasource crate labels Sep 7, 2026
@codecov-commenter

codecov-commenter commented Sep 7, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 87.29282% with 23 lines in your changes missing coverage. Please review.
✅ Project coverage is 82.64%. Comparing base (2566bc1) to head (558e83f).
⚠️ Report is 5 commits behind head on main.

Files with missing lines Patch % Lines
datafusion/datasource-parquet/src/sink.rs 87.29% 19 Missing and 4 partials ⚠️
Additional details and impacted files
@@           Coverage Diff            @@
##             main   #25041    +/-   ##
========================================
  Coverage   82.63%   82.64%            
========================================
  Files        1147     1147            
  Lines      445392   445596   +204     
  Branches   445392   445596   +204     
========================================
+ Hits       368070   368249   +179     
- Misses      54973    54989    +16     
- Partials    22349    22358     +9     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@Satyr09
Satyr09 marked this pull request as ready for review September 7, 2026 23:09

@kosiew kosiew left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Satyr09,

Thanks for working on this. The synchronized byte feedback and the coverage here look solid. I have one non-blocking suggestion below to strengthen the failure-path coverage.

}

#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn byte_limit_finishes_a_group_before_more_input_arrives() {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we add an end-to-end test with a failing AsyncWrite for the parallel path while max_row_group_bytes is set? The direct drop(group) test covers worker cleanup well, but it would also be useful to exercise an object-store write failure where output_single_parquet_file_parallelized exits while the dispatcher or finalizer may still be blocked.

It would be good to assert that the error returns promptly and that all pool reservations are released. This is non-blocking since SpawnedTask drop cancellation currently makes this safe, but having the test would lock down the top-level failure lifecycle.

@alamb alamb left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for this @Satyr09 and @kosiew

I am worried about the size / relative complexity of this PR. Can we perhaps break it into pieces and simplify the tests and cmments? I think it would be a very nice addition.

Comment thread datafusion/common/src/config.rs Outdated
Comment on lines +1457 to +1458
/// estimated. Subsequent batches are split using the observed average row size,
/// so this is not a hard byte or memory limit. The parallel writer synchronizes

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I found this pretty wordy and hard to read. I think the old text is clearer as it focuses on the user visible behavior (the memory usage for wide schemas) while this description describes a lot of internal implementation detail I don't think is particularly helpful to users

For example " Subsequent batches are split using the observed average row size, so this is not a hard byte or memory limit." is a lot of words, and I think we could simplify it by just adding "best effort" to the summary if you wanted to emphasize this point

"/// (writing) Target maximum estimated encoded size of each row group in bytes (best effort)"

use std::task::{Context, Poll};
use std::time::Duration;

struct TestOutput(SharedBuffer);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These tests seem very verbose to me and they aren't testing the public API surface (aka these are all basically unit tests)

Like @kosiew suggests, I think we should have an "end to end" type test -- namely create a SQL (or DataFrame test) that sets the row group limit and then writes into multiple parquet files and verifies the written row group limit is honored.

I don't think this kind of low level / orchestration test is super useful

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or put another way, what additional coverage is this adding that is not already covered by the slt test?

/// Once the channel is exhausted, returns the ArrowColumnWriter.
/// A leaf array and its number of root records. Nested leaf arrays can have a
/// different length, so the dispatcher supplies the record count explicitly.
struct ColumnInput {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need this? Isn't the row count the same across all leaves?

Comment thread datafusion/datasource-parquet/src/sink.rs
@Satyr09
Satyr09 force-pushed the daipayan/parallel-max-row-group-bytes-barrier-corrected branch from 28c48da to 558e83f Compare October 3, 2026 16:50
@github-actions github-actions Bot added the core Core DataFusion crate label Oct 3, 2026
kosiew

This comment was marked as outdated.

@kosiew
kosiew dismissed their stale review October 5, 2026 11:49

superceded

@alamb alamb left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @Satyr09 and @kosea -- this code looks much improved and easier to understand.

However, I am concerned about its impact on performance.

I did a brief ad-hoc benchmark locally and I think this change slows down writing substantially when the max row bytes are set (my adhoc testing looked like the binary keeps the cores less busy):

Specifically, I when I wrote it like this it ran 41 seconds on this branch, but 28 seconds on main 🤔

-- parallel writer, byte target set: the new code path
COPY hits TO 'out_parallel_bytes.parquet' STORED AS PARQUET
OPTIONS ('format.max_row_group_bytes' 134217728);

Here is the test script (in write_bench.sql)

Details
CREATE EXTERNAL TABLE hits STORED AS PARQUET
LOCATION '/Users/andrewlamb/Software/datafusion/benchmarks/data/hits_partitioned';

-- parallel writer, option unset: should be unchanged by the PR
COPY hits TO 'out_parallel_nobytes.parquet' STORED AS PARQUET;

-- parallel writer, byte target set: the new code path
COPY hits TO 'out_parallel_bytes.parquet' STORED AS PARQUET
OPTIONS ('format.max_row_group_bytes' 134217728);

-- Use smaller table for single threaded test
CREATE EXTERNAL TABLE hits0 STORED AS PARQUET
LOCATION '/Users/andrewlamb/Software/datafusion/benchmarks/data/hits_partitioned/hits_0.parquet';

-- serial writer, with byte target
COPY hits0 TO 'out_serial_bytes.parquet' STORED AS PARQUET
OPTIONS ('format.max_row_group_bytes' 134217728, 'format.allow_single_file_parallelism' false);

Then I ran it like datafusion-cli -f write_bench.sql

Raw data for main

Details
andrewlamb@Andrews-MacBook-Pro-3:~/Downloads$ datafusion-cli -f write_bench.sql
DataFusion CLI v55.1.0
0 row(s) fetched.
Elapsed 0.035 seconds.

+----------+
| count    |
+----------+
| 99997497 |
+----------+
1 row(s) fetched.
Elapsed 25.003 seconds.

+----------+
| count    |
+----------+
| 99997497 |
+----------+
1 row(s) fetched.
Elapsed 25.851 seconds.

0 row(s) fetched.
Elapsed 0.013 seconds.

+---------+
| count   |
+---------+
| 1000000 |
+---------+
1 row(s) fetched.
Elapsed 1.679 seconds.

Raw data for this branch (note the second query takes 40.9s vs 25.8s on main)

Details
./datafusion-cli-daipayan_parallel-max-row-group-bytes-barrier-corrected -f write_bench.sql
DataFusion CLI v55.1.0
0 row(s) fetched.
Elapsed 0.038 seconds.

+----------+
| count    |
+----------+
| 99997497 |
+----------+
1 row(s) fetched.
Elapsed 26.424 seconds.

+----------+
| count    |
+----------+
| 99997497 |
+----------+
1 row(s) fetched.
Elapsed 40.912 seconds.

0 row(s) fetched.
Elapsed 0.001 seconds.

+---------+
| count   |
+---------+
| 1000000 |
+---------+
1 row(s) fetched.
Elapsed 1.651 seconds.

task.join_unwind()
.await
.map_err(|e| DataFusionError::ExecutionJoin(Box::new(e)))??;
Err(internal_datafusion_err!(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this should be an execution error (rather than an internal error)

@alamb

alamb commented Oct 5, 2026

Copy link
Copy Markdown
Contributor

Also, it woud be nice if you could respond to any of the comments you have addressed (I think you have addrssed them all, but it would be good if you could note which you think you have covered so we can double check)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

common Related to common crate core Core DataFusion crate datasource Changes to the datasource crate documentation Improvements or additions to documentation sqllogictest SQL Logic Tests (.slt)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Honour max_row_group_bytes in the parallel Parquet writer

4 participants