Repository navigation
Conversation
Summary of ChangesHello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed! This pull request introduces native support for sorted writes in IcebergIO. By integrating a memory-safe, disk-spilling sorter, the implementation ensures that data written to sorted Iceberg tables is perfectly ordered, which significantly improves performance and reduces resource contention by minimizing concurrent file writers. The changes include a comprehensive byte-encoding protocol for lexicographical sorting and updated write-path logic to handle sorted tables efficiently. Highlights
New Features🧠 You can now enable Memory (public preview) to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console. Using Gemini Code AssistThe full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips. Invoking Gemini You can request assistance from Gemini at any point by creating a comment using either
Customization To customize the Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a Limitations & Feedback Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counterproductive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here. Footnotes
|
There was a problem hiding this comment.
Code Review
This pull request implements row sorting for Iceberg writes by introducing the IcebergRowSorter utility, which leverages BufferedExternalSorter to handle large datasets. The implementation supports complex Iceberg SortOrder configurations by encoding sort keys into byte arrays. Review feedback identified critical logic errors in the null-ordering byte prefix calculation for descending columns, which were also propagated into incorrect test assertions. Additionally, several performance optimizations were suggested to avoid expensive row conversions and redundant lookups within loops, and to reduce GC pressure from frequent buffer allocations. A potential reliability issue was also noted regarding the consumption of non-re-iterable collections when extracting schemas.
…racting Schema from Coder
…Order().isSorted() for readability
…e its conditional state
… key encoding performance
|
@ahmedabu98 mind taking a look? Thanks! |
ahmedabu98
left a comment
There was a problem hiding this comment.
Left some comments.
I think more thought needs to be put into the architecture of Sorted writes. This is a primitive approach that might work well for small writes, but will quickly degrade for medium~larger writes.
Maybe take a look at how Flink or Spark do it (look for "range" distribution mode). IIRC they do a global sort before passing data to writers. This way, input iterables are already sorted so writers can break them up into files with tight min/max ranges.
| if (icebergRecord == null) { | ||
| icebergRecord = IcebergUtils.beamRowToIcebergRecord(icebergSchema, row); | ||
| } | ||
| Object icebergVal = icebergRecord.getField(colName); | ||
| if (icebergVal != null) { | ||
| val = field.transform().apply(icebergVal); |
There was a problem hiding this comment.
This is pretty heavy to extract the transformed value. Will make writes pretty slow
Can we apply the transform to the Beam field object directly? If not, maybe we need a "beam object to iceberg object" conversion. IcebergUtils.copyFieldIntoRecord does something similar, but we may need to refactor it a little to fit this use case.
| try { | ||
| for (Row row : rows) { | ||
| byte[] keyBytes = encodeSortKey(row, sortOrder, columnNames, icebergSchema, beamSchema); | ||
| ByteArrayOutputStream baos = new ByteArrayOutputStream(); |
There was a problem hiding this comment.
Let's to clear and reuse the output stream object instead of creating a new one each iteration
| org.apache.beam.sdk.schemas.Schema beamSchema) | ||
| throws IOException { | ||
|
|
||
| ByteArrayOutputStream baos = new ByteArrayOutputStream(); |
There was a problem hiding this comment.
Same thing here, let's create ByteArrayOutputStream it in the caller function and pass it in here. Each call should reset and use the same one for efficiency
| } else { | ||
| writeString(val.toString(), baos, invert); | ||
| } |
There was a problem hiding this comment.
Does Iceberg support sorting on complex types (lists, maps, structs) ?
We should either 1) add support for that or 2) throw an Unsupported Exception early on.
I'd rather throw an error than fall back on String because it may lead to unexpected sorting behavior for some types.
There was a problem hiding this comment.
It is not supported. It now raises an UnsupportedOperationException
| Iterable<Row> sortedOrUnsortedRows = | ||
| IcebergRowSorter.sortRows( | ||
| element.getValue(), table.sortOrder(), table.schema(), dataSchema); | ||
| for (Row row : sortedOrUnsortedRows) { |
There was a problem hiding this comment.
We should only be sorting if the user asked us to.
There was a problem hiding this comment.
IcebergRowSorter.sortRows() will only sort if the table schema defines any sort configuration or else, it does nothing.
…xpose NONE, HASH, and RANGE modes, optimize stream reuse, and add comprehensive sorting tests
…ively document distribution modes
…n mode with custom sharding function
… auto-sharding overlap limitations
|
I have updated this PR to address your (Ahmed) feedback and optimize the sorting implementation. Below is a summary of the changes: 1. Configurable Distribution Modes
2. Performance Optimizations (GC & CPU Overhead Reductions)
3. Sorter Correctness & Safety
4. Verification & Test Coverage
|
fbad2a0 to
acf0e56
Compare
…value-level toString conversion
…o String conversions in IcebergUtilsTest.java
…ring conversions in IcebergUtilsTest.java
… in WritePartitionedRowsToFiles.java
|
Hi @atognolag, can you fix the conflicts and any other errors? Then @ahmedabu98 can review again. Thanks! |
|
This pull request has been marked as stale due to 60 days of inactivity. It will be closed in 1 week if no further activity occurs. If you think that’s incorrect or this pull request requires a review, please simply write any comment. If closed, you can revive the PR at any time and @mention a reviewer or discuss it on the dev@beam.apache.org list. Thank you for your contributions. |
|
This pull request has been closed due to lack of activity. If you think that is incorrect, or the pull request requires review, you can revive the PR at any time. |
|
Superseded by #40653 (rebased on |
Summary
Adds end-to-end support for sorting records on write according to an Iceberg table's
SortOrder(.withSortedWrites()/ Managed IOsort_on_write: true) as well asDistributionMode.RANGE(.withDistributionMode(DistributionMode.RANGE)/ Managed IOdistribution_mode: "RANGE") across both Batch and Streaming pipelines:Order-Preserving Binary Key Encoding & External Spill-to-Disk Sorting (
IcebergRowSorter):SortOrderkeys into lexicographically comparablebyte[]arrays supportingASC/DESC,NULLS_FIRST/NULLS_LAST, all Iceberg primitive/logical types (BOOLEAN,INTEGER,LONG,FLOAT,DOUBLE,DECIMAL,STRING,UUID,FIXED,BINARY,DATE,TIME,TIMESTAMP,TIMESTAMPTZ,EnumerationType), and non-identity sort transforms (month,day,hour,year,bucket,truncate).BufferedExternalSorter(NATIVE) with bounded per-instance memory (64 MBfor single-partition grouped writes,16 MBper destination in multi-destination bundle writes) to spill to local disk when a partition or bundle exceeds memory.Zero-Shuffle Local Bundle Sorting for
DistributionMode.NONE+withSortedWrites()(RecordWriterManager):IcebergRowSorterkeyed by(partitionPath, sortKey)without opening anyRecordWriters during@ProcessElement.@FinishBundle, flushes sorted rows one partition at a time—keeping only a single activeRecordWriteropen at any instant—eliminating any network shuffle forDistributionMode.NONEin both Batch and Streaming.Dynamic Quantile Sharding for
DistributionMode.RANGE(AssignDestinationsAndPartitions):withDistributionFunction(SerializableFunction<Row, Integer>)and automatic quantile sharding in Batch whendistributionFunctionis omitted.A-Res) weighted reservoir sampling without replacement (2,048samples per(destination, partition)) combined with estimated partition byte volume to compute balanced quantile split boundaries sized towrite.target-file-size-bytes(default512 MB).byte[]sort keys across the stage barrier for un-sampled rows; only encodes sort keys during shard assignment for partitions that split into multiple shards.Distribution & Sorting Modes Matrix
NONE(Unsorted).withDistributionMode(NONE)GroupByKeyonly if open writers overflowMAX_NUM_WRITERS)directWriteByteLimit) or 1 (GroupByKeyacrossshardingKey % 50)NONE(Sorted).withDistributionMode(NONE).withSortedWrites()(dest, partition))directWriteByteLimit) or 1 (GroupByKeyacrossshardingKey % 50)table.sortOrder(); min/max key ranges across different worker bundles may overlap.HASH.withDistributionMode(HASH)(+ optional.withSortedWrites(),.withAutosharding())GroupByKeyby(dest, partition))GroupIntoBatchesby(dest, partition)).withSortedWrites()(without autosharding), files within each partition have strictly non-overlapping sort-key ranges.RANGE(Dynamic Quantiles).withDistributionMode(RANGE)(nodistributionFunction, Batch)GroupByKeyby(dest, partition, quantileShard)+ lightweight quantile side-inputCombine.perKey)withDistributionFunctionor.withAutosharding())totalPartitionBytes / targetFileSizeusing weighted reservoir quantiles; files within each partition have non-overlapping sort-key ranges.RANGE(Custom Function).withDistributionMode(RANGE).withDistributionFunction(fn)GroupByKeyby(dest, partition, fn(row)))GroupIntoBatchesby(dest, partition, fn(row)))fn(row)is monotonic with respect totable.sortOrder().Testing & Verification
265tests in:sdks:java:io:iceberg:test):IcebergRowSorterTest: Order-preserving byte encoding across all primitive/logical types,ASC/DESCinversion,NULLS_FIRST/NULLS_LASTmatrix, non-identity transforms (month,hour,truncate), and external disk spill verification.IcebergIOWriteTest: End-to-end sorted writes acrossNONE,HASH,HASH + autosharding,RANGE(custom function and dynamic quantile sharding), post-sort file rolling without range overlap, and streamingdirectWriteByteLimitbundle sorting.AssignDestinationsAndPartitionsTest: Side-input metadata resolution and Efraimidis–Spirakis weighted reservoir sampling under extreme bundle skew.IcebergIOSideInputTableCacheTest&IcebergWriteSchemaTransformProviderTest: Parameterized acrossNONE,HASH,HASH + autosharding, andRANGE.batch_none_sorted,batch_range_dynamic,stream_none_sorted,stream_range_dynamic) writing to a 22-column partitioned & composite-sorted Iceberg table (primary_typeidentity partition, sorted bydate ASC NULLS FIRST, id DESC NULLS LAST) on GCS.min/max) and row-by-row ordering across all committed files (0sort violations,0overlapping key ranges across dynamicRANGEshards,0worker errors).