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

SteNicholas pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/paimon-cpp.git


The following commit(s) were added to refs/heads/main by this push:
     new e209445f fix(commit): handle ambiguous atomic snapshot writes (#397)
e209445f is described below

commit e209445f0e4ec21343ed41349b6531e4f8c12038
Author: lxy <[email protected]>
AuthorDate: Tue Sep 29 17:18:00 2026 +0800

    fix(commit): handle ambiguous atomic snapshot writes (#397)
---
 src/paimon/core/catalog/renaming_snapshot_commit.h | 23 ++++---
 .../core/catalog/renaming_snapshot_commit_test.cpp | 78 ++++++++++++++++++++++
 .../core/operation/file_store_commit_impl_test.cpp |  2 +-
 3 files changed, 94 insertions(+), 9 deletions(-)

diff --git a/src/paimon/core/catalog/renaming_snapshot_commit.h 
b/src/paimon/core/catalog/renaming_snapshot_commit.h
index b70ef0a8..da0269cb 100644
--- a/src/paimon/core/catalog/renaming_snapshot_commit.h
+++ b/src/paimon/core/catalog/renaming_snapshot_commit.h
@@ -65,14 +65,21 @@ class RenamingSnapshotCommit : public SnapshotCommit {
         if (is_exist) {
             return false;
         }
-        // To prevent the case where an atomic write times out but actually 
succeeds,
-        // retrying the commit could lead to the snapshot file being committed 
multiple times.
-        // Therefore, retries should be handled by the upper layer,
-        // which should call FilterAndCommit to avoid duplicate commits.
-        // Therefore, we should not trigger cleanup here,
-        // as it may delete meta files from a snapshot that was just written 
by ourselves,
-        // leading to an incomplete or corrupted snapshot.
-        PAIMON_RETURN_NOT_OK(fs_->AtomicStore(snapshot_path, json_str));
+        Status commit_status = fs_->AtomicStore(snapshot_path, json_str);
+        if (!commit_status.ok()) {
+            PAIMON_ASSIGN_OR_RAISE(bool snapshot_exists, 
fs_->Exists(snapshot_path));
+            if (!snapshot_exists) {
+                return commit_status;
+            }
+
+            std::string committed_json;
+            PAIMON_RETURN_NOT_OK(fs_->ReadFile(snapshot_path, 
&committed_json));
+            PAIMON_ASSIGN_OR_RAISE(Snapshot committed_snapshot,
+                                   Snapshot::FromJsonString(committed_json));
+            if (!(committed_snapshot == snapshot)) {
+                return false;
+            }
+        }
         
PAIMON_RETURN_NOT_OK(snapshot_manager_->CommitLatestHint(snapshot.Id()));
         return true;
     }
diff --git a/src/paimon/core/catalog/renaming_snapshot_commit_test.cpp 
b/src/paimon/core/catalog/renaming_snapshot_commit_test.cpp
index 4f363d74..4197cb5e 100644
--- a/src/paimon/core/catalog/renaming_snapshot_commit_test.cpp
+++ b/src/paimon/core/catalog/renaming_snapshot_commit_test.cpp
@@ -21,6 +21,7 @@
 #include <optional>
 #include <string>
 
+#include "gmock/gmock.h"
 #include "gtest/gtest.h"
 #include "paimon/common/utils/path_util.h"
 #include "paimon/core/partition/partition_statistics.h"
@@ -30,6 +31,15 @@
 #include "paimon/testing/utils/testharness.h"
 
 namespace paimon::test {
+namespace {
+
+class AtomicStoreMockFileSystem : public LocalFileSystem {
+ public:
+    MOCK_METHOD(Status, AtomicStore, (const std::string& path, const 
std::string& content),
+                (override));
+};
+
+}  // namespace
 
 TEST(RenamingSnapshotCommitTest, TestSimple) {
     auto fs = std::make_shared<LocalFileSystem>();
@@ -106,4 +116,72 @@ TEST(RenamingSnapshotCommitTest, 
TestBranchHasToMatchSnapshotManager) {
     ASSERT_TRUE(main_success);
 }
 
+TEST(RenamingSnapshotCommitTest, 
TestAtomicStoreErrorWithMatchingSnapshotIsSuccess) {
+    auto fs = std::make_shared<AtomicStoreMockFileSystem>();
+    auto dir = UniqueTestDirectory::Create();
+    ASSERT_TRUE(dir);
+    auto snapshot_manager = std::make_shared<SnapshotManager>(fs, dir->Str());
+    auto commit = std::make_shared<RenamingSnapshotCommit>(fs, 
snapshot_manager);
+    Snapshot snapshot = BuildTestSnapshot(1);
+    AtomicStoreMockFileSystem* fs_ptr = fs.get();
+
+    EXPECT_CALL(*fs,
+                AtomicStore(testing::A<const std::string&>(), testing::A<const 
std::string&>()))
+        .WillOnce(testing::Invoke(
+            [fs_ptr](const std::string& path, const std::string& content) -> 
Status {
+                PAIMON_RETURN_NOT_OK(fs_ptr->FileSystem::AtomicStore(path, 
content));
+                return Status::IOError("response lost after atomic store");
+            }));
+
+    ASSERT_OK_AND_ASSIGN(bool success, commit->Commit(std::nullopt, snapshot,
+                                                      
BranchManager::DEFAULT_MAIN_BRANCH, {}));
+    ASSERT_TRUE(success);
+    std::string latest_hint;
+    ASSERT_OK(fs->ReadFile(PathUtil::JoinPath(dir->Str(), "snapshot/LATEST"), 
&latest_hint));
+    ASSERT_EQ("1", latest_hint);
+}
+
+TEST(RenamingSnapshotCommitTest, 
TestAtomicStoreErrorWithConflictingSnapshotReturnsFalse) {
+    auto fs = std::make_shared<AtomicStoreMockFileSystem>();
+    auto dir = UniqueTestDirectory::Create();
+    ASSERT_TRUE(dir);
+    auto snapshot_manager = std::make_shared<SnapshotManager>(fs, dir->Str());
+    auto commit = std::make_shared<RenamingSnapshotCommit>(fs, 
snapshot_manager);
+    Snapshot snapshot = BuildTestSnapshot(1);
+    ASSERT_OK_AND_ASSIGN(std::string conflicting_json, 
BuildTestSnapshot(2).ToJsonString());
+    AtomicStoreMockFileSystem* fs_ptr = fs.get();
+
+    EXPECT_CALL(*fs,
+                AtomicStore(testing::A<const std::string&>(), testing::A<const 
std::string&>()))
+        .WillOnce(testing::Invoke(
+            [fs_ptr, &conflicting_json](const std::string& path, const 
std::string&) -> Status {
+                PAIMON_RETURN_NOT_OK(fs_ptr->FileSystem::AtomicStore(path, 
conflicting_json));
+                return Status::Exist("another writer committed this snapshot 
id");
+            }));
+
+    ASSERT_OK_AND_ASSIGN(bool success, commit->Commit(std::nullopt, snapshot,
+                                                      
BranchManager::DEFAULT_MAIN_BRANCH, {}));
+    ASSERT_FALSE(success);
+    ASSERT_OK_AND_ASSIGN(bool hint_exists,
+                         fs->Exists(PathUtil::JoinPath(dir->Str(), 
"snapshot/LATEST")));
+    ASSERT_FALSE(hint_exists);
+}
+
+TEST(RenamingSnapshotCommitTest, 
TestAtomicStoreErrorWithMissingSnapshotReturnsError) {
+    auto fs = std::make_shared<AtomicStoreMockFileSystem>();
+    auto dir = UniqueTestDirectory::Create();
+    ASSERT_TRUE(dir);
+    auto snapshot_manager = std::make_shared<SnapshotManager>(fs, dir->Str());
+    auto commit = std::make_shared<RenamingSnapshotCommit>(fs, 
snapshot_manager);
+    Snapshot snapshot = BuildTestSnapshot(1);
+
+    EXPECT_CALL(*fs,
+                AtomicStore(testing::A<const std::string&>(), testing::A<const 
std::string&>()))
+        .WillOnce(testing::Return(Status::IOError("atomic store failed")));
+
+    ASSERT_NOK_WITH_MSG(
+        commit->Commit(std::nullopt, snapshot, 
BranchManager::DEFAULT_MAIN_BRANCH, {}),
+        "atomic store failed");
+}
+
 }  // namespace paimon::test
diff --git a/src/paimon/core/operation/file_store_commit_impl_test.cpp 
b/src/paimon/core/operation/file_store_commit_impl_test.cpp
index b5d2b760..02ce10c4 100644
--- a/src/paimon/core/operation/file_store_commit_impl_test.cpp
+++ b/src/paimon/core/operation/file_store_commit_impl_test.cpp
@@ -1890,7 +1890,7 @@ TEST_F(FileStoreCommitImplTest, 
TestCommitWithAtomicWriteSnapshotTimeoutAndActua
                               
"/parquet/append_09.db/append_09/commit_messages/commit_messages-01",
                           /*version=*/3);
     ASSERT_GT(msgs.size(), 0);
-    ASSERT_NOK(commit->Commit(msgs, /*commit_identifier=*/1));
+    ASSERT_OK(commit->Commit(msgs, /*commit_identifier=*/1));
     ASSERT_OK_AND_ASSIGN(
         bool exist, file_system_->Exists(PathUtil::JoinPath(table_path, 
"snapshot/snapshot-6")));
     ASSERT_TRUE(exist);

Reply via email to