From ca464709fb59ed756f88888eec375e6d661e02b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BB=9F=E5=BC=8B?= Date: Sun, 16 Aug 2026 01:35:32 +0800 Subject: [PATCH 1/7] [core] Reuse projected entries in manifest run merge --- .../paimon/manifest/PartitionDictionary.java | 101 ++++++- .../manifest/ProjectedManifestEntry.java | 1 + .../operation/ManifestEntryRunMerge.java | 197 +++++--------- .../operation/ManifestEntryRunMergeEntry.java | 253 ++++++------------ .../operation/ManifestEntryRunMergePlan.java | 88 +++--- .../manifest/PartitionDictionaryTest.java | 98 +++++++ .../manifest/ProjectedManifestEntryTest.java | 19 ++ 7 files changed, 391 insertions(+), 366 deletions(-) create mode 100644 paimon-core/src/test/java/org/apache/paimon/manifest/PartitionDictionaryTest.java diff --git a/paimon-core/src/main/java/org/apache/paimon/manifest/PartitionDictionary.java b/paimon-core/src/main/java/org/apache/paimon/manifest/PartitionDictionary.java index b925457e68dd..08a6a733e3eb 100644 --- a/paimon-core/src/main/java/org/apache/paimon/manifest/PartitionDictionary.java +++ b/paimon-core/src/main/java/org/apache/paimon/manifest/PartitionDictionary.java @@ -23,37 +23,110 @@ import org.apache.paimon.utils.ByteArrayLookupKey; import org.apache.paimon.utils.SerializationUtils; +import javax.annotation.Nullable; + +import java.util.ArrayList; import java.util.Arrays; +import java.util.Comparator; import java.util.HashMap; +import java.util.List; import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import static org.apache.paimon.utils.Preconditions.checkArgument; +import static org.apache.paimon.utils.Preconditions.checkState; /** Deduplicates serialized partitions and assigns compact integer identifiers. */ public final class PartitionDictionary { - private final Map ids = new HashMap<>(); - private final ByteArrayLookupKey lookup = new ByteArrayLookupKey(); - private BinaryRow[] partitions = new BinaryRow[16]; + private final Map ids; + private final @Nullable ByteArrayLookupKey lookup; + private final @Nullable ThreadLocal concurrentLookup; + private final @Nullable Comparator comparator; + private volatile BinaryRow[] partitions = new BinaryRow[16]; private int partitionCount; + private @Nullable int[] ranks; + + /** Creates the low-overhead dictionary used by single-threaded manifest rewriting. */ + public PartitionDictionary() { + this.ids = new HashMap<>(); + this.lookup = new ByteArrayLookupKey(); + this.concurrentLookup = null; + this.comparator = null; + } + + /** + * Creates a dictionary which supports concurrent collection and comparator-compatible ranks. + */ + public PartitionDictionary(Comparator comparator) { + checkArgument(comparator != null, "Partition comparator cannot be null."); + this.ids = new ConcurrentHashMap<>(); + this.lookup = null; + this.concurrentLookup = ThreadLocal.withInitial(ByteArrayLookupKey::new); + this.comparator = comparator; + } public int id(byte[] bytes) { - lookup.reset(bytes); + ByteArrayLookupKey lookupKey = concurrentLookup == null ? lookup : concurrentLookup.get(); + checkState(lookupKey != null, "Partition lookup key is unavailable."); + lookupKey.reset(bytes); try { - Integer existing = ids.get(lookup); + Integer existing = ids.get(lookupKey); if (existing != null) { return existing; } - byte[] canonical = Arrays.copyOf(bytes, bytes.length); - int id = partitionCount; - if (id == partitions.length) { - partitions = Arrays.copyOf(partitions, partitions.length << 1); + if (concurrentLookup != null) { + synchronized (this) { + existing = ids.get(lookupKey); + if (existing != null) { + return existing; + } + return add(bytes); + } } - partitions[id] = SerializationUtils.deserializeBinaryRow(canonical); - ids.put(new ByteArrayKey(canonical), id); - partitionCount = id + 1; - return id; + return add(bytes); } finally { - lookup.clear(); + lookupKey.clear(); + } + } + + private int add(byte[] bytes) { + checkState(ranks == null, "Manifest scan found a partition after ranks were assigned."); + byte[] canonical = Arrays.copyOf(bytes, bytes.length); + int id = partitionCount; + if (id == partitions.length) { + partitions = Arrays.copyOf(partitions, partitions.length << 1); } + partitions[id] = SerializationUtils.deserializeBinaryRow(canonical); + ids.put(new ByteArrayKey(canonical), id); + partitionCount = id + 1; + return id; + } + + public void finish() { + checkState(comparator != null, "Partition dictionary has no comparator."); + List order = new ArrayList<>(partitionCount); + for (int id = 0; id < partitionCount; id++) { + order.add(id); + } + order.sort((left, right) -> compareIds(left, right)); + ranks = new int[partitionCount]; + int rank = 0; + for (int position = 0; position < order.size(); position++) { + if (position > 0 && compareIds(order.get(position - 1), order.get(position)) != 0) { + rank++; + } + ranks[order.get(position)] = rank; + } + } + + public int compareIds(int left, int right) { + checkState(comparator != null, "Partition dictionary has no comparator."); + return comparator.compare(partitions[left], partitions[right]); + } + + public int rank(int id) { + return ranks == null ? 0 : ranks[id]; } public BinaryRow partition(int id) { diff --git a/paimon-core/src/main/java/org/apache/paimon/manifest/ProjectedManifestEntry.java b/paimon-core/src/main/java/org/apache/paimon/manifest/ProjectedManifestEntry.java index 591d86759e6e..1d99c8bd918d 100644 --- a/paimon-core/src/main/java/org/apache/paimon/manifest/ProjectedManifestEntry.java +++ b/paimon-core/src/main/java/org/apache/paimon/manifest/ProjectedManifestEntry.java @@ -151,6 +151,7 @@ private static Projection createEntryLayoutProjection() { DataFileMeta.LEVEL, DataFileMeta.SCHEMA_ID, DataFileMeta.FIRST_ROW_ID, + DataFileMeta.MAX_SEQUENCE_NUMBER, DataFileMeta.EXTRA_FILES, DataFileMeta.EMBEDDED_FILE_INDEX, DataFileMeta.EXTERNAL_PATH))))); diff --git a/paimon-core/src/main/java/org/apache/paimon/operation/ManifestEntryRunMerge.java b/paimon-core/src/main/java/org/apache/paimon/operation/ManifestEntryRunMerge.java index 1b9d4f8db6a1..cd03e0ef9c9d 100644 --- a/paimon-core/src/main/java/org/apache/paimon/operation/ManifestEntryRunMerge.java +++ b/paimon-core/src/main/java/org/apache/paimon/operation/ManifestEntryRunMerge.java @@ -20,10 +20,7 @@ import org.apache.paimon.data.BinaryArray; import org.apache.paimon.data.BinaryRow; -import org.apache.paimon.data.GenericRow; -import org.apache.paimon.data.InternalRow; import org.apache.paimon.format.SimpleStatsCollector; -import org.apache.paimon.io.DataFileMeta; import org.apache.paimon.manifest.CompactFileIdentifierSet; import org.apache.paimon.manifest.DeletedRowIdSet; import org.apache.paimon.manifest.FileKind; @@ -31,12 +28,13 @@ import org.apache.paimon.manifest.ManifestAvroReader.RawBlock; import org.apache.paimon.manifest.ManifestAvroReader.RowIterator; import org.apache.paimon.manifest.ManifestAvroWriter.EncodedBlockMeta; -import org.apache.paimon.manifest.ManifestEntry; import org.apache.paimon.manifest.ManifestFile; import org.apache.paimon.manifest.ManifestFileMeta; +import org.apache.paimon.manifest.PartitionDictionary; +import org.apache.paimon.manifest.ProjectedManifestEntry; +import org.apache.paimon.memory.MemorySegmentUtils; import org.apache.paimon.stats.SimpleStats; import org.apache.paimon.stats.SimpleStatsConverter; -import org.apache.paimon.types.DataField; import org.apache.paimon.types.RowType; import org.apache.paimon.utils.Pair; @@ -57,91 +55,9 @@ final class ManifestEntryRunMerge { private static final long MAX_IN_MEMORY_FRAGMENTED_ENTRIES = 25_000L; private static final int MAX_STREAM_CURSORS = 128; private static final int MAX_STREAM_READ_AMPLIFICATION = 8; - static final int KIND = 0; - static final int PARTITION = 1; - static final int BUCKET = 2; - static final int FILE = 3; - static final int FILE_NAME = 0; - static final int ROW_COUNT = 1; - static final int LEVEL = 2; - static final int SCHEMA_ID = 3; - static final int FIRST_ROW_ID = 4; - static final int MAX_SEQUENCE_NUMBER = 5; - static final int EXTRA_FILES = 6; - static final int EMBEDDED_FILE_INDEX = 7; - static final int EXTERNAL_PATH = 8; - static final int FILE_FIELD_COUNT = 9; - private static final String[] ENTRY_FILE_FIELD_NAMES = { - DataFileMeta.FILE_NAME, - DataFileMeta.ROW_COUNT, - DataFileMeta.LEVEL, - DataFileMeta.SCHEMA_ID, - DataFileMeta.FIRST_ROW_ID, - DataFileMeta.MAX_SEQUENCE_NUMBER, - DataFileMeta.EXTRA_FILES, - DataFileMeta.EMBEDDED_FILE_INDEX, - DataFileMeta.EXTERNAL_PATH - }; - private static final InternalRow.FieldGetter[] ENTRY_FILE_GETTERS = entryFileGetters(); - static final RowType ENTRY_LAYOUT = entryLayout(); - private static final int FULL_KIND = - ManifestEntry.MANIFEST_ROW_TYPE.getFieldIndex(ManifestEntry.KIND); - private static final int FULL_PARTITION = - ManifestEntry.MANIFEST_ROW_TYPE.getFieldIndex(ManifestEntry.PARTITION); - private static final int FULL_BUCKET = - ManifestEntry.MANIFEST_ROW_TYPE.getFieldIndex(ManifestEntry.BUCKET); - private static final int FULL_FILE = - ManifestEntry.MANIFEST_ROW_TYPE.getFieldIndex(ManifestEntry.FILE); private ManifestEntryRunMerge() {} - private static InternalRow.FieldGetter[] entryFileGetters() { - InternalRow.FieldGetter[] getters = - new InternalRow.FieldGetter[ENTRY_FILE_FIELD_NAMES.length]; - for (int field = 0; field < getters.length; field++) { - int position = DataFileMeta.SCHEMA.getFieldIndex(ENTRY_FILE_FIELD_NAMES[field]); - getters[field] = - InternalRow.createFieldGetter( - DataFileMeta.SCHEMA.getTypeAt(position), position); - } - return getters; - } - - private static RowType entryLayout() { - List fields = new ArrayList<>(); - fields.add(ManifestEntry.MANIFEST_ROW_TYPE.getField(ManifestEntry.KIND)); - fields.add(ManifestEntry.MANIFEST_ROW_TYPE.getField(ManifestEntry.PARTITION)); - fields.add(ManifestEntry.MANIFEST_ROW_TYPE.getField(ManifestEntry.BUCKET)); - fields.add( - ManifestEntry.MANIFEST_ROW_TYPE - .getField(ManifestEntry.FILE) - .newType( - DataFileMeta.SCHEMA.project( - DataFileMeta.FILE_NAME, - DataFileMeta.ROW_COUNT, - DataFileMeta.LEVEL, - DataFileMeta.SCHEMA_ID, - DataFileMeta.FIRST_ROW_ID, - DataFileMeta.MAX_SEQUENCE_NUMBER, - DataFileMeta.EXTRA_FILES, - DataFileMeta.EMBEDDED_FILE_INDEX, - DataFileMeta.EXTERNAL_PATH))); - return new RowType(false, fields); - } - - static GenericRow projectEntryLayout( - GenericRow fullRow, GenericRow reuse, GenericRow reuseFile) { - reuse.setField(KIND, fullRow.getByte(FULL_KIND)); - reuse.setField(PARTITION, fullRow.getBinary(FULL_PARTITION)); - reuse.setField(BUCKET, fullRow.getInt(FULL_BUCKET)); - InternalRow fullFile = fullRow.getRow(FULL_FILE, DataFileMeta.SCHEMA.getFieldCount()); - for (int field = 0; field < ENTRY_FILE_GETTERS.length; field++) { - reuseFile.setField(field, ENTRY_FILE_GETTERS[field].getFieldOrNull(fullFile)); - } - reuse.setField(FILE, reuseFile); - return reuse; - } - /** * Returns null when the input is too fragmented for a bounded streaming merge. The caller must * fall back to the spillable external sorter in that case. @@ -234,8 +150,7 @@ private static ManifestEntryRunMergePlan discoverRuns( int maxNumFileHandles, @Nullable Integer manifestReadParallelism) throws Exception { - ManifestEntryRunMergeEntry.PartitionDictionary partitions = - new ManifestEntryRunMergeEntry.PartitionDictionary(sortKey); + PartitionDictionary partitions = new PartitionDictionary(sortKey::comparePartitions); List sources = new ArrayList<>(); int streamCursorCount = 0; long inMemoryEntries = 0; @@ -243,36 +158,56 @@ private static ManifestEntryRunMergePlan discoverRuns( if (section.size() <= 1 || (manifestReadParallelism != null && manifestReadParallelism <= 1)) { for (ManifestFileMeta meta : section) { + ManifestEntryRunMergeEntry.Filter discoveryFilter = filter.forDiscovery(); Discovery.DiscoveredManifest manifest = - discoverManifestRuns(meta, manifestFile, partitionType, partitions, filter); + discoverManifestRuns( + meta, manifestFile, partitionType, partitions, discoveryFilter); if (manifest.requiresExternalSort) { return null; } + filter.combine(discoveryFilter); discovered.add(manifest); } } else { - Function> reader = - meta -> { - try { - return Collections.singletonList( - discoverManifestRuns( - meta, manifestFile, partitionType, partitions, filter)); - } catch (Exception e) { - throw new RuntimeException( - "Failed to discover sorted Avro runs in " + meta.fileName(), e); - } - }; - for (Discovery.DiscoveredManifest manifest : + Function< + ManifestFileMeta, + List< + Pair< + Discovery.DiscoveredManifest, + ManifestEntryRunMergeEntry.Filter>>> + reader = + meta -> { + try { + ManifestEntryRunMergeEntry.Filter discoveryFilter = + filter.forDiscovery(); + return Collections.singletonList( + Pair.of( + discoverManifestRuns( + meta, + manifestFile, + partitionType, + partitions, + discoveryFilter), + discoveryFilter)); + } catch (Exception e) { + throw new RuntimeException( + "Failed to discover sorted Avro runs in " + + meta.fileName(), + e); + } + }; + for (Pair scan : sequentialBatchedExecute(reader, section, manifestReadParallelism)) { - discovered.add(manifest); + if (scan.getLeft().requiresExternalSort) { + return null; + } + filter.combine(scan.getRight()); + discovered.add(scan.getLeft()); } } for (int manifestIndex = 0; manifestIndex < section.size(); manifestIndex++) { ManifestFileMeta meta = section.get(manifestIndex); Discovery.DiscoveredManifest manifest = discovered.get(manifestIndex); - if (manifest.requiresExternalSort) { - return null; - } if (manifest.fragmented) { long entryCount = meta.numAddedFiles() + meta.numDeletedFiles(); inMemoryEntries += entryCount; @@ -301,7 +236,7 @@ private static Discovery.DiscoveredManifest discoverManifestRuns( ManifestFileMeta meta, ManifestFile manifestFile, RowType partitionType, - ManifestEntryRunMergeEntry.PartitionDictionary partitions, + PartitionDictionary partitions, ManifestEntryRunMergeEntry.Filter filter) throws Exception { try (ManifestAvroReader reader = @@ -309,6 +244,8 @@ private static Discovery.DiscoveredManifest discoverManifestRuns( return discoverManifestRuns(meta, reader, partitionType, partitions, filter); } catch (UnsupportedOperationException unsupported) { return Discovery.DiscoveredManifest.requiresExternalSort(); + } finally { + filter.releaseIdentifier(); } } @@ -316,7 +253,7 @@ private static Discovery.DiscoveredManifest discoverManifestRuns( ManifestFileMeta meta, ManifestAvroReader reader, RowType partitionType, - ManifestEntryRunMergeEntry.PartitionDictionary partitions, + PartitionDictionary partitions, ManifestEntryRunMergeEntry.Filter filter) throws Exception { SimpleStatsConverter partitionStatsConverter = new SimpleStatsConverter(partitionType); @@ -329,13 +266,15 @@ private static Discovery.DiscoveredManifest discoverManifestRuns( long position = 0; long entryCount = meta.numAddedFiles() + meta.numDeletedFiles(); boolean fragmented = false; + ProjectedManifestEntry entry = ProjectedManifestEntry.ENTRY_LAYOUT_PROJECTION.createEntry(); while (reader.hasNext()) { RawBlock rawBlock = reader.next(); - RowIterator rows = rawBlock.toRows(ENTRY_LAYOUT); + RowIterator rows = + rawBlock.toRows(ProjectedManifestEntry.ENTRY_LAYOUT_PROJECTION.projectedType()); while (rows.hasNext()) { - GenericRow row = rows.next(); - current.replace(row, partitions); - filter.observe(row, current); + entry.replace(rows.next()); + current.replace(entry, partitions); + filter.observe(entry, current); if (fragmented) { position++; continue; @@ -350,7 +289,7 @@ private static Discovery.DiscoveredManifest discoverManifestRuns( partitionType)); } Discovery.BlockInfo block = blocks.get(blocks.size() - 1); - block.collectForSort(row, current, partitions, filter); + block.collectForSort(entry, current, partitions, filter); boolean inversion = hasPrevious && compareDiscoveryKeys(previous, current, partitions) > 0; if (inversion) { @@ -415,7 +354,7 @@ private static boolean exceedsStreamingReadAmplification( private static int compareDiscoveryKeys( ManifestEntryRunMergeEntry.Key left, ManifestEntryRunMergeEntry.Key right, - ManifestEntryRunMergeEntry.PartitionDictionary partitions) { + PartitionDictionary partitions) { return compareRemainingKeys( left, right, partitions.compareIds(left.partitionId, right.partitionId)); } @@ -452,8 +391,12 @@ private static int compareBytes( ManifestEntryRunMergeEntry.Key left, ManifestEntryRunMergeEntry.Key right) { int minLength = Math.min(left.fileNameLength, right.fileNameLength); for (int i = 0; i < minLength; i++) { - int leftByte = left.fileNameBytes[left.fileNameOffset + i] & 0xFF; - int rightByte = right.fileNameBytes[right.fileNameOffset + i] & 0xFF; + int leftByte = + MemorySegmentUtils.getByte(left.fileNameSegments, left.fileNameOffset + i) + & 0xFF; + int rightByte = + MemorySegmentUtils.getByte(right.fileNameSegments, right.fileNameOffset + i) + & 0xFF; if (leftByte != rightByte) { return leftByte - rightByte; } @@ -500,7 +443,7 @@ static DiscoveredManifest requiresExternalSort() { Collections.emptyList(), Collections.emptyList(), false, true); } - void updatePartitionRanks(ManifestEntryRunMergeEntry.PartitionDictionary partitions) { + void updatePartitionRanks(PartitionDictionary partitions) { for (BlockInfo block : blocks) { block.updatePartitionRanks(partitions); } @@ -560,19 +503,19 @@ static final class BlockInfo { } void collectForSort( - GenericRow record, + ProjectedManifestEntry entry, ManifestEntryRunMergeEntry.Key key, - ManifestEntryRunMergeEntry.PartitionDictionary partitions, + PartitionDictionary partitions, ManifestEntryRunMergeEntry.Filter filter) { if (!eligible) { return; } - if (!filter.copyable(record, key)) { + if (!filter.copyable(entry, key)) { eligible = false; releasePartitionStats(); return; } - collectEntryStats(record, key); + collectEntryStats(entry, key); BinaryRow partition = partitions.partition(key.partitionId); if (singleFieldSortedPartitionStats) { if (partition.isNullAt(0)) { @@ -589,18 +532,18 @@ void collectForSort( } } - private void collectEntryStats(GenericRow record, ManifestEntryRunMergeEntry.Key key) { - InternalRow file = ManifestEntryRunMergeEntry.file(record); + private void collectEntryStats( + ProjectedManifestEntry entry, ManifestEntryRunMergeEntry.Key key) { if (key.kind == FileKind.ADD.toByteValue()) { addedFiles++; } else { deletedFiles++; } - schemaId = Math.max(schemaId, file.getLong(SCHEMA_ID)); - int bucket = record.getInt(BUCKET); + schemaId = Math.max(schemaId, entry.file().schemaId()); + int bucket = entry.bucket(); minBucket = Math.min(minBucket, bucket); maxBucket = Math.max(maxBucket, bucket); - int level = file.getInt(LEVEL); + int level = entry.file().level(); minLevel = Math.min(minLevel, level); maxLevel = Math.max(maxLevel, level); minRowId = Math.min(minRowId, key.firstRowId); @@ -665,7 +608,7 @@ void finishFiltering(ManifestEntryRunMergeEntry.Filter filter) { } } - void updatePartitionRanks(ManifestEntryRunMergeEntry.PartitionDictionary partitions) { + void updatePartitionRanks(PartitionDictionary partitions) { checkState(firstKey != null && lastKey != null, "Manifest block has no sort keys."); firstKey.partitionRank = partitions.rank(firstKey.partitionId); lastKey.partitionRank = partitions.rank(lastKey.partitionId); diff --git a/paimon-core/src/main/java/org/apache/paimon/operation/ManifestEntryRunMergeEntry.java b/paimon-core/src/main/java/org/apache/paimon/operation/ManifestEntryRunMergeEntry.java index 5e8c318e9fda..04e74f655ca3 100644 --- a/paimon-core/src/main/java/org/apache/paimon/operation/ManifestEntryRunMergeEntry.java +++ b/paimon-core/src/main/java/org/apache/paimon/operation/ManifestEntryRunMergeEntry.java @@ -18,25 +18,15 @@ package org.apache.paimon.operation; -import org.apache.paimon.data.BinaryRow; import org.apache.paimon.data.BinaryString; -import org.apache.paimon.data.GenericRow; -import org.apache.paimon.data.InternalRow; import org.apache.paimon.manifest.CompactFileIdentifierSet; import org.apache.paimon.manifest.DeletedRowIdSet; import org.apache.paimon.manifest.FileEntry.ReusableIdentifier; import org.apache.paimon.manifest.FileKind; +import org.apache.paimon.manifest.PartitionDictionary; import org.apache.paimon.manifest.ProjectedManifestEntry; +import org.apache.paimon.memory.MemorySegment; import org.apache.paimon.memory.MemorySegmentUtils; -import org.apache.paimon.utils.ByteArrayKey; -import org.apache.paimon.utils.ByteArrayLookupKey; -import org.apache.paimon.utils.SerializationUtils; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; import static org.apache.paimon.utils.Preconditions.checkState; @@ -50,13 +40,14 @@ static final class Key { int partitionId; int partitionRank; byte kind; - boolean hasRowId; long firstRowId; long rangeEnd; long reverseSequence; - byte[] fileNameBytes; + MemorySegment[] fileNameSegments; int fileNameOffset; int fileNameLength; + byte[] ownedFileNameBytes; + MemorySegment[] ownedFileNameSegments; static Key viewOf(ProjectedManifestEntry entry, PartitionDictionary partitions) { Key key = new Key(); @@ -69,155 +60,52 @@ void replace(ProjectedManifestEntry entry, PartitionDictionary partitions) { this.partitionId = partitions.id(entry.partitionBytes()); this.partitionRank = partitions.rank(partitionId); this.kind = entry.kind().toByteValue(); - this.hasRowId = true; this.firstRowId = firstRowId; this.rangeEnd = firstRowId + entry.file().rowCount() - 1L; this.reverseSequence = Long.MAX_VALUE - entry.file().maxSequenceNumber(); - this.fileNameBytes = entry.file().fileNameBinary().toBytes(); - this.fileNameOffset = 0; - this.fileNameLength = fileNameBytes.length; - } - - void replace(GenericRow record, PartitionDictionary partitions) { - InternalRow file = file(record); - checkState( - !file.isNullAt(ManifestEntryRunMerge.FIRST_ROW_ID), - "First row id should not be null."); - this.partitionId = partitions.id(record.getBinary(ManifestEntryRunMerge.PARTITION)); - this.partitionRank = partitions.rank(partitionId); - this.kind = record.getByte(ManifestEntryRunMerge.KIND); - this.hasRowId = true; - this.firstRowId = file.getLong(ManifestEntryRunMerge.FIRST_ROW_ID); - this.rangeEnd = firstRowId + file.getLong(ManifestEntryRunMerge.ROW_COUNT) - 1L; - this.reverseSequence = - Long.MAX_VALUE - file.getLong(ManifestEntryRunMerge.MAX_SEQUENCE_NUMBER); - BinaryString fileName = file.getString(ManifestEntryRunMerge.FILE_NAME); - this.fileNameBytes = - MemorySegmentUtils.copyToBytes( - fileName.getSegments(), - fileName.getOffset(), - fileName.getSizeInBytes()); - this.fileNameOffset = 0; - this.fileNameLength = fileNameBytes.length; - } - - void replaceForCompaction(GenericRow record) { - InternalRow file = file(record); - this.kind = record.getByte(ManifestEntryRunMerge.KIND); - this.hasRowId = !file.isNullAt(ManifestEntryRunMerge.FIRST_ROW_ID); - if (hasRowId) { - this.firstRowId = file.getLong(ManifestEntryRunMerge.FIRST_ROW_ID); - this.rangeEnd = firstRowId + file.getLong(ManifestEntryRunMerge.ROW_COUNT) - 1L; - } + BinaryString fileName = entry.file().fileNameBinary(); + this.fileNameSegments = fileName.getSegments(); + this.fileNameOffset = fileName.getOffset(); + this.fileNameLength = fileName.getSizeInBytes(); } void copyFrom(Key key) { this.partitionId = key.partitionId; this.partitionRank = key.partitionRank; this.kind = key.kind; - this.hasRowId = key.hasRowId; this.firstRowId = key.firstRowId; this.rangeEnd = key.rangeEnd; this.reverseSequence = key.reverseSequence; - this.fileNameBytes = key.fileNameBytes; - this.fileNameOffset = key.fileNameOffset; + ensureFileNameCapacity(key.fileNameLength); + MemorySegmentUtils.copyToBytes( + key.fileNameSegments, + key.fileNameOffset, + ownedFileNameBytes, + 0, + key.fileNameLength); + this.fileNameSegments = ownedFileNameSegments; + this.fileNameOffset = 0; this.fileNameLength = key.fileNameLength; } Key stableCopy() { Key copy = new Key(); copy.copyFrom(this); - copy.fileNameBytes = - Arrays.copyOfRange( - fileNameBytes, fileNameOffset, fileNameOffset + fileNameLength); - copy.fileNameOffset = 0; return copy; } - void clear() { - fileNameBytes = null; - } - } - - /** Interns variable-width partition bytes once and assigns comparator-compatible ranks. */ - static final class PartitionDictionary { - - final ManifestFileSorter.RowIdEntrySortKey sortKey; - final Map ids = new ConcurrentHashMap<>(); - final ThreadLocal lookup = - ThreadLocal.withInitial(ByteArrayLookupKey::new); - volatile BinaryRow[] partitions = new BinaryRow[16]; - int partitionCount; - int[] ranks; - - PartitionDictionary(ManifestFileSorter.RowIdEntrySortKey sortKey) { - this.sortKey = sortKey; - } - - PartitionDictionary() { - this.sortKey = null; - } - - int id(byte[] bytes) { - return id(bytes, 0, bytes.length); - } - - int id(byte[] bytes, int offset, int length) { - ByteArrayLookupKey lookupKey = lookup.get(); - lookupKey.reset(bytes, offset, length); - try { - Integer existing = ids.get(lookupKey); - if (existing != null) { - return existing; - } - synchronized (this) { - existing = ids.get(lookupKey); - if (existing != null) { - return existing; - } - checkState(ranks == null, "Full manifest scan found an unknown partition."); - byte[] canonical = Arrays.copyOfRange(bytes, offset, offset + length); - int id = partitionCount; - if (id == partitions.length) { - partitions = Arrays.copyOf(partitions, partitions.length << 1); - } - partitions[id] = SerializationUtils.deserializeBinaryRow(canonical); - ids.put(new ByteArrayKey(canonical), id); - partitionCount = id + 1; - return id; - } - } finally { - lookupKey.clear(); + private void ensureFileNameCapacity(int length) { + if (ownedFileNameBytes == null || ownedFileNameBytes.length < length) { + ownedFileNameBytes = new byte[length]; + ownedFileNameSegments = + new MemorySegment[] {MemorySegment.wrap(ownedFileNameBytes)}; } } - int compareIds(int left, int right) { - checkState(sortKey != null, "Partition dictionary has no sort key."); - return sortKey.comparePartitions(partitions[left], partitions[right]); - } - - void finish() { - List order = new ArrayList<>(partitionCount); - for (int id = 0; id < partitionCount; id++) { - order.add(id); - } - order.sort((left, right) -> compareIds(left, right)); - ranks = new int[partitionCount]; - int rank = 0; - for (int position = 0; position < order.size(); position++) { - if (position > 0 && compareIds(order.get(position - 1), order.get(position)) != 0) { - rank++; - } - ranks[order.get(position)] = rank; - } - } - - int rank(int id) { - return ranks == null ? 0 : ranks[id]; - } - - BinaryRow partition(int id) { - return partitions[id]; + void clear() { + fileNameSegments = null; + ownedFileNameBytes = null; + ownedFileNameSegments = null; } } @@ -226,8 +114,8 @@ static class Filter { final CompactFileIdentifierSet deletedIdentifiers; final DeletedRowIdSet deletedRowIds; final boolean useRowIdFilter; - final ThreadLocal identifier = - ThreadLocal.withInitial(IdentifierEncoder::new); + final ThreadLocal identifier = + ThreadLocal.withInitial(ReusableIdentifier::new); Filter( CompactFileIdentifierSet deletedIdentifiers, @@ -239,37 +127,50 @@ static class Filter { } boolean include(ProjectedManifestEntry entry) { - return entry.isAdd() && !deletedIdentifiers.contains(entry); + return entry.isAdd() && !deletedIdentifiers.contains(identifier(entry)); } - boolean include(GenericRow record, Key key) { - return key.kind == FileKind.ADD.toByteValue() && !isDeleted(record, key); + boolean include(ProjectedManifestEntry entry, Key key) { + return key.kind == FileKind.ADD.toByteValue() && !isDeleted(entry, key); } - boolean copyable(GenericRow record, Key key) { - return include(record, key); + boolean copyable(ProjectedManifestEntry entry, Key key) { + return include(entry, key); } - void observe(GenericRow record, Key key) {} + void observe(ProjectedManifestEntry entry, Key key) {} boolean copyableAfterDiscovery(long minRowId, long maxRowId) { return true; } - ReusableIdentifier identifier(GenericRow record) { - return identifier.get().replace(record); + Filter forDiscovery() { + return this; + } + + void combine(Filter other) { + checkState(other == this, "Immutable manifest filter cannot collect local DELETEs."); + } + + ReusableIdentifier identifier(ProjectedManifestEntry entry) { + return identifier.get().replaceWithPartition(entry); + } + + void releaseIdentifier() { + ReusableIdentifier reusableIdentifier = identifier.get(); + reusableIdentifier.release(); + identifier.remove(); } - boolean isDeleted(GenericRow record, Key key) { + boolean isDeleted(ProjectedManifestEntry entry, Key key) { // RowID is only a cheap negative filter. The complete identifier remains the // authoritative match, and is also sufficient for manifests which predate RowID. if (useRowIdFilter) { - checkState(key.hasRowId, "First row id should not be null."); if (!deletedRowIds.contains(key.firstRowId)) { return false; } } - return deletedIdentifiers.contains(identifier(record)); + return deletedIdentifiers.contains(identifier(entry)); } static final class Minor extends Filter { @@ -287,30 +188,42 @@ boolean include(ProjectedManifestEntry entry) { } @Override - boolean include(GenericRow record, Key key) { + boolean include(ProjectedManifestEntry entry, Key key) { return true; } @Override - boolean copyable(GenericRow record, Key key) { + boolean copyable(ProjectedManifestEntry entry, Key key) { return key.kind == FileKind.ADD.toByteValue(); } @Override - void observe(GenericRow record, Key key) { + void observe(ProjectedManifestEntry entry, Key key) { if (key.kind != FileKind.DELETE.toByteValue()) { return; } - ReusableIdentifier reusable = identifier(record); - synchronized (this) { - deletedIdentifiers.add(reusable); - if (useRowIdFilter) { - checkState(key.hasRowId, "First row id should not be null."); - deletedRowIds.add(key.firstRowId); - } + ReusableIdentifier reusable = identifier(entry); + deletedIdentifiers.add(reusable); + if (useRowIdFilter) { + deletedRowIds.add(key.firstRowId); } } + @Override + Filter forDiscovery() { + return new Minor( + new CompactFileIdentifierSet(), new DeletedRowIdSet(), useRowIdFilter); + } + + @Override + void combine(Filter other) { + checkState(other instanceof Minor, "Cannot combine incompatible manifest filters."); + deletedIdentifiers.addAll(other.deletedIdentifiers); + deletedRowIds.addAll(other.deletedRowIds); + other.deletedIdentifiers.release(); + other.deletedRowIds.releaseRangeIndex(); + } + @Override boolean copyableAfterDiscovery(long minRowId, long maxRowId) { // A DELETE preserves the deleted ADD's globally unique first RowID. A range hit may @@ -319,21 +232,5 @@ boolean copyableAfterDiscovery(long minRowId, long maxRowId) { return useRowIdFilter && !deletedRowIds.intersects(minRowId, maxRowId); } } - - private static final class IdentifierEncoder { - - final ProjectedManifestEntry entry = - ProjectedManifestEntry.Projection.create(ManifestEntryRunMerge.ENTRY_LAYOUT) - .createEntry(); - final ReusableIdentifier identifier = new ReusableIdentifier(); - - ReusableIdentifier replace(GenericRow record) { - return identifier.replaceWithPartition(entry.replace(record)); - } - } - } - - static InternalRow file(GenericRow record) { - return record.getRow(ManifestEntryRunMerge.FILE, ManifestEntryRunMerge.FILE_FIELD_COUNT); } } diff --git a/paimon-core/src/main/java/org/apache/paimon/operation/ManifestEntryRunMergePlan.java b/paimon-core/src/main/java/org/apache/paimon/operation/ManifestEntryRunMergePlan.java index eb7702b8aa5f..561da387df84 100644 --- a/paimon-core/src/main/java/org/apache/paimon/operation/ManifestEntryRunMergePlan.java +++ b/paimon-core/src/main/java/org/apache/paimon/operation/ManifestEntryRunMergePlan.java @@ -36,6 +36,7 @@ import org.apache.paimon.manifest.ManifestEntry; import org.apache.paimon.manifest.ManifestFile; import org.apache.paimon.manifest.ManifestFileMeta; +import org.apache.paimon.manifest.PartitionDictionary; import org.apache.paimon.manifest.ProjectedManifestEntry; import org.apache.paimon.utils.CloseableIterator; import org.apache.paimon.utils.Pair; @@ -53,10 +54,9 @@ final class ManifestEntryRunMergePlan { final List sources; - final ManifestEntryRunMergeEntry.PartitionDictionary partitions; + final PartitionDictionary partitions; - ManifestEntryRunMergePlan( - List sources, ManifestEntryRunMergeEntry.PartitionDictionary partitions) { + ManifestEntryRunMergePlan(List sources, PartitionDictionary partitions) { this.sources = sources; this.partitions = partitions; } @@ -271,7 +271,7 @@ Cursor open( ManifestFile manifestFile, ManifestFileSorter.RowIdEntrySortKey sortKey, ManifestEntryRunMergeEntry.Filter filter, - ManifestEntryRunMergeEntry.PartitionDictionary partitions) + PartitionDictionary partitions) throws Exception; } @@ -312,7 +312,7 @@ public Cursor open( ManifestFile manifestFile, ManifestFileSorter.RowIdEntrySortKey sortKey, ManifestEntryRunMergeEntry.Filter filter, - ManifestEntryRunMergeEntry.PartitionDictionary partitions) + PartitionDictionary partitions) throws Exception { return new PrimitiveManifestRunCursor( manifestFile, meta, start, end, blocks, filter, partitions); @@ -332,7 +332,7 @@ public Cursor open( ManifestFile manifestFile, ManifestFileSorter.RowIdEntrySortKey sortKey, ManifestEntryRunMergeEntry.Filter filter, - ManifestEntryRunMergeEntry.PartitionDictionary partitions) + PartitionDictionary partitions) throws Exception { return new InMemoryManifestCursor(manifestFile, meta, sortKey, filter, partitions); } @@ -393,9 +393,13 @@ static final class PrimitiveManifestRunCursor implements Cursor { final ManifestAvroReader reader; final boolean encodedRecordsCompatible; final ManifestEntryRunMergeEntry.Filter filter; - final ManifestEntryRunMergeEntry.PartitionDictionary partitions; + final PartitionDictionary partitions; final ManifestEntryRunMergeEntry.Key key = new ManifestEntryRunMergeEntry.Key(); final EncodedEntry metadata = new EncodedEntry(); + final ProjectedManifestEntry projectedEntry = + ProjectedManifestEntry.ENTRY_LAYOUT_PROJECTION.createEntry(); + final ProjectedManifestEntry fullEntry = + ProjectedManifestEntry.fullProjection().createEntry(); final List blocks; final long runStart; final long runEnd; @@ -406,10 +410,8 @@ static final class PrimitiveManifestRunCursor implements Cursor { boolean current; @Nullable RawBlock currentRawBlock; @Nullable RowIterator currentRows; - @Nullable GenericRow currentRow; @Nullable GenericRow currentSourceRow; - @Nullable GenericRow compactRow; - @Nullable GenericRow compactFile; + @Nullable ProjectedManifestEntry currentEntry; @Nullable ManifestEntryRunMerge.Discovery.BlockInfo currentBlock; boolean closed; @@ -420,15 +422,10 @@ static final class PrimitiveManifestRunCursor implements Cursor { long end, List blocks, ManifestEntryRunMergeEntry.Filter filter, - ManifestEntryRunMergeEntry.PartitionDictionary partitions) + PartitionDictionary partitions) throws Exception { this.reader = manifestFile.scanAvroBlocks(meta.fileName(), meta.fileSize()); this.encodedRecordsCompatible = reader.rawBlockCopySupported(); - if (!encodedRecordsCompatible) { - this.compactRow = - new GenericRow(ManifestEntryRunMerge.ENTRY_LAYOUT.getFieldCount()); - this.compactFile = new GenericRow(ManifestEntryRunMerge.FILE_FIELD_COUNT); - } this.filter = filter; this.partitions = partitions; this.blocks = blocks; @@ -469,24 +466,22 @@ public boolean advance() throws Exception { currentRows != null && currentRows.hasNext(), "Manifest block ends before its discovered boundary."); currentSourceRow = currentRows.next(); - currentRow = + currentEntry = encodedRecordsCompatible - ? currentSourceRow - : ManifestEntryRunMerge.projectEntryLayout( - currentSourceRow, compactRow, compactFile); + ? projectedEntry.replace(currentSourceRow) + : fullEntry.replace(currentSourceRow); decodedRemaining--; - key.replace(currentRow, partitions); - if (filter.include(currentRow, key)) { + key.replace(currentEntry, partitions); + if (filter.include(currentEntry, key)) { current = true; - InternalRow file = ManifestEntryRunMergeEntry.file(currentRow); metadata.replace( key.kind, partitions.partition(key.partitionId), - currentRow.getInt(ManifestEntryRunMerge.BUCKET), - file.getInt(ManifestEntryRunMerge.LEVEL), - file.getLong(ManifestEntryRunMerge.SCHEMA_ID), + currentEntry.bucket(), + currentEntry.file().level(), + currentEntry.file().schemaId(), key.firstRowId, - file.getLong(ManifestEntryRunMerge.ROW_COUNT)); + currentEntry.file().rowCount()); return true; } } @@ -496,8 +491,8 @@ boolean prepareNextBlock() throws Exception { rawBlock = false; current = false; currentRows = null; - currentRow = null; currentSourceRow = null; + currentEntry = null; while (blockIndex < blocks.size()) { ManifestEntryRunMerge.Discovery.BlockInfo info = blocks.get(blockIndex); if (info.start >= runEnd) { @@ -524,7 +519,8 @@ boolean prepareNextBlock() throws Exception { currentRows = currentRawBlock.toRows( encodedRecordsCompatible - ? ManifestEntryRunMerge.ENTRY_LAYOUT + ? ProjectedManifestEntry.ENTRY_LAYOUT_PROJECTION + .projectedType() : ManifestEntry.MANIFEST_ROW_TYPE); for (long i = 0; i < prefix; i++) { checkState( @@ -548,7 +544,7 @@ public boolean hasCurrent() { @Override public ProjectedManifestEntry current() { - return null; + return current ? currentEntry : null; } @Override @@ -574,7 +570,7 @@ public InternalRow decodedRow() { @Override public ReusableIdentifier identifier() { checkState(current, "Manifest entry has not been materialized."); - return filter.identifier(currentRow); + return filter.identifier(currentEntry); } @Override @@ -617,30 +613,28 @@ public void materializeCurrent() throws Exception { currentRows = currentRawBlock.toRows( encodedRecordsCompatible - ? ManifestEntryRunMerge.ENTRY_LAYOUT + ? ProjectedManifestEntry.ENTRY_LAYOUT_PROJECTION.projectedType() : ManifestEntry.MANIFEST_ROW_TYPE); checkState(currentRows.hasNext(), "Manifest block cannot be decompressed."); currentSourceRow = currentRows.next(); - currentRow = + currentEntry = encodedRecordsCompatible - ? currentSourceRow - : ManifestEntryRunMerge.projectEntryLayout( - currentSourceRow, compactRow, compactFile); + ? projectedEntry.replace(currentSourceRow) + : fullEntry.replace(currentSourceRow); decodedRemaining--; - key.replace(currentRow, partitions); + key.replace(currentEntry, partitions); checkState( - filter.include(currentRow, key), + filter.include(currentEntry, key), "Copyable manifest block contains a filtered entry."); current = true; - InternalRow file = ManifestEntryRunMergeEntry.file(currentRow); metadata.replace( key.kind, partitions.partition(key.partitionId), - currentRow.getInt(ManifestEntryRunMerge.BUCKET), - file.getInt(ManifestEntryRunMerge.LEVEL), - file.getLong(ManifestEntryRunMerge.SCHEMA_ID), + currentEntry.bucket(), + currentEntry.file().level(), + currentEntry.file().schemaId(), key.firstRowId, - file.getLong(ManifestEntryRunMerge.ROW_COUNT)); + currentEntry.file().rowCount()); blockIndex++; } @@ -653,10 +647,10 @@ public void close() throws Exception { current = false; currentRawBlock = null; currentRows = null; - currentRow = null; currentSourceRow = null; - compactRow = null; - compactFile = null; + currentEntry = null; + projectedEntry.clear(); + fullEntry.clear(); currentBlock = null; rawBlock = false; key.clear(); @@ -677,7 +671,7 @@ static final class InMemoryManifestCursor implements Cursor { ManifestFileMeta meta, ManifestFileSorter.RowIdEntrySortKey sortKey, ManifestEntryRunMergeEntry.Filter filter, - ManifestEntryRunMergeEntry.PartitionDictionary partitions) + PartitionDictionary partitions) throws Exception { long entryCount = meta.numAddedFiles() + meta.numDeletedFiles(); this.entries = new ArrayList<>((int) entryCount); diff --git a/paimon-core/src/test/java/org/apache/paimon/manifest/PartitionDictionaryTest.java b/paimon-core/src/test/java/org/apache/paimon/manifest/PartitionDictionaryTest.java new file mode 100644 index 000000000000..facbd29b0d0d --- /dev/null +++ b/paimon-core/src/test/java/org/apache/paimon/manifest/PartitionDictionaryTest.java @@ -0,0 +1,98 @@ +/* + * 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.paimon.manifest; + +import org.apache.paimon.data.BinaryRow; +import org.apache.paimon.data.BinaryRowWriter; + +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; + +import static org.apache.paimon.utils.SerializationUtils.serializeBinaryRow; +import static org.assertj.core.api.Assertions.assertThat; + +/** Tests for {@link PartitionDictionary}. */ +class PartitionDictionaryTest { + + @Test + void testComparatorEqualPartitionsKeepDistinctIds() { + PartitionDictionary dictionary = new PartitionDictionary((left, right) -> 0); + int first = dictionary.id(partitionBytes(1)); + int second = dictionary.id(partitionBytes(2)); + + dictionary.finish(); + + assertThat(first).isNotEqualTo(second); + assertThat(dictionary.partition(first).getInt(0)).isEqualTo(1); + assertThat(dictionary.partition(second).getInt(0)).isEqualTo(2); + assertThat(dictionary.rank(first)).isEqualTo(dictionary.rank(second)); + } + + @Test + void testConcurrentCollectionAndRanks() throws Exception { + PartitionDictionary dictionary = + new PartitionDictionary( + (left, right) -> Integer.compare(left.getInt(0), right.getInt(0))); + Map ids = new ConcurrentHashMap<>(); + ExecutorService executor = Executors.newFixedThreadPool(8); + List> futures = new ArrayList<>(); + try { + for (int thread = 0; thread < 8; thread++) { + futures.add( + executor.submit( + () -> { + for (int value = 31; value >= 0; value--) { + int id = dictionary.id(partitionBytes(value)); + Integer previous = ids.putIfAbsent(value, id); + if (previous != null) { + assertThat(id).isEqualTo(previous); + } + } + })); + } + for (Future future : futures) { + future.get(); + } + } finally { + executor.shutdownNow(); + } + + dictionary.finish(); + for (int value = 0; value < 32; value++) { + int id = dictionary.id(partitionBytes(value)); + assertThat(dictionary.partition(id).getInt(0)).isEqualTo(value); + assertThat(dictionary.rank(id)).isEqualTo(value); + } + } + + private static byte[] partitionBytes(int value) { + BinaryRow row = new BinaryRow(1); + BinaryRowWriter writer = new BinaryRowWriter(row); + writer.writeInt(0, value); + writer.complete(); + return serializeBinaryRow(row); + } +} diff --git a/paimon-core/src/test/java/org/apache/paimon/manifest/ProjectedManifestEntryTest.java b/paimon-core/src/test/java/org/apache/paimon/manifest/ProjectedManifestEntryTest.java index c78e7823671a..ede3a07402e1 100644 --- a/paimon-core/src/test/java/org/apache/paimon/manifest/ProjectedManifestEntryTest.java +++ b/paimon-core/src/test/java/org/apache/paimon/manifest/ProjectedManifestEntryTest.java @@ -266,6 +266,25 @@ void testDoesNotValidateFileKindOnReplace() { assertThat(entry.isDelete()).isFalse(); } + @Test + void testEntryLayoutProjectionContainsRunMergeFields() { + RowType projectedType = ProjectedManifestEntry.ENTRY_LAYOUT_PROJECTION.projectedType(); + RowType projectedFileType = + (RowType) projectedType.getTypeAt(projectedType.getFieldIndex(ManifestEntry.FILE)); + + assertThat(projectedFileType.getFieldNames()) + .containsExactly( + DataFileMeta.FILE_NAME, + DataFileMeta.ROW_COUNT, + DataFileMeta.LEVEL, + DataFileMeta.SCHEMA_ID, + DataFileMeta.FIRST_ROW_ID, + DataFileMeta.MAX_SEQUENCE_NUMBER, + DataFileMeta.EXTRA_FILES, + DataFileMeta.EMBEDDED_FILE_INDEX, + DataFileMeta.EXTERNAL_PATH); + } + private static ProjectedManifestEntry.Projection projection( boolean includeBucket, String... projectedFileFields) { RowType manifestType = ManifestEntry.MANIFEST_ROW_TYPE; From ee04cdc98343192d65956da12b00410cf63ccda2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BB=9F=E5=BC=8B?= Date: Sun, 16 Aug 2026 02:17:05 +0800 Subject: [PATCH 2/7] [core] Isolate run merge partition ordering --- .../paimon/manifest/PartitionDictionary.java | 101 +++-------------- .../operation/ManifestEntryRunMerge.java | 16 +-- .../operation/ManifestEntryRunMergeEntry.java | 7 +- ...ifestEntryRunMergePartitionDictionary.java | 106 ++++++++++++++++++ .../operation/ManifestEntryRunMergePlan.java | 18 +-- ...EntryRunMergePartitionDictionaryTest.java} | 13 ++- 6 files changed, 148 insertions(+), 113 deletions(-) create mode 100644 paimon-core/src/main/java/org/apache/paimon/operation/ManifestEntryRunMergePartitionDictionary.java rename paimon-core/src/test/java/org/apache/paimon/{manifest/PartitionDictionaryTest.java => operation/ManifestEntryRunMergePartitionDictionaryTest.java} (89%) diff --git a/paimon-core/src/main/java/org/apache/paimon/manifest/PartitionDictionary.java b/paimon-core/src/main/java/org/apache/paimon/manifest/PartitionDictionary.java index 08a6a733e3eb..b925457e68dd 100644 --- a/paimon-core/src/main/java/org/apache/paimon/manifest/PartitionDictionary.java +++ b/paimon-core/src/main/java/org/apache/paimon/manifest/PartitionDictionary.java @@ -23,110 +23,37 @@ import org.apache.paimon.utils.ByteArrayLookupKey; import org.apache.paimon.utils.SerializationUtils; -import javax.annotation.Nullable; - -import java.util.ArrayList; import java.util.Arrays; -import java.util.Comparator; import java.util.HashMap; -import java.util.List; import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; - -import static org.apache.paimon.utils.Preconditions.checkArgument; -import static org.apache.paimon.utils.Preconditions.checkState; /** Deduplicates serialized partitions and assigns compact integer identifiers. */ public final class PartitionDictionary { - private final Map ids; - private final @Nullable ByteArrayLookupKey lookup; - private final @Nullable ThreadLocal concurrentLookup; - private final @Nullable Comparator comparator; - private volatile BinaryRow[] partitions = new BinaryRow[16]; + private final Map ids = new HashMap<>(); + private final ByteArrayLookupKey lookup = new ByteArrayLookupKey(); + private BinaryRow[] partitions = new BinaryRow[16]; private int partitionCount; - private @Nullable int[] ranks; - - /** Creates the low-overhead dictionary used by single-threaded manifest rewriting. */ - public PartitionDictionary() { - this.ids = new HashMap<>(); - this.lookup = new ByteArrayLookupKey(); - this.concurrentLookup = null; - this.comparator = null; - } - - /** - * Creates a dictionary which supports concurrent collection and comparator-compatible ranks. - */ - public PartitionDictionary(Comparator comparator) { - checkArgument(comparator != null, "Partition comparator cannot be null."); - this.ids = new ConcurrentHashMap<>(); - this.lookup = null; - this.concurrentLookup = ThreadLocal.withInitial(ByteArrayLookupKey::new); - this.comparator = comparator; - } public int id(byte[] bytes) { - ByteArrayLookupKey lookupKey = concurrentLookup == null ? lookup : concurrentLookup.get(); - checkState(lookupKey != null, "Partition lookup key is unavailable."); - lookupKey.reset(bytes); + lookup.reset(bytes); try { - Integer existing = ids.get(lookupKey); + Integer existing = ids.get(lookup); if (existing != null) { return existing; } - if (concurrentLookup != null) { - synchronized (this) { - existing = ids.get(lookupKey); - if (existing != null) { - return existing; - } - return add(bytes); - } + byte[] canonical = Arrays.copyOf(bytes, bytes.length); + int id = partitionCount; + if (id == partitions.length) { + partitions = Arrays.copyOf(partitions, partitions.length << 1); } - return add(bytes); + partitions[id] = SerializationUtils.deserializeBinaryRow(canonical); + ids.put(new ByteArrayKey(canonical), id); + partitionCount = id + 1; + return id; } finally { - lookupKey.clear(); - } - } - - private int add(byte[] bytes) { - checkState(ranks == null, "Manifest scan found a partition after ranks were assigned."); - byte[] canonical = Arrays.copyOf(bytes, bytes.length); - int id = partitionCount; - if (id == partitions.length) { - partitions = Arrays.copyOf(partitions, partitions.length << 1); + lookup.clear(); } - partitions[id] = SerializationUtils.deserializeBinaryRow(canonical); - ids.put(new ByteArrayKey(canonical), id); - partitionCount = id + 1; - return id; - } - - public void finish() { - checkState(comparator != null, "Partition dictionary has no comparator."); - List order = new ArrayList<>(partitionCount); - for (int id = 0; id < partitionCount; id++) { - order.add(id); - } - order.sort((left, right) -> compareIds(left, right)); - ranks = new int[partitionCount]; - int rank = 0; - for (int position = 0; position < order.size(); position++) { - if (position > 0 && compareIds(order.get(position - 1), order.get(position)) != 0) { - rank++; - } - ranks[order.get(position)] = rank; - } - } - - public int compareIds(int left, int right) { - checkState(comparator != null, "Partition dictionary has no comparator."); - return comparator.compare(partitions[left], partitions[right]); - } - - public int rank(int id) { - return ranks == null ? 0 : ranks[id]; } public BinaryRow partition(int id) { diff --git a/paimon-core/src/main/java/org/apache/paimon/operation/ManifestEntryRunMerge.java b/paimon-core/src/main/java/org/apache/paimon/operation/ManifestEntryRunMerge.java index cd03e0ef9c9d..30d907ed1314 100644 --- a/paimon-core/src/main/java/org/apache/paimon/operation/ManifestEntryRunMerge.java +++ b/paimon-core/src/main/java/org/apache/paimon/operation/ManifestEntryRunMerge.java @@ -30,7 +30,6 @@ import org.apache.paimon.manifest.ManifestAvroWriter.EncodedBlockMeta; import org.apache.paimon.manifest.ManifestFile; import org.apache.paimon.manifest.ManifestFileMeta; -import org.apache.paimon.manifest.PartitionDictionary; import org.apache.paimon.manifest.ProjectedManifestEntry; import org.apache.paimon.memory.MemorySegmentUtils; import org.apache.paimon.stats.SimpleStats; @@ -150,7 +149,8 @@ private static ManifestEntryRunMergePlan discoverRuns( int maxNumFileHandles, @Nullable Integer manifestReadParallelism) throws Exception { - PartitionDictionary partitions = new PartitionDictionary(sortKey::comparePartitions); + ManifestEntryRunMergePartitionDictionary partitions = + new ManifestEntryRunMergePartitionDictionary(sortKey::comparePartitions); List sources = new ArrayList<>(); int streamCursorCount = 0; long inMemoryEntries = 0; @@ -236,7 +236,7 @@ private static Discovery.DiscoveredManifest discoverManifestRuns( ManifestFileMeta meta, ManifestFile manifestFile, RowType partitionType, - PartitionDictionary partitions, + ManifestEntryRunMergePartitionDictionary partitions, ManifestEntryRunMergeEntry.Filter filter) throws Exception { try (ManifestAvroReader reader = @@ -253,7 +253,7 @@ private static Discovery.DiscoveredManifest discoverManifestRuns( ManifestFileMeta meta, ManifestAvroReader reader, RowType partitionType, - PartitionDictionary partitions, + ManifestEntryRunMergePartitionDictionary partitions, ManifestEntryRunMergeEntry.Filter filter) throws Exception { SimpleStatsConverter partitionStatsConverter = new SimpleStatsConverter(partitionType); @@ -354,7 +354,7 @@ private static boolean exceedsStreamingReadAmplification( private static int compareDiscoveryKeys( ManifestEntryRunMergeEntry.Key left, ManifestEntryRunMergeEntry.Key right, - PartitionDictionary partitions) { + ManifestEntryRunMergePartitionDictionary partitions) { return compareRemainingKeys( left, right, partitions.compareIds(left.partitionId, right.partitionId)); } @@ -443,7 +443,7 @@ static DiscoveredManifest requiresExternalSort() { Collections.emptyList(), Collections.emptyList(), false, true); } - void updatePartitionRanks(PartitionDictionary partitions) { + void updatePartitionRanks(ManifestEntryRunMergePartitionDictionary partitions) { for (BlockInfo block : blocks) { block.updatePartitionRanks(partitions); } @@ -505,7 +505,7 @@ static final class BlockInfo { void collectForSort( ProjectedManifestEntry entry, ManifestEntryRunMergeEntry.Key key, - PartitionDictionary partitions, + ManifestEntryRunMergePartitionDictionary partitions, ManifestEntryRunMergeEntry.Filter filter) { if (!eligible) { return; @@ -608,7 +608,7 @@ void finishFiltering(ManifestEntryRunMergeEntry.Filter filter) { } } - void updatePartitionRanks(PartitionDictionary partitions) { + void updatePartitionRanks(ManifestEntryRunMergePartitionDictionary partitions) { checkState(firstKey != null && lastKey != null, "Manifest block has no sort keys."); firstKey.partitionRank = partitions.rank(firstKey.partitionId); lastKey.partitionRank = partitions.rank(lastKey.partitionId); diff --git a/paimon-core/src/main/java/org/apache/paimon/operation/ManifestEntryRunMergeEntry.java b/paimon-core/src/main/java/org/apache/paimon/operation/ManifestEntryRunMergeEntry.java index 04e74f655ca3..734548a94f1f 100644 --- a/paimon-core/src/main/java/org/apache/paimon/operation/ManifestEntryRunMergeEntry.java +++ b/paimon-core/src/main/java/org/apache/paimon/operation/ManifestEntryRunMergeEntry.java @@ -23,7 +23,6 @@ import org.apache.paimon.manifest.DeletedRowIdSet; import org.apache.paimon.manifest.FileEntry.ReusableIdentifier; import org.apache.paimon.manifest.FileKind; -import org.apache.paimon.manifest.PartitionDictionary; import org.apache.paimon.manifest.ProjectedManifestEntry; import org.apache.paimon.memory.MemorySegment; import org.apache.paimon.memory.MemorySegmentUtils; @@ -49,13 +48,15 @@ static final class Key { byte[] ownedFileNameBytes; MemorySegment[] ownedFileNameSegments; - static Key viewOf(ProjectedManifestEntry entry, PartitionDictionary partitions) { + static Key viewOf( + ProjectedManifestEntry entry, ManifestEntryRunMergePartitionDictionary partitions) { Key key = new Key(); key.replace(entry, partitions); return key; } - void replace(ProjectedManifestEntry entry, PartitionDictionary partitions) { + void replace( + ProjectedManifestEntry entry, ManifestEntryRunMergePartitionDictionary partitions) { long firstRowId = entry.file().nonNullFirstRowId(); this.partitionId = partitions.id(entry.partitionBytes()); this.partitionRank = partitions.rank(partitionId); diff --git a/paimon-core/src/main/java/org/apache/paimon/operation/ManifestEntryRunMergePartitionDictionary.java b/paimon-core/src/main/java/org/apache/paimon/operation/ManifestEntryRunMergePartitionDictionary.java new file mode 100644 index 000000000000..86f2c6f0c2f8 --- /dev/null +++ b/paimon-core/src/main/java/org/apache/paimon/operation/ManifestEntryRunMergePartitionDictionary.java @@ -0,0 +1,106 @@ +/* + * 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.paimon.operation; + +import org.apache.paimon.data.BinaryRow; +import org.apache.paimon.utils.ByteArrayKey; +import org.apache.paimon.utils.ByteArrayLookupKey; +import org.apache.paimon.utils.SerializationUtils; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import static org.apache.paimon.utils.Preconditions.checkState; + +/** Concurrent partition dictionary and ordering used only by manifest run merge. */ +final class ManifestEntryRunMergePartitionDictionary { + + private final Comparator comparator; + private final Map ids = new ConcurrentHashMap<>(); + private final ThreadLocal lookup = + ThreadLocal.withInitial(ByteArrayLookupKey::new); + private volatile BinaryRow[] partitions = new BinaryRow[16]; + private int partitionCount; + private int[] ranks; + + ManifestEntryRunMergePartitionDictionary(Comparator comparator) { + this.comparator = comparator; + } + + int id(byte[] bytes) { + ByteArrayLookupKey lookupKey = lookup.get(); + lookupKey.reset(bytes); + try { + Integer existing = ids.get(lookupKey); + if (existing != null) { + return existing; + } + synchronized (this) { + existing = ids.get(lookupKey); + if (existing != null) { + return existing; + } + checkState(ranks == null, "Manifest scan found an unknown partition."); + byte[] canonical = Arrays.copyOf(bytes, bytes.length); + int id = partitionCount; + if (id == partitions.length) { + partitions = Arrays.copyOf(partitions, partitions.length << 1); + } + partitions[id] = SerializationUtils.deserializeBinaryRow(canonical); + ids.put(new ByteArrayKey(canonical), id); + partitionCount = id + 1; + return id; + } + } finally { + lookupKey.clear(); + } + } + + void finish() { + List order = new ArrayList<>(partitionCount); + for (int id = 0; id < partitionCount; id++) { + order.add(id); + } + order.sort((left, right) -> compareIds(left, right)); + ranks = new int[partitionCount]; + int rank = 0; + for (int position = 0; position < order.size(); position++) { + if (position > 0 && compareIds(order.get(position - 1), order.get(position)) != 0) { + rank++; + } + ranks[order.get(position)] = rank; + } + } + + int compareIds(int left, int right) { + return comparator.compare(partitions[left], partitions[right]); + } + + int rank(int id) { + return ranks == null ? 0 : ranks[id]; + } + + BinaryRow partition(int id) { + return partitions[id]; + } +} diff --git a/paimon-core/src/main/java/org/apache/paimon/operation/ManifestEntryRunMergePlan.java b/paimon-core/src/main/java/org/apache/paimon/operation/ManifestEntryRunMergePlan.java index 561da387df84..f7b95d953a2a 100644 --- a/paimon-core/src/main/java/org/apache/paimon/operation/ManifestEntryRunMergePlan.java +++ b/paimon-core/src/main/java/org/apache/paimon/operation/ManifestEntryRunMergePlan.java @@ -36,7 +36,6 @@ import org.apache.paimon.manifest.ManifestEntry; import org.apache.paimon.manifest.ManifestFile; import org.apache.paimon.manifest.ManifestFileMeta; -import org.apache.paimon.manifest.PartitionDictionary; import org.apache.paimon.manifest.ProjectedManifestEntry; import org.apache.paimon.utils.CloseableIterator; import org.apache.paimon.utils.Pair; @@ -54,9 +53,10 @@ final class ManifestEntryRunMergePlan { final List sources; - final PartitionDictionary partitions; + final ManifestEntryRunMergePartitionDictionary partitions; - ManifestEntryRunMergePlan(List sources, PartitionDictionary partitions) { + ManifestEntryRunMergePlan( + List sources, ManifestEntryRunMergePartitionDictionary partitions) { this.sources = sources; this.partitions = partitions; } @@ -271,7 +271,7 @@ Cursor open( ManifestFile manifestFile, ManifestFileSorter.RowIdEntrySortKey sortKey, ManifestEntryRunMergeEntry.Filter filter, - PartitionDictionary partitions) + ManifestEntryRunMergePartitionDictionary partitions) throws Exception; } @@ -312,7 +312,7 @@ public Cursor open( ManifestFile manifestFile, ManifestFileSorter.RowIdEntrySortKey sortKey, ManifestEntryRunMergeEntry.Filter filter, - PartitionDictionary partitions) + ManifestEntryRunMergePartitionDictionary partitions) throws Exception { return new PrimitiveManifestRunCursor( manifestFile, meta, start, end, blocks, filter, partitions); @@ -332,7 +332,7 @@ public Cursor open( ManifestFile manifestFile, ManifestFileSorter.RowIdEntrySortKey sortKey, ManifestEntryRunMergeEntry.Filter filter, - PartitionDictionary partitions) + ManifestEntryRunMergePartitionDictionary partitions) throws Exception { return new InMemoryManifestCursor(manifestFile, meta, sortKey, filter, partitions); } @@ -393,7 +393,7 @@ static final class PrimitiveManifestRunCursor implements Cursor { final ManifestAvroReader reader; final boolean encodedRecordsCompatible; final ManifestEntryRunMergeEntry.Filter filter; - final PartitionDictionary partitions; + final ManifestEntryRunMergePartitionDictionary partitions; final ManifestEntryRunMergeEntry.Key key = new ManifestEntryRunMergeEntry.Key(); final EncodedEntry metadata = new EncodedEntry(); final ProjectedManifestEntry projectedEntry = @@ -422,7 +422,7 @@ static final class PrimitiveManifestRunCursor implements Cursor { long end, List blocks, ManifestEntryRunMergeEntry.Filter filter, - PartitionDictionary partitions) + ManifestEntryRunMergePartitionDictionary partitions) throws Exception { this.reader = manifestFile.scanAvroBlocks(meta.fileName(), meta.fileSize()); this.encodedRecordsCompatible = reader.rawBlockCopySupported(); @@ -671,7 +671,7 @@ static final class InMemoryManifestCursor implements Cursor { ManifestFileMeta meta, ManifestFileSorter.RowIdEntrySortKey sortKey, ManifestEntryRunMergeEntry.Filter filter, - PartitionDictionary partitions) + ManifestEntryRunMergePartitionDictionary partitions) throws Exception { long entryCount = meta.numAddedFiles() + meta.numDeletedFiles(); this.entries = new ArrayList<>((int) entryCount); diff --git a/paimon-core/src/test/java/org/apache/paimon/manifest/PartitionDictionaryTest.java b/paimon-core/src/test/java/org/apache/paimon/operation/ManifestEntryRunMergePartitionDictionaryTest.java similarity index 89% rename from paimon-core/src/test/java/org/apache/paimon/manifest/PartitionDictionaryTest.java rename to paimon-core/src/test/java/org/apache/paimon/operation/ManifestEntryRunMergePartitionDictionaryTest.java index facbd29b0d0d..0682f878049e 100644 --- a/paimon-core/src/test/java/org/apache/paimon/manifest/PartitionDictionaryTest.java +++ b/paimon-core/src/test/java/org/apache/paimon/operation/ManifestEntryRunMergePartitionDictionaryTest.java @@ -16,7 +16,7 @@ * limitations under the License. */ -package org.apache.paimon.manifest; +package org.apache.paimon.operation; import org.apache.paimon.data.BinaryRow; import org.apache.paimon.data.BinaryRowWriter; @@ -34,12 +34,13 @@ import static org.apache.paimon.utils.SerializationUtils.serializeBinaryRow; import static org.assertj.core.api.Assertions.assertThat; -/** Tests for {@link PartitionDictionary}. */ -class PartitionDictionaryTest { +/** Tests for {@link ManifestEntryRunMergePartitionDictionary}. */ +class ManifestEntryRunMergePartitionDictionaryTest { @Test void testComparatorEqualPartitionsKeepDistinctIds() { - PartitionDictionary dictionary = new PartitionDictionary((left, right) -> 0); + ManifestEntryRunMergePartitionDictionary dictionary = + new ManifestEntryRunMergePartitionDictionary((left, right) -> 0); int first = dictionary.id(partitionBytes(1)); int second = dictionary.id(partitionBytes(2)); @@ -53,8 +54,8 @@ void testComparatorEqualPartitionsKeepDistinctIds() { @Test void testConcurrentCollectionAndRanks() throws Exception { - PartitionDictionary dictionary = - new PartitionDictionary( + ManifestEntryRunMergePartitionDictionary dictionary = + new ManifestEntryRunMergePartitionDictionary( (left, right) -> Integer.compare(left.getInt(0), right.getInt(0))); Map ids = new ConcurrentHashMap<>(); ExecutorService executor = Executors.newFixedThreadPool(8); From b99ebaaffd83668a1557eaab3ed0339f3f9c96bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BB=9F=E5=BC=8B?= Date: Sun, 16 Aug 2026 02:19:44 +0800 Subject: [PATCH 3/7] [core] Keep run merge partition ordering local --- .../operation/ManifestEntryRunMerge.java | 95 ++++++++++++++-- .../operation/ManifestEntryRunMergeEntry.java | 6 +- ...ifestEntryRunMergePartitionDictionary.java | 106 ------------------ .../operation/ManifestEntryRunMergePlan.java | 16 +-- ...tEntryRunMergePartitionDictionaryTest.java | 10 +- 5 files changed, 104 insertions(+), 129 deletions(-) delete mode 100644 paimon-core/src/main/java/org/apache/paimon/operation/ManifestEntryRunMergePartitionDictionary.java diff --git a/paimon-core/src/main/java/org/apache/paimon/operation/ManifestEntryRunMerge.java b/paimon-core/src/main/java/org/apache/paimon/operation/ManifestEntryRunMerge.java index 30d907ed1314..2aed5582d043 100644 --- a/paimon-core/src/main/java/org/apache/paimon/operation/ManifestEntryRunMerge.java +++ b/paimon-core/src/main/java/org/apache/paimon/operation/ManifestEntryRunMerge.java @@ -35,13 +35,20 @@ import org.apache.paimon.stats.SimpleStats; import org.apache.paimon.stats.SimpleStatsConverter; import org.apache.paimon.types.RowType; +import org.apache.paimon.utils.ByteArrayKey; +import org.apache.paimon.utils.ByteArrayLookupKey; import org.apache.paimon.utils.Pair; +import org.apache.paimon.utils.SerializationUtils; import javax.annotation.Nullable; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; +import java.util.Comparator; import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; import java.util.function.Function; import static org.apache.paimon.utils.ManifestReadThreadPool.sequentialBatchedExecute; @@ -149,8 +156,7 @@ private static ManifestEntryRunMergePlan discoverRuns( int maxNumFileHandles, @Nullable Integer manifestReadParallelism) throws Exception { - ManifestEntryRunMergePartitionDictionary partitions = - new ManifestEntryRunMergePartitionDictionary(sortKey::comparePartitions); + PartitionDictionary partitions = new PartitionDictionary(sortKey::comparePartitions); List sources = new ArrayList<>(); int streamCursorCount = 0; long inMemoryEntries = 0; @@ -236,7 +242,7 @@ private static Discovery.DiscoveredManifest discoverManifestRuns( ManifestFileMeta meta, ManifestFile manifestFile, RowType partitionType, - ManifestEntryRunMergePartitionDictionary partitions, + PartitionDictionary partitions, ManifestEntryRunMergeEntry.Filter filter) throws Exception { try (ManifestAvroReader reader = @@ -253,7 +259,7 @@ private static Discovery.DiscoveredManifest discoverManifestRuns( ManifestFileMeta meta, ManifestAvroReader reader, RowType partitionType, - ManifestEntryRunMergePartitionDictionary partitions, + PartitionDictionary partitions, ManifestEntryRunMergeEntry.Filter filter) throws Exception { SimpleStatsConverter partitionStatsConverter = new SimpleStatsConverter(partitionType); @@ -354,7 +360,7 @@ private static boolean exceedsStreamingReadAmplification( private static int compareDiscoveryKeys( ManifestEntryRunMergeEntry.Key left, ManifestEntryRunMergeEntry.Key right, - ManifestEntryRunMergePartitionDictionary partitions) { + PartitionDictionary partitions) { return compareRemainingKeys( left, right, partitions.compareIds(left.partitionId, right.partitionId)); } @@ -443,7 +449,7 @@ static DiscoveredManifest requiresExternalSort() { Collections.emptyList(), Collections.emptyList(), false, true); } - void updatePartitionRanks(ManifestEntryRunMergePartitionDictionary partitions) { + void updatePartitionRanks(PartitionDictionary partitions) { for (BlockInfo block : blocks) { block.updatePartitionRanks(partitions); } @@ -505,7 +511,7 @@ static final class BlockInfo { void collectForSort( ProjectedManifestEntry entry, ManifestEntryRunMergeEntry.Key key, - ManifestEntryRunMergePartitionDictionary partitions, + PartitionDictionary partitions, ManifestEntryRunMergeEntry.Filter filter) { if (!eligible) { return; @@ -608,11 +614,84 @@ void finishFiltering(ManifestEntryRunMergeEntry.Filter filter) { } } - void updatePartitionRanks(ManifestEntryRunMergePartitionDictionary partitions) { + void updatePartitionRanks(PartitionDictionary partitions) { checkState(firstKey != null && lastKey != null, "Manifest block has no sort keys."); firstKey.partitionRank = partitions.rank(firstKey.partitionId); lastKey.partitionRank = partitions.rank(lastKey.partitionId); } } } + + /** Concurrent partition dictionary and ordering used only by manifest run merge. */ + static final class PartitionDictionary { + + private final Comparator comparator; + private final Map ids = new ConcurrentHashMap<>(); + private final ThreadLocal lookup = + ThreadLocal.withInitial(ByteArrayLookupKey::new); + private volatile BinaryRow[] partitions = new BinaryRow[16]; + private int partitionCount; + private int[] ranks; + + PartitionDictionary(Comparator comparator) { + this.comparator = comparator; + } + + int id(byte[] bytes) { + ByteArrayLookupKey lookupKey = lookup.get(); + lookupKey.reset(bytes); + try { + Integer existing = ids.get(lookupKey); + if (existing != null) { + return existing; + } + synchronized (this) { + existing = ids.get(lookupKey); + if (existing != null) { + return existing; + } + checkState(ranks == null, "Manifest scan found an unknown partition."); + byte[] canonical = Arrays.copyOf(bytes, bytes.length); + int id = partitionCount; + if (id == partitions.length) { + partitions = Arrays.copyOf(partitions, partitions.length << 1); + } + partitions[id] = SerializationUtils.deserializeBinaryRow(canonical); + ids.put(new ByteArrayKey(canonical), id); + partitionCount = id + 1; + return id; + } + } finally { + lookupKey.clear(); + } + } + + void finish() { + List order = new ArrayList<>(partitionCount); + for (int id = 0; id < partitionCount; id++) { + order.add(id); + } + order.sort((left, right) -> compareIds(left, right)); + ranks = new int[partitionCount]; + int rank = 0; + for (int position = 0; position < order.size(); position++) { + if (position > 0 && compareIds(order.get(position - 1), order.get(position)) != 0) { + rank++; + } + ranks[order.get(position)] = rank; + } + } + + int compareIds(int left, int right) { + return comparator.compare(partitions[left], partitions[right]); + } + + int rank(int id) { + return ranks == null ? 0 : ranks[id]; + } + + BinaryRow partition(int id) { + return partitions[id]; + } + } } diff --git a/paimon-core/src/main/java/org/apache/paimon/operation/ManifestEntryRunMergeEntry.java b/paimon-core/src/main/java/org/apache/paimon/operation/ManifestEntryRunMergeEntry.java index 734548a94f1f..affa3863fd75 100644 --- a/paimon-core/src/main/java/org/apache/paimon/operation/ManifestEntryRunMergeEntry.java +++ b/paimon-core/src/main/java/org/apache/paimon/operation/ManifestEntryRunMergeEntry.java @@ -49,14 +49,16 @@ static final class Key { MemorySegment[] ownedFileNameSegments; static Key viewOf( - ProjectedManifestEntry entry, ManifestEntryRunMergePartitionDictionary partitions) { + ProjectedManifestEntry entry, + ManifestEntryRunMerge.PartitionDictionary partitions) { Key key = new Key(); key.replace(entry, partitions); return key; } void replace( - ProjectedManifestEntry entry, ManifestEntryRunMergePartitionDictionary partitions) { + ProjectedManifestEntry entry, + ManifestEntryRunMerge.PartitionDictionary partitions) { long firstRowId = entry.file().nonNullFirstRowId(); this.partitionId = partitions.id(entry.partitionBytes()); this.partitionRank = partitions.rank(partitionId); diff --git a/paimon-core/src/main/java/org/apache/paimon/operation/ManifestEntryRunMergePartitionDictionary.java b/paimon-core/src/main/java/org/apache/paimon/operation/ManifestEntryRunMergePartitionDictionary.java deleted file mode 100644 index 86f2c6f0c2f8..000000000000 --- a/paimon-core/src/main/java/org/apache/paimon/operation/ManifestEntryRunMergePartitionDictionary.java +++ /dev/null @@ -1,106 +0,0 @@ -/* - * 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.paimon.operation; - -import org.apache.paimon.data.BinaryRow; -import org.apache.paimon.utils.ByteArrayKey; -import org.apache.paimon.utils.ByteArrayLookupKey; -import org.apache.paimon.utils.SerializationUtils; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Comparator; -import java.util.List; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; - -import static org.apache.paimon.utils.Preconditions.checkState; - -/** Concurrent partition dictionary and ordering used only by manifest run merge. */ -final class ManifestEntryRunMergePartitionDictionary { - - private final Comparator comparator; - private final Map ids = new ConcurrentHashMap<>(); - private final ThreadLocal lookup = - ThreadLocal.withInitial(ByteArrayLookupKey::new); - private volatile BinaryRow[] partitions = new BinaryRow[16]; - private int partitionCount; - private int[] ranks; - - ManifestEntryRunMergePartitionDictionary(Comparator comparator) { - this.comparator = comparator; - } - - int id(byte[] bytes) { - ByteArrayLookupKey lookupKey = lookup.get(); - lookupKey.reset(bytes); - try { - Integer existing = ids.get(lookupKey); - if (existing != null) { - return existing; - } - synchronized (this) { - existing = ids.get(lookupKey); - if (existing != null) { - return existing; - } - checkState(ranks == null, "Manifest scan found an unknown partition."); - byte[] canonical = Arrays.copyOf(bytes, bytes.length); - int id = partitionCount; - if (id == partitions.length) { - partitions = Arrays.copyOf(partitions, partitions.length << 1); - } - partitions[id] = SerializationUtils.deserializeBinaryRow(canonical); - ids.put(new ByteArrayKey(canonical), id); - partitionCount = id + 1; - return id; - } - } finally { - lookupKey.clear(); - } - } - - void finish() { - List order = new ArrayList<>(partitionCount); - for (int id = 0; id < partitionCount; id++) { - order.add(id); - } - order.sort((left, right) -> compareIds(left, right)); - ranks = new int[partitionCount]; - int rank = 0; - for (int position = 0; position < order.size(); position++) { - if (position > 0 && compareIds(order.get(position - 1), order.get(position)) != 0) { - rank++; - } - ranks[order.get(position)] = rank; - } - } - - int compareIds(int left, int right) { - return comparator.compare(partitions[left], partitions[right]); - } - - int rank(int id) { - return ranks == null ? 0 : ranks[id]; - } - - BinaryRow partition(int id) { - return partitions[id]; - } -} diff --git a/paimon-core/src/main/java/org/apache/paimon/operation/ManifestEntryRunMergePlan.java b/paimon-core/src/main/java/org/apache/paimon/operation/ManifestEntryRunMergePlan.java index f7b95d953a2a..b4f0ca11efd7 100644 --- a/paimon-core/src/main/java/org/apache/paimon/operation/ManifestEntryRunMergePlan.java +++ b/paimon-core/src/main/java/org/apache/paimon/operation/ManifestEntryRunMergePlan.java @@ -53,10 +53,10 @@ final class ManifestEntryRunMergePlan { final List sources; - final ManifestEntryRunMergePartitionDictionary partitions; + final ManifestEntryRunMerge.PartitionDictionary partitions; ManifestEntryRunMergePlan( - List sources, ManifestEntryRunMergePartitionDictionary partitions) { + List sources, ManifestEntryRunMerge.PartitionDictionary partitions) { this.sources = sources; this.partitions = partitions; } @@ -271,7 +271,7 @@ Cursor open( ManifestFile manifestFile, ManifestFileSorter.RowIdEntrySortKey sortKey, ManifestEntryRunMergeEntry.Filter filter, - ManifestEntryRunMergePartitionDictionary partitions) + ManifestEntryRunMerge.PartitionDictionary partitions) throws Exception; } @@ -312,7 +312,7 @@ public Cursor open( ManifestFile manifestFile, ManifestFileSorter.RowIdEntrySortKey sortKey, ManifestEntryRunMergeEntry.Filter filter, - ManifestEntryRunMergePartitionDictionary partitions) + ManifestEntryRunMerge.PartitionDictionary partitions) throws Exception { return new PrimitiveManifestRunCursor( manifestFile, meta, start, end, blocks, filter, partitions); @@ -332,7 +332,7 @@ public Cursor open( ManifestFile manifestFile, ManifestFileSorter.RowIdEntrySortKey sortKey, ManifestEntryRunMergeEntry.Filter filter, - ManifestEntryRunMergePartitionDictionary partitions) + ManifestEntryRunMerge.PartitionDictionary partitions) throws Exception { return new InMemoryManifestCursor(manifestFile, meta, sortKey, filter, partitions); } @@ -393,7 +393,7 @@ static final class PrimitiveManifestRunCursor implements Cursor { final ManifestAvroReader reader; final boolean encodedRecordsCompatible; final ManifestEntryRunMergeEntry.Filter filter; - final ManifestEntryRunMergePartitionDictionary partitions; + final ManifestEntryRunMerge.PartitionDictionary partitions; final ManifestEntryRunMergeEntry.Key key = new ManifestEntryRunMergeEntry.Key(); final EncodedEntry metadata = new EncodedEntry(); final ProjectedManifestEntry projectedEntry = @@ -422,7 +422,7 @@ static final class PrimitiveManifestRunCursor implements Cursor { long end, List blocks, ManifestEntryRunMergeEntry.Filter filter, - ManifestEntryRunMergePartitionDictionary partitions) + ManifestEntryRunMerge.PartitionDictionary partitions) throws Exception { this.reader = manifestFile.scanAvroBlocks(meta.fileName(), meta.fileSize()); this.encodedRecordsCompatible = reader.rawBlockCopySupported(); @@ -671,7 +671,7 @@ static final class InMemoryManifestCursor implements Cursor { ManifestFileMeta meta, ManifestFileSorter.RowIdEntrySortKey sortKey, ManifestEntryRunMergeEntry.Filter filter, - ManifestEntryRunMergePartitionDictionary partitions) + ManifestEntryRunMerge.PartitionDictionary partitions) throws Exception { long entryCount = meta.numAddedFiles() + meta.numDeletedFiles(); this.entries = new ArrayList<>((int) entryCount); diff --git a/paimon-core/src/test/java/org/apache/paimon/operation/ManifestEntryRunMergePartitionDictionaryTest.java b/paimon-core/src/test/java/org/apache/paimon/operation/ManifestEntryRunMergePartitionDictionaryTest.java index 0682f878049e..af20706978c5 100644 --- a/paimon-core/src/test/java/org/apache/paimon/operation/ManifestEntryRunMergePartitionDictionaryTest.java +++ b/paimon-core/src/test/java/org/apache/paimon/operation/ManifestEntryRunMergePartitionDictionaryTest.java @@ -34,13 +34,13 @@ import static org.apache.paimon.utils.SerializationUtils.serializeBinaryRow; import static org.assertj.core.api.Assertions.assertThat; -/** Tests for {@link ManifestEntryRunMergePartitionDictionary}. */ +/** Tests for {@link ManifestEntryRunMerge.PartitionDictionary}. */ class ManifestEntryRunMergePartitionDictionaryTest { @Test void testComparatorEqualPartitionsKeepDistinctIds() { - ManifestEntryRunMergePartitionDictionary dictionary = - new ManifestEntryRunMergePartitionDictionary((left, right) -> 0); + ManifestEntryRunMerge.PartitionDictionary dictionary = + new ManifestEntryRunMerge.PartitionDictionary((left, right) -> 0); int first = dictionary.id(partitionBytes(1)); int second = dictionary.id(partitionBytes(2)); @@ -54,8 +54,8 @@ void testComparatorEqualPartitionsKeepDistinctIds() { @Test void testConcurrentCollectionAndRanks() throws Exception { - ManifestEntryRunMergePartitionDictionary dictionary = - new ManifestEntryRunMergePartitionDictionary( + ManifestEntryRunMerge.PartitionDictionary dictionary = + new ManifestEntryRunMerge.PartitionDictionary( (left, right) -> Integer.compare(left.getInt(0), right.getInt(0))); Map ids = new ConcurrentHashMap<>(); ExecutorService executor = Executors.newFixedThreadPool(8); From d681ffa4c094308f3beddfd978aa89fe2e7dfa34 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BB=9F=E5=BC=8B?= Date: Sun, 16 Aug 2026 02:20:44 +0800 Subject: [PATCH 4/7] [core] Remove standalone run merge dictionary test --- ...tEntryRunMergePartitionDictionaryTest.java | 99 ------------------- 1 file changed, 99 deletions(-) delete mode 100644 paimon-core/src/test/java/org/apache/paimon/operation/ManifestEntryRunMergePartitionDictionaryTest.java diff --git a/paimon-core/src/test/java/org/apache/paimon/operation/ManifestEntryRunMergePartitionDictionaryTest.java b/paimon-core/src/test/java/org/apache/paimon/operation/ManifestEntryRunMergePartitionDictionaryTest.java deleted file mode 100644 index af20706978c5..000000000000 --- a/paimon-core/src/test/java/org/apache/paimon/operation/ManifestEntryRunMergePartitionDictionaryTest.java +++ /dev/null @@ -1,99 +0,0 @@ -/* - * 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.paimon.operation; - -import org.apache.paimon.data.BinaryRow; -import org.apache.paimon.data.BinaryRowWriter; - -import org.junit.jupiter.api.Test; - -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.Future; - -import static org.apache.paimon.utils.SerializationUtils.serializeBinaryRow; -import static org.assertj.core.api.Assertions.assertThat; - -/** Tests for {@link ManifestEntryRunMerge.PartitionDictionary}. */ -class ManifestEntryRunMergePartitionDictionaryTest { - - @Test - void testComparatorEqualPartitionsKeepDistinctIds() { - ManifestEntryRunMerge.PartitionDictionary dictionary = - new ManifestEntryRunMerge.PartitionDictionary((left, right) -> 0); - int first = dictionary.id(partitionBytes(1)); - int second = dictionary.id(partitionBytes(2)); - - dictionary.finish(); - - assertThat(first).isNotEqualTo(second); - assertThat(dictionary.partition(first).getInt(0)).isEqualTo(1); - assertThat(dictionary.partition(second).getInt(0)).isEqualTo(2); - assertThat(dictionary.rank(first)).isEqualTo(dictionary.rank(second)); - } - - @Test - void testConcurrentCollectionAndRanks() throws Exception { - ManifestEntryRunMerge.PartitionDictionary dictionary = - new ManifestEntryRunMerge.PartitionDictionary( - (left, right) -> Integer.compare(left.getInt(0), right.getInt(0))); - Map ids = new ConcurrentHashMap<>(); - ExecutorService executor = Executors.newFixedThreadPool(8); - List> futures = new ArrayList<>(); - try { - for (int thread = 0; thread < 8; thread++) { - futures.add( - executor.submit( - () -> { - for (int value = 31; value >= 0; value--) { - int id = dictionary.id(partitionBytes(value)); - Integer previous = ids.putIfAbsent(value, id); - if (previous != null) { - assertThat(id).isEqualTo(previous); - } - } - })); - } - for (Future future : futures) { - future.get(); - } - } finally { - executor.shutdownNow(); - } - - dictionary.finish(); - for (int value = 0; value < 32; value++) { - int id = dictionary.id(partitionBytes(value)); - assertThat(dictionary.partition(id).getInt(0)).isEqualTo(value); - assertThat(dictionary.rank(id)).isEqualTo(value); - } - } - - private static byte[] partitionBytes(int value) { - BinaryRow row = new BinaryRow(1); - BinaryRowWriter writer = new BinaryRowWriter(row); - writer.writeInt(0, value); - writer.complete(); - return serializeBinaryRow(row); - } -} From 0c9211f7e7aa17357e676f5ac8d56a03d44c7579 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BB=9F=E5=BC=8B?= Date: Sun, 16 Aug 2026 02:26:44 +0800 Subject: [PATCH 5/7] [core] Reuse partition dictionary in run merge --- .../operation/ManifestEntryRunMerge.java | 36 +++++++++---------- .../operation/ManifestEntryRunMergeEntry.java | 4 +-- .../operation/ManifestEntryRunMergePlan.java | 16 ++++----- 3 files changed, 26 insertions(+), 30 deletions(-) diff --git a/paimon-core/src/main/java/org/apache/paimon/operation/ManifestEntryRunMerge.java b/paimon-core/src/main/java/org/apache/paimon/operation/ManifestEntryRunMerge.java index 2aed5582d043..0b8a374a2b3d 100644 --- a/paimon-core/src/main/java/org/apache/paimon/operation/ManifestEntryRunMerge.java +++ b/paimon-core/src/main/java/org/apache/paimon/operation/ManifestEntryRunMerge.java @@ -30,6 +30,7 @@ import org.apache.paimon.manifest.ManifestAvroWriter.EncodedBlockMeta; import org.apache.paimon.manifest.ManifestFile; import org.apache.paimon.manifest.ManifestFileMeta; +import org.apache.paimon.manifest.PartitionDictionary; import org.apache.paimon.manifest.ProjectedManifestEntry; import org.apache.paimon.memory.MemorySegmentUtils; import org.apache.paimon.stats.SimpleStats; @@ -38,7 +39,6 @@ import org.apache.paimon.utils.ByteArrayKey; import org.apache.paimon.utils.ByteArrayLookupKey; import org.apache.paimon.utils.Pair; -import org.apache.paimon.utils.SerializationUtils; import javax.annotation.Nullable; @@ -156,7 +156,8 @@ private static ManifestEntryRunMergePlan discoverRuns( int maxNumFileHandles, @Nullable Integer manifestReadParallelism) throws Exception { - PartitionDictionary partitions = new PartitionDictionary(sortKey::comparePartitions); + SortPartitionDictionary partitions = + new SortPartitionDictionary(sortKey::comparePartitions); List sources = new ArrayList<>(); int streamCursorCount = 0; long inMemoryEntries = 0; @@ -242,7 +243,7 @@ private static Discovery.DiscoveredManifest discoverManifestRuns( ManifestFileMeta meta, ManifestFile manifestFile, RowType partitionType, - PartitionDictionary partitions, + SortPartitionDictionary partitions, ManifestEntryRunMergeEntry.Filter filter) throws Exception { try (ManifestAvroReader reader = @@ -259,7 +260,7 @@ private static Discovery.DiscoveredManifest discoverManifestRuns( ManifestFileMeta meta, ManifestAvroReader reader, RowType partitionType, - PartitionDictionary partitions, + SortPartitionDictionary partitions, ManifestEntryRunMergeEntry.Filter filter) throws Exception { SimpleStatsConverter partitionStatsConverter = new SimpleStatsConverter(partitionType); @@ -360,7 +361,7 @@ private static boolean exceedsStreamingReadAmplification( private static int compareDiscoveryKeys( ManifestEntryRunMergeEntry.Key left, ManifestEntryRunMergeEntry.Key right, - PartitionDictionary partitions) { + SortPartitionDictionary partitions) { return compareRemainingKeys( left, right, partitions.compareIds(left.partitionId, right.partitionId)); } @@ -449,7 +450,7 @@ static DiscoveredManifest requiresExternalSort() { Collections.emptyList(), Collections.emptyList(), false, true); } - void updatePartitionRanks(PartitionDictionary partitions) { + void updatePartitionRanks(SortPartitionDictionary partitions) { for (BlockInfo block : blocks) { block.updatePartitionRanks(partitions); } @@ -511,7 +512,7 @@ static final class BlockInfo { void collectForSort( ProjectedManifestEntry entry, ManifestEntryRunMergeEntry.Key key, - PartitionDictionary partitions, + SortPartitionDictionary partitions, ManifestEntryRunMergeEntry.Filter filter) { if (!eligible) { return; @@ -614,7 +615,7 @@ void finishFiltering(ManifestEntryRunMergeEntry.Filter filter) { } } - void updatePartitionRanks(PartitionDictionary partitions) { + void updatePartitionRanks(SortPartitionDictionary partitions) { checkState(firstKey != null && lastKey != null, "Manifest block has no sort keys."); firstKey.partitionRank = partitions.rank(firstKey.partitionId); lastKey.partitionRank = partitions.rank(lastKey.partitionId); @@ -623,17 +624,16 @@ void updatePartitionRanks(PartitionDictionary partitions) { } /** Concurrent partition dictionary and ordering used only by manifest run merge. */ - static final class PartitionDictionary { + static final class SortPartitionDictionary { private final Comparator comparator; + private final PartitionDictionary partitions = new PartitionDictionary(); private final Map ids = new ConcurrentHashMap<>(); private final ThreadLocal lookup = ThreadLocal.withInitial(ByteArrayLookupKey::new); - private volatile BinaryRow[] partitions = new BinaryRow[16]; - private int partitionCount; private int[] ranks; - PartitionDictionary(Comparator comparator) { + SortPartitionDictionary(Comparator comparator) { this.comparator = comparator; } @@ -652,13 +652,8 @@ int id(byte[] bytes) { } checkState(ranks == null, "Manifest scan found an unknown partition."); byte[] canonical = Arrays.copyOf(bytes, bytes.length); - int id = partitionCount; - if (id == partitions.length) { - partitions = Arrays.copyOf(partitions, partitions.length << 1); - } - partitions[id] = SerializationUtils.deserializeBinaryRow(canonical); + int id = partitions.id(canonical); ids.put(new ByteArrayKey(canonical), id); - partitionCount = id + 1; return id; } } finally { @@ -667,6 +662,7 @@ int id(byte[] bytes) { } void finish() { + int partitionCount = ids.size(); List order = new ArrayList<>(partitionCount); for (int id = 0; id < partitionCount; id++) { order.add(id); @@ -683,7 +679,7 @@ void finish() { } int compareIds(int left, int right) { - return comparator.compare(partitions[left], partitions[right]); + return comparator.compare(partitions.partition(left), partitions.partition(right)); } int rank(int id) { @@ -691,7 +687,7 @@ int rank(int id) { } BinaryRow partition(int id) { - return partitions[id]; + return partitions.partition(id); } } } diff --git a/paimon-core/src/main/java/org/apache/paimon/operation/ManifestEntryRunMergeEntry.java b/paimon-core/src/main/java/org/apache/paimon/operation/ManifestEntryRunMergeEntry.java index affa3863fd75..49bc399ac327 100644 --- a/paimon-core/src/main/java/org/apache/paimon/operation/ManifestEntryRunMergeEntry.java +++ b/paimon-core/src/main/java/org/apache/paimon/operation/ManifestEntryRunMergeEntry.java @@ -50,7 +50,7 @@ static final class Key { static Key viewOf( ProjectedManifestEntry entry, - ManifestEntryRunMerge.PartitionDictionary partitions) { + ManifestEntryRunMerge.SortPartitionDictionary partitions) { Key key = new Key(); key.replace(entry, partitions); return key; @@ -58,7 +58,7 @@ static Key viewOf( void replace( ProjectedManifestEntry entry, - ManifestEntryRunMerge.PartitionDictionary partitions) { + ManifestEntryRunMerge.SortPartitionDictionary partitions) { long firstRowId = entry.file().nonNullFirstRowId(); this.partitionId = partitions.id(entry.partitionBytes()); this.partitionRank = partitions.rank(partitionId); diff --git a/paimon-core/src/main/java/org/apache/paimon/operation/ManifestEntryRunMergePlan.java b/paimon-core/src/main/java/org/apache/paimon/operation/ManifestEntryRunMergePlan.java index b4f0ca11efd7..5528589fdcd8 100644 --- a/paimon-core/src/main/java/org/apache/paimon/operation/ManifestEntryRunMergePlan.java +++ b/paimon-core/src/main/java/org/apache/paimon/operation/ManifestEntryRunMergePlan.java @@ -53,10 +53,10 @@ final class ManifestEntryRunMergePlan { final List sources; - final ManifestEntryRunMerge.PartitionDictionary partitions; + final ManifestEntryRunMerge.SortPartitionDictionary partitions; ManifestEntryRunMergePlan( - List sources, ManifestEntryRunMerge.PartitionDictionary partitions) { + List sources, ManifestEntryRunMerge.SortPartitionDictionary partitions) { this.sources = sources; this.partitions = partitions; } @@ -271,7 +271,7 @@ Cursor open( ManifestFile manifestFile, ManifestFileSorter.RowIdEntrySortKey sortKey, ManifestEntryRunMergeEntry.Filter filter, - ManifestEntryRunMerge.PartitionDictionary partitions) + ManifestEntryRunMerge.SortPartitionDictionary partitions) throws Exception; } @@ -312,7 +312,7 @@ public Cursor open( ManifestFile manifestFile, ManifestFileSorter.RowIdEntrySortKey sortKey, ManifestEntryRunMergeEntry.Filter filter, - ManifestEntryRunMerge.PartitionDictionary partitions) + ManifestEntryRunMerge.SortPartitionDictionary partitions) throws Exception { return new PrimitiveManifestRunCursor( manifestFile, meta, start, end, blocks, filter, partitions); @@ -332,7 +332,7 @@ public Cursor open( ManifestFile manifestFile, ManifestFileSorter.RowIdEntrySortKey sortKey, ManifestEntryRunMergeEntry.Filter filter, - ManifestEntryRunMerge.PartitionDictionary partitions) + ManifestEntryRunMerge.SortPartitionDictionary partitions) throws Exception { return new InMemoryManifestCursor(manifestFile, meta, sortKey, filter, partitions); } @@ -393,7 +393,7 @@ static final class PrimitiveManifestRunCursor implements Cursor { final ManifestAvroReader reader; final boolean encodedRecordsCompatible; final ManifestEntryRunMergeEntry.Filter filter; - final ManifestEntryRunMerge.PartitionDictionary partitions; + final ManifestEntryRunMerge.SortPartitionDictionary partitions; final ManifestEntryRunMergeEntry.Key key = new ManifestEntryRunMergeEntry.Key(); final EncodedEntry metadata = new EncodedEntry(); final ProjectedManifestEntry projectedEntry = @@ -422,7 +422,7 @@ static final class PrimitiveManifestRunCursor implements Cursor { long end, List blocks, ManifestEntryRunMergeEntry.Filter filter, - ManifestEntryRunMerge.PartitionDictionary partitions) + ManifestEntryRunMerge.SortPartitionDictionary partitions) throws Exception { this.reader = manifestFile.scanAvroBlocks(meta.fileName(), meta.fileSize()); this.encodedRecordsCompatible = reader.rawBlockCopySupported(); @@ -671,7 +671,7 @@ static final class InMemoryManifestCursor implements Cursor { ManifestFileMeta meta, ManifestFileSorter.RowIdEntrySortKey sortKey, ManifestEntryRunMergeEntry.Filter filter, - ManifestEntryRunMerge.PartitionDictionary partitions) + ManifestEntryRunMerge.SortPartitionDictionary partitions) throws Exception { long entryCount = meta.numAddedFiles() + meta.numDeletedFiles(); this.entries = new ArrayList<>((int) entryCount); From 125f7fc5a9aa1b2bdf6d74498b3a6168f1bddabf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BB=9F=E5=BC=8B?= Date: Sun, 16 Aug 2026 02:35:48 +0800 Subject: [PATCH 6/7] [core] Drain run discovery before sort fallback --- .../paimon/operation/ManifestEntryRunMerge.java | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/paimon-core/src/main/java/org/apache/paimon/operation/ManifestEntryRunMerge.java b/paimon-core/src/main/java/org/apache/paimon/operation/ManifestEntryRunMerge.java index 0b8a374a2b3d..827c0d8a201e 100644 --- a/paimon-core/src/main/java/org/apache/paimon/operation/ManifestEntryRunMerge.java +++ b/paimon-core/src/main/java/org/apache/paimon/operation/ManifestEntryRunMerge.java @@ -176,6 +176,7 @@ private static ManifestEntryRunMergePlan discoverRuns( discovered.add(manifest); } } else { + boolean requiresExternalSort = false; Function< ManifestFileMeta, List< @@ -205,12 +206,16 @@ private static ManifestEntryRunMergePlan discoverRuns( }; for (Pair scan : sequentialBatchedExecute(reader, section, manifestReadParallelism)) { - if (scan.getLeft().requiresExternalSort) { - return null; - } + requiresExternalSort |= scan.getLeft().requiresExternalSort; filter.combine(scan.getRight()); discovered.add(scan.getLeft()); } + // Drain every task in the bounded discovery batch before falling back. Returning from + // the lazy iterator early would leave already submitted manifest scans running beside + // the external sorter and duplicate their I/O and retained memory. + if (requiresExternalSort) { + return null; + } } for (int manifestIndex = 0; manifestIndex < section.size(); manifestIndex++) { ManifestFileMeta meta = section.get(manifestIndex); From 825cff38d75cba09d0df4fbd632db2c4b227caca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BB=9F=E5=BC=8B?= Date: Sun, 16 Aug 2026 12:25:21 +0800 Subject: [PATCH 7/7] [core] Simplify manifest run merge state --- .../operation/ManifestEntryExternalSort.java | 8 +- .../operation/ManifestEntryRunMerge.java | 477 +++++++++++------- .../operation/ManifestEntryRunMergeEntry.java | 239 --------- .../operation/ManifestEntryRunMergePlan.java | 95 ++-- .../paimon/operation/ManifestFileSorter.java | 410 +++++++-------- 5 files changed, 526 insertions(+), 703 deletions(-) delete mode 100644 paimon-core/src/main/java/org/apache/paimon/operation/ManifestEntryRunMergeEntry.java diff --git a/paimon-core/src/main/java/org/apache/paimon/operation/ManifestEntryExternalSort.java b/paimon-core/src/main/java/org/apache/paimon/operation/ManifestEntryExternalSort.java index 5c9b83772e8e..c2613890938a 100644 --- a/paimon-core/src/main/java/org/apache/paimon/operation/ManifestEntryExternalSort.java +++ b/paimon-core/src/main/java/org/apache/paimon/operation/ManifestEntryExternalSort.java @@ -24,6 +24,7 @@ import org.apache.paimon.data.GenericRow; import org.apache.paimon.data.serializer.InternalRowSerializer; import org.apache.paimon.disk.IOManager; +import org.apache.paimon.manifest.CollectedDeletes; import org.apache.paimon.manifest.CompactFileIdentifierSet; import org.apache.paimon.manifest.FileEntry.ReusableIdentifier; import org.apache.paimon.manifest.ManifestAvroWriter; @@ -84,9 +85,10 @@ static List sortAndWriteFullEntries( ExternalSortConfig config, ManifestFile manifestFile, List newFilesForAbort, - CompactFileIdentifierSet deleteEntries, + CollectedDeletes deletes, @Nullable Integer manifestReadParallelism) throws Exception { + ReusableIdentifier identifier = new ReusableIdentifier(); try (EntrySorter sorter = new EntrySorter(sortKey, config)) { scanEntries( section, @@ -94,13 +96,15 @@ static List sortAndWriteFullEntries( manifestReadParallelism, entry -> { if (entry.isAdd() - && (deleteEntries.isEmpty() || !deleteEntries.contains(entry))) { + && (deletes.isEmpty() || !deletes.isDeleted(entry, identifier))) { sorter.write(entry); } }); List files = sorter.writeToManifest(manifestFile); newFilesForAbort.addAll(files); return files; + } finally { + identifier.release(); } } diff --git a/paimon-core/src/main/java/org/apache/paimon/operation/ManifestEntryRunMerge.java b/paimon-core/src/main/java/org/apache/paimon/operation/ManifestEntryRunMerge.java index 827c0d8a201e..18cd08dfb35d 100644 --- a/paimon-core/src/main/java/org/apache/paimon/operation/ManifestEntryRunMerge.java +++ b/paimon-core/src/main/java/org/apache/paimon/operation/ManifestEntryRunMerge.java @@ -20,10 +20,10 @@ import org.apache.paimon.data.BinaryArray; import org.apache.paimon.data.BinaryRow; +import org.apache.paimon.data.BinaryString; import org.apache.paimon.format.SimpleStatsCollector; -import org.apache.paimon.manifest.CompactFileIdentifierSet; -import org.apache.paimon.manifest.DeletedRowIdSet; -import org.apache.paimon.manifest.FileKind; +import org.apache.paimon.manifest.CollectedDeletes; +import org.apache.paimon.manifest.FileEntry.ReusableIdentifier; import org.apache.paimon.manifest.ManifestAvroReader; import org.apache.paimon.manifest.ManifestAvroReader.RawBlock; import org.apache.paimon.manifest.ManifestAvroReader.RowIterator; @@ -32,6 +32,7 @@ import org.apache.paimon.manifest.ManifestFileMeta; import org.apache.paimon.manifest.PartitionDictionary; import org.apache.paimon.manifest.ProjectedManifestEntry; +import org.apache.paimon.memory.MemorySegment; import org.apache.paimon.memory.MemorySegmentUtils; import org.apache.paimon.stats.SimpleStats; import org.apache.paimon.stats.SimpleStatsConverter; @@ -75,26 +76,24 @@ static List sortAndWriteFullEntries( RowType partitionType, ManifestFile manifestFile, List newFilesForAbort, - CompactFileIdentifierSet deletedIdentifiers, - DeletedRowIdSet deletedRowIds, + CollectedDeletes deletes, int maxNumFileHandles, @Nullable Integer manifestReadParallelism) throws Exception { - ManifestEntryRunMergeEntry.Filter filter = - new ManifestEntryRunMergeEntry.Filter(deletedIdentifiers, deletedRowIds, true); ManifestEntryRunMergePlan plan = discoverRuns( section, sortKey, partitionType, manifestFile, - filter, + deletes, + false, maxNumFileHandles, manifestReadParallelism); if (plan == null) { return null; } - return plan.mergeToManifest(sortKey, manifestFile, filter, newFilesForAbort); + return plan.mergeToManifest(sortKey, manifestFile, newFilesForAbort); } /** @@ -111,38 +110,24 @@ static Pair, List> sortAndWriteMinorEnt int maxNumFileHandles, @Nullable Integer manifestReadParallelism) throws Exception { - CompactFileIdentifierSet deletedIdentifiers = new CompactFileIdentifierSet(); - DeletedRowIdSet deletedRowIds = new DeletedRowIdSet(); - ManifestEntryRunMergeEntry.Filter.Minor filter = - new ManifestEntryRunMergeEntry.Filter.Minor( - deletedIdentifiers, deletedRowIds, true); + CollectedDeletes deletes = new CollectedDeletes(true); try { - ManifestEntryRunMergePlan plan; - try { - plan = - discoverRuns( - section, - sortKey, - partitionType, - manifestFile, - filter, - maxNumFileHandles, - manifestReadParallelism); - } finally { - deletedRowIds.releaseRangeIndex(); - } + ManifestEntryRunMergePlan plan = + discoverRuns( + section, + sortKey, + partitionType, + manifestFile, + deletes, + true, + maxNumFileHandles, + manifestReadParallelism); if (plan == null) { return null; } - return plan.mergeMinorToManifest( - sortKey, - manifestFile, - filter, - deletedIdentifiers, - deletedRowIds, - newFilesForAbort); + return plan.mergeMinorToManifest(sortKey, manifestFile, newFilesForAbort); } finally { - deletedIdentifiers.release(); + deletes.release(); } } @@ -152,7 +137,8 @@ private static ManifestEntryRunMergePlan discoverRuns( ManifestFileSorter.RowIdEntrySortKey sortKey, RowType partitionType, ManifestFile manifestFile, - ManifestEntryRunMergeEntry.Filter filter, + CollectedDeletes deletes, + boolean minor, int maxNumFileHandles, @Nullable Integer manifestReadParallelism) throws Exception { @@ -165,29 +151,46 @@ private static ManifestEntryRunMergePlan discoverRuns( if (section.size() <= 1 || (manifestReadParallelism != null && manifestReadParallelism <= 1)) { for (ManifestFileMeta meta : section) { - ManifestEntryRunMergeEntry.Filter discoveryFilter = filter.forDiscovery(); - Discovery.DiscoveredManifest manifest = - discoverManifestRuns( - meta, manifestFile, partitionType, partitions, discoveryFilter); + CollectedDeletes discoveryDeletes = + minor ? new CollectedDeletes(deletes.useRowIdFilter()) : deletes; + Discovery.DiscoveredManifest manifest; + try { + manifest = + discoverManifestRuns( + meta, + manifestFile, + partitionType, + partitions, + discoveryDeletes, + minor); + } catch (Exception e) { + if (minor) { + discoveryDeletes.release(); + } + throw e; + } + if (minor) { + try { + deletes.combine(discoveryDeletes); + } finally { + discoveryDeletes.release(); + } + } if (manifest.requiresExternalSort) { return null; } - filter.combine(discoveryFilter); discovered.add(manifest); } } else { boolean requiresExternalSort = false; - Function< - ManifestFileMeta, - List< - Pair< - Discovery.DiscoveredManifest, - ManifestEntryRunMergeEntry.Filter>>> + Function>> reader = meta -> { + CollectedDeletes discoveryDeletes = + minor + ? new CollectedDeletes(deletes.useRowIdFilter()) + : deletes; try { - ManifestEntryRunMergeEntry.Filter discoveryFilter = - filter.forDiscovery(); return Collections.singletonList( Pair.of( discoverManifestRuns( @@ -195,19 +198,29 @@ private static ManifestEntryRunMergePlan discoverRuns( manifestFile, partitionType, partitions, - discoveryFilter), - discoveryFilter)); + discoveryDeletes, + minor), + discoveryDeletes)); } catch (Exception e) { + if (minor) { + discoveryDeletes.release(); + } throw new RuntimeException( "Failed to discover sorted Avro runs in " + meta.fileName(), e); } }; - for (Pair scan : + for (Pair scan : sequentialBatchedExecute(reader, section, manifestReadParallelism)) { requiresExternalSort |= scan.getLeft().requiresExternalSort; - filter.combine(scan.getRight()); + if (minor) { + try { + deletes.combine(scan.getRight()); + } finally { + scan.getRight().release(); + } + } discovered.add(scan.getLeft()); } // Drain every task in the bounded discovery batch before falling back. Returning from @@ -236,12 +249,15 @@ private static ManifestEntryRunMergePlan discoverRuns( return null; } } + if (minor) { + deletes.toImmutable(); + } partitions.finish(); for (Discovery.DiscoveredManifest manifest : discovered) { - manifest.finishFiltering(filter); + manifest.finishFiltering(deletes, minor); manifest.updatePartitionRanks(partitions); } - return new ManifestEntryRunMergePlan(sources, partitions); + return new ManifestEntryRunMergePlan(sources, partitions, deletes, minor); } private static Discovery.DiscoveredManifest discoverManifestRuns( @@ -249,15 +265,18 @@ private static Discovery.DiscoveredManifest discoverManifestRuns( ManifestFile manifestFile, RowType partitionType, SortPartitionDictionary partitions, - ManifestEntryRunMergeEntry.Filter filter) + CollectedDeletes deletes, + boolean minor) throws Exception { + ReusableIdentifier identifier = new ReusableIdentifier(); try (ManifestAvroReader reader = manifestFile.scanAvroBlocks(meta.fileName(), meta.fileSize())) { - return discoverManifestRuns(meta, reader, partitionType, partitions, filter); + return discoverManifestRuns( + meta, reader, partitionType, partitions, deletes, minor, identifier); } catch (UnsupportedOperationException unsupported) { return Discovery.DiscoveredManifest.requiresExternalSort(); } finally { - filter.releaseIdentifier(); + identifier.release(); } } @@ -266,13 +285,15 @@ private static Discovery.DiscoveredManifest discoverManifestRuns( ManifestAvroReader reader, RowType partitionType, SortPartitionDictionary partitions, - ManifestEntryRunMergeEntry.Filter filter) + CollectedDeletes deletes, + boolean minor, + ReusableIdentifier identifier) throws Exception { SimpleStatsConverter partitionStatsConverter = new SimpleStatsConverter(partitionType); List runs = new ArrayList<>(); List blocks = new ArrayList<>(); - ManifestEntryRunMergeEntry.Key previous = new ManifestEntryRunMergeEntry.Key(); - ManifestEntryRunMergeEntry.Key current = new ManifestEntryRunMergeEntry.Key(); + SortKey previous = new SortKey(); + SortKey current = new SortKey(); boolean hasPrevious = false; long runStart = 0; long position = 0; @@ -286,7 +307,9 @@ private static Discovery.DiscoveredManifest discoverManifestRuns( while (rows.hasNext()) { entry.replace(rows.next()); current.replace(entry, partitions); - filter.observe(entry, current); + if (minor && entry.isDelete()) { + deletes.add(entry, deletes.useRowIdFilter(), false); + } if (fragmented) { position++; continue; @@ -301,7 +324,7 @@ private static Discovery.DiscoveredManifest discoverManifestRuns( partitionType)); } Discovery.BlockInfo block = blocks.get(blocks.size() - 1); - block.collectForSort(entry, current, partitions, filter); + block.collectForSort(entry, current, partitions, deletes, identifier, minor); boolean inversion = hasPrevious && compareDiscoveryKeys(previous, current, partitions) > 0; if (inversion) { @@ -325,7 +348,7 @@ private static Discovery.DiscoveredManifest discoverManifestRuns( } position++; if (rows.recordIndex() + 1 == rawBlock.recordCount()) { - ManifestEntryRunMergeEntry.Key stableLastKey = current.stableCopy(); + SortKey stableLastKey = current.stableCopy(); block.finishSort(position, stableLastKey, partitionStatsConverter); previous.copyFrom(stableLastKey); } else { @@ -364,23 +387,17 @@ private static boolean exceedsStreamingReadAmplification( } private static int compareDiscoveryKeys( - ManifestEntryRunMergeEntry.Key left, - ManifestEntryRunMergeEntry.Key right, - SortPartitionDictionary partitions) { + SortKey left, SortKey right, SortPartitionDictionary partitions) { return compareRemainingKeys( left, right, partitions.compareIds(left.partitionId, right.partitionId)); } - static int compareMergeKeys( - ManifestEntryRunMergeEntry.Key left, ManifestEntryRunMergeEntry.Key right) { + static int compareMergeKeys(SortKey left, SortKey right) { return compareRemainingKeys( left, right, Integer.compare(left.partitionRank, right.partitionRank)); } - private static int compareRemainingKeys( - ManifestEntryRunMergeEntry.Key left, - ManifestEntryRunMergeEntry.Key right, - int comparison) { + private static int compareRemainingKeys(SortKey left, SortKey right, int comparison) { if (comparison == 0) { comparison = Byte.compare(left.kind, right.kind); } @@ -388,10 +405,10 @@ private static int compareRemainingKeys( comparison = Long.compare(left.firstRowId, right.firstRowId); } if (comparison == 0) { - comparison = Long.compare(left.rangeEnd, right.rangeEnd); + comparison = Long.compare(left.lastRowId, right.lastRowId); } if (comparison == 0) { - comparison = Long.compare(left.reverseSequence, right.reverseSequence); + comparison = Long.compare(left.descendingSequenceKey, right.descendingSequenceKey); } if (comparison == 0) { comparison = compareBytes(left, right); @@ -399,8 +416,7 @@ private static int compareRemainingKeys( return comparison; } - private static int compareBytes( - ManifestEntryRunMergeEntry.Key left, ManifestEntryRunMergeEntry.Key right) { + private static int compareBytes(SortKey left, SortKey right) { int minLength = Math.min(left.fileNameLength, right.fileNameLength); for (int i = 0; i < minLength; i++) { int leftByte = @@ -416,6 +432,80 @@ private static int compareBytes( return left.fileNameLength - right.fileNameLength; } + static final class SortKey { + + int partitionId; + int partitionRank; + byte kind; + long firstRowId; + long lastRowId; + long descendingSequenceKey; + MemorySegment[] fileNameSegments; + int fileNameOffset; + int fileNameLength; + byte[] ownedFileNameBytes; + MemorySegment[] ownedFileNameSegments; + + static SortKey viewOf(ProjectedManifestEntry entry, SortPartitionDictionary partitions) { + SortKey key = new SortKey(); + key.replace(entry, partitions); + return key; + } + + void replace(ProjectedManifestEntry entry, SortPartitionDictionary partitions) { + long firstRowId = entry.file().nonNullFirstRowId(); + this.partitionId = partitions.id(entry.partitionBytes()); + this.partitionRank = partitions.rank(partitionId); + this.kind = entry.kind().toByteValue(); + this.firstRowId = firstRowId; + this.lastRowId = firstRowId + entry.file().rowCount() - 1L; + this.descendingSequenceKey = Long.MAX_VALUE - entry.file().maxSequenceNumber(); + BinaryString fileName = entry.file().fileNameBinary(); + this.fileNameSegments = fileName.getSegments(); + this.fileNameOffset = fileName.getOffset(); + this.fileNameLength = fileName.getSizeInBytes(); + } + + void copyFrom(SortKey key) { + this.partitionId = key.partitionId; + this.partitionRank = key.partitionRank; + this.kind = key.kind; + this.firstRowId = key.firstRowId; + this.lastRowId = key.lastRowId; + this.descendingSequenceKey = key.descendingSequenceKey; + ensureFileNameCapacity(key.fileNameLength); + MemorySegmentUtils.copyToBytes( + key.fileNameSegments, + key.fileNameOffset, + ownedFileNameBytes, + 0, + key.fileNameLength); + this.fileNameSegments = ownedFileNameSegments; + this.fileNameOffset = 0; + this.fileNameLength = key.fileNameLength; + } + + SortKey stableCopy() { + SortKey copy = new SortKey(); + copy.copyFrom(this); + return copy; + } + + private void ensureFileNameCapacity(int length) { + if (ownedFileNameBytes == null || ownedFileNameBytes.length < length) { + ownedFileNameBytes = new byte[length]; + ownedFileNameSegments = + new MemorySegment[] {MemorySegment.wrap(ownedFileNameBytes)}; + } + } + + void clear() { + fileNameSegments = null; + ownedFileNameBytes = null; + ownedFileNameSegments = null; + } + } + /** Results and Avro block metadata collected while discovering natural manifest runs. */ static final class Discovery { @@ -461,9 +551,9 @@ void updatePartitionRanks(SortPartitionDictionary partitions) { } } - void finishFiltering(ManifestEntryRunMergeEntry.Filter filter) { + void finishFiltering(CollectedDeletes deletes, boolean minor) { for (BlockInfo block : blocks) { - block.finishFiltering(filter); + block.finishFiltering(deletes, minor); } } } @@ -472,81 +562,99 @@ static final class BlockInfo { final long ordinal; final long start; - final @Nullable ManifestEntryRunMergeEntry.Key firstKey; - boolean eligible; + final @Nullable SortKey firstKey; boolean sorted = true; long end; - @Nullable ManifestEntryRunMergeEntry.Key lastKey; - long addedFiles; - long deletedFiles; - long schemaId = Long.MIN_VALUE; - int minBucket = Integer.MAX_VALUE; - int maxBucket = Integer.MIN_VALUE; - int minLevel = Integer.MAX_VALUE; - int maxLevel = Integer.MIN_VALUE; - long minRowId = Long.MAX_VALUE; - long maxRowId = Long.MIN_VALUE; - final boolean singleFieldSortedPartitionStats; - @Nullable SimpleStatsCollector partitionStats; - final RowType partitionType; - @Nullable BinaryRow nullPartition; - long nullPartitionCount; - @Nullable BinaryRow minNonNullPartition; - @Nullable BinaryRow maxNonNullPartition; - EncodedBlockMeta metadata; + @Nullable SortKey lastKey; + long minRowId; + long maxRowId; + @Nullable BlockMetadataAccumulator metadataAccumulator; + @Nullable EncodedBlockMeta metadata; BlockInfo( long ordinal, long start, - boolean eligible, - ManifestEntryRunMergeEntry.Key firstKey, + boolean rawBlockCopySupported, + SortKey firstKey, RowType partitionType) { this.ordinal = ordinal; this.start = start; - this.eligible = eligible; this.firstKey = firstKey; - this.partitionType = partitionType; - this.singleFieldSortedPartitionStats = - eligible && firstKey != null && partitionType.getFieldCount() == 1; - this.partitionStats = - eligible && firstKey != null && !singleFieldSortedPartitionStats - ? new SimpleStatsCollector(partitionType) + this.metadataAccumulator = + rawBlockCopySupported && firstKey != null + ? new BlockMetadataAccumulator(partitionType) : null; } void collectForSort( ProjectedManifestEntry entry, - ManifestEntryRunMergeEntry.Key key, + SortKey key, SortPartitionDictionary partitions, - ManifestEntryRunMergeEntry.Filter filter) { - if (!eligible) { + CollectedDeletes deletes, + ReusableIdentifier identifier, + boolean minor) { + if (metadataAccumulator == null) { return; } - if (!filter.copyable(entry, key)) { - eligible = false; - releasePartitionStats(); + if (!deletes.copyable(entry, identifier, minor)) { + metadataAccumulator = null; return; } - collectEntryStats(entry, key); - BinaryRow partition = partitions.partition(key.partitionId); - if (singleFieldSortedPartitionStats) { - if (partition.isNullAt(0)) { - nullPartition = partition; - nullPartitionCount++; - } else { - if (minNonNullPartition == null) { - minNonNullPartition = partition; - } - maxNonNullPartition = partition; - } - } else { - partitionStats.collect(partition); + metadataAccumulator.collect(entry, key, partitions.partition(key.partitionId)); + } + + void finishSort( + long end, SortKey lastKey, SimpleStatsConverter partitionStatsConverter) { + this.end = end; + this.lastKey = lastKey; + if (metadataAccumulator != null && sorted) { + minRowId = metadataAccumulator.minRowId; + maxRowId = metadataAccumulator.maxRowId; + metadata = metadataAccumulator.finish(partitionStatsConverter); + } + metadataAccumulator = null; + } + + boolean copyable(long runStart, long runEnd) { + return metadata != null && start >= runStart && end <= runEnd; + } + + void finishFiltering(CollectedDeletes deletes, boolean minor) { + if (metadata != null + && minor + && (!deletes.useRowIdFilter() + || deletes.intersectsRowIds(minRowId, maxRowId))) { + metadata = null; } } - private void collectEntryStats( - ProjectedManifestEntry entry, ManifestEntryRunMergeEntry.Key key) { - if (key.kind == FileKind.ADD.toByteValue()) { + void updatePartitionRanks(SortPartitionDictionary partitions) { + checkState(firstKey != null && lastKey != null, "Manifest block has no sort keys."); + firstKey.partitionRank = partitions.rank(firstKey.partitionId); + lastKey.partitionRank = partitions.rank(lastKey.partitionId); + } + } + + /** Mutable statistics retained only while the current Avro block is being inspected. */ + private static final class BlockMetadataAccumulator { + + private long addedFiles; + private long deletedFiles; + private long schemaId = Long.MIN_VALUE; + private int minBucket = Integer.MAX_VALUE; + private int maxBucket = Integer.MIN_VALUE; + private int minLevel = Integer.MAX_VALUE; + private int maxLevel = Integer.MIN_VALUE; + private long minRowId = Long.MAX_VALUE; + private long maxRowId = Long.MIN_VALUE; + private final BlockPartitionStats partitionStats; + + private BlockMetadataAccumulator(RowType partitionType) { + this.partitionStats = new BlockPartitionStats(partitionType); + } + + private void collect(ProjectedManifestEntry entry, SortKey key, BinaryRow partition) { + if (entry.isAdd()) { addedFiles++; } else { deletedFiles++; @@ -559,71 +667,66 @@ private void collectEntryStats( minLevel = Math.min(minLevel, level); maxLevel = Math.max(maxLevel, level); minRowId = Math.min(minRowId, key.firstRowId); - maxRowId = Math.max(maxRowId, key.rangeEnd); + maxRowId = Math.max(maxRowId, key.lastRowId); + partitionStats.collect(partition); } - void finishSort( - long end, - ManifestEntryRunMergeEntry.Key lastKey, - SimpleStatsConverter partitionStatsConverter) { - this.end = end; - this.lastKey = lastKey; - if (eligible && sorted) { - SimpleStats encodedPartitionStats; - if (singleFieldSortedPartitionStats) { - BinaryRow min = - minNonNullPartition == null ? nullPartition : minNonNullPartition; - BinaryRow max = - maxNonNullPartition == null ? nullPartition : maxNonNullPartition; - checkState(min != null && max != null, "Manifest block has no partition."); - encodedPartitionStats = - new SimpleStats( - min, - max, - BinaryArray.fromLongArray(new Long[] {nullPartitionCount})); - } else { - checkState( - partitionStats != null, "Manifest block has no partition stats."); - encodedPartitionStats = - partitionStatsConverter.toBinaryAllMode(partitionStats.extract()); - } - metadata = - new EncodedBlockMeta( - addedFiles, - deletedFiles, - schemaId, - minBucket, - maxBucket, - minLevel, - maxLevel, - minRowId, - maxRowId, - encodedPartitionStats); - } - releasePartitionStats(); + private EncodedBlockMeta finish(SimpleStatsConverter partitionStatsConverter) { + return new EncodedBlockMeta( + addedFiles, + deletedFiles, + schemaId, + minBucket, + maxBucket, + minLevel, + maxLevel, + minRowId, + maxRowId, + partitionStats.finish(partitionStatsConverter)); } + } - private void releasePartitionStats() { - partitionStats = null; - nullPartition = null; - minNonNullPartition = null; - maxNonNullPartition = null; - } + /** Partition statistics for one sorted Avro block. */ + private static final class BlockPartitionStats { - boolean copyable(long runStart, long runEnd) { - return metadata != null && start >= runStart && end <= runEnd; + private final boolean singleField; + private final @Nullable SimpleStatsCollector collector; + private @Nullable BinaryRow nullPartition; + private @Nullable BinaryRow minNonNullPartition; + private @Nullable BinaryRow maxNonNullPartition; + private long nullCount; + + private BlockPartitionStats(RowType partitionType) { + this.singleField = partitionType.getFieldCount() == 1; + this.collector = singleField ? null : new SimpleStatsCollector(partitionType); } - void finishFiltering(ManifestEntryRunMergeEntry.Filter filter) { - if (metadata != null && !filter.copyableAfterDiscovery(minRowId, maxRowId)) { - metadata = null; + private void collect(BinaryRow partition) { + if (!singleField) { + checkState(collector != null, "Manifest block has no partition collector."); + collector.collect(partition); + return; + } + if (partition.isNullAt(0)) { + nullPartition = partition; + nullCount++; + } else { + if (minNonNullPartition == null) { + minNonNullPartition = partition; + } + maxNonNullPartition = partition; } } - void updatePartitionRanks(SortPartitionDictionary partitions) { - checkState(firstKey != null && lastKey != null, "Manifest block has no sort keys."); - firstKey.partitionRank = partitions.rank(firstKey.partitionId); - lastKey.partitionRank = partitions.rank(lastKey.partitionId); + private SimpleStats finish(SimpleStatsConverter converter) { + if (!singleField) { + checkState(collector != null, "Manifest block has no partition collector."); + return converter.toBinaryAllMode(collector.extract()); + } + BinaryRow min = minNonNullPartition == null ? nullPartition : minNonNullPartition; + BinaryRow max = maxNonNullPartition == null ? nullPartition : maxNonNullPartition; + checkState(min != null && max != null, "Manifest block has no partition."); + return new SimpleStats(min, max, BinaryArray.fromLongArray(new Long[] {nullCount})); } } } diff --git a/paimon-core/src/main/java/org/apache/paimon/operation/ManifestEntryRunMergeEntry.java b/paimon-core/src/main/java/org/apache/paimon/operation/ManifestEntryRunMergeEntry.java deleted file mode 100644 index 49bc399ac327..000000000000 --- a/paimon-core/src/main/java/org/apache/paimon/operation/ManifestEntryRunMergeEntry.java +++ /dev/null @@ -1,239 +0,0 @@ -/* - * 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.paimon.operation; - -import org.apache.paimon.data.BinaryString; -import org.apache.paimon.manifest.CompactFileIdentifierSet; -import org.apache.paimon.manifest.DeletedRowIdSet; -import org.apache.paimon.manifest.FileEntry.ReusableIdentifier; -import org.apache.paimon.manifest.FileKind; -import org.apache.paimon.manifest.ProjectedManifestEntry; -import org.apache.paimon.memory.MemorySegment; -import org.apache.paimon.memory.MemorySegmentUtils; - -import static org.apache.paimon.utils.Preconditions.checkState; - -/** Entry-level state shared by manifest run discovery and merge execution. */ -final class ManifestEntryRunMergeEntry { - - private ManifestEntryRunMergeEntry() {} - - static final class Key { - - int partitionId; - int partitionRank; - byte kind; - long firstRowId; - long rangeEnd; - long reverseSequence; - MemorySegment[] fileNameSegments; - int fileNameOffset; - int fileNameLength; - byte[] ownedFileNameBytes; - MemorySegment[] ownedFileNameSegments; - - static Key viewOf( - ProjectedManifestEntry entry, - ManifestEntryRunMerge.SortPartitionDictionary partitions) { - Key key = new Key(); - key.replace(entry, partitions); - return key; - } - - void replace( - ProjectedManifestEntry entry, - ManifestEntryRunMerge.SortPartitionDictionary partitions) { - long firstRowId = entry.file().nonNullFirstRowId(); - this.partitionId = partitions.id(entry.partitionBytes()); - this.partitionRank = partitions.rank(partitionId); - this.kind = entry.kind().toByteValue(); - this.firstRowId = firstRowId; - this.rangeEnd = firstRowId + entry.file().rowCount() - 1L; - this.reverseSequence = Long.MAX_VALUE - entry.file().maxSequenceNumber(); - BinaryString fileName = entry.file().fileNameBinary(); - this.fileNameSegments = fileName.getSegments(); - this.fileNameOffset = fileName.getOffset(); - this.fileNameLength = fileName.getSizeInBytes(); - } - - void copyFrom(Key key) { - this.partitionId = key.partitionId; - this.partitionRank = key.partitionRank; - this.kind = key.kind; - this.firstRowId = key.firstRowId; - this.rangeEnd = key.rangeEnd; - this.reverseSequence = key.reverseSequence; - ensureFileNameCapacity(key.fileNameLength); - MemorySegmentUtils.copyToBytes( - key.fileNameSegments, - key.fileNameOffset, - ownedFileNameBytes, - 0, - key.fileNameLength); - this.fileNameSegments = ownedFileNameSegments; - this.fileNameOffset = 0; - this.fileNameLength = key.fileNameLength; - } - - Key stableCopy() { - Key copy = new Key(); - copy.copyFrom(this); - return copy; - } - - private void ensureFileNameCapacity(int length) { - if (ownedFileNameBytes == null || ownedFileNameBytes.length < length) { - ownedFileNameBytes = new byte[length]; - ownedFileNameSegments = - new MemorySegment[] {MemorySegment.wrap(ownedFileNameBytes)}; - } - } - - void clear() { - fileNameSegments = null; - ownedFileNameBytes = null; - ownedFileNameSegments = null; - } - } - - static class Filter { - - final CompactFileIdentifierSet deletedIdentifiers; - final DeletedRowIdSet deletedRowIds; - final boolean useRowIdFilter; - final ThreadLocal identifier = - ThreadLocal.withInitial(ReusableIdentifier::new); - - Filter( - CompactFileIdentifierSet deletedIdentifiers, - DeletedRowIdSet deletedRowIds, - boolean useRowIdFilter) { - this.deletedIdentifiers = deletedIdentifiers; - this.deletedRowIds = deletedRowIds; - this.useRowIdFilter = useRowIdFilter; - } - - boolean include(ProjectedManifestEntry entry) { - return entry.isAdd() && !deletedIdentifiers.contains(identifier(entry)); - } - - boolean include(ProjectedManifestEntry entry, Key key) { - return key.kind == FileKind.ADD.toByteValue() && !isDeleted(entry, key); - } - - boolean copyable(ProjectedManifestEntry entry, Key key) { - return include(entry, key); - } - - void observe(ProjectedManifestEntry entry, Key key) {} - - boolean copyableAfterDiscovery(long minRowId, long maxRowId) { - return true; - } - - Filter forDiscovery() { - return this; - } - - void combine(Filter other) { - checkState(other == this, "Immutable manifest filter cannot collect local DELETEs."); - } - - ReusableIdentifier identifier(ProjectedManifestEntry entry) { - return identifier.get().replaceWithPartition(entry); - } - - void releaseIdentifier() { - ReusableIdentifier reusableIdentifier = identifier.get(); - reusableIdentifier.release(); - identifier.remove(); - } - - boolean isDeleted(ProjectedManifestEntry entry, Key key) { - // RowID is only a cheap negative filter. The complete identifier remains the - // authoritative match, and is also sufficient for manifests which predate RowID. - if (useRowIdFilter) { - if (!deletedRowIds.contains(key.firstRowId)) { - return false; - } - } - return deletedIdentifiers.contains(identifier(entry)); - } - - static final class Minor extends Filter { - - Minor( - CompactFileIdentifierSet deletedIdentifiers, - DeletedRowIdSet deletedRowIds, - boolean useRowIdFilter) { - super(deletedIdentifiers, deletedRowIds, useRowIdFilter); - } - - @Override - boolean include(ProjectedManifestEntry entry) { - return true; - } - - @Override - boolean include(ProjectedManifestEntry entry, Key key) { - return true; - } - - @Override - boolean copyable(ProjectedManifestEntry entry, Key key) { - return key.kind == FileKind.ADD.toByteValue(); - } - - @Override - void observe(ProjectedManifestEntry entry, Key key) { - if (key.kind != FileKind.DELETE.toByteValue()) { - return; - } - ReusableIdentifier reusable = identifier(entry); - deletedIdentifiers.add(reusable); - if (useRowIdFilter) { - deletedRowIds.add(key.firstRowId); - } - } - - @Override - Filter forDiscovery() { - return new Minor( - new CompactFileIdentifierSet(), new DeletedRowIdSet(), useRowIdFilter); - } - - @Override - void combine(Filter other) { - checkState(other instanceof Minor, "Cannot combine incompatible manifest filters."); - deletedIdentifiers.addAll(other.deletedIdentifiers); - deletedRowIds.addAll(other.deletedRowIds); - other.deletedIdentifiers.release(); - other.deletedRowIds.releaseRangeIndex(); - } - - @Override - boolean copyableAfterDiscovery(long minRowId, long maxRowId) { - // A DELETE preserves the deleted ADD's globally unique first RowID. A range hit may - // be a false positive and only disables block copying; a miss proves the block has - // no deleted ADD. - return useRowIdFilter && !deletedRowIds.intersects(minRowId, maxRowId); - } - } - } -} diff --git a/paimon-core/src/main/java/org/apache/paimon/operation/ManifestEntryRunMergePlan.java b/paimon-core/src/main/java/org/apache/paimon/operation/ManifestEntryRunMergePlan.java index 5528589fdcd8..997bfb362b7b 100644 --- a/paimon-core/src/main/java/org/apache/paimon/operation/ManifestEntryRunMergePlan.java +++ b/paimon-core/src/main/java/org/apache/paimon/operation/ManifestEntryRunMergePlan.java @@ -23,8 +23,8 @@ import org.apache.paimon.data.InternalRow; import org.apache.paimon.data.serializer.InternalRowSerializer; import org.apache.paimon.format.avro.AvroRawBlock; +import org.apache.paimon.manifest.CollectedDeletes; import org.apache.paimon.manifest.CompactFileIdentifierSet; -import org.apache.paimon.manifest.DeletedRowIdSet; import org.apache.paimon.manifest.FileEntry.ReusableIdentifier; import org.apache.paimon.manifest.FileKind; import org.apache.paimon.manifest.ManifestAvroReader; @@ -54,24 +54,30 @@ final class ManifestEntryRunMergePlan { final List sources; final ManifestEntryRunMerge.SortPartitionDictionary partitions; + final CollectedDeletes deletes; + final boolean minor; ManifestEntryRunMergePlan( - List sources, ManifestEntryRunMerge.SortPartitionDictionary partitions) { + List sources, + ManifestEntryRunMerge.SortPartitionDictionary partitions, + CollectedDeletes deletes, + boolean minor) { this.sources = sources; this.partitions = partitions; + this.deletes = deletes; + this.minor = minor; } List mergeToManifest( ManifestFileSorter.RowIdEntrySortKey sortKey, ManifestFile manifestFile, - ManifestEntryRunMergeEntry.Filter filter, List newFilesForAbort) throws Exception { List cursors = new ArrayList<>(sources.size()); Exception failure = null; try { for (Source.Spec source : sources) { - Cursor cursor = source.open(manifestFile, sortKey, filter, partitions); + Cursor cursor = source.open(manifestFile, sortKey, deletes, minor, partitions); cursors.add(cursor); cursor.advance(); } @@ -100,16 +106,13 @@ List mergeToManifest( Pair, List> mergeMinorToManifest( ManifestFileSorter.RowIdEntrySortKey sortKey, ManifestFile manifestFile, - ManifestEntryRunMergeEntry.Filter filter, - CompactFileIdentifierSet deletedIdentifiers, - DeletedRowIdSet deletedRowIds, List newFilesForAbort) throws Exception { List cursors = new ArrayList<>(sources.size()); Exception failure = null; try { for (Source.Spec source : sources) { - Cursor cursor = source.open(manifestFile, sortKey, filter, partitions); + Cursor cursor = source.open(manifestFile, sortKey, deletes, minor, partitions); cursors.add(cursor); cursor.advance(); } @@ -118,8 +121,7 @@ Pair, List> mergeMinorToManifest( return Pair.of(Collections.emptyList(), Collections.emptyList()); } Pair, List> files = - writeMinorSelected( - selectionTree, manifestFile, deletedIdentifiers, deletedRowIds); + writeMinorSelected(selectionTree, manifestFile, deletes); newFilesForAbort.addAll(files.getLeft()); newFilesForAbort.addAll(files.getRight()); return files; @@ -169,10 +171,7 @@ static List writeSelected( } private static Pair, List> writeMinorSelected( - SelectionTree selectionTree, - ManifestFile manifestFile, - CompactFileIdentifierSet deletedIdentifiers, - DeletedRowIdSet deletedRowIds) + SelectionTree selectionTree, ManifestFile manifestFile, CollectedDeletes deletes) throws Exception { ManifestAvroWriter addWriter = manifestFile.createAvroWriter(); ManifestAvroWriter deleteWriter = manifestFile.createAvroWriter(); @@ -192,15 +191,11 @@ private static Pair, List> writeMinorSe cursor.materializeCurrent(); if (cursor.key().kind == FileKind.ADD.toByteValue()) { - if (!deletedRowIds.contains(cursor.key().firstRowId)) { - writeCurrent(addWriter, cursor); + ReusableIdentifier identifier = cursor.identifier(); + if (deletes.isDeleted(cursor.current(), identifier)) { + matchedEntries.add(identifier); } else { - ReusableIdentifier identifier = cursor.identifier(); - if (deletedIdentifiers.contains(identifier)) { - matchedEntries.add(identifier); - } else { - writeCurrent(addWriter, cursor); - } + writeCurrent(addWriter, cursor); } } else { ReusableIdentifier identifier = cursor.identifier(); @@ -270,7 +265,8 @@ interface Spec { Cursor open( ManifestFile manifestFile, ManifestFileSorter.RowIdEntrySortKey sortKey, - ManifestEntryRunMergeEntry.Filter filter, + CollectedDeletes deletes, + boolean minor, ManifestEntryRunMerge.SortPartitionDictionary partitions) throws Exception; } @@ -311,11 +307,12 @@ long prefixBlockCount() { public Cursor open( ManifestFile manifestFile, ManifestFileSorter.RowIdEntrySortKey sortKey, - ManifestEntryRunMergeEntry.Filter filter, + CollectedDeletes deletes, + boolean minor, ManifestEntryRunMerge.SortPartitionDictionary partitions) throws Exception { return new PrimitiveManifestRunCursor( - manifestFile, meta, start, end, blocks, filter, partitions); + manifestFile, meta, start, end, blocks, deletes, minor, partitions); } } @@ -331,10 +328,12 @@ static final class FragmentedManifestSpec implements Spec { public Cursor open( ManifestFile manifestFile, ManifestFileSorter.RowIdEntrySortKey sortKey, - ManifestEntryRunMergeEntry.Filter filter, + CollectedDeletes deletes, + boolean minor, ManifestEntryRunMerge.SortPartitionDictionary partitions) throws Exception { - return new InMemoryManifestCursor(manifestFile, meta, sortKey, filter, partitions); + return new InMemoryManifestCursor( + manifestFile, meta, sortKey, deletes, minor, partitions); } } } @@ -351,7 +350,7 @@ interface Cursor extends AutoCloseable { @Nullable EncodedEntry metadata(); - ManifestEntryRunMergeEntry.Key key(); + ManifestEntryRunMerge.SortKey key(); @Nullable ByteBuffer encodedRecord(); @@ -366,7 +365,7 @@ default boolean hasCopyableBlock() { return false; } - default ManifestEntryRunMergeEntry.Key blockLastKey() { + default ManifestEntryRunMerge.SortKey blockLastKey() { throw new UnsupportedOperationException(); } @@ -392,14 +391,16 @@ static final class PrimitiveManifestRunCursor implements Cursor { final ManifestAvroReader reader; final boolean encodedRecordsCompatible; - final ManifestEntryRunMergeEntry.Filter filter; + final CollectedDeletes deletes; + final boolean minor; final ManifestEntryRunMerge.SortPartitionDictionary partitions; - final ManifestEntryRunMergeEntry.Key key = new ManifestEntryRunMergeEntry.Key(); + final ManifestEntryRunMerge.SortKey key = new ManifestEntryRunMerge.SortKey(); final EncodedEntry metadata = new EncodedEntry(); final ProjectedManifestEntry projectedEntry = ProjectedManifestEntry.ENTRY_LAYOUT_PROJECTION.createEntry(); final ProjectedManifestEntry fullEntry = ProjectedManifestEntry.fullProjection().createEntry(); + final ReusableIdentifier identifier = new ReusableIdentifier(); final List blocks; final long runStart; final long runEnd; @@ -421,12 +422,14 @@ static final class PrimitiveManifestRunCursor implements Cursor { long start, long end, List blocks, - ManifestEntryRunMergeEntry.Filter filter, + CollectedDeletes deletes, + boolean minor, ManifestEntryRunMerge.SortPartitionDictionary partitions) throws Exception { this.reader = manifestFile.scanAvroBlocks(meta.fileName(), meta.fileSize()); this.encodedRecordsCompatible = reader.rawBlockCopySupported(); - this.filter = filter; + this.deletes = deletes; + this.minor = minor; this.partitions = partitions; this.blocks = blocks; this.runStart = start; @@ -472,7 +475,7 @@ public boolean advance() throws Exception { : fullEntry.replace(currentSourceRow); decodedRemaining--; key.replace(currentEntry, partitions); - if (filter.include(currentEntry, key)) { + if (minor || deletes.copyable(currentEntry, identifier, false)) { current = true; metadata.replace( key.kind, @@ -553,7 +556,7 @@ public EncodedEntry metadata() { } @Override - public ManifestEntryRunMergeEntry.Key key() { + public ManifestEntryRunMerge.SortKey key() { return key; } @@ -570,7 +573,7 @@ public InternalRow decodedRow() { @Override public ReusableIdentifier identifier() { checkState(current, "Manifest entry has not been materialized."); - return filter.identifier(currentEntry); + return identifier.replaceWithPartition(currentEntry); } @Override @@ -579,7 +582,7 @@ public boolean hasCopyableBlock() { } @Override - public ManifestEntryRunMergeEntry.Key blockLastKey() { + public ManifestEntryRunMerge.SortKey blockLastKey() { return currentBlock.lastKey; } @@ -624,7 +627,7 @@ public void materializeCurrent() throws Exception { decodedRemaining--; key.replace(currentEntry, partitions); checkState( - filter.include(currentEntry, key), + minor || deletes.copyable(currentEntry, identifier, false), "Copyable manifest block contains a filtered entry."); current = true; metadata.replace( @@ -651,6 +654,7 @@ public void close() throws Exception { currentEntry = null; projectedEntry.clear(); fullEntry.clear(); + identifier.release(); currentBlock = null; rawBlock = false; key.clear(); @@ -670,7 +674,8 @@ static final class InMemoryManifestCursor implements Cursor { ManifestFile manifestFile, ManifestFileMeta meta, ManifestFileSorter.RowIdEntrySortKey sortKey, - ManifestEntryRunMergeEntry.Filter filter, + CollectedDeletes deletes, + boolean minor, ManifestEntryRunMerge.SortPartitionDictionary partitions) throws Exception { long entryCount = meta.numAddedFiles() + meta.numDeletedFiles(); @@ -682,14 +687,14 @@ static final class InMemoryManifestCursor implements Cursor { manifestFile.scan(meta.fileName(), ProjectedManifestEntry.fullProjection())) { while (iterator.hasNext()) { ProjectedManifestEntry entry = iterator.next(); - if (!filter.include(entry)) { + if (!minor && !deletes.copyable(entry, identifier, false)) { continue; } BinaryRow row = serializer.toBinaryRow(entry.fullRow()).copy(); entries.add( new StoredEntry( row, - ManifestEntryRunMergeEntry.Key.viewOf( + ManifestEntryRunMerge.SortKey.viewOf( view.replace(row), partitions))); } } @@ -726,7 +731,7 @@ public EncodedEntry metadata() { } @Override - public ManifestEntryRunMergeEntry.Key key() { + public ManifestEntryRunMerge.SortKey key() { return entries.get(position).key; } @@ -752,9 +757,9 @@ public void close() { private static final class StoredEntry { final BinaryRow row; - final ManifestEntryRunMergeEntry.Key key; + final ManifestEntryRunMerge.SortKey key; - StoredEntry(BinaryRow row, ManifestEntryRunMergeEntry.Key key) { + StoredEntry(BinaryRow row, ManifestEntryRunMerge.SortKey key) { this.row = row; this.key = key; } @@ -815,7 +820,7 @@ int select(int left, int right) { return comparison < 0 || (comparison == 0 && left < right) ? left : right; } - boolean blockPrecedesOthers(int cursor, ManifestEntryRunMergeEntry.Key blockLastKey) { + boolean blockPrecedesOthers(int cursor, ManifestEntryRunMerge.SortKey blockLastKey) { for (int other = 0; other < cursors.size(); other++) { if (other == cursor || !cursors.get(other).hasCurrent()) { continue; diff --git a/paimon-core/src/main/java/org/apache/paimon/operation/ManifestFileSorter.java b/paimon-core/src/main/java/org/apache/paimon/operation/ManifestFileSorter.java index eda776751df8..76c5b0ef5ca0 100644 --- a/paimon-core/src/main/java/org/apache/paimon/operation/ManifestFileSorter.java +++ b/paimon-core/src/main/java/org/apache/paimon/operation/ManifestFileSorter.java @@ -26,8 +26,7 @@ import org.apache.paimon.data.GenericRow; import org.apache.paimon.data.InternalRow; import org.apache.paimon.disk.IOManager; -import org.apache.paimon.manifest.CompactFileIdentifierSet; -import org.apache.paimon.manifest.DeletedRowIdSet; +import org.apache.paimon.manifest.CollectedDeletes; import org.apache.paimon.manifest.ManifestEntry; import org.apache.paimon.manifest.ManifestFile; import org.apache.paimon.manifest.ManifestFileMeta; @@ -75,8 +74,7 @@ static class CompactionContext { final ManifestSortKey sortKey; final RowType partitionType; final ManifestEntryExternalSort.ExternalSortConfig externalSortConfig; - final CompactFileIdentifierSet deleteEntries; - final DeletedRowIdSet deletedRowIds; + final CollectedDeletes deletes; /** * Manifest files that need unsorted compaction. * @@ -96,8 +94,7 @@ static class CompactionContext { ManifestSortKey sortKey, RowType partitionType, ManifestEntryExternalSort.ExternalSortConfig externalSortConfig, - CompactFileIdentifierSet deleteEntries, - DeletedRowIdSet deletedRowIds, + CollectedDeletes deletes, Map defaultCompactFiles, List levelRuns, List pickedRuns) { @@ -106,8 +103,7 @@ static class CompactionContext { this.sortKey = sortKey; this.partitionType = partitionType; this.externalSortConfig = externalSortConfig; - this.deleteEntries = deleteEntries; - this.deletedRowIds = deletedRowIds; + this.deletes = deletes; this.defaultCompactFiles = defaultCompactFiles; this.levelRuns = levelRuns; this.pickedRuns = pickedRuns; @@ -122,8 +118,7 @@ boolean isMarkedForDefaultCompaction(ManifestFileMeta file) { /** Result of classifying manifest files. */ static class ClassifyResult { final List lsmFiles; - final CompactFileIdentifierSet deleteEntries; - final DeletedRowIdSet deletedRowIds; + final CollectedDeletes deletes; /** * Manifest files that need unsorted compaction. * @@ -136,32 +131,14 @@ static class ClassifyResult { ClassifyResult( List lsmFiles, - CompactFileIdentifierSet deleteEntries, - DeletedRowIdSet deletedRowIds, + CollectedDeletes deletes, Map compactWithoutSort) { this.lsmFiles = lsmFiles; - this.deleteEntries = deleteEntries; - this.deletedRowIds = deletedRowIds; + this.deletes = deletes; this.compactWithoutSort = compactWithoutSort; } } - /** Binary identifiers and partition values collected from DELETE entries. */ - private static class DeletedEntryInfo { - final CompactFileIdentifierSet identifiers; - final DeletedRowIdSet rowIds; - final Set partitions; - - private DeletedEntryInfo( - CompactFileIdentifierSet identifiers, - DeletedRowIdSet rowIds, - Set partitions) { - this.identifiers = identifiers; - this.rowIds = rowIds; - this.partitions = partitions; - } - } - /** * Try to sort-rewrite the merged manifest list by a configured partition field. If the sort * field cannot be resolved, the input is returned as-is. @@ -268,64 +245,68 @@ private static Optional> tryFullCompaction( sortedRunSizeRatio, externalSortConfig, manifestReadParallelism); - List levelRuns = ctx.levelRuns; - List pickedRuns = ctx.pickedRuns; - - if (pickedRuns.isEmpty() && ctx.defaultCompactFiles.isEmpty()) { - LOG.debug( - "Manifest sort full compact skipped: no runs picked and no defaultCompactFiles."); - return Optional.empty(); - } + try { + List levelRuns = ctx.levelRuns; + List pickedRuns = ctx.pickedRuns; + + if (pickedRuns.isEmpty() && ctx.defaultCompactFiles.isEmpty()) { + LOG.debug( + "Manifest sort full compact skipped: no runs picked and no defaultCompactFiles."); + return Optional.empty(); + } - LOG.info( - "Manifest sort full compact: input={} files, lsm={} runs, picked={} runs, " - + "defaultCompactFiles={}.", - input.size(), - levelRuns.size(), - pickedRuns.size(), - ctx.defaultCompactFiles.size()); - - // Step 3: Collect reused files (not picked) and picked files - Set pickedSet = new HashSet<>(pickedRuns); - List result = new ArrayList<>(); - for (ManifestAdjacentSortedRun run : levelRuns) { - if (!pickedSet.contains(run)) { - result.addAll(run.files()); + LOG.info( + "Manifest sort full compact: input={} files, lsm={} runs, picked={} runs, " + + "defaultCompactFiles={}.", + input.size(), + levelRuns.size(), + pickedRuns.size(), + ctx.defaultCompactFiles.size()); + + // Step 3: Collect reused files (not picked) and picked files + Set pickedSet = new HashSet<>(pickedRuns); + List result = new ArrayList<>(); + for (ManifestAdjacentSortedRun run : levelRuns) { + if (!pickedSet.contains(run)) { + result.addAll(run.files()); + } } - } - List pickedFiles = new ArrayList<>(); - for (ManifestAdjacentSortedRun run : pickedRuns) { - pickedFiles.addAll(run.files()); - } - pickedFiles.addAll(ctx.defaultCompactFiles.keySet()); + List pickedFiles = new ArrayList<>(); + for (ManifestAdjacentSortedRun run : pickedRuns) { + pickedFiles.addAll(run.files()); + } + pickedFiles.addAll(ctx.defaultCompactFiles.keySet()); - // Step 4: Split into sections and merge small adjacent sections - List
sections = splitIntoSections(pickedFiles, ctx); - sections = mergeSmallAdjacentSections(sections, suggestedMetaSize); + // Step 4: Split into sections and merge small adjacent sections + List
sections = splitIntoSections(pickedFiles, ctx); + sections = mergeSmallAdjacentSections(sections, suggestedMetaSize); - LOG.info( - "Manifest sort full compact: pickedFiles={}, sections={}.", - pickedFiles.size(), - sections.size()); + LOG.info( + "Manifest sort full compact: pickedFiles={}, sections={}.", + pickedFiles.size(), + sections.size()); - // Step 5: Rewrite sections - FullCompactOutput output = new FullCompactOutput(result); - rewriteSections( - sections, - output, - newFilesForAbort, - ctx, - manifestFile, - suggestedMetaSize, - suggestedMinMetaCount, - maxRewriteSize, - manifestReadParallelism); + // Step 5: Rewrite sections + FullCompactOutput output = new FullCompactOutput(result); + rewriteSections( + sections, + output, + newFilesForAbort, + ctx, + manifestFile, + suggestedMetaSize, + suggestedMinMetaCount, + maxRewriteSize, + manifestReadParallelism); - LOG.info( - "Manifest sort full compact completed: input={}, resultFiles={}.", - input.size(), - result.size()); - return Optional.of(result); + LOG.info( + "Manifest sort full compact completed: input={}, resultFiles={}.", + input.size(), + result.size()); + return Optional.of(result); + } finally { + ctx.deletes.release(); + } } /** @@ -365,95 +346,99 @@ private static List tryMinorCompaction( sortedRunSizeRatio, externalSortConfig, manifestReadParallelism); - List levelRuns = ctx.levelRuns; - List pickedRuns = ctx.pickedRuns; - - if (pickedRuns.isEmpty() && ctx.defaultCompactFiles.isEmpty()) { - LOG.debug( - "Manifest sort minor compact skipped: no runs picked and no defaultCompactFiles."); - return input; - } - - LOG.info( - "Manifest sort minor compact: input={} files, lsm={} runs, picked={} runs, " - + "defaultCompactFiles={}.", - input.size(), - levelRuns.size(), - pickedRuns.size(), - ctx.defaultCompactFiles.size()); - - // Step 2: Build fileName -> index mapping and initialize 2D result - Map fileNameToIndex = new HashMap<>(); - List> result = new ArrayList<>(input.size()); - for (int i = 0; i < input.size(); i++) { - fileNameToIndex.put(input.get(i).fileName(), i); - result.add(new ArrayList<>()); - } - - // Step 3: Collect reused files and picked files - Set pickedSet = new HashSet<>(pickedRuns); - for (ManifestAdjacentSortedRun run : levelRuns) { - if (!pickedSet.contains(run)) { - for (ManifestFileMeta file : run.files()) { - Integer idx = fileNameToIndex.get(file.fileName()); - if (idx != null) { - result.get(idx).add(file); + try { + List levelRuns = ctx.levelRuns; + List pickedRuns = ctx.pickedRuns; + + if (pickedRuns.isEmpty() && ctx.defaultCompactFiles.isEmpty()) { + LOG.debug( + "Manifest sort minor compact skipped: no runs picked and no defaultCompactFiles."); + return input; + } + + LOG.info( + "Manifest sort minor compact: input={} files, lsm={} runs, picked={} runs, " + + "defaultCompactFiles={}.", + input.size(), + levelRuns.size(), + pickedRuns.size(), + ctx.defaultCompactFiles.size()); + + // Step 2: Build fileName -> index mapping and initialize 2D result + Map fileNameToIndex = new HashMap<>(); + List> result = new ArrayList<>(input.size()); + for (int i = 0; i < input.size(); i++) { + fileNameToIndex.put(input.get(i).fileName(), i); + result.add(new ArrayList<>()); + } + + // Step 3: Collect reused files and picked files + Set pickedSet = new HashSet<>(pickedRuns); + for (ManifestAdjacentSortedRun run : levelRuns) { + if (!pickedSet.contains(run)) { + for (ManifestFileMeta file : run.files()) { + Integer idx = fileNameToIndex.get(file.fileName()); + if (idx != null) { + result.get(idx).add(file); + } } } } - } - List pickedFiles = new ArrayList<>(); - for (ManifestAdjacentSortedRun run : pickedRuns) { - pickedFiles.addAll(run.files()); - } - pickedFiles.addAll(ctx.defaultCompactFiles.keySet()); - - // Step 4: Compute index range - int minIdx = Integer.MAX_VALUE; - int maxIdx = Integer.MIN_VALUE; - for (ManifestFileMeta meta : pickedFiles) { - Integer idx = fileNameToIndex.get(meta.fileName()); - if (idx != null) { - minIdx = Math.min(minIdx, idx); - maxIdx = Math.max(maxIdx, idx); + List pickedFiles = new ArrayList<>(); + for (ManifestAdjacentSortedRun run : pickedRuns) { + pickedFiles.addAll(run.files()); } - } - Pair indexRange = Pair.of(minIdx, maxIdx); + pickedFiles.addAll(ctx.defaultCompactFiles.keySet()); + + // Step 4: Compute index range + int minIdx = Integer.MAX_VALUE; + int maxIdx = Integer.MIN_VALUE; + for (ManifestFileMeta meta : pickedFiles) { + Integer idx = fileNameToIndex.get(meta.fileName()); + if (idx != null) { + minIdx = Math.min(minIdx, idx); + maxIdx = Math.max(maxIdx, idx); + } + } + Pair indexRange = Pair.of(minIdx, maxIdx); - // Step 5: Split into sections and merge small adjacent sections - List
sections = splitIntoSections(pickedFiles, ctx); - sections = mergeSmallAdjacentSections(sections, suggestedMetaSize); + // Step 5: Split into sections and merge small adjacent sections + List
sections = splitIntoSections(pickedFiles, ctx); + sections = mergeSmallAdjacentSections(sections, suggestedMetaSize); - LOG.info( - "Manifest sort minor compact: pickedFiles={}, sections={}.", - pickedFiles.size(), - sections.size()); + LOG.info( + "Manifest sort minor compact: pickedFiles={}, sections={}.", + pickedFiles.size(), + sections.size()); - // Step 6: Rewrite sections - MinorCompactOutput output = new MinorCompactOutput(result, indexRange, fileNameToIndex); - rewriteSections( - sections, - output, - newFilesForAbort, - ctx, - manifestFile, - suggestedMetaSize, - suggestedMinMetaCount, - maxRewriteSize, - manifestReadParallelism); + // Step 6: Rewrite sections + MinorCompactOutput output = new MinorCompactOutput(result, indexRange, fileNameToIndex); + rewriteSections( + sections, + output, + newFilesForAbort, + ctx, + manifestFile, + suggestedMetaSize, + suggestedMinMetaCount, + maxRewriteSize, + manifestReadParallelism); - // Step 7: Flatten 2D result into a single list - List flatResult = new ArrayList<>(); - for (List subList : result) { - flatResult.addAll(subList); - } + // Step 7: Flatten 2D result into a single list + List flatResult = new ArrayList<>(); + for (List subList : result) { + flatResult.addAll(subList); + } - LOG.info( - "Manifest sort minor compact completed: input={}, resultFiles={}.", - input.size(), - flatResult.size()); - return flatResult; + LOG.info( + "Manifest sort minor compact completed: input={}, resultFiles={}.", + input.size(), + flatResult.size()); + return flatResult; + } finally { + ctx.deletes.release(); + } } /** @@ -508,8 +493,7 @@ private static CompactionContext prepareCompaction( sortKey, partitionType, externalSortConfig, - classification.deleteEntries, - classification.deletedRowIds, + classification.deletes, classification.compactWithoutSort, levelRuns, pickedRuns); @@ -535,7 +519,7 @@ static boolean reachesFullCompactionThreshold( *

Non-full compaction: small files go to defaultCompactFiles for minor-style merge; the rest * are returned as lsmFiles. * - * @return classification containing lsmFiles, deleteEntries, and defaultCompactFiles + * @return classification containing lsmFiles, collected DELETEs, and defaultCompactFiles */ static ClassifyResult classifyManifests( List input, @@ -565,28 +549,26 @@ private static ClassifyResult classifyManifests( // Initialize classification containers and read delete entries Map defaultCompactFiles = new LinkedHashMap<>(); List lsmFiles = new LinkedList<>(input); - CompactFileIdentifierSet classifiedDeleteEntries = new CompactFileIdentifierSet(); - DeletedRowIdSet deletedRowIds = new DeletedRowIdSet(); - Set deletePartitions = Collections.emptySet(); + CollectedDeletes deletes; PartitionPredicate predicate = null; if (fullCompaction) { - DeletedEntryInfo deletedEntries = + deletes = readDeletedEntries( manifestFile, input, runMergeOptimizeEnabled, manifestReadParallelism); - classifiedDeleteEntries = deletedEntries.identifiers; - deletedRowIds = deletedEntries.rowIds; - deletePartitions = deletedEntries.partitions; // Build partition predicate from delete entries for overlap detection. - if (classifiedDeleteEntries.isEmpty()) { + if (deletes.isEmpty()) { predicate = PartitionPredicate.ALWAYS_FALSE; } else { if (partitionType.getFieldCount() > 0) { - predicate = PartitionPredicate.fromMultiple(partitionType, deletePartitions); + predicate = + PartitionPredicate.fromMultiple(partitionType, deletes.partitions()); } else { predicate = PartitionPredicate.ALWAYS_TRUE; } } + } else { + deletes = new CollectedDeletes(runMergeOptimizeEnabled); } // Classify each file based on size and delete-partition overlap @@ -607,18 +589,15 @@ private static ClassifyResult classifyManifests( } } - return new ClassifyResult( - lsmFiles, classifiedDeleteEntries, deletedRowIds, defaultCompactFiles); + return new ClassifyResult(lsmFiles, deletes.toImmutable(), defaultCompactFiles); } - private static DeletedEntryInfo readDeletedEntries( + private static CollectedDeletes readDeletedEntries( ManifestFile manifestFile, List manifestFiles, boolean runMergeOptimizeEnabled, @Nullable Integer manifestReadParallelism) { - CompactFileIdentifierSet identifiers = new CompactFileIdentifierSet(); - DeletedRowIdSet rowIds = new DeletedRowIdSet(); - Set partitions = new HashSet<>(); + CollectedDeletes deletes = new CollectedDeletes(runMergeOptimizeEnabled); List filesWithDeletes = new ArrayList<>(); for (ManifestFileMeta meta : manifestFiles) { if (meta.numDeletedFiles() > 0) { @@ -629,44 +608,29 @@ private static DeletedEntryInfo readDeletedEntries( if (filesWithDeletes.size() <= 1 || (manifestReadParallelism != null && manifestReadParallelism <= 1)) { for (ManifestFileMeta meta : filesWithDeletes) { - collectDeletedEntries( - meta, - manifestFile, - identifiers, - rowIds, - partitions, - runMergeOptimizeEnabled, - false); + CollectedDeletes local = + collectDeletedEntries(meta, manifestFile, runMergeOptimizeEnabled); + deletes.combine(local); + local.release(); } } else { - Function> reader = - meta -> { - collectDeletedEntries( - meta, - manifestFile, - identifiers, - rowIds, - partitions, - runMergeOptimizeEnabled, - true); - return Collections.singletonList(Boolean.TRUE); - }; - for (Boolean ignored : + Function> reader = + meta -> + Collections.singletonList( + collectDeletedEntries( + meta, manifestFile, runMergeOptimizeEnabled)); + for (CollectedDeletes local : sequentialBatchedExecute(reader, filesWithDeletes, manifestReadParallelism)) { - // Iteration waits for each bounded batch of parallel reads. + deletes.combine(local); + local.release(); } } - return new DeletedEntryInfo(identifiers, rowIds, partitions); + return deletes; } - private static void collectDeletedEntries( - ManifestFileMeta meta, - ManifestFile manifestFile, - CompactFileIdentifierSet identifiers, - DeletedRowIdSet rowIds, - Set partitions, - boolean runMergeOptimizeEnabled, - boolean synchronize) { + private static CollectedDeletes collectDeletedEntries( + ManifestFileMeta meta, ManifestFile manifestFile, boolean runMergeOptimizeEnabled) { + CollectedDeletes deletes = new CollectedDeletes(runMergeOptimizeEnabled); try (CloseableIterator entries = manifestFile.scan( meta.fileName(), ProjectedManifestEntry.DELETE_ENTRY_PROJECTION)) { @@ -675,27 +639,14 @@ private static void collectDeletedEntries( if (!entry.isDelete()) { continue; } - BinaryRow partition = entry.partition().copy(); - if (synchronize) { - synchronized (identifiers) { - identifiers.add(entry); - if (runMergeOptimizeEnabled) { - rowIds.add(entry.file().nonNullFirstRowId()); - } - partitions.add(partition); - } - } else { - identifiers.add(entry); - if (runMergeOptimizeEnabled) { - rowIds.add(entry.file().nonNullFirstRowId()); - } - partitions.add(partition); - } + deletes.add(entry, runMergeOptimizeEnabled, true); } } catch (Exception e) { + deletes.release(); throw new RuntimeException( String.format("Failed to scan manifest file '%s'.", meta.fileName()), e); } + return deletes; } /** @@ -1089,7 +1040,7 @@ private static void unsortedCompactSection( } // Flush tail only if delete entries exist or file count >= minCount. if (!candidates.isEmpty()) { - if (!ctx.deleteEntries.isEmpty() || candidates.size() >= suggestedMinMetaCount) { + if (!ctx.deletes.isEmpty() || candidates.size() >= suggestedMinMetaCount) { rewriteSection( candidates, output, @@ -1153,8 +1104,7 @@ private static void rewriteFull( ctx.partitionType, manifestFile, sortNewFiles, - ctx.deleteEntries, - ctx.deletedRowIds, + ctx.deletes, ctx.externalSortConfig.maxNumFileHandles, manifestReadParallelism); } @@ -1166,7 +1116,7 @@ private static void rewriteFull( ctx.externalSortConfig, manifestFile, sortNewFiles, - ctx.deleteEntries, + ctx.deletes, manifestReadParallelism); } if (!sorted.isEmpty()) {