This is an automated email from the ASF dual-hosted git repository.
danny0405 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/hudi.git
The following commit(s) were added to refs/heads/master by this push:
new 72bae164b4ff fix(flink): preserve arrival order for equal keys in LSM
writes (#20138)
72bae164b4ff is described below
commit 72bae164b4ff70ec8d81590bc2c19b6e5ade8f4f
Author: Shuo Cheng <[email protected]>
AuthorDate: Thu Oct 1 10:01:01 2026 +0800
fix(flink): preserve arrival order for equal keys in LSM writes (#20138)
* fix(flink): preserve arrival order for equal keys in LSM writes
---
.../io/log/block/HoodieFlinkParquetDataBlock.java | 2 +-
.../org/apache/hudi/sink/StreamWriteFunction.java | 12 +-
.../AppendWriteFunctionWithBIMBufferSort.java | 4 +-
...AppendWriteFunctionWithDisruptorBufferSort.java | 2 +-
.../org/apache/hudi/sink/buffer/RowDataBucket.java | 18 +-
.../apache/hudi/sink/buffer/StableSortBuffer.java | 232 ++++++++++++++++++++
.../exception/MemoryPagesExhaustedException.java | 2 +-
.../sink/partitioner/index/IndexWriteFunction.java | 8 +-
.../org/apache/hudi/sink/utils/BufferUtils.java | 24 ++-
.../hudi/sink/utils/NaturalOrderKeyComputer.java | 2 +-
.../sink/utils/NaturalOrderRecordComparator.java | 2 +-
.../org/apache/hudi/sink/TestWriteCopyOnWrite.java | 2 +-
.../org/apache/hudi/sink/TestWriteMergeOnRead.java | 72 +++++++
.../apache/hudi/sink/buffer/TestRowDataBucket.java | 233 ++++++++++++++++++++-
.../partitioner/index/TestIndexWriteFunction.java | 6 +-
.../sink/utils/TestRecordKeySortKeyComputer.java | 6 +-
16 files changed, 590 insertions(+), 37 deletions(-)
diff --git
a/hudi-client/hudi-flink-client/src/main/java/org/apache/hudi/io/log/block/HoodieFlinkParquetDataBlock.java
b/hudi-client/hudi-flink-client/src/main/java/org/apache/hudi/io/log/block/HoodieFlinkParquetDataBlock.java
index 08a989879b2c..2d312061d7cf 100644
---
a/hudi-client/hudi-flink-client/src/main/java/org/apache/hudi/io/log/block/HoodieFlinkParquetDataBlock.java
+++
b/hudi-client/hudi-flink-client/src/main/java/org/apache/hudi/io/log/block/HoodieFlinkParquetDataBlock.java
@@ -50,7 +50,7 @@ import static
org.apache.hudi.common.model.HoodieFileFormat.PARQUET;
/**
* HoodieFlinkParquetDataBlock employs an HoodieRecord iterator rather than a
HoodieRecord list for
- * parquet data block, aiming to better utilize the optimizations of {@code
BinaryInMemorySortBuffer},
+ * parquet data block, aiming to better utilize the optimizations of {@code
StableSortBuffer},
* for example, object reusing to decrease GC costs.
*/
public class HoodieFlinkParquetDataBlock extends HoodieParquetDataBlock
implements ColumnRangeMetadataProvider {
diff --git
a/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/sink/StreamWriteFunction.java
b/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/sink/StreamWriteFunction.java
index e34dd0b5c11a..4333b358dc89 100644
---
a/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/sink/StreamWriteFunction.java
+++
b/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/sink/StreamWriteFunction.java
@@ -36,6 +36,7 @@ import org.apache.hudi.exception.HoodieException;
import org.apache.hudi.metrics.FlinkStreamWriteMetrics;
import org.apache.hudi.sink.buffer.PreemptiveMemorySegmentPool;
import org.apache.hudi.sink.buffer.RowDataBucket;
+import org.apache.hudi.sink.buffer.StableSortBuffer;
import org.apache.hudi.sink.buffer.TotalSizeTracer;
import org.apache.hudi.sink.bulk.RowDataKeyGen;
import org.apache.hudi.sink.bulk.RowDataKeyGens;
@@ -61,7 +62,6 @@ import org.apache.flink.table.data.RowData;
import org.apache.flink.table.data.binary.BinaryRowData;
import org.apache.flink.table.runtime.generated.NormalizedKeyComputer;
import org.apache.flink.table.runtime.generated.RecordComparator;
-import org.apache.flink.table.runtime.operators.sort.BinaryInMemorySortBuffer;
import org.apache.flink.table.runtime.util.MemorySegmentPool;
import org.apache.flink.table.types.logical.RowType;
import org.apache.flink.util.Collector;
@@ -84,7 +84,7 @@ import static
org.apache.hudi.common.util.HoodieRecordUtils.getOrderingFieldName
*
* <p><h2>Work Flow</h2>
*
- * <p>The function firstly buffers the data (RowData) in a binary buffer based
on {@code BinaryInMemorySortBuffer}.
+ * <p>The function firstly buffers the data (RowData) in a {@link
StableSortBuffer}.
* It flushes(write) the records batch when the batch size exceeds the
configured size {@link FlinkOptions#WRITE_BATCH_SIZE}
* or the memory of the binary buffer is exhausted, and could not append any
more data or a Flink checkpoint starts.
* After a batch has been written successfully, the function notifies its
operator coordinator {@link StreamWriteOperatorCoordinator}
@@ -355,7 +355,7 @@ public class StreamWriteFunction extends
AbstractStreamWriteFunction<HoodieFlink
boolean success = doBufferRecord(bucketID, record);
if (!success) {
// 2. reclaim pages. A buffer whose write returned false must never be
reused because
- // BinaryInMemorySortBuffer may already have changed its variable-length
storage state.
+ // StableSortBuffer may already have changed its variable-length storage
state.
reclaimMemoryAfterFailedWrite(bucketID);
// 2.1 retry once with a newly-created buffer
@@ -566,12 +566,12 @@ public class StreamWriteFunction extends
AbstractStreamWriteFunction<HoodieFlink
return statuses;
}
- private BinaryInMemorySortBuffer createDataBuffer() {
+ private StableSortBuffer createDataBuffer() {
if (recordKeyComputer == null) {
- return BufferUtils.createBuffer(rowType, preemptiveMemorySegmentPool);
+ return BufferUtils.createStableSortBuffer(rowType,
preemptiveMemorySegmentPool);
}
try {
- return BufferUtils.createBuffer(
+ return BufferUtils.createStableSortBuffer(
rowType,
preemptiveMemorySegmentPool,
recordKeyComputer,
diff --git
a/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/sink/append/AppendWriteFunctionWithBIMBufferSort.java
b/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/sink/append/AppendWriteFunctionWithBIMBufferSort.java
index 11017d9e4fc2..3c6f9ca4c04c 100644
---
a/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/sink/append/AppendWriteFunctionWithBIMBufferSort.java
+++
b/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/sink/append/AppendWriteFunctionWithBIMBufferSort.java
@@ -92,11 +92,11 @@ public class AppendWriteFunctionWithBIMBufferSort<T>
extends AppendWriteFunction
GeneratedRecordComparator recordComparator =
sortOperatorGen.generateRecordComparator("SortComparator");
this.memorySegmentPools =
this.memorySegmentPoolFactory.createMemorySegmentPools(config, 2,
OptionsResolver.getWriteBufferSizeInBytes(config));
- this.activeBuffer = BufferUtils.createBuffer(rowType,
+ this.activeBuffer = BufferUtils.createSortBuffer(rowType,
memorySegmentPools[0],
keyComputer.newInstance(Thread.currentThread().getContextClassLoader()),
recordComparator.newInstance(Thread.currentThread().getContextClassLoader()));
- this.backgroundBuffer = BufferUtils.createBuffer(rowType,
+ this.backgroundBuffer = BufferUtils.createSortBuffer(rowType,
memorySegmentPools[1],
keyComputer.newInstance(Thread.currentThread().getContextClassLoader()),
recordComparator.newInstance(Thread.currentThread().getContextClassLoader()));
diff --git
a/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/sink/append/AppendWriteFunctionWithDisruptorBufferSort.java
b/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/sink/append/AppendWriteFunctionWithDisruptorBufferSort.java
index 347ad81da22d..4653651e254c 100644
---
a/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/sink/append/AppendWriteFunctionWithDisruptorBufferSort.java
+++
b/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/sink/append/AppendWriteFunctionWithDisruptorBufferSort.java
@@ -106,7 +106,7 @@ public class AppendWriteFunctionWithDisruptorBufferSort<T>
extends AppendWriteFu
private void initDisruptorBuffer() throws Exception {
if (sortBuffer == null) {
- this.sortBuffer = BufferUtils.createBuffer(rowType,
+ this.sortBuffer = BufferUtils.createSortBuffer(rowType,
memorySegmentPool,
keyComputer.newInstance(Thread.currentThread().getContextClassLoader()),
recordComparator.newInstance(Thread.currentThread().getContextClassLoader()));
diff --git
a/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/sink/buffer/RowDataBucket.java
b/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/sink/buffer/RowDataBucket.java
index 84ca929100c4..8a6f124af317 100644
---
a/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/sink/buffer/RowDataBucket.java
+++
b/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/sink/buffer/RowDataBucket.java
@@ -25,17 +25,16 @@ import lombok.Getter;
import org.apache.flink.runtime.operators.sort.QuickSort;
import org.apache.flink.table.data.RowData;
import org.apache.flink.table.data.binary.BinaryRowData;
-import org.apache.flink.table.runtime.operators.sort.BinaryInMemorySortBuffer;
import org.apache.flink.util.MutableObjectIterator;
import java.io.IOException;
/**
- * RowData buffer for a specific data bucket, and the buffer is based on
{@code BinaryInMemorySortBuffer}
- * from Flink, which is backed by a managed {@code MemorySegmentPool} to
minimize GC costs.
+ * RowData buffer for a specific data bucket, backed by a managed {@code
MemorySegmentPool}
+ * to minimize GC costs.
*/
public class RowDataBucket {
- private final BinaryInMemorySortBuffer dataBuffer;
+ private final StableSortBuffer dataBuffer;
@Getter
private final BucketInfo bucketInfo;
private final BufferSizeDetector detector;
@@ -46,7 +45,7 @@ public class RowDataBucket {
public RowDataBucket(
String bucketId,
- BinaryInMemorySortBuffer dataBuffer,
+ StableSortBuffer dataBuffer,
BucketInfo bucketInfo,
Double batchSize) {
this.bucketId = bucketId;
@@ -59,8 +58,13 @@ public class RowDataBucket {
return dataBuffer.getIterator();
}
+ /**
+ * Sorts by record key, retaining arrival order for equal keys.
+ */
public void sort() throws IOException {
- new QuickSort().sort(dataBuffer);
+ if (dataBuffer.size() > 1) {
+ new QuickSort().sort(dataBuffer);
+ }
}
public boolean writeRow(RowData rowData) throws IOException {
@@ -71,7 +75,7 @@ public class RowDataBucket {
if (success) {
detector.detect(rowData);
} else {
- // BinaryInMemorySortBuffer may have partially appended variable-length
data before
+ // The buffer may have partially appended variable-length data before
// returning false. Its internal pointers can no longer be trusted for
another write.
diverged = true;
}
diff --git
a/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/sink/buffer/StableSortBuffer.java
b/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/sink/buffer/StableSortBuffer.java
new file mode 100644
index 000000000000..8cddc08b9114
--- /dev/null
+++
b/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/sink/buffer/StableSortBuffer.java
@@ -0,0 +1,232 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hudi.sink.buffer;
+
+import org.apache.flink.core.memory.MemorySegment;
+import org.apache.flink.runtime.io.disk.SimpleCollectingOutputView;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.binary.BinaryRowData;
+import org.apache.flink.table.runtime.generated.NormalizedKeyComputer;
+import org.apache.flink.table.runtime.generated.RecordComparator;
+import org.apache.flink.table.runtime.operators.sort.BinaryIndexedSortable;
+import org.apache.flink.table.runtime.typeutils.AbstractRowDataSerializer;
+import org.apache.flink.table.runtime.typeutils.BinaryRowDataSerializer;
+import org.apache.flink.table.runtime.util.MemorySegmentPool;
+import org.apache.flink.util.MutableObjectIterator;
+
+import java.io.EOFException;
+import java.io.IOException;
+import java.util.ArrayList;
+
+import static org.apache.flink.util.Preconditions.checkArgument;
+
+/**
+ * Copied from Flink's {@code BinaryInMemorySortBuffer}.
+ *
+ * <p>Provides stable sorting by comparing record offsets when keys are equal,
+ * preserving the original insertion order of records with equal keys.
+ */
+public final class StableSortBuffer extends BinaryIndexedSortable {
+
+ private static final int MIN_REQUIRED_BUFFERS = 3;
+
+ private final AbstractRowDataSerializer<RowData> inputSerializer;
+ private final ArrayList<MemorySegment> recordBufferSegments;
+ private final SimpleCollectingOutputView recordCollector;
+ private final int totalNumBuffers;
+
+ private long currentDataBufferOffset;
+ private long sortIndexBytes;
+
+ /** Create a memory sorter in `insert` way. */
+ public static StableSortBuffer createBuffer(
+ NormalizedKeyComputer normalizedKeyComputer,
+ AbstractRowDataSerializer<RowData> inputSerializer,
+ BinaryRowDataSerializer serializer,
+ RecordComparator comparator,
+ MemorySegmentPool memoryPool) {
+ checkArgument(memoryPool.freePages() >= MIN_REQUIRED_BUFFERS);
+ int totalNumBuffers = memoryPool.freePages();
+ ArrayList<MemorySegment> recordBufferSegments = new ArrayList<>(16);
+ return new StableSortBuffer(
+ normalizedKeyComputer,
+ inputSerializer,
+ serializer,
+ comparator,
+ recordBufferSegments,
+ new SimpleCollectingOutputView(
+ recordBufferSegments, memoryPool, memoryPool.pageSize()),
+ memoryPool,
+ totalNumBuffers);
+ }
+
+ private StableSortBuffer(
+ NormalizedKeyComputer normalizedKeyComputer,
+ AbstractRowDataSerializer<RowData> inputSerializer,
+ BinaryRowDataSerializer serializer,
+ RecordComparator comparator,
+ ArrayList<MemorySegment> recordBufferSegments,
+ SimpleCollectingOutputView recordCollector,
+ MemorySegmentPool pool,
+ int totalNumBuffers) {
+ super(normalizedKeyComputer, serializer, comparator, recordBufferSegments,
pool);
+ this.inputSerializer = inputSerializer;
+ this.recordBufferSegments = recordBufferSegments;
+ this.recordCollector = recordCollector;
+ this.totalNumBuffers = totalNumBuffers;
+ }
+
+ // Preserve arrival order for equal keys using the existing record offsets.
+ @Override
+ public int compare(int segmentNumberI, int segmentOffsetI, int
segmentNumberJ, int segmentOffsetJ) {
+ int result = super.compare(segmentNumberI, segmentOffsetI, segmentNumberJ,
segmentOffsetJ);
+ // ------------------ hoodie update ---------------------
+ if (result != 0) {
+ return result;
+ }
+ // super.compare establishes full-key equality, including fully
determining normalized keys.
+ // Offset order is always ascending, even when the key comparator sorts in
descending order.
+ return Long.compare(sortIndex.get(segmentNumberI).getLong(segmentOffsetI),
+ sortIndex.get(segmentNumberJ).getLong(segmentOffsetJ));
+ }
+
+ /**
+ * Resets the sort buffer back to the state where it is empty. All contained
data is discarded.
+ */
+ public void reset() {
+ // reset all offsets
+ this.numRecords = 0;
+ this.currentSortIndexOffset = 0;
+ this.currentDataBufferOffset = 0;
+ this.sortIndexBytes = 0;
+ // return all memory
+ returnToSegmentPool();
+ // grab first buffers
+ this.currentSortIndexSegment = nextMemorySegment();
+ this.sortIndex.add(this.currentSortIndexSegment);
+ this.recordCollector.reset();
+ }
+
+ public void returnToSegmentPool() {
+ // return all memory
+ this.memorySegmentPool.returnAll(this.sortIndex);
+ this.memorySegmentPool.returnAll(this.recordBufferSegments);
+ this.sortIndex.clear();
+ this.recordBufferSegments.clear();
+ }
+
+ public boolean isEmpty() {
+ return this.numRecords == 0;
+ }
+
+ public void dispose() {
+ returnToSegmentPool();
+ }
+
+ public long getCapacity() {
+ return ((long) this.totalNumBuffers) * memorySegmentPool.pageSize();
+ }
+
+ public long getOccupancy() {
+ return this.currentDataBufferOffset + this.sortIndexBytes;
+ }
+
+ /**
+ * Writes a given record to this sort buffer. The written record will be
appended and take the
+ * last logical position.
+ *
+ * @param record The record to be written.
+ * @return True, if the record was successfully written, false, if the sort
buffer was full.
+ * @throws IOException Thrown, if an error occurred while serializing the
record into the
+ * buffers.
+ */
+ public boolean write(RowData record) throws IOException {
+ // check whether we need a new memory segment for the sort index
+ if (!checkNextIndexOffset()) {
+ return false;
+ }
+
+ // serialize the record into the data buffers
+ int skip;
+ try {
+ skip = this.inputSerializer.serializeToPages(record,
this.recordCollector);
+ } catch (EOFException e) {
+ return false;
+ }
+
+ final long newOffset = this.recordCollector.getCurrentOffset();
+ long currOffset = currentDataBufferOffset + skip;
+
+ writeIndexAndNormalizedKey(record, currOffset);
+
+ this.currentDataBufferOffset = newOffset;
+
+ return true;
+ }
+
+ private BinaryRowData getRecordFromBuffer(BinaryRowData reuse, long pointer)
+ throws IOException {
+ this.recordBuffer.setReadPosition(pointer);
+ return this.serializer.mapFromPages(reuse, this.recordBuffer);
+ }
+
+ /**
+ * Gets an iterator over all records in this buffer in their logical order.
+ *
+ * @return An iterator returning the records in their logical order.
+ */
+ public MutableObjectIterator<BinaryRowData> getIterator() {
+ return new MutableObjectIterator<BinaryRowData>() {
+ private final int size = size();
+ private int current = 0;
+
+ private int currentSegment = 0;
+ private int currentOffset = 0;
+
+ private MemorySegment currentIndexSegment = sortIndex.get(0);
+
+ @Override
+ public BinaryRowData next(BinaryRowData target) {
+ if (this.current < this.size) {
+ this.current++;
+ if (this.currentOffset > lastIndexEntryOffset) {
+ this.currentOffset = 0;
+ this.currentIndexSegment = sortIndex.get(++this.currentSegment);
+ }
+
+ long pointer = this.currentIndexSegment.getLong(this.currentOffset);
+ this.currentOffset += indexEntrySize;
+
+ try {
+ return getRecordFromBuffer(target, pointer);
+ } catch (IOException ioe) {
+ throw new RuntimeException(ioe);
+ }
+ } else {
+ return null;
+ }
+ }
+
+ @Override
+ public BinaryRowData next() {
+ throw new RuntimeException("Not support!");
+ }
+ };
+ }
+}
diff --git
a/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/sink/exception/MemoryPagesExhaustedException.java
b/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/sink/exception/MemoryPagesExhaustedException.java
index 470c55fe9c2e..32d8e3c1778b 100644
---
a/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/sink/exception/MemoryPagesExhaustedException.java
+++
b/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/sink/exception/MemoryPagesExhaustedException.java
@@ -22,7 +22,7 @@ import org.apache.hudi.exception.HoodieException;
/**
* Exception thrown for memory pages in {@code MemorySegmentPool} are not
enough to
- * create a {@code BinaryInMemorySortBuffer}.
+ * create a sort buffer.
*/
public class MemoryPagesExhaustedException extends HoodieException {
diff --git
a/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/sink/partitioner/index/IndexWriteFunction.java
b/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/sink/partitioner/index/IndexWriteFunction.java
index de8359ca779a..a6c9b1c9b2a0 100644
---
a/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/sink/partitioner/index/IndexWriteFunction.java
+++
b/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/sink/partitioner/index/IndexWriteFunction.java
@@ -28,6 +28,7 @@ import org.apache.hudi.config.HoodieWriteConfig;
import org.apache.hudi.configuration.FlinkOptions;
import org.apache.hudi.exception.HoodieException;
import org.apache.hudi.metadata.HoodieMetadataPayload;
+import org.apache.hudi.sink.buffer.StableSortBuffer;
import org.apache.hudi.sink.common.AbstractStreamWriteFunction;
import org.apache.hudi.sink.event.WriteMetadataEvent;
import org.apache.hudi.sink.utils.BufferUtils;
@@ -39,7 +40,6 @@ import org.apache.flink.configuration.Configuration;
import org.apache.flink.runtime.operators.coordination.OperatorEvent;
import org.apache.flink.table.data.RowData;
import org.apache.flink.table.data.binary.BinaryRowData;
-import org.apache.flink.table.runtime.operators.sort.BinaryInMemorySortBuffer;
import org.apache.flink.table.runtime.util.MemorySegmentPool;
import org.apache.flink.util.Collector;
import org.apache.flink.util.IOUtils;
@@ -78,7 +78,7 @@ public class IndexWriteFunction extends
AbstractStreamWriteFunction<RowData> {
private transient MemorySegmentPool memorySegmentPool;
- private transient BinaryInMemorySortBuffer indexDataBuffer;
+ private transient StableSortBuffer indexDataBuffer;
/**
* Hoodie Flink table.
@@ -98,7 +98,7 @@ public class IndexWriteFunction extends
AbstractStreamWriteFunction<RowData> {
this.memorySegmentPool =
this.memorySegmentPoolFactory.createMemorySegmentPool(
config,
config.get(FlinkOptions.INDEX_RLI_WRITE_BUFFER_SIZE) * 1024 * 1024);
- this.indexDataBuffer =
BufferUtils.createBuffer(IndexRowUtils.INDEX_ROW_TYPE, memorySegmentPool);
+ this.indexDataBuffer =
BufferUtils.createStableSortBuffer(IndexRowUtils.INDEX_ROW_TYPE,
memorySegmentPool);
this.dedupKeyExtractor =
this.writeClient.getConfig().isRecordLevelIndexEnabled()
? IndexWriteFunction::getPartitionedDedupKey :
IndexRowUtils::getRecordKey;
}
@@ -155,7 +155,7 @@ public class IndexWriteFunction extends
AbstractStreamWriteFunction<RowData> {
}
}
- private Pair<List<HoodieRecord>, Set<String>>
prepareIndexRecordsAndPartitions(BinaryInMemorySortBuffer indexDataBuffer) {
+ private Pair<List<HoodieRecord>, Set<String>>
prepareIndexRecordsAndPartitions(StableSortBuffer indexDataBuffer) {
BinaryRowData reusedRow = new
BinaryRowData(IndexRowUtils.INDEX_ROW_TYPE.getFieldCount());
Iterator<BinaryRowData> rowItr = new
MutableIteratorWrapperIterator<>(indexDataBuffer.getIterator(), () ->
reusedRow);
Set<String> dataPartitions = new HashSet<>();
diff --git
a/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/sink/utils/BufferUtils.java
b/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/sink/utils/BufferUtils.java
index 831db7961639..861335ae2a4c 100644
---
a/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/sink/utils/BufferUtils.java
+++
b/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/sink/utils/BufferUtils.java
@@ -18,6 +18,7 @@
package org.apache.hudi.sink.utils;
+import org.apache.hudi.sink.buffer.StableSortBuffer;
import org.apache.hudi.sink.exception.MemoryPagesExhaustedException;
import org.apache.flink.table.runtime.generated.NormalizedKeyComputer;
@@ -33,16 +34,31 @@ import org.apache.flink.table.types.logical.RowType;
* Utilities to create binary buffer for writing functions.
*/
public class BufferUtils {
- // minimum pages for a BinaryInMemorySortBuffer
+ // minimum pages for either sort buffer
private static final int MIN_REQUIRED_BUFFERS = 3;
- public static BinaryInMemorySortBuffer createBuffer(RowType rowType,
MemorySegmentPool memorySegmentPool) {
- return createBuffer(rowType, memorySegmentPool, new
NaturalOrderKeyComputer(), new NaturalOrderRecordComparator());
+ public static StableSortBuffer createStableSortBuffer(RowType rowType,
MemorySegmentPool memorySegmentPool) {
+ return createStableSortBuffer(rowType, memorySegmentPool, new
NaturalOrderKeyComputer(), new NaturalOrderRecordComparator());
}
- public static BinaryInMemorySortBuffer createBuffer(RowType rowType,
MemorySegmentPool memorySegmentPool, NormalizedKeyComputer keyComputer,
RecordComparator recordComparator) {
+ public static StableSortBuffer createStableSortBuffer(RowType rowType,
MemorySegmentPool memorySegmentPool, NormalizedKeyComputer keyComputer,
RecordComparator recordComparator) {
if (memorySegmentPool.freePages() < MIN_REQUIRED_BUFFERS) {
// there is no enough free pages to create a binary buffer, may need
flush first.
+ throw new MemoryPagesExhaustedException("Free pages are not enough to
create a StableSortBuffer.");
+ }
+ return StableSortBuffer.createBuffer(
+ keyComputer,
+ new RowDataSerializer(rowType),
+ new BinaryRowDataSerializer(rowType.getFieldCount()),
+ recordComparator,
+ memorySegmentPool);
+ }
+
+ /** Creates a Flink sort buffer, which do not require stable ordering. */
+ public static BinaryInMemorySortBuffer createSortBuffer(
+ RowType rowType, MemorySegmentPool memorySegmentPool,
+ NormalizedKeyComputer keyComputer, RecordComparator recordComparator) {
+ if (memorySegmentPool.freePages() < MIN_REQUIRED_BUFFERS) {
throw new MemoryPagesExhaustedException("Free pages are not enough to
create a BinaryInMemorySortBuffer.");
}
return BinaryInMemorySortBuffer.createBuffer(
diff --git
a/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/sink/utils/NaturalOrderKeyComputer.java
b/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/sink/utils/NaturalOrderKeyComputer.java
index 32b7af13fa3c..7e1303385164 100644
---
a/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/sink/utils/NaturalOrderKeyComputer.java
+++
b/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/sink/utils/NaturalOrderKeyComputer.java
@@ -24,7 +24,7 @@ import
org.apache.flink.table.runtime.generated.NormalizedKeyComputer;
import org.apache.flink.table.runtime.operators.sort.SortUtil;
/**
- * An implementation of @{code NormalizedKeyComputer} for {@code
BinaryInMemorySortBuffer} in case sort is not needed,
+ * An implementation of @{code NormalizedKeyComputer} for {@code
StableSortBuffer} in case sort is not needed,
* and the returned iterator follows natural order as inserted.
*/
public class NaturalOrderKeyComputer implements NormalizedKeyComputer {
diff --git
a/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/sink/utils/NaturalOrderRecordComparator.java
b/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/sink/utils/NaturalOrderRecordComparator.java
index 9cd2807d096f..3f891f09317e 100644
---
a/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/sink/utils/NaturalOrderRecordComparator.java
+++
b/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/sink/utils/NaturalOrderRecordComparator.java
@@ -22,7 +22,7 @@ import org.apache.flink.table.data.RowData;
import org.apache.flink.table.runtime.generated.RecordComparator;
/**
- * An implementation of @{code RecordComparator} for {@code
BinaryInMemorySortBuffer} in case sort is not needed,
+ * An implementation of @{code RecordComparator} for {@code StableSortBuffer}
in case sort is not needed,
* and the returned iterator follows natural order as inserted.
*/
public class NaturalOrderRecordComparator implements RecordComparator {
diff --git
a/hudi-flink-datasource/hudi-flink/src/test/java/org/apache/hudi/sink/TestWriteCopyOnWrite.java
b/hudi-flink-datasource/hudi-flink/src/test/java/org/apache/hudi/sink/TestWriteCopyOnWrite.java
index 3b3798062e7f..7f53f9fb8efc 100644
---
a/hudi-flink-datasource/hudi-flink/src/test/java/org/apache/hudi/sink/TestWriteCopyOnWrite.java
+++
b/hudi-flink-datasource/hudi-flink/src/test/java/org/apache/hudi/sink/TestWriteCopyOnWrite.java
@@ -673,7 +673,7 @@ public class TestWriteCopyOnWrite extends TestWriteBase {
@Test
public void testInsertWithSmallBufferSize() throws Exception {
// reset the config option
- // In rowdata write mode, BinaryInMemorySortBuffer need at least 2 memory
segments for auxiliary information,
+ // In rowdata write mode, StableSortBuffer need at least 2 memory segments
for auxiliary information,
// the page size is tuned to 64 to make sure 3 records from 5 will trigger
a mini-batch write.
conf.set(FlinkOptions.WRITE_MEMORY_SEGMENT_PAGE_SIZE, 64);
conf.set(FlinkOptions.WRITE_TASK_MAX_SIZE, 200 + BUFFER_SIZE_MB);
diff --git
a/hudi-flink-datasource/hudi-flink/src/test/java/org/apache/hudi/sink/TestWriteMergeOnRead.java
b/hudi-flink-datasource/hudi-flink/src/test/java/org/apache/hudi/sink/TestWriteMergeOnRead.java
index 64cdd0405518..cdc530456e80 100644
---
a/hudi-flink-datasource/hudi-flink/src/test/java/org/apache/hudi/sink/TestWriteMergeOnRead.java
+++
b/hudi-flink-datasource/hudi-flink/src/test/java/org/apache/hudi/sink/TestWriteMergeOnRead.java
@@ -20,12 +20,14 @@ package org.apache.hudi.sink;
import org.apache.hudi.common.config.HoodieMetadataConfig;
import org.apache.hudi.common.config.HoodieStorageConfig;
+import org.apache.hudi.common.config.RecordMergeMode;
import org.apache.hudi.common.model.EventTimeAvroPayload;
import org.apache.hudi.common.model.HoodieCommitMetadata;
import org.apache.hudi.common.model.HoodieKey;
import org.apache.hudi.common.model.HoodieTableType;
import org.apache.hudi.common.model.HoodieWriteStat;
import org.apache.hudi.common.model.MetaFieldsMode;
+import org.apache.hudi.common.table.HoodieTableConfig;
import org.apache.hudi.common.table.HoodieTableMetaClient;
import org.apache.hudi.common.table.timeline.HoodieInstant;
import org.apache.hudi.config.HoodieClusteringConfig;
@@ -34,22 +36,31 @@ import org.apache.hudi.configuration.FlinkOptions;
import org.apache.hudi.exception.HoodieException;
import org.apache.hudi.index.HoodieIndex;
import org.apache.hudi.util.StreamerUtil;
+import org.apache.hudi.utils.TestConfigurations;
import org.apache.hudi.utils.TestData;
+import org.apache.avro.generic.GenericRecord;
import org.apache.flink.configuration.Configuration;
import org.apache.flink.table.data.RowData;
import org.apache.flink.table.data.StringData;
import org.apache.flink.table.data.TimestampData;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.CsvSource;
import org.junit.jupiter.params.provider.ValueSource;
+import java.io.File;
+import java.util.ArrayList;
+import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import java.util.stream.Collectors;
import static org.apache.hudi.utils.TestData.insertRow;
import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
/**
* Test cases for delta stream write.
@@ -61,6 +72,67 @@ public class TestWriteMergeOnRead extends
TestWriteCopyOnWrite {
conf.set(FlinkOptions.COMPACTION_ASYNC_ENABLED, false);
}
+ @ParameterizedTest
+ @CsvSource({
+ "COMMIT_TIME_ORDERING, false, false", "COMMIT_TIME_ORDERING, true,
false",
+ "EVENT_TIME_ORDERING, false, false", "EVENT_TIME_ORDERING, true, false",
+ "EVENT_TIME_ORDERING, false, true", "EVENT_TIME_ORDERING, true, true"
+ })
+ void testLsmDuplicateKeysRespectMergeMode(RecordMergeMode mergeMode, boolean
preCombine, boolean equalOrderingValues) throws Exception {
+ conf.setString(HoodieTableConfig.TABLE_STORAGE_LAYOUT.key(), "LSM_TREE");
+ conf.set(FlinkOptions.RECORD_MERGE_MODE, mergeMode.name());
+ conf.set(FlinkOptions.PRE_COMBINE, preCombine);
+ conf.set(FlinkOptions.INDEX_TYPE, "BUCKET");
+ conf.set(FlinkOptions.BUCKET_INDEX_NUM_BUCKETS, 1);
+ conf.set(FlinkOptions.WRITE_BATCH_SIZE, 256.0);
+ boolean commitTimeOrdering = mergeMode ==
RecordMergeMode.COMMIT_TIME_ORDERING;
+ boolean lastArrivalWins = commitTimeOrdering || equalOrderingValues;
+ List<RowData> records = new ArrayList<>();
+ Map<String, Integer> expectedWinners = new HashMap<>();
+ for (int i = 0; i < 1000; i++) {
+ String key = "id" + i % 17;
+ // Decreasing event times distinguish event-time merging from
commit-time merging.
+ // Equal event times must retain the last arrival, just like commit-time
merging.
+ records.add(insertRow(StringData.fromString(key),
StringData.fromString("name"), i,
+ TimestampData.fromEpochMillis(equalOrderingValues ? 1000 : 1000 -
i), StringData.fromString("par1")));
+ if (lastArrivalWins) {
+ expectedWinners.put(key, i);
+ } else {
+ expectedWinners.putIfAbsent(key, i);
+ }
+ }
+ String expected =
expectedWinners.entrySet().stream().sorted(Map.Entry.comparingByKey())
+ .map(entry -> entry.getKey() + ",par1," + entry.getKey() + ",name," +
entry.getValue()
+ + "," + (equalOrderingValues ? 1000 : 1000 - entry.getValue()) +
",par1")
+ .collect(Collectors.joining(", ", "[", "]"));
+ preparePipeline(conf)
+ .consume(records)
+ .checkpoint(1)
+ .assertNextEvent()
+ .checkpointComplete(1)
+ .checkWrittenData(Collections.singletonMap("par1", expected), 1)
+ .end();
+
+ // With pre-combine off, all versions must reach one native log file in
arrival order
+ // within each key, including event-time ties resolved during subsequent
reads.
+ File[] logFiles = new File(tempFile, "par1").listFiles(file ->
file.getName().endsWith(".log.parquet"));
+ assertNotNull(logFiles);
+ assertEquals(1, logFiles.length, "duplicate versions must be tested within
a single native log file");
+ List<GenericRecord> written = TestData.readAllData(tempFile,
TestConfigurations.ROW_TYPE, 1);
+ assertEquals(preCombine ? expectedWinners.size() : records.size(),
written.size());
+ Map<String, Integer> previous = new HashMap<>();
+ Map<String, Integer> actualWinners = new HashMap<>();
+ for (GenericRecord record : written) {
+ String key = record.get("uuid").toString();
+ int arrival = (Integer) record.get("age");
+ Integer oldArrival = previous.put(key, arrival);
+ assertTrue(oldArrival == null || arrival > oldArrival, "file order must
preserve arrivals for " + key);
+ actualWinners.merge(key, arrival, (oldValue, newValue) ->
+ lastArrivalWins ? Math.max(oldValue, newValue) : Math.min(oldValue,
newValue));
+ }
+ assertEquals(expectedWinners, actualWinners);
+ }
+
@Test
public void testIndexStateBootstrapWithMultiFilesInOneSlice() throws
Exception {
// open the function and ingest data
diff --git
a/hudi-flink-datasource/hudi-flink/src/test/java/org/apache/hudi/sink/buffer/TestRowDataBucket.java
b/hudi-flink-datasource/hudi-flink/src/test/java/org/apache/hudi/sink/buffer/TestRowDataBucket.java
index e98e26bbd7dc..dc8af04bd423 100644
---
a/hudi-flink-datasource/hudi-flink/src/test/java/org/apache/hudi/sink/buffer/TestRowDataBucket.java
+++
b/hudi-flink-datasource/hudi-flink/src/test/java/org/apache/hudi/sink/buffer/TestRowDataBucket.java
@@ -18,30 +18,259 @@
package org.apache.hudi.sink.buffer;
+import org.apache.hudi.configuration.FlinkOptions;
+import org.apache.hudi.sink.bulk.RowDataKeyGen;
+import org.apache.hudi.sink.bulk.RowDataKeyGens;
+import org.apache.hudi.sink.bulk.sort.SortOperatorGen;
+import org.apache.hudi.sink.exception.MemoryPagesExhaustedException;
import org.apache.hudi.sink.utils.BufferUtils;
+import org.apache.hudi.sink.utils.RecordKeySortComparator;
+import org.apache.hudi.sink.utils.RecordKeySortKeyComputer;
import org.apache.hudi.table.action.commit.BucketInfo;
import org.apache.hudi.table.action.commit.BucketType;
import org.apache.hudi.utils.TestData;
+import org.apache.flink.configuration.Configuration;
+import org.apache.flink.core.memory.MemorySegment;
+import org.apache.flink.runtime.operators.sort.HeapSort;
+import org.apache.flink.runtime.operators.sort.QuickSort;
import org.apache.flink.table.api.DataTypes;
+import org.apache.flink.table.data.GenericRowData;
import org.apache.flink.table.data.RowData;
import org.apache.flink.table.data.StringData;
import org.apache.flink.table.data.binary.BinaryRowData;
+import org.apache.flink.table.runtime.generated.NormalizedKeyComputer;
+import org.apache.flink.table.runtime.generated.RecordComparator;
import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.RowKind;
import org.apache.flink.util.MutableObjectIterator;
import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.CsvSource;
import java.util.ArrayList;
+import java.util.Comparator;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
/** Tests for {@link RowDataBucket}. */
class TestRowDataBucket {
+ @ParameterizedTest
+ @CsvSource({
+ "true, false, 0", "true, false, 1", "true, false, 12",
+ "true, false, 10000", "false, false, 10000",
+ "true, true, 10000", "false, true, 10000"
+ })
+ void testSortPreservesArrivalOrder(boolean codegen, boolean heapSort, int
count) throws Exception {
+ RowType rowType = (RowType) DataTypes.ROW(
+ DataTypes.FIELD("uuid", DataTypes.STRING()),
+ DataTypes.FIELD("arrival", DataTypes.INT()),
+ DataTypes.FIELD("payload", DataTypes.STRING())).getLogicalType();
+ HeapMemorySegmentPool pool = new HeapMemorySegmentPool(32 * 1024, 4 * 1024
* 1024);
+ int initialFreePages = pool.freePages();
+ NormalizedKeyComputer keyComputer;
+ RecordComparator comparator;
+ if (codegen) {
+ SortOperatorGen generator = new SortOperatorGen(rowType, new String[]
{"uuid"});
+ ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
+ keyComputer =
generator.generateNormalizedKeyComputer("StableKeyComputer").newInstance(classLoader);
+ comparator =
generator.generateRecordComparator("StableComparator").newInstance(classLoader);
+ } else {
+ Configuration conf = new Configuration();
+ conf.set(FlinkOptions.RECORD_KEY_FIELD, "uuid");
+ conf.set(FlinkOptions.PARTITION_PATH_FIELD, "");
+ RowDataKeyGen keyGen = RowDataKeyGens.instance(conf, rowType);
+ keyComputer = new RecordKeySortKeyComputer(keyGen, 1);
+ comparator = new RecordKeySortComparator(keyGen);
+ }
+ StableSortBuffer buffer = BufferUtils.createStableSortBuffer(rowType,
pool, keyComputer, comparator);
+ RowDataBucket bucket = new RowDataBucket(
+ "bucket-0", buffer, new BucketInfo(BucketType.INSERT, "file-0",
"partition-0"), 256.0);
+ List<RowData> expected = new ArrayList<>();
+ try {
+ for (int i = 0; i < count; i++) {
+ // Interleaved keys sharing a normalized prefix exercise full-key
comparisons as well as
+ // equal keys. The large input spans several sort-index pages and
exceeds insertion sort.
+ // Variable lengths and records larger than a page exercise byte
offsets and page padding.
+ String payload = repeat('x', i % 997 == 0 ? 40 * 1024 : i % 257);
+ RowData row = GenericRowData.of(StringData.fromString("shared-prefix-"
+ i % 17), i,
+ StringData.fromString(payload));
+ row.setRowKind(i % 3 == 0 ? RowKind.DELETE : RowKind.UPDATE_AFTER);
+ assertTrue(bucket.writeRow(row));
+ expected.add(row);
+ }
+ expected.sort(Comparator.comparing(row -> row.getString(0).toString()));
+ if (heapSort) {
+ // QuickSort's fallback uses the index-addressed compare/swap
overloads.
+ new HeapSort().sort(buffer);
+ } else {
+ bucket.sort();
+ }
+ MutableObjectIterator<BinaryRowData> iterator = bucket.getDataIterator();
+ BinaryRowData reuse = new BinaryRowData(3);
+ boolean[] seen = new boolean[count];
+ for (RowData row : expected) {
+ BinaryRowData actual = iterator.next(reuse);
+ assertNotNull(actual);
+ assertEquals(row.getString(0), actual.getString(0));
+ int arrival = actual.getInt(1);
+ assertFalse(seen[arrival], "each input record must be emitted exactly
once");
+ seen[arrival] = true;
+ assertEquals(StringData.fromString("shared-prefix-" + arrival % 17),
actual.getString(0));
+ assertEquals(row.getInt(1), arrival, "equal keys must retain arrival
order");
+ assertEquals(row.getString(2), actual.getString(2));
+ assertEquals(arrival % 3 == 0 ? RowKind.DELETE : RowKind.UPDATE_AFTER,
actual.getRowKind());
+ }
+ assertNull(iterator.next(reuse));
+ } finally {
+ bucket.dispose();
+ }
+ assertEquals(initialFreePages, pool.freePages());
+ }
+
+ @ParameterizedTest
+ @CsvSource({
+ "false, false, 1", "false, true, 1", "true, false, 1", "true, true, 1",
+ "false, false, 17", "false, true, 17", "true, false, 17", "true, true,
17"
+ })
+ void testFullyDeterminingKeysPreserveOffsetOrder(boolean heapSort, boolean
descending, int distinctKeys) throws Exception {
+ RowType rowType = (RowType) DataTypes.ROW(
+ DataTypes.FIELD("key", DataTypes.INT()),
+ DataTypes.FIELD("arrival", DataTypes.INT())).getLogicalType();
+ HeapMemorySegmentPool pool = new HeapMemorySegmentPool(128, 1024 * 1024);
+ int initialFreePages = pool.freePages();
+ NormalizedKeyComputer keyComputer = new NormalizedKeyComputer() {
+ @Override
+ public void putKey(RowData record, MemorySegment target, int offset) {
+ target.putInt(offset, record.getInt(0));
+ }
+
+ @Override
+ public int compareKey(MemorySegment left, int leftOffset, MemorySegment
right, int rightOffset) {
+ return Integer.compare(left.getInt(leftOffset),
right.getInt(rightOffset));
+ }
+
+ @Override
+ public void swapKey(MemorySegment left, int leftOffset, MemorySegment
right, int rightOffset) {
+ int key = left.getInt(leftOffset);
+ left.putInt(leftOffset, right.getInt(rightOffset));
+ right.putInt(rightOffset, key);
+ }
+
+ @Override
+ public int getNumKeyBytes() {
+ return Integer.BYTES;
+ }
+
+ @Override
+ public boolean isKeyFullyDetermines() {
+ return true;
+ }
+
+ @Override
+ public boolean invertKey() {
+ return descending;
+ }
+ };
+ StableSortBuffer buffer = BufferUtils.createStableSortBuffer(rowType,
pool, keyComputer, (left, right) -> {
+ throw new AssertionError("Fully determining normalized keys must not
deserialize records for comparison");
+ });
+ List<RowData> expected = new ArrayList<>();
+ try {
+ for (int i = 0; i < 2000; i++) {
+ RowData row = GenericRowData.of(i % distinctKeys - 8, i);
+ assertTrue(buffer.write(row));
+ expected.add(row);
+ }
+ Comparator<RowData> comparator = Comparator.comparingInt(row ->
row.getInt(0));
+ expected.sort(descending ? comparator.reversed() : comparator);
+
+ // Scramble index entries using both swap APIs: byte offsets must
continue to represent
+ // original append order, rather than the positions at which sorting
starts.
+ buffer.swap(0, buffer.size() - 1);
+ buffer.swap(0, buffer.recordSize(), 1, 0);
+ if (heapSort) {
+ new HeapSort().sort(buffer);
+ } else {
+ new QuickSort().sort(buffer);
+ }
+ MutableObjectIterator<BinaryRowData> iterator = buffer.getIterator();
+ BinaryRowData reuse = new BinaryRowData(2);
+ for (RowData row : expected) {
+ BinaryRowData actual = iterator.next(reuse);
+ assertNotNull(actual);
+ assertEquals(row.getInt(0), actual.getInt(0));
+ assertEquals(row.getInt(1), actual.getInt(1));
+ }
+ assertNull(iterator.next(reuse));
+ } finally {
+ buffer.dispose();
+ }
+ assertEquals(initialFreePages, pool.freePages());
+ }
+
+ @Test
+ void testResetPreservesArrivalOrderAcrossBatches() throws Exception {
+ RowType rowType = (RowType) DataTypes.ROW(
+ DataTypes.FIELD("key", DataTypes.STRING()),
+ DataTypes.FIELD("arrival", DataTypes.INT())).getLogicalType();
+ HeapMemorySegmentPool pool = new HeapMemorySegmentPool(32 * 1024, 1024 *
1024);
+ int initialFreePages = pool.freePages();
+ SortOperatorGen generator = new SortOperatorGen(rowType, new String[]
{"key"});
+ ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
+ StableSortBuffer buffer = BufferUtils.createStableSortBuffer(rowType, pool,
+
generator.generateNormalizedKeyComputer("ResetKeyComputer").newInstance(classLoader),
+
generator.generateRecordComparator("ResetComparator").newInstance(classLoader));
+ int emptyBufferFreePages = pool.freePages();
+ try {
+ assertEquals((long) initialFreePages * pool.pageSize(),
buffer.getCapacity());
+ for (int batch = 0; batch < 3; batch++) {
+ buffer.reset();
+ assertTrue(buffer.isEmpty());
+ assertEquals(0, buffer.getOccupancy());
+ assertEquals(emptyBufferFreePages, pool.freePages(), "reset must
release pages from the previous batch");
+ List<RowData> expected = new ArrayList<>();
+ for (int i = 0; i < 2000; i++) {
+ RowData row =
GenericRowData.of(StringData.fromString("shared-prefix-" + i % 17), batch *
2000 + i);
+ assertTrue(buffer.write(row));
+ expected.add(row);
+ }
+ assertTrue(buffer.getOccupancy() > 0);
+ assertTrue(pool.freePages() < emptyBufferFreePages);
+ expected.sort(Comparator.comparing(row ->
row.getString(0).toString()));
+ new QuickSort().sort(buffer);
+ MutableObjectIterator<BinaryRowData> iterator = buffer.getIterator();
+ BinaryRowData reuse = new BinaryRowData(2);
+ for (RowData row : expected) {
+ BinaryRowData actual = iterator.next(reuse);
+ assertNotNull(actual);
+ assertEquals(row.getString(0), actual.getString(0));
+ assertEquals(row.getInt(1), actual.getInt(1));
+ }
+ assertNull(iterator.next(reuse));
+ }
+ } finally {
+ buffer.dispose();
+ }
+ assertEquals(initialFreePages, pool.freePages());
+ }
+
+ @Test
+ void testInsufficientPagesDoNotAllocateBuffer() {
+ RowType rowType = (RowType) DataTypes.ROW(DataTypes.FIELD("key",
DataTypes.INT())).getLogicalType();
+ HeapMemorySegmentPool pool = new HeapMemorySegmentPool(32 * 1024, 64 *
1024);
+ int initialFreePages = pool.freePages();
+ assertThrows(MemoryPagesExhaustedException.class, () ->
BufferUtils.createStableSortBuffer(rowType, pool));
+ assertEquals(initialFreePages, pool.freePages());
+ }
+
@Test
void testDivergedBucketCannotBeReusedAndReleasesAllPages() throws Exception {
RowType rowType = (RowType) DataTypes.ROW(
@@ -53,7 +282,7 @@ class TestRowDataBucket {
int initialFreePages = pool.freePages();
RowDataBucket bucket = new RowDataBucket(
"bucket-0",
- BufferUtils.createBuffer(rowType, pool),
+ BufferUtils.createStableSortBuffer(rowType, pool),
new BucketInfo(BucketType.INSERT, "file-0", "partition-0"),
256.0);
@@ -72,7 +301,7 @@ class TestRowDataBucket {
expectedIds.add(id);
}
- assertTrue(writeFailed, "the tiny pool should cause
BinaryInMemorySortBuffer.write to return false");
+ assertTrue(writeFailed, "the tiny pool should cause
StableSortBuffer.write to return false");
assertTrue(bucket.isDiverged());
assertFalse(bucket.isEmpty(), "successful rows written before exhaustion
should remain readable");
diff --git
a/hudi-flink-datasource/hudi-flink/src/test/java/org/apache/hudi/sink/partitioner/index/TestIndexWriteFunction.java
b/hudi-flink-datasource/hudi-flink/src/test/java/org/apache/hudi/sink/partitioner/index/TestIndexWriteFunction.java
index b0f17658dc29..2ed12c443d09 100644
---
a/hudi-flink-datasource/hudi-flink/src/test/java/org/apache/hudi/sink/partitioner/index/TestIndexWriteFunction.java
+++
b/hudi-flink-datasource/hudi-flink/src/test/java/org/apache/hudi/sink/partitioner/index/TestIndexWriteFunction.java
@@ -19,11 +19,11 @@
package org.apache.hudi.sink.partitioner.index;
import org.apache.hudi.client.HoodieFlinkWriteClient;
+import org.apache.hudi.sink.buffer.StableSortBuffer;
import org.apache.hudi.sink.utils.BufferUtils;
import org.apache.flink.configuration.Configuration;
import org.apache.flink.runtime.memory.MemoryManager;
-import org.apache.flink.table.runtime.operators.sort.BinaryInMemorySortBuffer;
import org.apache.flink.table.runtime.util.LazyMemorySegmentPool;
import org.apache.flink.table.runtime.util.MemorySegmentPool;
import org.junit.jupiter.api.Test;
@@ -69,7 +69,7 @@ class TestIndexWriteFunction {
void testCloseContinuesAfterCleanupFailure(boolean bufferFails) throws
Exception {
HoodieFlinkWriteClient writeClient = mock(HoodieFlinkWriteClient.class);
IndexWriteFunction function = new TestFunction(writeClient);
- BinaryInMemorySortBuffer buffer = mock(BinaryInMemorySortBuffer.class);
+ StableSortBuffer buffer = mock(StableSortBuffer.class);
CloseableMemorySegmentPool pool = mock(CloseableMemorySegmentPool.class);
setField(function, "indexDataBuffer", buffer);
setField(function, "memorySegmentPool", pool);
@@ -99,7 +99,7 @@ class TestIndexWriteFunction {
MemoryManager memoryManager = MemoryManager.create(8L * pageSize,
pageSize);
try {
LazyMemorySegmentPool pool = new LazyMemorySegmentPool(new Object(),
memoryManager, 8);
- BinaryInMemorySortBuffer buffer =
BufferUtils.createBuffer(IndexRowUtils.INDEX_ROW_TYPE, pool);
+ StableSortBuffer buffer =
BufferUtils.createStableSortBuffer(IndexRowUtils.INDEX_ROW_TYPE, pool);
HoodieFlinkWriteClient writeClient = mock(HoodieFlinkWriteClient.class);
IndexWriteFunction function = new TestFunction(writeClient);
setField(function, "indexDataBuffer", buffer);
diff --git
a/hudi-flink-datasource/hudi-flink/src/test/java/org/apache/hudi/sink/utils/TestRecordKeySortKeyComputer.java
b/hudi-flink-datasource/hudi-flink/src/test/java/org/apache/hudi/sink/utils/TestRecordKeySortKeyComputer.java
index fb0d998448f5..9306295eba9e 100644
---
a/hudi-flink-datasource/hudi-flink/src/test/java/org/apache/hudi/sink/utils/TestRecordKeySortKeyComputer.java
+++
b/hudi-flink-datasource/hudi-flink/src/test/java/org/apache/hudi/sink/utils/TestRecordKeySortKeyComputer.java
@@ -21,6 +21,7 @@ package org.apache.hudi.sink.utils;
import org.apache.hudi.common.util.StringUtils;
import org.apache.hudi.configuration.FlinkOptions;
import org.apache.hudi.sink.buffer.HeapMemorySegmentPool;
+import org.apache.hudi.sink.buffer.StableSortBuffer;
import org.apache.hudi.sink.bulk.RowDataKeyGen;
import org.apache.hudi.sink.bulk.RowDataKeyGens;
@@ -33,7 +34,6 @@ import org.apache.flink.table.data.GenericRowData;
import org.apache.flink.table.data.RowData;
import org.apache.flink.table.data.StringData;
import org.apache.flink.table.data.binary.BinaryRowData;
-import org.apache.flink.table.runtime.operators.sort.BinaryInMemorySortBuffer;
import org.apache.flink.table.types.logical.BigIntType;
import org.apache.flink.table.types.logical.LogicalType;
import org.apache.flink.table.types.logical.RowType;
@@ -212,7 +212,7 @@ class TestRecordKeySortKeyComputer {
}
@Test
- void testBinaryInMemorySortBufferOrdering() throws Exception {
+ void testStableSortBufferOrdering() throws Exception {
RowType longRowType = rowType(new String[] {"key"}, new LogicalType[] {new
BigIntType()});
assertBufferSort(longRowType, "key", Arrays.asList(
GenericRowData.of(2L), GenericRowData.of(10L), GenericRowData.of(-1L),
GenericRowData.of(100L)));
@@ -308,7 +308,7 @@ class TestRecordKeySortKeyComputer {
throws Exception {
RowDataKeyGen keyGen = keyGen(rowType, recordKeyFields);
int recordKeyFieldCount = recordKeyFields.split(",").length;
- BinaryInMemorySortBuffer buffer = BufferUtils.createBuffer(
+ StableSortBuffer buffer = BufferUtils.createStableSortBuffer(
rowType,
new HeapMemorySegmentPool(MemoryManager.DEFAULT_PAGE_SIZE,
BUFFER_SIZE_BYTES),
new RecordKeySortKeyComputer(keyGen, recordKeyFieldCount),