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 cca01af0fd [core] Reuse manifest merge results on commit retry (#8229)
cca01af0fd is described below

commit cca01af0fdda277171212a27c16ba2311850428d
Author: YeJunHao <[email protected]>
AuthorDate: Sat Jun 20 13:11:20 2026 +0800

    [core] Reuse manifest merge results on commit retry (#8229)
---
 .../java/org/apache/paimon/utils/ListUtils.java    |  34 ++
 .../org/apache/paimon/utils/ListUtilsTest.java     |  87 ++++
 .../paimon/operation/FileStoreCommitImpl.java      |  72 ++-
 .../paimon/operation/commit/RetryCommitResult.java |  31 +-
 .../paimon/operation/FileStoreCommitTest.java      | 564 ++++++++++++++++++++-
 5 files changed, 779 insertions(+), 9 deletions(-)

diff --git a/paimon-common/src/main/java/org/apache/paimon/utils/ListUtils.java 
b/paimon-common/src/main/java/org/apache/paimon/utils/ListUtils.java
index bd35c3798c..dc9c9233cf 100644
--- a/paimon-common/src/main/java/org/apache/paimon/utils/ListUtils.java
+++ b/paimon-common/src/main/java/org/apache/paimon/utils/ListUtils.java
@@ -18,10 +18,13 @@
 
 package org.apache.paimon.utils;
 
+import javax.annotation.Nullable;
+
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Iterator;
 import java.util.List;
+import java.util.Objects;
 import java.util.concurrent.ThreadLocalRandom;
 
 import static org.apache.paimon.utils.Preconditions.checkArgument;
@@ -53,4 +56,35 @@ public class ListUtils {
         result.addAll(list2);
         return result;
     }
+
+    /**
+     * Replace the first continuous occurrence of {@code replaced} in {@code 
list} with {@code
+     * replacement}. Return null if {@code replaced} does not appear as a 
continuous sub-list.
+     */
+    @Nullable
+    public static <E> List<E> tryReplace(
+            List<? extends E> list, List<?> replaced, List<? extends E> 
replacement) {
+        checkArgument(!replaced.isEmpty(), "Cannot replace an empty list.");
+
+        for (int start = 0; start <= list.size() - replaced.size(); start++) {
+            boolean found = true;
+            for (int i = 0; i < replaced.size(); i++) {
+                if (!Objects.equals(list.get(start + i), replaced.get(i))) {
+                    found = false;
+                    break;
+                }
+            }
+
+            if (found) {
+                ArrayList<E> result =
+                        new ArrayList<>(list.size() - replaced.size() + 
replacement.size());
+                result.addAll(list.subList(0, start));
+                result.addAll(replacement);
+                result.addAll(list.subList(start + replaced.size(), 
list.size()));
+                return result;
+            }
+        }
+
+        return null;
+    }
 }
diff --git 
a/paimon-common/src/test/java/org/apache/paimon/utils/ListUtilsTest.java 
b/paimon-common/src/test/java/org/apache/paimon/utils/ListUtilsTest.java
new file mode 100644
index 0000000000..44068d254d
--- /dev/null
+++ b/paimon-common/src/test/java/org/apache/paimon/utils/ListUtilsTest.java
@@ -0,0 +1,87 @@
+/*
+ * 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.utils;
+
+import org.junit.jupiter.api.Test;
+
+import java.util.Arrays;
+import java.util.Collections;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+/** Test for {@link ListUtils}. */
+public class ListUtilsTest {
+
+    @Test
+    public void testTryReplace() {
+        assertThat(
+                        ListUtils.tryReplace(
+                                Arrays.asList("a", "b", "c", "d", "e", "f"),
+                                Arrays.asList("b", "c", "d"),
+                                Arrays.asList("n", "w")))
+                .containsExactly("a", "n", "w", "e", "f");
+
+        assertThat(
+                        ListUtils.tryReplace(
+                                Arrays.asList("b", "c", "d"),
+                                Arrays.asList("b", "c", "d"),
+                                Collections.singletonList("n")))
+                .containsExactly("n");
+
+        assertThat(
+                        ListUtils.tryReplace(
+                                Arrays.asList("a", "b", "c"),
+                                Collections.singletonList("b"),
+                                Collections.emptyList()))
+                .containsExactly("a", "c");
+    }
+
+    @Test
+    public void testTryReplaceRequiresContinuousMatch() {
+        assertThat(
+                        ListUtils.tryReplace(
+                                Arrays.asList("a", "b", "x", "c", "d"),
+                                Arrays.asList("b", "c", "d"),
+                                Collections.singletonList("n")))
+                .isNull();
+    }
+
+    @Test
+    public void testTryReplaceRequiresSameOrder() {
+        assertThat(
+                        ListUtils.tryReplace(
+                                Arrays.asList("a", "d", "c", "b", "e"),
+                                Arrays.asList("b", "c", "d"),
+                                Collections.singletonList("n")))
+                .isNull();
+    }
+
+    @Test
+    public void testTryReplaceRequiresNonEmptyReplacedList() {
+        assertThatThrownBy(
+                        () ->
+                                ListUtils.tryReplace(
+                                        Collections.singletonList("a"),
+                                        Collections.emptyList(),
+                                        Collections.singletonList("n")))
+                .isInstanceOf(IllegalArgumentException.class)
+                .hasMessageContaining("Cannot replace an empty list");
+    }
+}
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 a5cc109614..47442f1b78 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
@@ -49,6 +49,7 @@ import org.apache.paimon.operation.commit.ConflictDetection;
 import org.apache.paimon.operation.commit.ManifestEntryChanges;
 import org.apache.paimon.operation.commit.RetryCommitResult;
 import 
org.apache.paimon.operation.commit.RetryCommitResult.CommitFailRetryResult;
+import 
org.apache.paimon.operation.commit.RetryCommitResult.ManifestMergeResult;
 import org.apache.paimon.operation.commit.RowIdColumnConflictChecker;
 import 
org.apache.paimon.operation.commit.RowTrackingCommitUtils.RowTrackingAssigned;
 import org.apache.paimon.operation.commit.StrictModeChecker;
@@ -74,6 +75,7 @@ import org.apache.paimon.utils.DataFilePathFactories;
 import org.apache.paimon.utils.FileStorePathFactory;
 import org.apache.paimon.utils.IOUtils;
 import org.apache.paimon.utils.InternalRowPartitionComputer;
+import org.apache.paimon.utils.ListUtils;
 import org.apache.paimon.utils.Pair;
 import org.apache.paimon.utils.RetryWaiter;
 import org.apache.paimon.utils.SnapshotManager;
@@ -979,6 +981,7 @@ public class FileStoreCommitImpl implements FileStoreCommit 
{
         String indexManifest = null;
         List<ManifestFileMeta> mergeBeforeManifests = new ArrayList<>();
         List<ManifestFileMeta> mergeAfterManifests = new ArrayList<>();
+        boolean skipManifestMergeOnRetry = false;
         long nextRowIdStart = firstRowIdStart;
         try {
             long previousTotalRecordCount = 0L;
@@ -998,9 +1001,19 @@ public class FileStoreCommitImpl implements 
FileStoreCommit {
             }
 
             // try to merge old manifest files to create base manifest list
-            mergeAfterManifests =
-                    ManifestFileMerger.merge(
-                            mergeBeforeManifests, manifestFile, partitionType, 
options);
+            ManifestMergeReuse manifestMergeReuse =
+                    tryReuseManifestMergeResult(retryResult, 
mergeBeforeManifests);
+            skipManifestMergeOnRetry = manifestMergeReuse == null && 
retryResult != null;
+            if (manifestMergeReuse != null) {
+                mergeBeforeManifests = manifestMergeReuse.preservedManifests;
+                mergeAfterManifests = manifestMergeReuse.mergeAfterManifests;
+            } else if (skipManifestMergeOnRetry) {
+                mergeAfterManifests = mergeBeforeManifests;
+            } else {
+                mergeAfterManifests =
+                        ManifestFileMerger.merge(
+                                mergeBeforeManifests, manifestFile, 
partitionType, options);
+            }
             baseManifestList = manifestList.write(mergeAfterManifests);
 
             if (options.rowTrackingEnabled()) {
@@ -1120,7 +1133,7 @@ public class FileStoreCommitImpl implements 
FileStoreCommit {
         } catch (Exception e) {
             // commit exception, not sure about the situation and should not 
clean up the files
             LOG.warn("Retry commit for exception.", e);
-            return RetryCommitResult.forCommitFail(latestSnapshot, 
baseDataFiles, e);
+            return RetryCommitResult.forCommitFail(latestSnapshot, 
baseDataFiles, e, null);
         }
 
         if (!success) {
@@ -1134,7 +1147,13 @@ public class FileStoreCommitImpl implements 
FileStoreCommit {
                     identifier,
                     commitKind.name(),
                     commitTime);
-            return RetryCommitResult.forCommitFail(latestSnapshot, 
baseDataFiles, null);
+            return RetryCommitResult.forCommitFail(
+                    latestSnapshot,
+                    baseDataFiles,
+                    null,
+                    skipManifestMergeOnRetry
+                            ? null
+                            : new ManifestMergeResult(mergeBeforeManifests, 
mergeAfterManifests));
         }
 
         LOG.info(
@@ -1156,6 +1175,49 @@ public class FileStoreCommitImpl implements 
FileStoreCommit {
         return new SuccessCommitResult();
     }
 
+    @Nullable
+    private ManifestMergeReuse tryReuseManifestMergeResult(
+            @Nullable RetryCommitResult retryResult, List<ManifestFileMeta> 
currentManifests) {
+        if (!(retryResult instanceof CommitFailRetryResult)) {
+            return null;
+        }
+
+        CommitFailRetryResult commitFailRetry = (CommitFailRetryResult) 
retryResult;
+        ManifestMergeResult previous = commitFailRetry.manifestMergeResult;
+        if (previous == null) {
+            return null;
+        }
+        if (previous.mergeBeforeManifests.isEmpty()) {
+            return currentManifests.isEmpty()
+                    ? new ManifestMergeReuse(currentManifests, 
previous.mergeAfterManifests)
+                    : null;
+        }
+
+        List<ManifestFileMeta> mergeAfterManifests =
+                ListUtils.tryReplace(
+                        currentManifests,
+                        previous.mergeBeforeManifests,
+                        previous.mergeAfterManifests);
+        if (mergeAfterManifests == null) {
+            return null;
+        }
+
+        return new ManifestMergeReuse(currentManifests, mergeAfterManifests);
+    }
+
+    private static class ManifestMergeReuse {
+
+        private final List<ManifestFileMeta> preservedManifests;
+        private final List<ManifestFileMeta> mergeAfterManifests;
+
+        private ManifestMergeReuse(
+                List<ManifestFileMeta> preservedManifests,
+                List<ManifestFileMeta> mergeAfterManifests) {
+            this.preservedManifests = preservedManifests;
+            this.mergeAfterManifests = mergeAfterManifests;
+        }
+    }
+
     public boolean replaceManifestList(
             Snapshot latest,
             long totalRecordCount,
diff --git 
a/paimon-core/src/main/java/org/apache/paimon/operation/commit/RetryCommitResult.java
 
b/paimon-core/src/main/java/org/apache/paimon/operation/commit/RetryCommitResult.java
index b9e0ab2a2e..717df209ce 100644
--- 
a/paimon-core/src/main/java/org/apache/paimon/operation/commit/RetryCommitResult.java
+++ 
b/paimon-core/src/main/java/org/apache/paimon/operation/commit/RetryCommitResult.java
@@ -19,10 +19,13 @@
 package org.apache.paimon.operation.commit;
 
 import org.apache.paimon.Snapshot;
+import org.apache.paimon.manifest.ManifestFileMeta;
 import org.apache.paimon.manifest.SimpleFileEntry;
 
 import javax.annotation.Nullable;
 
+import java.util.ArrayList;
+import java.util.Collections;
 import java.util.List;
 
 /** Need to retry commit of {@link CommitResult}. */
@@ -35,8 +38,11 @@ public abstract class RetryCommitResult implements 
CommitResult {
     }
 
     public static RetryCommitResult forCommitFail(
-            Snapshot snapshot, List<SimpleFileEntry> baseDataFiles, Exception 
exception) {
-        return new CommitFailRetryResult(snapshot, baseDataFiles, exception);
+            Snapshot snapshot,
+            List<SimpleFileEntry> baseDataFiles,
+            Exception exception,
+            @Nullable ManifestMergeResult manifestMergeResult) {
+        return new CommitFailRetryResult(snapshot, baseDataFiles, exception, 
manifestMergeResult);
     }
 
     public static RetryCommitResult forRollback(Exception exception) {
@@ -53,14 +59,33 @@ public abstract class RetryCommitResult implements 
CommitResult {
 
         public final @Nullable Snapshot latestSnapshot;
         public final @Nullable List<SimpleFileEntry> baseDataFiles;
+        public final @Nullable ManifestMergeResult manifestMergeResult;
 
         private CommitFailRetryResult(
                 @Nullable Snapshot latestSnapshot,
                 @Nullable List<SimpleFileEntry> baseDataFiles,
-                Exception exception) {
+                Exception exception,
+                @Nullable ManifestMergeResult manifestMergeResult) {
             super(exception);
             this.latestSnapshot = latestSnapshot;
             this.baseDataFiles = baseDataFiles;
+            this.manifestMergeResult = manifestMergeResult;
+        }
+    }
+
+    /** Manifest merge result which can be reused by commit retry. */
+    public static class ManifestMergeResult {
+
+        public final List<ManifestFileMeta> mergeBeforeManifests;
+        public final List<ManifestFileMeta> mergeAfterManifests;
+
+        public ManifestMergeResult(
+                List<ManifestFileMeta> mergeBeforeManifests,
+                List<ManifestFileMeta> mergeAfterManifests) {
+            this.mergeBeforeManifests =
+                    Collections.unmodifiableList(new 
ArrayList<>(mergeBeforeManifests));
+            this.mergeAfterManifests =
+                    Collections.unmodifiableList(new 
ArrayList<>(mergeAfterManifests));
         }
     }
 
diff --git 
a/paimon-core/src/test/java/org/apache/paimon/operation/FileStoreCommitTest.java
 
b/paimon-core/src/test/java/org/apache/paimon/operation/FileStoreCommitTest.java
index bed2a3863c..5a386947c1 100644
--- 
a/paimon-core/src/test/java/org/apache/paimon/operation/FileStoreCommitTest.java
+++ 
b/paimon-core/src/test/java/org/apache/paimon/operation/FileStoreCommitTest.java
@@ -43,8 +43,10 @@ import org.apache.paimon.manifest.ManifestCommittable;
 import org.apache.paimon.manifest.ManifestEntry;
 import org.apache.paimon.manifest.ManifestFile;
 import org.apache.paimon.manifest.ManifestFileMeta;
+import org.apache.paimon.manifest.ManifestList;
 import org.apache.paimon.mergetree.compact.DeduplicateMergeFunction;
 import org.apache.paimon.operation.commit.ConflictDetection;
+import org.apache.paimon.operation.commit.ManifestEntryChanges;
 import org.apache.paimon.operation.commit.RetryCommitResult;
 import org.apache.paimon.predicate.PredicateBuilder;
 import org.apache.paimon.schema.Schema;
@@ -61,6 +63,7 @@ import org.apache.paimon.types.DataTypes;
 import org.apache.paimon.types.RowKind;
 import org.apache.paimon.types.RowType;
 import org.apache.paimon.utils.FailingFileIO;
+import org.apache.paimon.utils.Pair;
 import org.apache.paimon.utils.SnapshotManager;
 import org.apache.paimon.utils.TraceableFileIO;
 
@@ -1132,7 +1135,8 @@ public class FileStoreCommitTest {
                     null);
             // Compact
             commit.tryCommitOnce(
-                    RetryCommitResult.forCommitFail(firstLatest, 
Collections.emptyList(), null),
+                    RetryCommitResult.forCommitFail(
+                            firstLatest, Collections.emptyList(), null, null),
                     Collections.emptyList(),
                     Collections.emptyList(),
                     Collections.emptyList(),
@@ -1183,6 +1187,393 @@ public class FileStoreCommitTest {
         assertThat(store.readKvsFromSnapshot(latestSnapshot.id())).hasSize(1);
     }
 
+    @Test
+    public void 
testCommitRetryReusePreviousManifestMergeResultWhenBeforeStillExists()
+            throws Exception {
+        TestFileStore store =
+                createStore(
+                        false,
+                        Collections.singletonMap(
+                                
CoreOptions.MANIFEST_FULL_COMPACTION_FILE_SIZE.key(), "1"));
+
+        store.commitData(
+                Collections.singletonList(gen.nextInsert("20211110", 8, 1L, 
null, "a")),
+                gen::getPartition,
+                kv -> 0);
+        store.commitData(
+                Collections.singletonList(gen.nextInsert("20211111", 9, 2L, 
null, "b")),
+                gen::getPartition,
+                kv -> 0);
+
+        AtomicReference<ManifestCommittable> committableRef = new 
AtomicReference<>();
+        store.commitDataImpl(
+                Collections.singletonList(gen.nextInsert("20211110", 9, 3L, 
null, "c")),
+                gen::getPartition,
+                value -> 0,
+                false,
+                23L,
+                null,
+                Collections.emptyList(),
+                (commit, committable) -> committableRef.set(committable));
+
+        ConflictingSnapshotCommit snapshotCommit =
+                new ConflictingSnapshotCommit(
+                        new RenamingSnapshotCommit(store.snapshotManager(), 
Lock.empty()),
+                        store.snapshotManager(),
+                        store.manifestListFactory().create(),
+                        store.manifestFileFactory().create(),
+                        false,
+                        conflictAttempts(Collections.emptyList()));
+        try (FileStoreCommitImpl commit =
+                newCommitWithSnapshotCommit(store, "retry-reuse-merge", 
snapshotCommit)) {
+            commit.commit(checkNotNull(committableRef.get()), false);
+        }
+
+        Snapshot latestSnapshot = 
checkNotNull(store.snapshotManager().latestSnapshot());
+        List<ManifestFileMeta> finalBaseManifests =
+                store.manifestListFactory()
+                        .create()
+                        .read(
+                                latestSnapshot.baseManifestList(),
+                                latestSnapshot.baseManifestListSize());
+        assertThat(finalBaseManifests)
+                
.containsExactlyElementsOf(snapshotCommit.firstAttemptBaseManifests());
+        assertThat(store.readKvsFromSnapshot(latestSnapshot.id())).hasSize(3);
+    }
+
+    @Test
+    public void 
testCommitRetrySkipsManifestMergeWhenBeforeExistsNonContiguously()
+            throws Exception {
+        TestFileStore store =
+                createStore(
+                        false,
+                        Collections.singletonMap(
+                                
CoreOptions.MANIFEST_FULL_COMPACTION_FILE_SIZE.key(), "1"));
+
+        store.commitData(
+                Collections.singletonList(gen.nextInsert("20211110", 8, 1L, 
null, "a")),
+                gen::getPartition,
+                kv -> 0);
+        store.commitData(
+                Collections.singletonList(gen.nextInsert("20211111", 9, 2L, 
null, "b")),
+                gen::getPartition,
+                kv -> 0);
+
+        AtomicReference<ManifestCommittable> conflictCommittableRef = new 
AtomicReference<>();
+        store.commitDataImpl(
+                Collections.singletonList(gen.nextInsert("20211112", 10, 4L, 
null, "d")),
+                gen::getPartition,
+                value -> 0,
+                false,
+                22L,
+                null,
+                Collections.emptyList(),
+                (commit, committable) -> 
conflictCommittableRef.set(committable));
+        List<ManifestEntry> conflictDeltaFiles =
+                tableFilesFrom(checkNotNull(conflictCommittableRef.get()), 
store.options());
+
+        AtomicReference<ManifestCommittable> committableRef = new 
AtomicReference<>();
+        store.commitDataImpl(
+                Collections.singletonList(gen.nextInsert("20211110", 9, 3L, 
null, "c")),
+                gen::getPartition,
+                value -> 0,
+                false,
+                23L,
+                null,
+                Collections.emptyList(),
+                (commit, committable) -> committableRef.set(committable));
+
+        ConflictingSnapshotCommit snapshotCommit =
+                new ConflictingSnapshotCommit(
+                        new RenamingSnapshotCommit(store.snapshotManager(), 
Lock.empty()),
+                        store.snapshotManager(),
+                        store.manifestListFactory().create(),
+                        store.manifestFileFactory().create(),
+                        false,
+                        conflictAttempts(conflictDeltaFiles));
+        try (FileStoreCommitImpl commit =
+                newCommitWithSnapshotCommit(store, 
"retry-skip-non-contiguous", snapshotCommit)) {
+            commit.commit(checkNotNull(committableRef.get()), false);
+        }
+
+        Snapshot latestSnapshot = 
checkNotNull(store.snapshotManager().latestSnapshot());
+        List<ManifestFileMeta> finalBaseManifests =
+                store.manifestListFactory()
+                        .create()
+                        .read(
+                                latestSnapshot.baseManifestList(),
+                                latestSnapshot.baseManifestListSize());
+        assertThat(finalBaseManifests)
+                
.containsExactlyElementsOf(snapshotCommit.conflictBaseManifests());
+        assertThat(store.readKvsFromSnapshot(latestSnapshot.id())).hasSize(4);
+    }
+
+    @Test
+    public void 
testCommitRetrySkipsManifestMergeWhenPreviousMergeCannotBeReused()
+            throws Exception {
+        Map<String, String> options = new HashMap<>();
+        options.put(CoreOptions.MANIFEST_FULL_COMPACTION_FILE_SIZE.key(), "1");
+        TestFileStore store = createStore(false, options);
+
+        store.commitData(
+                Collections.singletonList(gen.nextInsert("20211110", 8, 1L, 
null, "a")),
+                gen::getPartition,
+                kv -> 0);
+        store.commitData(
+                Collections.singletonList(gen.nextInsert("20211111", 9, 2L, 
null, "b")),
+                gen::getPartition,
+                kv -> 0);
+
+        Snapshot latestBeforeRetry = 
checkNotNull(store.snapshotManager().latestSnapshot());
+        
assertThat(store.manifestListFactory().create().readDataManifests(latestBeforeRetry))
+                .hasSize(2);
+
+        AtomicReference<ManifestCommittable> committableRef = new 
AtomicReference<>();
+        store.commitDataImpl(
+                Collections.singletonList(gen.nextInsert("20211110", 9, 3L, 
null, "c")),
+                gen::getPartition,
+                value -> 0,
+                false,
+                23L,
+                null,
+                Collections.emptyList(),
+                (commit, committable) -> committableRef.set(committable));
+
+        ConflictingSnapshotCommit snapshotCommit =
+                new ConflictingSnapshotCommit(
+                        new RenamingSnapshotCommit(store.snapshotManager(), 
Lock.empty()),
+                        store.snapshotManager(),
+                        store.manifestListFactory().create(),
+                        store.manifestFileFactory().create(),
+                        true,
+                        conflictAttempts(Collections.emptyList()));
+        try (FileStoreCommitImpl commit =
+                newCommitWithSnapshotCommit(store, "retry-reuse-merge", 
snapshotCommit)) {
+            commit.commit(checkNotNull(committableRef.get()), false);
+        }
+
+        Snapshot latestSnapshot = 
checkNotNull(store.snapshotManager().latestSnapshot());
+        List<ManifestFileMeta> finalBaseManifests =
+                store.manifestListFactory()
+                        .create()
+                        .read(
+                                latestSnapshot.baseManifestList(),
+                                latestSnapshot.baseManifestListSize());
+        assertThat(snapshotCommit.conflictBaseManifests()).hasSize(2);
+        assertThat(finalBaseManifests)
+                
.containsExactlyElementsOf(snapshotCommit.conflictBaseManifests());
+        
assertThat(finalBaseManifests).isNotEqualTo(snapshotCommit.firstAttemptBaseManifests());
+        assertThat(store.readKvsFromSnapshot(latestSnapshot.id())).hasSize(3);
+    }
+
+    @Test
+    public void testCommitRetrySkipsManifestMergeAcrossMultipleRetries() 
throws Exception {
+        TestFileStore store =
+                createStore(
+                        false,
+                        Collections.singletonMap(
+                                
CoreOptions.MANIFEST_FULL_COMPACTION_FILE_SIZE.key(), "1"));
+
+        store.commitData(
+                Collections.singletonList(gen.nextInsert("20211110", 8, 1L, 
null, "a")),
+                gen::getPartition,
+                kv -> 0);
+        store.commitData(
+                Collections.singletonList(gen.nextInsert("20211111", 9, 2L, 
null, "b")),
+                gen::getPartition,
+                kv -> 0);
+
+        AtomicReference<ManifestCommittable> firstConflictCommittableRef = new 
AtomicReference<>();
+        store.commitDataImpl(
+                Collections.singletonList(gen.nextInsert("20211112", 10, 4L, 
null, "d")),
+                gen::getPartition,
+                value -> 0,
+                false,
+                22L,
+                null,
+                Collections.emptyList(),
+                (commit, committable) -> 
firstConflictCommittableRef.set(committable));
+        List<ManifestEntry> firstConflictDeltaFiles =
+                
tableFilesFrom(checkNotNull(firstConflictCommittableRef.get()), 
store.options());
+
+        AtomicReference<ManifestCommittable> secondConflictCommittableRef = 
new AtomicReference<>();
+        store.commitDataImpl(
+                Collections.singletonList(gen.nextInsert("20211113", 11, 5L, 
null, "e")),
+                gen::getPartition,
+                value -> 0,
+                false,
+                24L,
+                null,
+                Collections.emptyList(),
+                (commit, committable) -> 
secondConflictCommittableRef.set(committable));
+        List<ManifestEntry> secondConflictDeltaFiles =
+                
tableFilesFrom(checkNotNull(secondConflictCommittableRef.get()), 
store.options());
+
+        AtomicReference<ManifestCommittable> committableRef = new 
AtomicReference<>();
+        store.commitDataImpl(
+                Collections.singletonList(gen.nextInsert("20211110", 9, 3L, 
null, "c")),
+                gen::getPartition,
+                value -> 0,
+                false,
+                23L,
+                null,
+                Collections.emptyList(),
+                (commit, committable) -> committableRef.set(committable));
+
+        ConflictingSnapshotCommit snapshotCommit =
+                new ConflictingSnapshotCommit(
+                        new RenamingSnapshotCommit(store.snapshotManager(), 
Lock.empty()),
+                        store.snapshotManager(),
+                        store.manifestListFactory().create(),
+                        store.manifestFileFactory().create(),
+                        false,
+                        conflictAttempts(firstConflictDeltaFiles, 
secondConflictDeltaFiles));
+        try (FileStoreCommitImpl commit =
+                newCommitWithSnapshotCommit(store, "retry-reuse-multiple", 
snapshotCommit)) {
+            commit.commit(checkNotNull(committableRef.get()), false);
+        }
+
+        Snapshot latestSnapshot = 
checkNotNull(store.snapshotManager().latestSnapshot());
+        List<ManifestFileMeta> finalBaseManifests =
+                store.manifestListFactory()
+                        .create()
+                        .read(
+                                latestSnapshot.baseManifestList(),
+                                latestSnapshot.baseManifestListSize());
+        assertThat(finalBaseManifests)
+                
.containsExactlyElementsOf(snapshotCommit.conflictBaseManifests());
+        assertThat(store.readKvsFromSnapshot(latestSnapshot.id())).hasSize(5);
+    }
+
+    @Test
+    public void testCommitRetryFromEmptyTableWithConcurrentFirstSnapshot() 
throws Exception {
+        TestFileStore store =
+                createStore(
+                        false,
+                        Collections.singletonMap(
+                                
CoreOptions.MANIFEST_FULL_COMPACTION_FILE_SIZE.key(), "1"));
+
+        AtomicReference<ManifestCommittable> conflictCommittableRef = new 
AtomicReference<>();
+        store.commitDataImpl(
+                Collections.singletonList(gen.nextInsert("20211112", 10, 4L, 
null, "d")),
+                gen::getPartition,
+                value -> 0,
+                false,
+                22L,
+                null,
+                Collections.emptyList(),
+                (commit, committable) -> 
conflictCommittableRef.set(committable));
+        List<ManifestEntry> conflictDeltaFiles =
+                tableFilesFrom(checkNotNull(conflictCommittableRef.get()), 
store.options());
+
+        AtomicReference<ManifestCommittable> committableRef = new 
AtomicReference<>();
+        store.commitDataImpl(
+                Collections.singletonList(gen.nextInsert("20211110", 9, 3L, 
null, "c")),
+                gen::getPartition,
+                value -> 0,
+                false,
+                23L,
+                null,
+                Collections.emptyList(),
+                (commit, committable) -> committableRef.set(committable));
+
+        ConflictingSnapshotCommit snapshotCommit =
+                new ConflictingSnapshotCommit(
+                        new RenamingSnapshotCommit(store.snapshotManager(), 
Lock.empty()),
+                        store.snapshotManager(),
+                        store.manifestListFactory().create(),
+                        store.manifestFileFactory().create(),
+                        false,
+                        conflictAttempts(conflictDeltaFiles));
+        try (FileStoreCommitImpl commit =
+                newCommitWithSnapshotCommit(store, "retry-reuse-empty-table", 
snapshotCommit)) {
+            commit.commit(checkNotNull(committableRef.get()), false);
+        }
+
+        Snapshot latestSnapshot = 
checkNotNull(store.snapshotManager().latestSnapshot());
+        List<ManifestFileMeta> finalBaseManifests =
+                store.manifestListFactory()
+                        .create()
+                        .read(
+                                latestSnapshot.baseManifestList(),
+                                latestSnapshot.baseManifestListSize());
+        assertThat(finalBaseManifests)
+                
.containsExactlyElementsOf(snapshotCommit.conflictDeltaManifests());
+        assertThat(store.readKvsFromSnapshot(latestSnapshot.id())).hasSize(2);
+    }
+
+    @Test
+    public void testCommitRetrySkipsManifestMergePreservesDeleteOrder() throws 
Exception {
+        TestFileStore store =
+                createStore(
+                        false,
+                        Collections.singletonMap(
+                                
CoreOptions.MANIFEST_FULL_COMPACTION_FILE_SIZE.key(), "1"));
+
+        store.commitData(
+                Collections.singletonList(gen.nextInsert("20211110", 8, 1L, 
null, "a")),
+                gen::getPartition,
+                kv -> 0);
+        store.commitData(
+                Collections.singletonList(gen.nextInsert("20211111", 9, 2L, 
null, "b")),
+                gen::getPartition,
+                kv -> 0);
+
+        Snapshot latestBeforeRetry = 
checkNotNull(store.snapshotManager().latestSnapshot());
+        List<ManifestFileMeta> manifestsBeforeRetry =
+                
store.manifestListFactory().create().readDataManifests(latestBeforeRetry);
+        assertThat(manifestsBeforeRetry).hasSize(2);
+        ManifestEntry firstEntry =
+                store.manifestFileFactory()
+                        .create()
+                        .read(manifestsBeforeRetry.get(0).fileName())
+                        .get(0);
+        ManifestEntry deleteFirstEntry =
+                ManifestEntry.create(
+                        FileKind.DELETE,
+                        firstEntry.partition(),
+                        firstEntry.bucket(),
+                        firstEntry.totalBuckets(),
+                        firstEntry.file());
+
+        AtomicReference<ManifestCommittable> committableRef = new 
AtomicReference<>();
+        store.commitDataImpl(
+                Collections.singletonList(gen.nextInsert("20211110", 9, 3L, 
null, "c")),
+                gen::getPartition,
+                value -> 0,
+                false,
+                23L,
+                null,
+                Collections.emptyList(),
+                (commit, committable) -> committableRef.set(committable));
+
+        ConflictingSnapshotCommit snapshotCommit =
+                new ConflictingSnapshotCommit(
+                        new RenamingSnapshotCommit(store.snapshotManager(), 
Lock.empty()),
+                        store.snapshotManager(),
+                        store.manifestListFactory().create(),
+                        store.manifestFileFactory().create(),
+                        false,
+                        
conflictAttempts(Collections.singletonList(deleteFirstEntry)));
+        try (FileStoreCommitImpl commit =
+                newCommitWithSnapshotCommit(store, "retry-reuse-delete-order", 
snapshotCommit)) {
+            commit.commit(checkNotNull(committableRef.get()), false);
+        }
+
+        Snapshot latestSnapshot = 
checkNotNull(store.snapshotManager().latestSnapshot());
+        List<ManifestFileMeta> finalBaseManifests =
+                store.manifestListFactory()
+                        .create()
+                        .read(
+                                latestSnapshot.baseManifestList(),
+                                latestSnapshot.baseManifestListSize());
+        assertThat(finalBaseManifests)
+                
.containsExactlyElementsOf(snapshotCommit.conflictBaseManifests());
+        assertThat(store.readKvsFromSnapshot(latestSnapshot.id()))
+                .extracting(kv -> kv.value().getString(6).toString())
+                .containsExactlyInAnyOrder("b", "c");
+    }
+
     private FileStoreCommitImpl newCommitWithSnapshotCommit(
             TestFileStore store, String commitUser, SnapshotCommit 
snapshotCommit) {
         return newCommitWithSnapshotCommit(
@@ -1281,6 +1672,177 @@ public class FileStoreCommitTest {
         return committable;
     }
 
+    private static List<ManifestEntry> tableFilesFrom(
+            ManifestCommittable committable, CoreOptions options) {
+        ManifestEntryChanges changes = new 
ManifestEntryChanges(options.bucket());
+        committable.fileCommittables().forEach(changes::collect);
+        return new ArrayList<>(changes.appendTableFiles);
+    }
+
+    @SafeVarargs
+    private static List<List<ManifestEntry>> 
conflictAttempts(List<ManifestEntry>... attempts) {
+        return Arrays.asList(attempts);
+    }
+
+    private static class ConflictingSnapshotCommit implements SnapshotCommit {
+
+        private final SnapshotCommit delegate;
+        private final SnapshotManager snapshotManager;
+        private final ManifestList manifestList;
+        private final ManifestFile manifestFile;
+        private final boolean mergeConflictManifests;
+        private final List<List<ManifestEntry>> conflictDeltaFilesByAttempt;
+        private int commitAttempt = 0;
+        private final List<List<ManifestFileMeta>> attemptBaseManifests;
+        private final List<List<ManifestFileMeta>> 
conflictDeltaManifestsByAttempt;
+        private List<ManifestFileMeta> conflictBaseManifests;
+
+        private ConflictingSnapshotCommit(
+                SnapshotCommit delegate,
+                SnapshotManager snapshotManager,
+                ManifestList manifestList,
+                ManifestFile manifestFile,
+                boolean mergeConflictManifests,
+                List<List<ManifestEntry>> conflictDeltaFilesByAttempt) {
+            this.delegate = delegate;
+            this.snapshotManager = snapshotManager;
+            this.manifestList = manifestList;
+            this.manifestFile = manifestFile;
+            this.mergeConflictManifests = mergeConflictManifests;
+            this.conflictDeltaFilesByAttempt = conflictDeltaFilesByAttempt;
+            this.attemptBaseManifests = new ArrayList<>();
+            this.conflictDeltaManifestsByAttempt = new ArrayList<>();
+        }
+
+        @Override
+        public boolean commit(
+                Snapshot snapshot,
+                String branch,
+                List<org.apache.paimon.partition.PartitionStatistics> 
statistics)
+                throws Exception {
+            if (commitAttempt >= conflictDeltaFilesByAttempt.size()) {
+                return delegate.commit(snapshot, branch, statistics);
+            }
+
+            List<ManifestEntry> conflictDeltaFiles = 
conflictDeltaFilesByAttempt.get(commitAttempt);
+            commitAttempt++;
+            attemptBaseManifests.add(
+                    manifestList.read(
+                            snapshot.baseManifestList(), 
snapshot.baseManifestListSize()));
+
+            Snapshot previousSnapshot =
+                    snapshot.id() == Snapshot.FIRST_SNAPSHOT_ID
+                            ? null
+                            : snapshotManager.snapshot(snapshot.id() - 1);
+            List<ManifestFileMeta> previousManifests =
+                    previousSnapshot == null
+                            ? Collections.emptyList()
+                            : manifestList.readDataManifests(previousSnapshot);
+            conflictBaseManifests =
+                    mergeConflictManifests
+                            ? rewriteManifests(previousManifests)
+                            : previousManifests;
+            List<ManifestFileMeta> conflictNewManifests =
+                    conflictDeltaFiles.isEmpty()
+                            ? Collections.emptyList()
+                            : manifestFile.write(conflictDeltaFiles);
+            conflictDeltaManifestsByAttempt.add(conflictNewManifests);
+            boolean putConflictNewManifestsInBase =
+                    !mergeConflictManifests
+                            && !conflictNewManifests.isEmpty()
+                            && !previousManifests.isEmpty();
+            if (putConflictNewManifestsInBase) {
+                conflictBaseManifests = new ArrayList<>();
+                conflictBaseManifests.add(previousManifests.get(0));
+                conflictBaseManifests.addAll(conflictNewManifests);
+                conflictBaseManifests.addAll(
+                        previousManifests.subList(1, 
previousManifests.size()));
+            }
+            Pair<String, Long> conflictBaseManifestList = 
manifestList.write(conflictBaseManifests);
+            long conflictDeltaRecordCount =
+                    conflictDeltaFiles.stream()
+                            .mapToLong(
+                                    entry ->
+                                            entry.kind() == FileKind.ADD
+                                                    ? entry.file().rowCount()
+                                                    : -entry.file().rowCount())
+                            .sum();
+            Pair<String, Long> conflictDeltaManifestList =
+                    manifestList.write(
+                            putConflictNewManifestsInBase
+                                    ? Collections.emptyList()
+                                    : conflictNewManifests);
+            Snapshot conflictSnapshot =
+                    new Snapshot(
+                            snapshot.id(),
+                            previousSnapshot == null
+                                    ? snapshot.schemaId()
+                                    : previousSnapshot.schemaId(),
+                            conflictBaseManifestList.getLeft(),
+                            conflictBaseManifestList.getRight(),
+                            conflictDeltaManifestList.getLeft(),
+                            conflictDeltaManifestList.getRight(),
+                            null,
+                            null,
+                            previousSnapshot == null ? null : 
previousSnapshot.indexManifest(),
+                            "conflict-user",
+                            Long.MAX_VALUE,
+                            Snapshot.CommitKind.ANALYZE,
+                            System.currentTimeMillis(),
+                            (previousSnapshot == null ? 0L : 
previousSnapshot.totalRecordCount())
+                                    + conflictDeltaRecordCount,
+                            conflictDeltaRecordCount,
+                            null,
+                            previousSnapshot == null ? null : 
previousSnapshot.watermark(),
+                            previousSnapshot == null ? null : 
previousSnapshot.statistics(),
+                            previousSnapshot == null ? null : 
previousSnapshot.properties(),
+                            previousSnapshot == null ? null : 
previousSnapshot.nextRowId());
+            assertThat(delegate.commit(conflictSnapshot, branch, 
Collections.emptyList())).isTrue();
+            return false;
+        }
+
+        private List<ManifestFileMeta> rewriteManifests(List<ManifestFileMeta> 
manifests) {
+            if (manifests.isEmpty()) {
+                return Collections.emptyList();
+            }
+
+            List<ManifestFileMeta> rewrittenManifests = new ArrayList<>();
+            for (ManifestFileMeta manifest : manifests) {
+                rewrittenManifests.addAll(
+                        
manifestFile.write(manifestFile.read(manifest.fileName())));
+            }
+            return rewrittenManifests;
+        }
+
+        private List<ManifestFileMeta> firstAttemptBaseManifests() {
+            return attemptBaseManifests(0);
+        }
+
+        private List<ManifestFileMeta> attemptBaseManifests(int attempt) {
+            assertThat(attemptBaseManifests).hasSizeGreaterThan(attempt);
+            return attemptBaseManifests.get(attempt);
+        }
+
+        private List<ManifestFileMeta> conflictBaseManifests() {
+            return checkNotNull(conflictBaseManifests);
+        }
+
+        private List<ManifestFileMeta> conflictDeltaManifests() {
+            assertThat(conflictDeltaManifestsByAttempt).isNotEmpty();
+            return 
conflictDeltaManifestsByAttempt.get(conflictDeltaManifestsByAttempt.size() - 1);
+        }
+
+        private List<ManifestFileMeta> conflictDeltaManifests(int attempt) {
+            
assertThat(conflictDeltaManifestsByAttempt).hasSizeGreaterThan(attempt);
+            return conflictDeltaManifestsByAttempt.get(attempt);
+        }
+
+        @Override
+        public void close() throws Exception {
+            delegate.close();
+        }
+    }
+
     private static class FalseSuccessSnapshotCommit implements SnapshotCommit {
 
         private final SnapshotCommit delegate;


Reply via email to