Conversation
|
Thank you for opening this pull request! Reviewer note: cargo-semver-checks reported the current version number is not SemVer-compatible with the changes in this pull request (compared against the base branch). Details |
20cc5e6 to
94e622b
Compare
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #24924 +/- ##
==========================================
+ Coverage 81.93% 82.42% +0.49%
==========================================
Files 1136 1139 +3
Lines 429152 435636 +6484
Branches 429152 435636 +6484
==========================================
+ Hits 351633 359082 +7449
+ Misses 56475 54836 -1639
- Partials 21044 21718 +674 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
kosiew
left a comment
There was a problem hiding this comment.
@Tpt,
Thanks for working on this. I like the goal of making output file sizing more practical when row counts are a poor proxy for size, especially for blob-heavy datasets.
I found one issue that I think needs to be addressed before merging. The new byte limit is currently based on Arrow RecordBatch memory size rather than the number of serialized bytes written to the output file. For Parquet, compression can make those two values very different, so the current behavior can rotate files much earlier than the documented output-file byte target suggests.
I also left one small test coverage suggestion for the new config option.
| part_idx += 1; | ||
| } | ||
| row_counts[next_send_steam] += rb.num_rows(); | ||
| bytes_counts[next_send_steam] += rb.get_array_memory_size(); |
There was a problem hiding this comment.
I think this needs to measure serialized output bytes rather than the Arrow allocation size. get_array_memory_size() gives us the pre-serialization RecordBatch size, but Parquet defaults to zstd(3), so a highly compressible blob batch could be hundreds of MiB in memory and only a small fraction of that on disk.
In that case, the demuxer would rotate before the next batch and could create much smaller files than the configured soft_max_bytes_per_output_file suggests. That also seems particularly relevant to the blob use case this option is intended to help with.
Could we move the accounting to a point where serialized or emitted bytes are known, and feed that back to the demuxer if needed? If the intent is instead to use input memory size as an estimate, I think the option and docs should be narrowed to make that explicit.
It would also be useful to add a regression test using a large, highly compressible binary column with Parquet compression enabled. The current parameterized test uses UNCOMPRESSED, so it would not catch this difference.
There was a problem hiding this comment.
Sorry for the slow reply, got trapped by other things and thank you so much for the review
This is a great point. Done. I used a Arc<AtomicUsize> to record and report back the output size without blocking the code. An other approach might be to have an other channel giving back the written size but:
- either the demuxer block on it and we lose must of the concurrency and pipelining advantage (I guess very much a no go)
- we don't block and we get the same behavior of
Arc<AtomicUsize>with more overhead and code
The downside of Arc<AtomicUsize> is indeed that the reporting might lag quite a lot. I have updated the documentation to highlight this.
Allows to roughly limit the size of parquet files when the number of rows is a hard-to-use estimator
94e622b to
f9054bc
Compare
f9054bc to
cb03a02
Compare
kosiew
left a comment
There was a problem hiding this comment.
@Tpt,
Thanks for working on this. I like the direction of adding a byte-based soft limit, especially for workloads where row count is a poor proxy for file size.
I found one blocking issue with how the byte count is measured for Parquet. The current accounting is based on Arrow memory size rather than the encoded output size, which can cause highly compressible data to rotate files far too early. I also left one small suggestion to add SQL-path coverage for rejecting zero.
Once the byte accounting reflects serialized or emitted bytes, this should line up much better with the option's documented behavior.
There was a problem hiding this comment.
@Tpt,
Thanks for the follow-up. I reviewed commit cb03a02189 (Use the actual written file size). The Parquet side of the Arrow-memory versus encoded-size issue looks fixed now, including the compressed binary regression coverage. The SQL coverage for rejecting soft_max_bytes_per_output_file = 0 is also in place.
There is still one remaining issue with compressed CSV/JSON output. The current accounting uses the serializer byte count before compression, so the soft limit can still be based on uncompressed bytes for gzip, bzip2, xz, and zstd output.
Because of that, the claim that this now uses the actual written file size is still a bit broader than the implementation. Please either move the accounting to the compressed writer/object-store boundary and add a compressed stateless-format regression test, or explicitly restrict this option to uncompressed stateless writers.
| while let Some(task) = rx.recv().await { | ||
| match task.join().await { | ||
| Ok(Ok((cnt, bytes))) => { | ||
| serialized_bytes += bytes.len(); |
There was a problem hiding this comment.
serialized_bytes is counting the bytes returned by the CSV/JSON serializer before they go through the compression wrapper installed by ObjectWriterBuilder.
That means COPY ... TO with gzip, bzip2, xz, or zstd can still rotate based on the uncompressed size. For highly compressible data, this reproduces the original issue outside Parquet.
Could we account for bytes at the compressed writer/object-store boundary and add a regression test for a compressed stateless format? Alternatively, if that is not intended to be supported, I think the option should be explicitly restricted to uncompressed stateless writers.
There was a problem hiding this comment.
Thank you! I indeed missed that!
Could we account for bytes at the compressed writer/object-store boundary
Indeed. I have updated ObjectWriterBuilder to count the number of bytes flushed to the underlying writer. It is done with a new intermediate AsyncWrite implementation that sits between object store and the compression implementation
702e0f3 to
ae3ee4c
Compare
ae3ee4c to
1adf75a
Compare
kosiew
left a comment
There was a problem hiding this comment.
@Tpt,
Thanks for the updates. The implementation now counts bytes at the right layer, below whole-file compression, and the SQL zero-value validation is covered as well.
I still see two things that should be addressed before merge: regression coverage for compressed output accounting, and the existing SemVer issue from adding the public config field.
| let config = SessionConfig::from_string_hash_map(&config_map)?; | ||
| let ctx = SessionContext::new_with_config(config); | ||
|
|
||
| // A deterministic high-entropy payload avoids compression reducing |
There was a problem hiding this comment.
Could we add a regression case that specifically verifies compressed-size accounting here?
The gzip/zstd cases use high-entropy payloads and only check that files.len() > 1. The previous implementation based on RecordBatch::get_array_memory_size() would also rotate for this data, so these cases would still pass with the behavior we are trying to prevent.
A deterministic, highly compressible CSV or JSON case would cover this better. Ideally it would assert an exact or bounded file count, or otherwise verify emitted size, so that it fails with the previous batch-memory accounting but passes when rotation is based on the compressed bytes actually written.
| /// process RecordBatches. The final file size may exceed this limit due | ||
| /// to batches buffered before the limit is observed, the size of a batch, | ||
| /// and file metadata written when the file is finalized. | ||
| pub soft_max_bytes_per_output_file: ConfigNonZeroUsize, default = non_zero_usize_default(4294967295) |
There was a problem hiding this comment.
Adding this field breaks downstream struct literals.
Could we either follow the project's approved breaking API process, including any required upgrade or release documentation, or use a compatible design?
There was a problem hiding this comment.
This MR is for the main branch, that is targeting next DataFusion breaking release. I guess config changes are fine in them
There was a problem hiding this comment.
Agreed this can land on main for the next major release. The concern is process/documentation, not that the change is prohibited.
Per the API health policy, please:
- add an entry to the next-major Upgrade Guide explaining affected ExecutionOptions / ConfigOptions struct literals and migration (..Default::default() or initialize the new field).
Rationale for this change
Allows to roughly limit the size of parquet files when the number of rows is a hard-to-use estimator.
This is especially useful when a column contains potentially large blobs.
What changes are included in this PR?
AsyncWriteimplementation created byObjectWriterBuilder. It sits between the optional compression and theBufWriterimplementation, counting the number of effectively flushed bytes in aArc<AtomicUsize>.Arc<AtomicU64>is used in the demuxer logic to get an estimation of the written size. This estimation lags because writes happens asynchronously but this avoid having to block on encoding and compression.ObjectWriterBuilderwhen creating the single fileAsyncArrowWriter