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 78c94a5515 [core] add snapshot existence check for failed rename 
operation (#6778)
78c94a5515 is described below

commit 78c94a551517977af1dfe268ef4bc46171b66ddd
Author: aidadenski <[email protected]>
AuthorDate: Sun May 24 11:17:37 2026 +0900

    [core] add snapshot existence check for failed rename operation (#6778)
---
 .../paimon/catalog/RenamingSnapshotCommit.java     |  15 ++
 .../paimon/catalog/RenamingSnapshotCommitTest.java | 176 +++++++++++++++++++++
 2 files changed, 191 insertions(+)

diff --git 
a/paimon-core/src/main/java/org/apache/paimon/catalog/RenamingSnapshotCommit.java
 
b/paimon-core/src/main/java/org/apache/paimon/catalog/RenamingSnapshotCommit.java
index 5cc9d2a019..a79aafb298 100644
--- 
a/paimon-core/src/main/java/org/apache/paimon/catalog/RenamingSnapshotCommit.java
+++ 
b/paimon-core/src/main/java/org/apache/paimon/catalog/RenamingSnapshotCommit.java
@@ -25,6 +25,7 @@ import org.apache.paimon.operation.Lock;
 import org.apache.paimon.partition.PartitionStatistics;
 import org.apache.paimon.utils.SnapshotManager;
 
+import java.io.IOException;
 import java.util.List;
 import java.util.concurrent.Callable;
 
@@ -57,6 +58,20 @@ public class RenamingSnapshotCommit implements 
SnapshotCommit {
         Callable<Boolean> callable =
                 () -> {
                     boolean committed = 
fileIO.tryToWriteAtomic(newSnapshotPath, snapshot.toJson());
+                    if (!committed) {
+                        if (!fileIO.exists(newSnapshotPath)) {
+                            throw new IOException(
+                                    "Commit snapshot "
+                                            + snapshot.id()
+                                            + " failed and "
+                                            + newSnapshotPath
+                                            + " not found");
+                        }
+                        committed =
+                                snapshot.equals(
+                                        
Snapshot.fromJson(fileIO.readFileUtf8(newSnapshotPath)));
+                    }
+
                     if (committed) {
                         snapshotManager.commitLatestHint(snapshot.id());
                     }
diff --git 
a/paimon-core/src/test/java/org/apache/paimon/catalog/RenamingSnapshotCommitTest.java
 
b/paimon-core/src/test/java/org/apache/paimon/catalog/RenamingSnapshotCommitTest.java
new file mode 100644
index 0000000000..d58e4c86a4
--- /dev/null
+++ 
b/paimon-core/src/test/java/org/apache/paimon/catalog/RenamingSnapshotCommitTest.java
@@ -0,0 +1,176 @@
+/*
+ * 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.catalog;
+
+import org.apache.paimon.Snapshot;
+import org.apache.paimon.fs.FileIO;
+import org.apache.paimon.fs.Path;
+import org.apache.paimon.fs.local.LocalFileIO;
+import org.apache.paimon.operation.Lock;
+import org.apache.paimon.utils.HintFileUtils;
+import org.apache.paimon.utils.SnapshotManager;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+/**
+ * Tests for {@link RenamingSnapshotCommit} to verify robustness when object 
storage rename returns
+ * false while the target file is actually created, and to verify failure when 
target is missing.
+ */
+public class RenamingSnapshotCommitTest {
+
+    /**
+     * Simulate the object storage scenario where rename actually succeeds but 
returns false, which
+     * causes {@link FileIO#tryToWriteAtomic(Path, String)} to return false 
while the target file
+     * exists with correct content.
+     */
+    @Test
+    public void testCommitSucceedsAndRenameReturnsFalse(@TempDir 
java.nio.file.Path tmp)
+            throws Exception {
+        FileIO fileIO = new AlwaysFalseRenameLocalFileIO(true);
+        Path tablePath = new Path(tmp.toUri());
+        SnapshotManager snapshotManager = new SnapshotManager(fileIO, 
tablePath, null, null, null);
+
+        RenamingSnapshotCommit commit = new 
RenamingSnapshotCommit(snapshotManager, Lock.empty());
+
+        Snapshot snapshot = createSnapshot(1L);
+
+        boolean committed = commit.commit(snapshot, "main", 
Collections.emptyList());
+
+        assertThat(committed).isTrue();
+
+        // snapshot file should exist and match content
+        Path snapshotPath = snapshotManager.snapshotPath(snapshot.id());
+        assertThat(fileIO.exists(snapshotPath)).isTrue();
+        Snapshot loaded = Snapshot.fromJson(fileIO.readFileUtf8(snapshotPath));
+        assertThat(loaded).isEqualTo(snapshot);
+
+        // LATEST hint should be committed
+        Path latestHint = new Path(snapshotManager.snapshotDirectory(), 
HintFileUtils.LATEST);
+        assertThat(fileIO.readOverwrittenFileUtf8(latestHint).orElse(null))
+                .isEqualTo(String.valueOf(snapshot.id()));
+    }
+
+    /**
+     * Simulate the scenario where rename fails and the target snapshot file 
is not created; the
+     * commit should throw IOException according to the new check.
+     */
+    @Test
+    public void testCommitTargetSnapshotMissing(@TempDir java.nio.file.Path 
tmp) throws Exception {
+        FileIO fileIO = new AlwaysFalseRenameLocalFileIO(false);
+        Path tablePath = new Path(tmp.toUri());
+        SnapshotManager snapshotManager = new SnapshotManager(fileIO, 
tablePath, null, null, null);
+
+        RenamingSnapshotCommit commit = new 
RenamingSnapshotCommit(snapshotManager, Lock.empty());
+
+        Snapshot snapshot = createSnapshot(2L);
+
+        IOException ex =
+                assertThrows(
+                        IOException.class,
+                        () -> commit.commit(snapshot, "main", 
Collections.emptyList()));
+
+        assertThat(ex)
+                .hasMessageContaining("Commit snapshot " + snapshot.id() + " 
failed")
+                .hasMessageContaining("not found");
+
+        // ensure snapshot file is not accidentally created
+        
assertThat(fileIO.exists(snapshotManager.snapshotPath(snapshot.id()))).isFalse();
+    }
+
+    private static Snapshot createSnapshot(long id) throws IOException {
+        long schemaId = 1L;
+        String baseManifestList = "manifest-list-base";
+        String deltaManifestList = "manifest-list-delta";
+        String changelogManifestList = null;
+        Long baseManifestListSize = 10L;
+        Long deltaManifestListSize = 20L;
+        Long changelogManifestListSize = null;
+        String indexManifest = null;
+        String commitUser = "user";
+        long commitIdentifier = id;
+        Snapshot.CommitKind commitKind = Snapshot.CommitKind.APPEND;
+        long timeMillis = System.currentTimeMillis();
+        Map<Integer, Long> logOffsets = new HashMap<>();
+        Long totalRecordCount = 100L;
+        Long deltaRecordCount = 100L;
+
+        return new Snapshot(
+                id,
+                schemaId,
+                baseManifestList,
+                baseManifestListSize,
+                deltaManifestList,
+                deltaManifestListSize,
+                changelogManifestList,
+                changelogManifestListSize,
+                indexManifest,
+                commitUser,
+                commitIdentifier,
+                commitKind,
+                timeMillis,
+                logOffsets,
+                totalRecordCount,
+                deltaRecordCount,
+                null,
+                null,
+                null,
+                null,
+                null);
+    }
+
+    /**
+     * A FileIO based on LocalFileIO that always returns false from rename().
+     *
+     * <p>When {@code actuallyMove} is true, it performs the move then returns 
false; otherwise it
+     * returns false without moving, simulating a failed rename.
+     */
+    private static class AlwaysFalseRenameLocalFileIO extends LocalFileIO {
+
+        private final boolean actuallyMove;
+
+        AlwaysFalseRenameLocalFileIO(boolean actuallyMove) {
+            this.actuallyMove = actuallyMove;
+        }
+
+        @Override
+        public boolean rename(Path src, Path dst) throws IOException {
+            if (actuallyMove) {
+                // perform actual move to create target, but report failure
+                boolean ignored = super.rename(src, dst);
+                return false;
+            } else {
+                // do not move, report failure
+                // ensure parent directory exists to avoid side effects in 
tests
+                java.nio.file.Path parent = 
java.nio.file.Paths.get(dst.toUri());
+                Files.createDirectories(parent.getParent());
+                return false;
+            }
+        }
+    }
+}

Reply via email to