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 92e39e101b [iceberg] Make REST catalog namespace creation idempotent 
under concurrent commits (#8471)
92e39e101b is described below

commit 92e39e101bbcfd8bb287f345a07be1aeac2b3022
Author: Andreas Bube <[email protected]>
AuthorDate: Mon Jul 6 14:47:13 2026 +0200

    [iceberg] Make REST catalog namespace creation idempotent under concurrent 
commits (#8471)
    
    When two Paimon sinks commit to different tables in the same Iceberg
    REST catalog database, and that database (namespace) does not yet exist,
    they can race on namespace creation.
    `IcebergRestMetadataCommitter.commitMetadataImpl()` uses a
    check-then-act pattern across two separate REST round-trips:
    
    ```java
    // create database if not exist
    if (!databaseExists()) {
        createDatabase();
    }
    ```
    
    Both commits can observe the namespace as missing and both call
    `createNamespace()`. The loser fails — per the Iceberg REST spec with a
    409 `AlreadyExistsException` (as `RESTServerExtension` returns), or with
    a 500 `ServiceFailureException` on the non-spec-compliant
    `apache/iceberg-rest-fixture` docker image. This causes a spurious
    commit failure and sink restart. It recovers after the restart (the
    namespace exists by then), but produces noise on the first commit of a
    fresh database.
    
    This makes namespace creation idempotent: `createDatabase()` now catches
    `AlreadyExistsException` and proceeds, since the required namespace now
    exists. (A spec-violating 500 from a non-compliant server is a separate,
    server-side problem and is intentionally not swallowed.)
---
 .../iceberg/IcebergRestMetadataCommitter.java      | 14 +++++++++++--
 .../iceberg/IcebergRestMetadataCommitterTest.java  | 24 ++++++++++++++++++++++
 2 files changed, 36 insertions(+), 2 deletions(-)

diff --git 
a/paimon-iceberg/src/main/java/org/apache/paimon/iceberg/IcebergRestMetadataCommitter.java
 
b/paimon-iceberg/src/main/java/org/apache/paimon/iceberg/IcebergRestMetadataCommitter.java
index 820fb2ff94..fbc352e0c5 100644
--- 
a/paimon-iceberg/src/main/java/org/apache/paimon/iceberg/IcebergRestMetadataCommitter.java
+++ 
b/paimon-iceberg/src/main/java/org/apache/paimon/iceberg/IcebergRestMetadataCommitter.java
@@ -18,6 +18,7 @@
 
 package org.apache.paimon.iceberg;
 
+import org.apache.paimon.annotation.VisibleForTesting;
 import org.apache.paimon.catalog.Identifier;
 import org.apache.paimon.fs.Path;
 import org.apache.paimon.iceberg.metadata.IcebergMetadata;
@@ -42,6 +43,7 @@ import org.apache.iceberg.TableMetadataParser;
 import org.apache.iceberg.catalog.Catalog;
 import org.apache.iceberg.catalog.Namespace;
 import org.apache.iceberg.catalog.TableIdentifier;
+import org.apache.iceberg.exceptions.AlreadyExistsException;
 import org.apache.iceberg.rest.RESTCatalog;
 import org.apache.iceberg.types.Types;
 import org.apache.iceberg.types.Types.NestedField;
@@ -287,8 +289,16 @@ public class IcebergRestMetadataCommitter implements 
IcebergMetadataCommitter {
         return restCatalog.tableExists(icebergTableIdentifier);
     }
 
-    private void createDatabase() {
-        restCatalog.createNamespace(Namespace.of(icebergDatabaseName));
+    @VisibleForTesting
+    void createDatabase() {
+        try {
+            restCatalog.createNamespace(Namespace.of(icebergDatabaseName));
+        } catch (AlreadyExistsException e) {
+            // Benign check-then-act race: another commit created the 
namespace concurrently.
+            LOG.info(
+                    "Namespace {} already exists, created concurrently by 
another commit.",
+                    icebergDatabaseName);
+        }
     }
 
     private Table createTable(TableMetadata newMetadata) {
diff --git 
a/paimon-iceberg/src/test/java/org/apache/paimon/iceberg/IcebergRestMetadataCommitterTest.java
 
b/paimon-iceberg/src/test/java/org/apache/paimon/iceberg/IcebergRestMetadataCommitterTest.java
index a3ae135686..0fbf130a73 100644
--- 
a/paimon-iceberg/src/test/java/org/apache/paimon/iceberg/IcebergRestMetadataCommitterTest.java
+++ 
b/paimon-iceberg/src/test/java/org/apache/paimon/iceberg/IcebergRestMetadataCommitterTest.java
@@ -46,6 +46,7 @@ import org.apache.iceberg.CatalogProperties;
 import org.apache.iceberg.PartitionSpec;
 import org.apache.iceberg.Schema;
 import org.apache.iceberg.Table;
+import org.apache.iceberg.catalog.Namespace;
 import org.apache.iceberg.catalog.TableIdentifier;
 import org.apache.iceberg.data.IcebergGenerics;
 import org.apache.iceberg.data.Record;
@@ -952,6 +953,29 @@ public class IcebergRestMetadataCommitterTest {
         commit.close();
     }
 
+    @Test
+    public void testCreateDatabaseIsIdempotentUnderRace() throws Exception {
+        // Two commits targeting the same namespace can both observe it as 
missing and both
+        // call createNamespace() (the check-then-act race in 
commitMetadataImpl). The loser
+        // must not fail. Here the second createDatabase() hits an 
already-existing namespace.
+        RowType rowType =
+                RowType.of(
+                        new DataType[] {DataTypes.INT(), DataTypes.INT()}, new 
String[] {"k", "v"});
+        FileStoreTable table =
+                createPaimonTable(
+                        rowType,
+                        Collections.emptyList(),
+                        Collections.singletonList("k"),
+                        1,
+                        randomFormat(),
+                        Collections.emptyMap());
+
+        IcebergRestMetadataCommitter committer = new 
IcebergRestMetadataCommitter(table);
+        committer.createDatabase();
+        committer.createDatabase();
+        assertThat(restCatalog.namespaceExists(Namespace.of("mydb"))).isTrue();
+    }
+
     @Test
     public void testToRestLocationNormalisesScheme() {
         // s3a:// and legacy s3n:// are rewritten to s3:// (Glue's REST 
endpoint only accepts

Reply via email to