This is an automated email from the ASF dual-hosted git repository.

JingsongLi pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/paimon.git


The following commit(s) were added to refs/heads/master by this push:
     new 653175d8e2 [core][flink] Support external sort for manifest sort 
(#8357)
653175d8e2 is described below

commit 653175d8e23cec69035e8636dd342a0c08ac970e
Author: umi <[email protected]>
AuthorDate: Sun Jun 28 00:12:58 2026 +0800

    [core][flink] Support external sort for manifest sort (#8357)
    
    Improve manifest file sort compaction to avoid heap-heavy in-memory
    sorting for large manifest entries by introducing spillable external
    sorting. The change also propagates a Paimon IOManager through commit
    paths so manifest sort can use configured spill directories in Flink,
    while non-Flink/default contexts can keep IOManager creation local to
    the sorter.
---
 .../apache/paimon/operation/FileStoreCommit.java   |   3 +
 .../paimon/operation/FileStoreCommitImpl.java      |  17 +-
 .../operation/ManifestEntryExternalSort.java       | 304 ++++++++++++++++++
 .../paimon/operation/ManifestFileMerger.java       |  12 +-
 .../paimon/operation/ManifestFileSorter.java       | 342 ++++++++++++---------
 .../org/apache/paimon/table/sink/TableCommit.java  |   6 +
 .../apache/paimon/table/sink/TableCommitImpl.java  |   7 +
 .../paimon/manifest/ManifestFileMetaTest.java      |  95 ++++++
 .../org/apache/paimon/flink/sink/Committer.java    |  31 ++
 .../paimon/flink/sink/CommitterOperator.java       |   6 +-
 .../apache/paimon/flink/sink/StoreCommitter.java   |  14 +-
 11 files changed, 681 insertions(+), 156 deletions(-)

diff --git 
a/paimon-core/src/main/java/org/apache/paimon/operation/FileStoreCommit.java 
b/paimon-core/src/main/java/org/apache/paimon/operation/FileStoreCommit.java
index dbd316d8fa..2dab92854c 100644
--- a/paimon-core/src/main/java/org/apache/paimon/operation/FileStoreCommit.java
+++ b/paimon-core/src/main/java/org/apache/paimon/operation/FileStoreCommit.java
@@ -19,6 +19,7 @@
 package org.apache.paimon.operation;
 
 import org.apache.paimon.Snapshot;
+import org.apache.paimon.disk.IOManager;
 import org.apache.paimon.fs.FileIO;
 import org.apache.paimon.manifest.ManifestCommittable;
 import org.apache.paimon.operation.metrics.CommitMetrics;
@@ -34,6 +35,8 @@ import java.util.Map;
 /** Commit operation which provides commit and overwrite. */
 public interface FileStoreCommit extends AutoCloseable {
 
+    FileStoreCommit withIOManager(IOManager ioManager);
+
     FileStoreCommit ignoreEmptyCommit(boolean ignoreEmptyCommit);
 
     FileStoreCommit withPartitionExpire(PartitionExpire partitionExpire);
diff --git 
a/paimon-core/src/main/java/org/apache/paimon/operation/FileStoreCommitImpl.java
 
b/paimon-core/src/main/java/org/apache/paimon/operation/FileStoreCommitImpl.java
index b97b12f74e..6495d10c27 100644
--- 
a/paimon-core/src/main/java/org/apache/paimon/operation/FileStoreCommitImpl.java
+++ 
b/paimon-core/src/main/java/org/apache/paimon/operation/FileStoreCommitImpl.java
@@ -25,6 +25,7 @@ import org.apache.paimon.annotation.VisibleForTesting;
 import org.apache.paimon.catalog.SnapshotCommit;
 import org.apache.paimon.data.BinaryRow;
 import org.apache.paimon.data.InternalRow;
+import org.apache.paimon.disk.IOManager;
 import org.apache.paimon.fs.FileIO;
 import org.apache.paimon.io.DataFileMeta;
 import org.apache.paimon.io.DataFilePathFactory;
@@ -164,6 +165,7 @@ public class FileStoreCommitImpl implements FileStoreCommit 
{
     private boolean appendCommitCheckConflict = false;
     private long lastCommittedSnapshotId = -1L;
     @Nullable private Snapshot.Operation operation;
+    @Nullable private IOManager ioManager;
 
     public FileStoreCommitImpl(
             SnapshotCommit snapshotCommit,
@@ -228,6 +230,12 @@ public class FileStoreCommitImpl implements 
FileStoreCommit {
         this.commitCleaner = new CommitCleaner(manifestList, manifestFile, 
indexManifestFile);
     }
 
+    @Override
+    public FileStoreCommit withIOManager(IOManager ioManager) {
+        this.ioManager = ioManager;
+        return this;
+    }
+
     @Override
     public FileStoreCommit ignoreEmptyCommit(boolean ignoreEmptyCommit) {
         this.ignoreEmptyCommit = ignoreEmptyCommit;
@@ -1019,7 +1027,11 @@ public class FileStoreCommitImpl implements 
FileStoreCommit {
             } else {
                 mergeAfterManifests =
                         ManifestFileMerger.merge(
-                                mergeBeforeManifests, manifestFile, 
partitionType, options);
+                                mergeBeforeManifests,
+                                manifestFile,
+                                partitionType,
+                                options,
+                                ioManager);
             }
             baseManifestList = manifestList.write(mergeAfterManifests);
 
@@ -1317,7 +1329,8 @@ public class FileStoreCommitImpl implements 
FileStoreCommit {
                         mergeBeforeManifests,
                         manifestFile,
                         partitionType,
-                        new CoreOptions(compactOptions));
+                        new CoreOptions(compactOptions),
+                        ioManager);
 
         if (new HashSet<>(mergeBeforeManifests).equals(new 
HashSet<>(mergeAfterManifests))) {
             // no need to commit this snapshot, because no compact were 
happened
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
new file mode 100644
index 0000000000..5f2c342cc4
--- /dev/null
+++ 
b/paimon-core/src/main/java/org/apache/paimon/operation/ManifestEntryExternalSort.java
@@ -0,0 +1,304 @@
+/*
+ * 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.CoreOptions;
+import org.apache.paimon.codegen.CodeGenUtils;
+import org.apache.paimon.codegen.RecordComparator;
+import org.apache.paimon.compression.CompressOptions;
+import org.apache.paimon.data.BinaryRow;
+import org.apache.paimon.disk.IOManager;
+import org.apache.paimon.io.RollingFileWriter;
+import org.apache.paimon.manifest.FileEntry;
+import org.apache.paimon.manifest.FileKind;
+import org.apache.paimon.manifest.ManifestEntry;
+import org.apache.paimon.manifest.ManifestEntrySerializer;
+import org.apache.paimon.manifest.ManifestFile;
+import org.apache.paimon.manifest.ManifestFileMeta;
+import org.apache.paimon.options.MemorySize;
+import org.apache.paimon.sort.BinaryExternalSortBuffer;
+import org.apache.paimon.utils.Filter;
+import org.apache.paimon.utils.MutableObjectIterator;
+import org.apache.paimon.utils.Pair;
+
+import javax.annotation.Nullable;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.function.Function;
+
+import static 
org.apache.paimon.utils.ManifestReadThreadPool.sequentialBatchedExecute;
+
+/** Spillable external sort utilities for manifest entries. */
+public class ManifestEntryExternalSort {
+
+    static Pair<List<ManifestFileMeta>, List<ManifestFileMeta>> 
sortAndWriteMinorEntries(
+            List<ManifestFileMeta> section,
+            ManifestFileSorter.ManifestSortKey sortKey,
+            ExternalSortConfig config,
+            ManifestFile manifestFile,
+            List<ManifestFileMeta> newFilesForAbort,
+            @Nullable Integer manifestReadParallelism)
+            throws Exception {
+        try (EntrySorter sorter = new EntrySorter(sortKey, config)) {
+            Map<FileEntry.Identifier, ManifestEntry> deleteEntries = new 
HashMap<>();
+            Function<ManifestFileMeta, List<ManifestEntry>> reader =
+                    meta -> manifestFile.read(meta.fileName(), 
meta.fileSize());
+            for (ManifestEntry entry :
+                    sequentialBatchedExecute(reader, section, 
manifestReadParallelism)) {
+                if (entry.kind() == FileKind.DELETE) {
+                    deleteEntries.put(entry.identifier(), entry);
+                } else {
+                    sorter.write(entry);
+                }
+            }
+
+            List<ManifestFileMeta> addFiles =
+                    sorter.writeSurvivingAddsToManifest(manifestFile, 
deleteEntries);
+            // Register ADD files for abort cleanup right after they are 
written, before the
+            // DELETE files below. Otherwise, if sortAndWriteDeleteEntries 
throws, the already
+            // written ADD manifest files would not be in newFilesForAbort and 
would leak as
+            // orphan files on commit abort.
+            newFilesForAbort.addAll(addFiles);
+            List<ManifestFileMeta> deleteFiles =
+                    sortAndWriteDeleteEntries(deleteEntries.values(), sortKey, 
manifestFile);
+            newFilesForAbort.addAll(deleteFiles);
+            return Pair.of(addFiles, deleteFiles);
+        }
+    }
+
+    static List<ManifestFileMeta> sortAndWriteFullEntries(
+            List<ManifestFileMeta> section,
+            ManifestFileSorter.ManifestSortKey sortKey,
+            ExternalSortConfig config,
+            ManifestFile manifestFile,
+            List<ManifestFileMeta> newFilesForAbort,
+            Set<FileEntry.Identifier> deleteEntries,
+            @Nullable Integer manifestReadParallelism)
+            throws Exception {
+        try (EntrySorter sorter = new EntrySorter(sortKey, config)) {
+            Function<ManifestFileMeta, List<ManifestEntry>> reader =
+                    meta -> {
+                        List<ManifestEntry> batch = new ArrayList<>();
+                        for (ManifestEntry entry :
+                                manifestFile.read(
+                                        meta.fileName(),
+                                        meta.fileSize(),
+                                        FileEntry.addFilter(),
+                                        Filter.alwaysTrue())) {
+                            if (!deleteEntries.contains(entry.identifier())) {
+                                batch.add(entry);
+                            }
+                        }
+                        return batch;
+                    };
+            for (ManifestEntry entry :
+                    sequentialBatchedExecute(reader, section, 
manifestReadParallelism)) {
+                sorter.write(entry);
+            }
+            List<ManifestFileMeta> files = 
sorter.writeToManifest(manifestFile);
+            newFilesForAbort.addAll(files);
+            return files;
+        }
+    }
+
+    /** Config used by manifest entry external sort. */
+    static class ExternalSortConfig {
+        final long bufferSize;
+        final int pageSize;
+        final int maxNumFileHandles;
+        final CompressOptions compression;
+        final MemorySize maxDiskSize;
+        @Nullable final IOManager ioManager;
+
+        ExternalSortConfig(
+                long bufferSize,
+                int pageSize,
+                int maxNumFileHandles,
+                CompressOptions compression,
+                MemorySize maxDiskSize,
+                @Nullable IOManager ioManager) {
+            this.bufferSize = bufferSize;
+            this.pageSize = pageSize;
+            this.maxNumFileHandles = maxNumFileHandles;
+            this.compression = compression;
+            this.maxDiskSize = maxDiskSize;
+            this.ioManager = ioManager;
+        }
+
+        static ExternalSortConfig from(CoreOptions options, @Nullable 
IOManager ioManager) {
+            return new ExternalSortConfig(
+                    options.sortSpillBufferSize(),
+                    options.pageSize(),
+                    options.localSortMaxNumFileHandles(),
+                    options.spillCompressOptions(),
+                    options.writeBufferSpillDiskSize(),
+                    ioManager);
+        }
+    }
+
+    /** Spillable sorter that stores sort keys plus serialized manifest 
entries in BinaryRow. */
+    private static class EntrySorter implements AutoCloseable {
+        private final ManifestFileSorter.ManifestSortKey sortKey;
+        private final ManifestEntrySerializer entrySerializer;
+        private final IOManager ioManager;
+        private final boolean ownedIOManager;
+        private final BinaryExternalSortBuffer sortBuffer;
+
+        private EntrySorter(ManifestFileSorter.ManifestSortKey sortKey, 
ExternalSortConfig config) {
+            this.sortKey = sortKey;
+            this.entrySerializer = new ManifestEntrySerializer();
+            this.ioManager =
+                    config.ioManager == null
+                            ? 
IOManager.create(System.getProperty("java.io.tmpdir"))
+                            : config.ioManager;
+            this.ownedIOManager = config.ioManager == null;
+            this.sortBuffer =
+                    BinaryExternalSortBuffer.create(
+                            ioManager,
+                            sortKey.externalSortRowType(),
+                            sortKey.externalSortKeyFields(),
+                            config.bufferSize,
+                            config.pageSize,
+                            config.maxNumFileHandles,
+                            config.compression,
+                            config.maxDiskSize);
+        }
+
+        private void write(ManifestEntry entry) throws Exception {
+            sortBuffer.write(
+                    sortKey.toExternalSortRow(entry, 
entrySerializer.serializeToBytes(entry)));
+        }
+
+        private boolean isEmpty() {
+            return sortBuffer.isEmpty();
+        }
+
+        private List<ManifestFileMeta> writeToManifest(ManifestFile 
manifestFile) throws Exception {
+            if (isEmpty()) {
+                return Collections.emptyList();
+            }
+
+            RollingFileWriter<ManifestEntry, ManifestFileMeta> writer =
+                    manifestFile.createRollingWriter();
+            Exception exception = null;
+            try {
+                MutableObjectIterator<BinaryRow> iterator = 
sortBuffer.sortedIterator();
+                BinaryRow reuse = new 
BinaryRow(sortKey.externalSortRowType().getFieldCount());
+                BinaryRow row;
+                while ((row = iterator.next(reuse)) != null) {
+                    
writer.write(entrySerializer.deserializeFromBytes(sortKey.entryBytes(row)));
+                }
+            } catch (Exception e) {
+                exception = e;
+            } finally {
+                if (exception != null) {
+                    writer.abort();
+                    throw exception;
+                }
+                writer.close();
+            }
+            return writer.result();
+        }
+
+        private List<ManifestFileMeta> writeSurvivingAddsToManifest(
+                ManifestFile manifestFile, Map<FileEntry.Identifier, 
ManifestEntry> deleteEntries)
+                throws Exception {
+            if (isEmpty()) {
+                return Collections.emptyList();
+            }
+
+            RollingFileWriter<ManifestEntry, ManifestFileMeta> writer =
+                    manifestFile.createRollingWriter();
+            Exception exception = null;
+            try {
+                MutableObjectIterator<BinaryRow> iterator = 
sortBuffer.sortedIterator();
+                BinaryRow reuse = new 
BinaryRow(sortKey.externalSortRowType().getFieldCount());
+                BinaryRow row;
+                while ((row = iterator.next(reuse)) != null) {
+                    ManifestEntry entry =
+                            
entrySerializer.deserializeFromBytes(sortKey.entryBytes(row));
+                    if (deleteEntries.remove(entry.identifier()) != null) {
+                        continue;
+                    }
+                    writer.write(entry);
+                }
+            } catch (Exception e) {
+                exception = e;
+            } finally {
+                if (exception != null) {
+                    writer.abort();
+                    throw exception;
+                }
+                writer.close();
+            }
+            return writer.result();
+        }
+
+        @Override
+        public void close() throws Exception {
+            sortBuffer.clear();
+            if (ownedIOManager) {
+                ioManager.close();
+            }
+        }
+    }
+
+    private static List<ManifestFileMeta> sortAndWriteDeleteEntries(
+            Collection<ManifestEntry> entries,
+            ManifestFileSorter.ManifestSortKey sortKey,
+            ManifestFile manifestFile)
+            throws Exception {
+        if (entries.isEmpty()) {
+            return Collections.emptyList();
+        }
+
+        List<ManifestEntry> sorted = new ArrayList<>(entries);
+        RecordComparator comparator =
+                CodeGenUtils.newRecordComparator(
+                        sortKey.externalSortRowType().getFieldTypes(),
+                        sortKey.externalSortKeyFields());
+        sorted.sort(
+                (a, b) ->
+                        comparator.compare(
+                                sortKey.toExternalSortRow(a, new byte[0]),
+                                sortKey.toExternalSortRow(b, new byte[0])));
+
+        RollingFileWriter<ManifestEntry, ManifestFileMeta> writer =
+                manifestFile.createRollingWriter();
+        Exception exception = null;
+        try {
+            writer.write(sorted);
+        } catch (Exception e) {
+            exception = e;
+        } finally {
+            if (exception != null) {
+                writer.abort();
+                throw exception;
+            }
+            writer.close();
+        }
+        return writer.result();
+    }
+}
diff --git 
a/paimon-core/src/main/java/org/apache/paimon/operation/ManifestFileMerger.java 
b/paimon-core/src/main/java/org/apache/paimon/operation/ManifestFileMerger.java
index f1caa03bcf..0313b2b12b 100644
--- 
a/paimon-core/src/main/java/org/apache/paimon/operation/ManifestFileMerger.java
+++ 
b/paimon-core/src/main/java/org/apache/paimon/operation/ManifestFileMerger.java
@@ -20,6 +20,7 @@ package org.apache.paimon.operation;
 
 import org.apache.paimon.CoreOptions;
 import org.apache.paimon.data.BinaryRow;
+import org.apache.paimon.disk.IOManager;
 import org.apache.paimon.io.RollingFileWriter;
 import org.apache.paimon.manifest.FileEntry;
 import org.apache.paimon.manifest.ManifestEntry;
@@ -66,6 +67,15 @@ public class ManifestFileMerger {
             ManifestFile manifestFile,
             RowType partitionType,
             CoreOptions options) {
+        return merge(input, manifestFile, partitionType, options, null);
+    }
+
+    public static List<ManifestFileMeta> merge(
+            List<ManifestFileMeta> input,
+            ManifestFile manifestFile,
+            RowType partitionType,
+            CoreOptions options,
+            @Nullable IOManager ioManager) {
         // Extract configuration from options
         long suggestedMetaSize = options.manifestTargetSize().getBytes();
         int suggestedMinMetaCount = options.manifestMergeMinCount();
@@ -83,7 +93,7 @@ public class ManifestFileMerger {
                     && (partitionType.getFieldCount() > 0
                             || (options.dataEvolutionEnabled() && 
allContainsRowId(input)))) {
                 return ManifestFileSorter.trySortCompaction(
-                        input, newFilesForAbort, manifestFile, partitionType, 
options);
+                        input, newFilesForAbort, manifestFile, partitionType, 
options, ioManager);
             } else {
                 // Otherwise try full compaction first, then minor compaction 
if needed
                 Optional<List<ManifestFileMeta>> fullCompacted =
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 71edb1924e..6201029968 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
@@ -22,15 +22,18 @@ import org.apache.paimon.CoreOptions;
 import org.apache.paimon.codegen.CodeGenUtils;
 import org.apache.paimon.codegen.RecordComparator;
 import org.apache.paimon.data.BinaryRow;
-import org.apache.paimon.io.RollingFileWriter;
+import org.apache.paimon.data.BinaryString;
+import org.apache.paimon.data.GenericRow;
+import org.apache.paimon.data.InternalRow;
+import org.apache.paimon.disk.IOManager;
 import org.apache.paimon.manifest.FileEntry;
-import org.apache.paimon.manifest.FileKind;
 import org.apache.paimon.manifest.ManifestEntry;
 import org.apache.paimon.manifest.ManifestFile;
 import org.apache.paimon.manifest.ManifestFileMeta;
 import org.apache.paimon.partition.PartitionPredicate;
+import org.apache.paimon.types.DataType;
+import org.apache.paimon.types.DataTypes;
 import org.apache.paimon.types.RowType;
-import org.apache.paimon.utils.Filter;
 import org.apache.paimon.utils.Pair;
 
 import org.slf4j.Logger;
@@ -51,10 +54,6 @@ import java.util.Map;
 import java.util.Optional;
 import java.util.PriorityQueue;
 import java.util.Set;
-import java.util.function.Function;
-
-import static java.util.Collections.singletonList;
-import static 
org.apache.paimon.utils.ManifestReadThreadPool.sequentialBatchedExecute;
 
 /**
  * Manifest file sorter that sorts and rewrites manifest files by a configured 
partition field, or
@@ -68,6 +67,7 @@ public class ManifestFileSorter {
     static class CompactionContext {
         final boolean fullCompaction;
         final ManifestSortKey sortKey;
+        final ManifestEntryExternalSort.ExternalSortConfig externalSortConfig;
         final Set<FileEntry.Identifier> deleteEntries;
         /**
          * Manifest files that need unsorted compaction.
@@ -85,12 +85,14 @@ public class ManifestFileSorter {
         CompactionContext(
                 boolean fullCompaction,
                 ManifestSortKey sortKey,
+                ManifestEntryExternalSort.ExternalSortConfig 
externalSortConfig,
                 Set<FileEntry.Identifier> deleteEntries,
                 Map<ManifestFileMeta, Boolean> compactWithoutSort,
                 List<ManifestAdjacentSortedRun> levelRuns,
                 List<ManifestAdjacentSortedRun> pickedRuns) {
             this.fullCompaction = fullCompaction;
             this.sortKey = sortKey;
+            this.externalSortConfig = externalSortConfig;
             this.deleteEntries = deleteEntries;
             this.compactWithoutSort = compactWithoutSort;
             this.levelRuns = levelRuns;
@@ -139,7 +141,8 @@ public class ManifestFileSorter {
             List<ManifestFileMeta> newFilesForAbort,
             ManifestFile manifestFile,
             RowType partitionType,
-            CoreOptions options)
+            CoreOptions options,
+            @Nullable IOManager ioManager)
             throws Exception {
         String sortPartitionField = options.manifestSortPartitionField();
         long suggestedMetaSize = options.manifestTargetSize().getBytes();
@@ -149,6 +152,8 @@ public class ManifestFileSorter {
         int maxSizeAmplificationPercent = 
options.maxSizeAmplificationPercent();
         int sortedRunSizeRatio = options.sortedRunSizeRatio();
         Integer manifestReadParallelism = options.scanManifestParallelism();
+        ManifestEntryExternalSort.ExternalSortConfig externalSortConfig =
+                ManifestEntryExternalSort.ExternalSortConfig.from(options, 
ioManager);
 
         Optional<List<ManifestFileMeta>> fullCompacted =
                 tryFullCompaction(
@@ -164,6 +169,7 @@ public class ManifestFileSorter {
                         maxRewriteSize,
                         maxSizeAmplificationPercent,
                         sortedRunSizeRatio,
+                        externalSortConfig,
                         manifestReadParallelism);
         if (fullCompacted.isPresent()) {
             return fullCompacted.get();
@@ -180,6 +186,7 @@ public class ManifestFileSorter {
                 maxRewriteSize,
                 maxSizeAmplificationPercent,
                 sortedRunSizeRatio,
+                externalSortConfig,
                 manifestReadParallelism);
     }
 
@@ -202,6 +209,7 @@ public class ManifestFileSorter {
             long maxRewriteSize,
             int maxSizeAmplificationPercent,
             int sortedRunSizeRatio,
+            ManifestEntryExternalSort.ExternalSortConfig externalSortConfig,
             @Nullable Integer manifestReadParallelism)
             throws Exception {
         // Step 1: Check if full compaction threshold is met
@@ -226,6 +234,7 @@ public class ManifestFileSorter {
                         suggestedMetaSize,
                         maxSizeAmplificationPercent,
                         sortedRunSizeRatio,
+                        externalSortConfig,
                         manifestReadParallelism);
         List<ManifestAdjacentSortedRun> levelRuns = ctx.levelRuns;
         List<ManifestAdjacentSortedRun> pickedRuns = ctx.pickedRuns;
@@ -305,6 +314,7 @@ public class ManifestFileSorter {
             long maxRewriteSize,
             int maxSizeAmplificationPercent,
             int sortedRunSizeRatio,
+            ManifestEntryExternalSort.ExternalSortConfig externalSortConfig,
             @Nullable Integer manifestReadParallelism)
             throws Exception {
         // Step 1: Prepare compaction context (early-return if nothing to 
compact)
@@ -319,6 +329,7 @@ public class ManifestFileSorter {
                         suggestedMetaSize,
                         maxSizeAmplificationPercent,
                         sortedRunSizeRatio,
+                        externalSortConfig,
                         manifestReadParallelism);
         List<ManifestAdjacentSortedRun> levelRuns = ctx.levelRuns;
         List<ManifestAdjacentSortedRun> pickedRuns = ctx.pickedRuns;
@@ -427,6 +438,7 @@ public class ManifestFileSorter {
             long suggestedMetaSize,
             int maxSizeAmplificationPercent,
             int sortedRunSizeRatio,
+            ManifestEntryExternalSort.ExternalSortConfig externalSortConfig,
             @Nullable Integer manifestReadParallelism) {
 
         // Step 1: Resolve sort key. Data evolution tables prefer RowID ranges 
when available.
@@ -456,6 +468,7 @@ public class ManifestFileSorter {
         return new CompactionContext(
                 fullCompaction,
                 sortKey,
+                externalSortConfig,
                 classifyResult.deleteEntries,
                 classifyResult.compactWithoutSort,
                 levelRuns,
@@ -489,7 +502,7 @@ public class ManifestFileSorter {
             classifiedDeleteEntries =
                     FileEntry.readDeletedEntries(manifestFile, input, 
manifestReadParallelism);
 
-            // Build partition predicate from delete entries for overlap 
detection
+            // Build partition predicate from delete entries for overlap 
detection.
             if (classifiedDeleteEntries.isEmpty()) {
                 predicate = PartitionPredicate.ALWAYS_FALSE;
             } else {
@@ -942,7 +955,9 @@ public class ManifestFileSorter {
             return;
         }
 
-        if (ctx.fullCompaction) {
+        // Add-only minor sections can use the full rewrite path to avoid 
keeping DELETE entries in
+        // memory.
+        if (ctx.fullCompaction || containsNoDeleteEntries(section)) {
             rewriteFull(section, output, sortNewFiles, ctx, manifestFile, 
manifestReadParallelism);
         } else {
             rewriteMinor(section, output, sortNewFiles, ctx, manifestFile, 
manifestReadParallelism);
@@ -961,43 +976,24 @@ public class ManifestFileSorter {
             ManifestFile manifestFile,
             @Nullable Integer manifestReadParallelism)
             throws Exception {
-        // Read surviving ADD entries: filter out entries cancelled by 
deleteEntries.
-        Function<ManifestFileMeta, List<ManifestEntry>> reader =
-                meta -> {
-                    List<ManifestEntry> batch = new ArrayList<>();
-                    for (ManifestEntry entry :
-                            manifestFile.read(
-                                    meta.fileName(),
-                                    meta.fileSize(),
-                                    FileEntry.addFilter(),
-                                    Filter.alwaysTrue())) {
-                        if (!ctx.deleteEntries.contains(entry.identifier())) {
-                            batch.add(entry);
-                        }
-                    }
-                    return batch;
-                };
-
-        List<ManifestEntry> entries = new ArrayList<>();
-        for (ManifestEntry entry :
-                sequentialBatchedExecute(reader, section, 
manifestReadParallelism)) {
-            entries.add(entry);
-        }
-
-        if (!entries.isEmpty()) {
-            List<ManifestFileMeta> sorted = sortAndWriteEntries(entries, 
ctx.sortKey, manifestFile);
+        List<ManifestFileMeta> sorted =
+                ManifestEntryExternalSort.sortAndWriteFullEntries(
+                        section,
+                        ctx.sortKey,
+                        ctx.externalSortConfig,
+                        manifestFile,
+                        sortNewFiles,
+                        ctx.deleteEntries,
+                        manifestReadParallelism);
+        if (!sorted.isEmpty()) {
             output.addSortedFiles(sorted);
-            sortNewFiles.addAll(sorted);
         }
     }
 
     /**
-     * Minor compaction path: read entries with ADD/DELETE classified in a 
single pass per file,
-     * then sort each group independently and write them to output.
-     *
-     * <p>Each file is read in parallel (via sequentialBatchedExecute). The 
reader classifies
-     * entries into ADD and DELETE within each file, returning a Pair. Results 
are merged in the
-     * main thread.
+     * Minor compaction path: collect DELETE entries in memory while 
external-sorting all entries,
+     * then write surviving ADD entries from the sorted stream and remaining 
DELETE entries from
+     * memory.
      */
     private static void rewriteMinor(
             List<ManifestFileMeta> section,
@@ -1007,90 +1003,31 @@ public class ManifestFileSorter {
             ManifestFile manifestFile,
             @Nullable Integer manifestReadParallelism)
             throws Exception {
-        // Read and classify ADD/DELETE in one pass per file.
-        Function<ManifestFileMeta, List<Pair<List<ManifestEntry>, 
List<ManifestEntry>>>> reader =
-                meta -> {
-                    List<ManifestEntry> addBatch = new ArrayList<>();
-                    List<ManifestEntry> deleteBatch = new ArrayList<>();
-                    for (ManifestEntry entry :
-                            manifestFile.read(meta.fileName(), 
meta.fileSize())) {
-                        if (entry.kind() == FileKind.ADD) {
-                            addBatch.add(entry);
-                        } else {
-                            deleteBatch.add(entry);
-                        }
-                    }
-                    return singletonList(Pair.of(addBatch, deleteBatch));
-                };
-
-        Map<FileEntry.Identifier, ManifestEntry> addMap = new HashMap<>();
-        List<ManifestEntry> minorDeleteEntries = new ArrayList<>();
-        for (Pair<List<ManifestEntry>, List<ManifestEntry>> pair :
-                sequentialBatchedExecute(reader, section, 
manifestReadParallelism)) {
-            for (ManifestEntry entry : pair.getLeft()) {
-                addMap.put(entry.identifier(), entry);
-            }
-            minorDeleteEntries.addAll(pair.getRight());
-        }
-
-        // Cancel out ADD+DELETE pairs with the same identifier within the 
section.
-        minorDeleteEntries.removeIf(
-                manifestEntry -> addMap.remove(manifestEntry.identifier()) != 
null);
-        List<ManifestEntry> addEntries = new ArrayList<>(addMap.values());
+        Pair<List<ManifestFileMeta>, List<ManifestFileMeta>> sorted =
+                ManifestEntryExternalSort.sortAndWriteMinorEntries(
+                        section,
+                        ctx.sortKey,
+                        ctx.externalSortConfig,
+                        manifestFile,
+                        sortNewFiles,
+                        manifestReadParallelism);
 
-        if (!addEntries.isEmpty()) {
-            List<ManifestFileMeta> sorted =
-                    sortAndWriteEntries(addEntries, ctx.sortKey, manifestFile);
-            output.addSortedFiles(sorted);
-            sortNewFiles.addAll(sorted);
+        if (!sorted.getLeft().isEmpty()) {
+            output.addSortedFiles(sorted.getLeft());
         }
 
-        if (!minorDeleteEntries.isEmpty()) {
-            List<ManifestFileMeta> sorted =
-                    sortAndWriteEntries(minorDeleteEntries, ctx.sortKey, 
manifestFile);
-            output.addDeleteFiles(sorted);
-            sortNewFiles.addAll(sorted);
+        if (!sorted.getRight().isEmpty()) {
+            output.addDeleteFiles(sorted.getRight());
         }
     }
 
-    /** Sort entries and write them to a new manifest file with proper error 
handling. */
-    private static List<ManifestFileMeta> sortAndWriteEntries(
-            List<ManifestEntry> entries, ManifestSortKey sortKey, ManifestFile 
manifestFile)
-            throws Exception {
-        entries.sort((a, b) -> compareSortKey(a, b, sortKey));
-        RollingFileWriter<ManifestEntry, ManifestFileMeta> writer =
-                manifestFile.createRollingWriter();
-        Exception exception = null;
-        try {
-            writer.write(entries);
-        } catch (Exception e) {
-            exception = e;
-        } finally {
-            if (exception != null) {
-                writer.abort();
-                throw exception;
+    private static boolean containsNoDeleteEntries(List<ManifestFileMeta> 
section) {
+        for (ManifestFileMeta meta : section) {
+            if (meta.numDeletedFiles() > 0) {
+                return false;
             }
-            writer.close();
         }
-        return writer.result();
-    }
-
-    /**
-     * Compare two {@link ManifestEntry}s by the composite key {@code 
(sort-key, kind, fileName)}.
-     * {@code fileName} is used as the tie-breaker so that all entries sharing 
the same sort-field
-     * value AND the same data file are emitted contiguously.
-     */
-    private static int compareSortKey(ManifestEntry a, ManifestEntry b, 
ManifestSortKey sortKey) {
-        int c = sortKey.compareEntry(a, b);
-        if (c != 0) {
-            return c;
-        }
-        // ADD before DELETE
-        int kindCmp = a.kind().compareTo(b.kind());
-        if (kindCmp != 0) {
-            return kindCmp;
-        }
-        return a.file().fileName().compareTo(b.file().fileName());
+        return true;
     }
 
     private static ManifestSortKey createSortKey(
@@ -1102,9 +1039,11 @@ public class ManifestFileSorter {
             // RowID sorting uses the configured partition field as the 
primary key when specified,
             // otherwise it uses the full partition row to preserve partition 
locality. It then
             // orders files by RowID.
+            int[] partitionSortFields =
+                    createPartitionSortFields(sortPartitionField, 
partitionType);
             RecordComparator partitionComparator =
-                    createPartitionComparator(sortPartitionField, 
partitionType);
-            return new RowIdSortKey(partitionComparator);
+                    createPartitionComparator(partitionType, 
partitionSortFields);
+            return new RowIdSortKey(partitionComparator, partitionType, 
partitionSortFields);
         }
 
         if (partitionType.getFieldCount() == 0) {
@@ -1124,11 +1063,10 @@ public class ManifestFileSorter {
         RecordComparator fieldComparator =
                 CodeGenUtils.newRecordComparator(
                         partitionType.getFieldTypes(), new int[] 
{sortFieldIndex});
-        return new PartitionSortKey(fieldComparator);
+        return new PartitionSortKey(fieldComparator, partitionType, 
sortFieldIndex);
     }
 
-    @Nullable
-    private static RecordComparator createPartitionComparator(
+    private static int[] createPartitionSortFields(
             String sortPartitionField, RowType partitionType) {
         if (sortPartitionField != null && !sortPartitionField.isEmpty()) {
             int sortFieldIndex = 
partitionType.getFieldNames().indexOf(sortPartitionField);
@@ -1138,15 +1076,24 @@ public class ManifestFileSorter {
                                 "Cannot resolve sort field '%s' for manifest 
sort rewrite.",
                                 sortPartitionField));
             }
-            return CodeGenUtils.newRecordComparator(
-                    partitionType.getFieldTypes(), new int[] {sortFieldIndex});
+            return new int[] {sortFieldIndex};
         }
 
-        if (partitionType.getFieldCount() == 0) {
-            return null;
+        int fieldCount = partitionType.getFieldCount();
+        int[] sortFields = new int[fieldCount];
+        for (int i = 0; i < fieldCount; i++) {
+            sortFields[i] = i;
         }
+        return sortFields;
+    }
 
-        return CodeGenUtils.newRecordComparator(partitionType.getFieldTypes());
+    @Nullable
+    private static RecordComparator createPartitionComparator(
+            RowType partitionType, int[] sortFields) {
+        if (sortFields.length == 0) {
+            return null;
+        }
+        return CodeGenUtils.newRecordComparator(partitionType.getFieldTypes(), 
sortFields);
     }
 
     interface ManifestSortKey {
@@ -1157,15 +1104,36 @@ public class ManifestFileSorter {
 
         boolean isAfterMax(ManifestFileMeta file, ManifestFileMeta maxFile);
 
-        int compareEntry(ManifestEntry a, ManifestEntry b);
+        RowType externalSortRowType();
+
+        int[] externalSortKeyFields();
+
+        InternalRow toExternalSortRow(ManifestEntry entry, byte[] entryBytes);
+
+        byte[] entryBytes(BinaryRow row);
     }
 
     private static class PartitionSortKey implements ManifestSortKey {
 
         private final RecordComparator fieldComparator;
+        private final InternalRow.FieldGetter sortFieldGetter;
+        private final RowType externalSortRowType;
+        private final int[] externalSortKeyFields;
+        private final int sortFieldNum;
 
-        private PartitionSortKey(RecordComparator fieldComparator) {
+        private PartitionSortKey(
+                RecordComparator fieldComparator, RowType partitionType, int 
sortFieldIndex) {
             this.fieldComparator = fieldComparator;
+            DataType sortFieldType = partitionType.getTypeAt(sortFieldIndex);
+            this.sortFieldGetter = 
InternalRow.createFieldGetter(sortFieldType, sortFieldIndex);
+            this.sortFieldNum = 3;
+            this.externalSortRowType =
+                    DataTypes.ROW(
+                            sortFieldType,
+                            DataTypes.TINYINT(),
+                            DataTypes.STRING(),
+                            DataTypes.BYTES());
+            this.externalSortKeyFields = createSequentialFields(sortFieldNum);
         }
 
         @Override
@@ -1188,17 +1156,60 @@ public class ManifestFileSorter {
         }
 
         @Override
-        public int compareEntry(ManifestEntry a, ManifestEntry b) {
-            return fieldComparator.compare(a.partition(), b.partition());
+        public RowType externalSortRowType() {
+            return externalSortRowType;
+        }
+
+        @Override
+        public int[] externalSortKeyFields() {
+            return externalSortKeyFields;
+        }
+
+        @Override
+        public InternalRow toExternalSortRow(ManifestEntry entry, byte[] 
entryBytes) {
+            GenericRow row = new 
GenericRow(externalSortRowType.getFieldCount());
+            row.setField(0, sortFieldGetter.getFieldOrNull(entry.partition()));
+            row.setField(1, entry.kind().toByteValue());
+            row.setField(2, BinaryString.fromString(entry.file().fileName()));
+            row.setField(3, entryBytes);
+            return row;
+        }
+
+        @Override
+        public byte[] entryBytes(BinaryRow row) {
+            return row.getBinary(sortFieldNum);
         }
     }
 
     private static class RowIdSortKey implements ManifestSortKey {
 
         @Nullable private final RecordComparator partitionComparator;
-
-        private RowIdSortKey(@Nullable RecordComparator partitionComparator) {
+        private final InternalRow.FieldGetter[] partitionFieldGetters;
+        private final RowType externalSortRowType;
+        private final int[] externalSortKeyFields;
+        private final int sortFieldNum;
+
+        private RowIdSortKey(
+                @Nullable RecordComparator partitionComparator,
+                RowType partitionType,
+                int[] partitionSortFields) {
             this.partitionComparator = partitionComparator;
+            this.partitionFieldGetters =
+                    createPartitionFieldGetters(partitionType, 
partitionSortFields);
+
+            List<DataType> fieldTypes = new ArrayList<>();
+            for (int partitionSortField : partitionSortFields) {
+                fieldTypes.add(partitionType.getTypeAt(partitionSortField));
+            }
+            fieldTypes.add(DataTypes.BIGINT());
+            fieldTypes.add(DataTypes.BIGINT());
+            fieldTypes.add(DataTypes.BIGINT());
+            fieldTypes.add(DataTypes.TINYINT());
+            fieldTypes.add(DataTypes.STRING());
+            fieldTypes.add(DataTypes.BYTES());
+            this.externalSortRowType = DataTypes.ROW(fieldTypes.toArray(new 
DataType[0]));
+            this.sortFieldNum = externalSortRowType.getFieldCount() - 1;
+            this.externalSortKeyFields = createSequentialFields(sortFieldNum);
         }
 
         @Override
@@ -1234,23 +1245,34 @@ public class ManifestFileSorter {
         }
 
         @Override
-        public int compareEntry(ManifestEntry a, ManifestEntry b) {
-            int c = 0;
-            if (partitionComparator != null) {
-                c = partitionComparator.compare(a.partition(), b.partition());
-                if (c != 0) {
-                    return c;
-                }
-            }
-            c = Long.compare(a.file().nonNullFirstRowId(), 
b.file().nonNullFirstRowId());
-            if (c != 0) {
-                return c;
-            }
-            c = Long.compare(rowIdRangeEnd(a), rowIdRangeEnd(b));
-            if (c != 0) {
-                return c;
+        public RowType externalSortRowType() {
+            return externalSortRowType;
+        }
+
+        @Override
+        public int[] externalSortKeyFields() {
+            return externalSortKeyFields;
+        }
+
+        @Override
+        public InternalRow toExternalSortRow(ManifestEntry entry, byte[] 
entryBytes) {
+            GenericRow row = new 
GenericRow(externalSortRowType.getFieldCount());
+            int pos = 0;
+            for (InternalRow.FieldGetter partitionFieldGetter : 
partitionFieldGetters) {
+                row.setField(pos++, 
partitionFieldGetter.getFieldOrNull(entry.partition()));
             }
-            return Long.compare(b.file().maxSequenceNumber(), 
a.file().maxSequenceNumber());
+            row.setField(pos++, entry.file().nonNullFirstRowId());
+            row.setField(pos++, rowIdRangeEnd(entry));
+            row.setField(pos++, Long.MAX_VALUE - 
entry.file().maxSequenceNumber());
+            row.setField(pos++, entry.kind().toByteValue());
+            row.setField(pos++, 
BinaryString.fromString(entry.file().fileName()));
+            row.setField(pos, entryBytes);
+            return row;
+        }
+
+        @Override
+        public byte[] entryBytes(BinaryRow row) {
+            return row.getBinary(sortFieldNum);
         }
 
         private int comparePartitionMin(ManifestFileMeta a, ManifestFileMeta 
b) {
@@ -1292,6 +1314,26 @@ public class ManifestFileSorter {
         }
     }
 
+    private static int[] createSequentialFields(int fieldCount) {
+        int[] fields = new int[fieldCount];
+        for (int i = 0; i < fieldCount; i++) {
+            fields[i] = i;
+        }
+        return fields;
+    }
+
+    private static InternalRow.FieldGetter[] createPartitionFieldGetters(
+            RowType partitionType, int[] partitionSortFields) {
+        InternalRow.FieldGetter[] fieldGetters =
+                new InternalRow.FieldGetter[partitionSortFields.length];
+        for (int i = 0; i < partitionSortFields.length; i++) {
+            int fieldIndex = partitionSortFields[i];
+            fieldGetters[i] =
+                    
InternalRow.createFieldGetter(partitionType.getTypeAt(fieldIndex), fieldIndex);
+        }
+        return fieldGetters;
+    }
+
     /**
      * Resolve the partition field to sort manifests by.
      *
diff --git 
a/paimon-core/src/main/java/org/apache/paimon/table/sink/TableCommit.java 
b/paimon-core/src/main/java/org/apache/paimon/table/sink/TableCommit.java
index 26f4ffd163..f616891406 100644
--- a/paimon-core/src/main/java/org/apache/paimon/table/sink/TableCommit.java
+++ b/paimon-core/src/main/java/org/apache/paimon/table/sink/TableCommit.java
@@ -19,6 +19,7 @@
 package org.apache.paimon.table.sink;
 
 import org.apache.paimon.annotation.Public;
+import org.apache.paimon.disk.IOManager;
 import org.apache.paimon.metrics.MetricRegistry;
 import org.apache.paimon.table.Table;
 
@@ -36,6 +37,11 @@ import java.util.List;
 @Public
 public interface TableCommit extends AutoCloseable {
 
+    /** With {@link IOManager}, this is needed if commit-time external sort is 
used. */
+    default TableCommit withIOManager(IOManager ioManager) {
+        return this;
+    }
+
     /** Set {@link MetricRegistry} to table commit. */
     TableCommit withMetricRegistry(MetricRegistry registry);
 
diff --git 
a/paimon-core/src/main/java/org/apache/paimon/table/sink/TableCommitImpl.java 
b/paimon-core/src/main/java/org/apache/paimon/table/sink/TableCommitImpl.java
index 6a9e3046cc..9ec742b354 100644
--- 
a/paimon-core/src/main/java/org/apache/paimon/table/sink/TableCommitImpl.java
+++ 
b/paimon-core/src/main/java/org/apache/paimon/table/sink/TableCommitImpl.java
@@ -21,6 +21,7 @@ package org.apache.paimon.table.sink;
 import org.apache.paimon.Snapshot;
 import org.apache.paimon.annotation.VisibleForTesting;
 import org.apache.paimon.consumer.ConsumerManager;
+import org.apache.paimon.disk.IOManager;
 import org.apache.paimon.fs.Path;
 import org.apache.paimon.index.IndexPathFactory;
 import org.apache.paimon.io.DataFileMeta;
@@ -177,6 +178,12 @@ public class TableCommitImpl implements InnerTableCommit {
         return this;
     }
 
+    @Override
+    public TableCommitImpl withIOManager(IOManager ioManager) {
+        commit.withIOManager(ioManager);
+        return this;
+    }
+
     @Override
     public InnerTableCommit withMetricRegistry(MetricRegistry registry) {
         commit.withMetrics(new CommitMetrics(registry, tableName));
diff --git 
a/paimon-core/src/test/java/org/apache/paimon/manifest/ManifestFileMetaTest.java
 
b/paimon-core/src/test/java/org/apache/paimon/manifest/ManifestFileMetaTest.java
index 42638aa24f..eced1bf4ef 100644
--- 
a/paimon-core/src/test/java/org/apache/paimon/manifest/ManifestFileMetaTest.java
+++ 
b/paimon-core/src/test/java/org/apache/paimon/manifest/ManifestFileMetaTest.java
@@ -22,6 +22,7 @@ import org.apache.paimon.CoreOptions;
 import org.apache.paimon.data.BinaryRow;
 import org.apache.paimon.data.BinaryRowWriter;
 import org.apache.paimon.data.Timestamp;
+import org.apache.paimon.disk.IOManager;
 import org.apache.paimon.fs.FileIO;
 import org.apache.paimon.fs.FileIOFinder;
 import org.apache.paimon.fs.Path;
@@ -960,6 +961,96 @@ public class ManifestFileMetaTest extends 
ManifestFileMetaTestBase {
         }
     }
 
+    @Test
+    public void testManifestSortWithSpillableExternalSortBuffer() {
+        List<ManifestFileMeta> input = new ArrayList<>();
+        for (int manifest = 0; manifest < 4; manifest++) {
+            List<ManifestEntry> entries = new ArrayList<>();
+            for (int i = 0; i < 80; i++) {
+                int partition = manifest % 2 == 0 ? 79 - i : i;
+                entries.add(
+                        makeEntry(
+                                true,
+                                String.format(
+                                        
"spill-manifest-%02d-entry-%03d-payload-padding-%040d",
+                                        manifest, i, i),
+                                partition));
+            }
+            input.add(makeManifest(entries.toArray(new ManifestEntry[0])));
+        }
+
+        Options testOptions = new Options();
+        testOptions.set("manifest-sort.enabled", "true");
+        testOptions.set("manifest.full-compaction-threshold-size", "1B");
+        testOptions.set("page-size", "1kb");
+        testOptions.set("sort-spill-buffer-size", "4kb");
+        testOptions.set("local-sort.max-num-file-handles", "2");
+
+        List<ManifestFileMeta> merged =
+                ManifestFileMerger.merge(
+                        input,
+                        manifestFile,
+                        getPartitionType(),
+                        CoreOptions.fromMap(testOptions.toMap()));
+
+        assertEquivalentEntries(input, merged);
+        for (ManifestFileMeta meta : merged) {
+            List<ManifestEntry> entries = manifestFile.read(meta.fileName(), 
meta.fileSize());
+            for (int i = 1; i < entries.size(); i++) {
+                int prevPartition = entries.get(i - 1).partition().getInt(0);
+                int currPartition = entries.get(i).partition().getInt(0);
+                assertThat(currPartition)
+                        .as("Entries within a manifest should be sorted after 
spill")
+                        .isGreaterThanOrEqualTo(prevPartition);
+            }
+        }
+    }
+
+    @Test
+    public void testManifestSortUsesExternalIOManagerWithoutClosingIt() throws 
Exception {
+        List<ManifestFileMeta> input = new ArrayList<>();
+        for (int manifest = 0; manifest < 2; manifest++) {
+            List<ManifestEntry> entries = new ArrayList<>();
+            for (int i = 0; i < 40; i++) {
+                int partition = manifest == 0 ? 39 - i : i;
+                entries.add(
+                        makeEntry(
+                                true,
+                                String.format(
+                                        
"external-io-manager-%02d-entry-%03d-payload-%040d",
+                                        manifest, i, i),
+                                partition));
+            }
+            input.add(makeManifest(entries.toArray(new ManifestEntry[0])));
+        }
+
+        Options testOptions = new Options();
+        testOptions.set("manifest-sort.enabled", "true");
+        testOptions.set("manifest.full-compaction-threshold-size", "1B");
+        testOptions.set("page-size", "1kb");
+        testOptions.set("sort-spill-buffer-size", "4kb");
+
+        java.nio.file.Path spillBase = tempDir.resolve("manifest-spill");
+        java.nio.file.Files.createDirectories(spillBase);
+        IOManager ioManager = IOManager.create(spillBase.toString());
+        try {
+            List<ManifestFileMeta> merged =
+                    ManifestFileMerger.merge(
+                            input,
+                            manifestFile,
+                            getPartitionType(),
+                            CoreOptions.fromMap(testOptions.toMap()),
+                            ioManager);
+
+            assertEquivalentEntries(input, merged);
+            assertThat(spillBase.toFile().list((dir, name) -> 
name.startsWith("paimon-")))
+                    .isNotEmpty();
+        } finally {
+            ioManager.close();
+        }
+        assertThat(spillBase.toFile().list()).isEmpty();
+    }
+
     /**
      * Test that sort rewrite correctly eliminates DELETE entries and their 
corresponding ADD
      * entries. The key condition is that totalDeltaFileSize must reach 
manifestFullCompactionSize
@@ -1021,6 +1112,10 @@ public class ManifestFileMetaTest extends 
ManifestFileMetaTestBase {
         Options testOptions = new Options();
         testOptions.set("manifest-sort.enabled", "true");
         testOptions.set("manifest.full-compaction-threshold-size", "10B");
+        testOptions.set("manifest-sort.max-rewrite-size", "1B");
+        testOptions.set("page-size", "1kb");
+        testOptions.set("sort-spill-buffer-size", "4kb");
+        testOptions.set("local-sort.max-num-file-handles", "2");
 
         List<ManifestFileMeta> merged =
                 ManifestFileMerger.merge(
diff --git 
a/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/sink/Committer.java
 
b/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/sink/Committer.java
index f1684be96e..a556f6cc3f 100644
--- 
a/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/sink/Committer.java
+++ 
b/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/sink/Committer.java
@@ -94,6 +94,11 @@ public interface Committer<CommitT, GlobalCommitT> extends 
AutoCloseable {
         int getParallelism();
 
         int getSubtaskIndex();
+
+        @Nullable
+        default String[] tempDirs() {
+            return null;
+        }
     }
 
     static Context createContext(
@@ -104,6 +109,26 @@ public interface Committer<CommitT, GlobalCommitT> extends 
AutoCloseable {
             StateStore stateStore,
             int parallelism,
             int subtaskIndex) {
+        return createContext(
+                commitUser,
+                metricGroup,
+                streamingCheckpointEnabled,
+                isRestored,
+                stateStore,
+                parallelism,
+                subtaskIndex,
+                null);
+    }
+
+    static Context createContext(
+            String commitUser,
+            @Nullable MetricGroup metricGroup,
+            boolean streamingCheckpointEnabled,
+            boolean isRestored,
+            StateStore stateStore,
+            int parallelism,
+            int subtaskIndex,
+            @Nullable String[] tempDirs) {
         return new Committer.Context() {
             @Override
             public String commitUser() {
@@ -139,6 +164,12 @@ public interface Committer<CommitT, GlobalCommitT> extends 
AutoCloseable {
             public int getSubtaskIndex() {
                 return subtaskIndex;
             }
+
+            @Override
+            @Nullable
+            public String[] tempDirs() {
+                return tempDirs;
+            }
         };
     }
 }
diff --git 
a/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/sink/CommitterOperator.java
 
b/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/sink/CommitterOperator.java
index 034ded9c64..9056440cf6 100644
--- 
a/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/sink/CommitterOperator.java
+++ 
b/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/sink/CommitterOperator.java
@@ -142,7 +142,11 @@ public class CommitterOperator<CommitT, GlobalCommitT> 
extends AbstractStreamOpe
                         context.isRestored(),
                         new 
OperatorBackendStateStore(context.getOperatorStateStore()),
                         parallelism,
-                        index);
+                        index,
+                        getContainingTask()
+                                .getEnvironment()
+                                .getIOManager()
+                                .getSpillingDirectoriesPaths());
         committer = committerFactory.create(committerContext);
 
         committableStateManager.initializeState(committerContext, committer);
diff --git 
a/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/sink/StoreCommitter.java
 
b/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/sink/StoreCommitter.java
index 48daabc8d8..4c353517c3 100644
--- 
a/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/sink/StoreCommitter.java
+++ 
b/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/sink/StoreCommitter.java
@@ -19,6 +19,7 @@
 package org.apache.paimon.flink.sink;
 
 import org.apache.paimon.annotation.VisibleForTesting;
+import org.apache.paimon.disk.IOManager;
 import org.apache.paimon.flink.metrics.FlinkMetricRegistry;
 import org.apache.paimon.flink.sink.listener.CommitListeners;
 import org.apache.paimon.io.DataFileMeta;
@@ -29,6 +30,7 @@ import org.apache.paimon.table.sink.CommitMessage;
 import org.apache.paimon.table.sink.CommitMessageImpl;
 import org.apache.paimon.table.sink.TableCommit;
 import org.apache.paimon.table.sink.TableCommitImpl;
+import org.apache.paimon.utils.IOUtils;
 
 import javax.annotation.Nullable;
 
@@ -45,6 +47,7 @@ public class StoreCommitter implements Committer<Committable, 
ManifestCommittabl
     private final TableCommitImpl commit;
     @Nullable private final CommitterMetrics committerMetrics;
     private final CommitListeners commitListeners;
+    @Nullable private final IOManager commitIOManager;
     private final boolean allowLogOffsetDuplicate;
 
     public StoreCommitter(FileStoreTable table, TableCommit commit, Context 
context) {
@@ -62,6 +65,14 @@ public class StoreCommitter implements 
Committer<Committable, ManifestCommittabl
         } catch (Exception e) {
             throw new RuntimeException(e);
         }
+
+        String[] tempDirs = context.tempDirs();
+        if (tempDirs == null) {
+            this.commitIOManager = null;
+        } else {
+            this.commitIOManager = IOManager.create(tempDirs);
+            this.commit.withIOManager(commitIOManager);
+        }
         allowLogOffsetDuplicate = table.bucketMode() == 
BucketMode.BUCKET_UNAWARE;
     }
 
@@ -132,8 +143,7 @@ public class StoreCommitter implements 
Committer<Committable, ManifestCommittabl
 
     @Override
     public void close() throws Exception {
-        commit.close();
-        commitListeners.close();
+        IOUtils.closeAll(commit, commitListeners, commitIOManager);
     }
 
     public boolean allowLogOffsetDuplicate() {

Reply via email to