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

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


The following commit(s) were added to refs/heads/master by this push:
     new 0526f419746 [fix](iceberg) Tolerate concurrent namespace drops during 
listing (#68248)
0526f419746 is described below

commit 0526f419746f80c2823b79a6a1c1966da0868ab2
Author: Gabriel <[email protected]>
AuthorDate: Sun Sep 20 18:18:11 2026 +0800

    [fix](iceberg) Tolerate concurrent namespace drops during listing (#68248)
    
    ### What problem does this PR solve?
    
    When another session drops an Iceberg namespace between listing its
    parent and recursively listing its children, nested namespace discovery
    fails for the entire catalog. Unrelated `SHOW DATABASES` queries then
    fail with `NoSuchNamespaceException`, and database initialization can
    surface this as `Unknown database` for an existing database.
    
    Skip only child branches that disappear during recursive traversal,
    including their stale namespace names. Preserve failures when listing
    the root and propagate authorization and service errors. Also make the
    existing nested-namespace regression suite fail when its refresh retries
    are exhausted, instead of logging and passing.
    
    ### Release note
    
    Fix Iceberg REST nested namespace listings failing when another session
    concurrently drops an unrelated namespace.
    
    ### Check List (For Author)
    
    - Test
        - [x] Unit Test
    - [x] Regression test (strengthened existing retry assertion; cluster
    execution pending CI)
        - [x] Manual test (isolated Groovy retry helper)
    - Behavior changed:
    - [x] Yes. A concurrently deleted child namespace no longer aborts the
    catalog listing. Other listing errors remain visible.
    - Does this need documentation?
        - [x] No.
    
    Validation:
    
    - New deterministic namespace-deletion test fails on the original
    implementation; all 27 `CatalogBackedIcebergCatalogOpsTest` tests pass
    with the fix, including missing-root, permission, and service-error
    checks. Maven Checkstyle and the connector import gate pass.
    - Full Iceberg module test run: 1,450 tests, 1 failure, 5 skipped. The
    sole failure,
    
`IcebergWritePlanProviderTest#planMergePreservesExplicitlyEmptyReadAcrossConcurrentFirstAppend`
    (expected `null`, actual `-1`), also fails on unmodified master
    `4e3b9673`; it is outside this change.
    - Executed the regression suite's actual retry closure in an isolated
    Groovy harness: exhaustion previously returned successfully and now
    throws the original exception; a transient failure still recovers after
    refresh.
    - No live Doris/Iceberg cluster regression was run locally.
    
    Commands:
    
    ```bash
    mvn -f fe/pom.xml -pl :fe-connector-iceberg -am test \
      -Dtest=CatalogBackedIcebergCatalogOpsTest -DfailIfNoTests=false \
      -Dmaven.build.cache.enabled=false
    mvn -f fe/pom.xml -pl :fe-connector-iceberg -am test \
      -Dmaven.build.cache.enabled=false
    ```
    
    ### Check List (For Reviewer who merge this PR)
    
    - [ ] Confirm the release note
    - [ ] Confirm test cases
    - [ ] Confirm document
    - [ ] Add branch pick label
---
 .../doris/connector/iceberg/IcebergCatalogOps.java | 15 ++++-
 .../CatalogBackedIcebergCatalogOpsTest.java        | 64 ++++++++++++++++++++++
 .../iceberg_and_internal_nested_namespace.groovy   |  4 +-
 3 files changed, 78 insertions(+), 5 deletions(-)

diff --git 
a/fe/fe-connector/fe-connector-iceberg/src/main/java/org/apache/doris/connector/iceberg/IcebergCatalogOps.java
 
b/fe/fe-connector/fe-connector-iceberg/src/main/java/org/apache/doris/connector/iceberg/IcebergCatalogOps.java
index 3b55a98be7c..cb2d420ed6d 100644
--- 
a/fe/fe-connector/fe-connector-iceberg/src/main/java/org/apache/doris/connector/iceberg/IcebergCatalogOps.java
+++ 
b/fe/fe-connector/fe-connector-iceberg/src/main/java/org/apache/doris/connector/iceberg/IcebergCatalogOps.java
@@ -40,6 +40,7 @@ import org.apache.iceberg.catalog.Namespace;
 import org.apache.iceberg.catalog.SupportsNamespaces;
 import org.apache.iceberg.catalog.TableIdentifier;
 import org.apache.iceberg.catalog.ViewCatalog;
+import org.apache.iceberg.exceptions.NoSuchNamespaceException;
 import org.apache.iceberg.expressions.Expressions;
 import org.apache.iceberg.expressions.Term;
 import org.apache.iceberg.types.Type;
@@ -323,9 +324,17 @@ public interface IcebergCatalogOps {
             SupportsNamespaces nsCatalog = (SupportsNamespaces) catalog;
             if (restFlavor && nestedNamespaceEnabled) {
                 return nsCatalog.listNamespaces(parentNs).stream()
-                        .flatMap(childNs -> Stream.concat(
-                                Stream.of(childNs.toString()),
-                                listNestedNamespaces(childNs).stream()))
+                        .flatMap(childNs -> {
+                            try {
+                                List<String> descendants = 
listNestedNamespaces(childNs);
+                                return 
Stream.concat(Stream.of(childNs.toString()), descendants.stream());
+                            } catch (NoSuchNamespaceException e) {
+                                // A child can be dropped after its parent was 
listed. Skip that branch,
+                                // including the stale child name, without 
hiding failures to list the root.
+                                LOG.debug("Namespace {} was dropped during 
listing", childNs, e);
+                                return Stream.empty();
+                            }
+                        })
                         .collect(Collectors.toList());
             }
             return nsCatalog.listNamespaces(parentNs).stream()
diff --git 
a/fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/CatalogBackedIcebergCatalogOpsTest.java
 
b/fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/CatalogBackedIcebergCatalogOpsTest.java
index 119b0ce953f..5b8f1bfd5e7 100644
--- 
a/fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/CatalogBackedIcebergCatalogOpsTest.java
+++ 
b/fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/CatalogBackedIcebergCatalogOpsTest.java
@@ -23,6 +23,9 @@ import org.apache.doris.connector.spi.DorisConnectorException;
 import org.apache.iceberg.catalog.Catalog;
 import org.apache.iceberg.catalog.Namespace;
 import org.apache.iceberg.catalog.TableIdentifier;
+import org.apache.iceberg.exceptions.ForbiddenException;
+import org.apache.iceberg.exceptions.NoSuchNamespaceException;
+import org.apache.iceberg.exceptions.ServiceFailureException;
 import org.apache.iceberg.view.View;
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.Test;
@@ -30,7 +33,9 @@ import org.junit.jupiter.api.Test;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
 import java.util.Optional;
 
 /**
@@ -84,6 +89,52 @@ public class CatalogBackedIcebergCatalogOpsTest {
                 "REST + nested-enabled must recurse and emit dotted namespace 
names depth-first");
     }
 
+    @Test
+    public void listDatabaseNamesSkipsNamespacesDeletedDuringTraversal() {
+        FailingNamespaceCatalog catalog = new FailingNamespaceCatalog();
+        Namespace deleted = Namespace.of("deleted");
+        Namespace parent = Namespace.of("parent");
+        Namespace deletedChild = Namespace.of("parent", "deleted");
+        Namespace emptyChild = Namespace.of("parent", "empty");
+        Namespace sibling = Namespace.of("sibling");
+        catalog.childNamespaces.put(Namespace.empty(), Arrays.asList(deleted, 
parent, sibling));
+        catalog.childNamespaces.put(parent, Arrays.asList(deletedChild, 
emptyChild));
+        // The parent listing still contains namespaces that a concurrent DROP 
has already removed.
+        catalog.failures.put(deleted, new NoSuchNamespaceException("Namespace 
does not exist: %s", deleted));
+        catalog.failures.put(deletedChild, new 
NoSuchNamespaceException("Namespace does not exist: %s", deletedChild));
+
+        List<String> result = ops(catalog, true, true, true, 
Optional.empty()).listDatabaseNames();
+
+        Assertions.assertEquals(Arrays.asList("parent", "parent.empty", 
"sibling"), result);
+    }
+
+    @Test
+    public void listDatabaseNamesPropagatesMissingRootNamespace() {
+        for (Optional<String> externalCatalogName : 
Arrays.asList(Optional.<String>empty(), Optional.of("cat"))) {
+            FailingNamespaceCatalog catalog = new FailingNamespaceCatalog();
+            Namespace root = 
externalCatalogName.map(Namespace::of).orElse(Namespace.empty());
+            NoSuchNamespaceException failure = new 
NoSuchNamespaceException("Missing root: %s", root);
+            catalog.failures.put(root, failure);
+
+            Assertions.assertSame(failure, 
Assertions.assertThrows(NoSuchNamespaceException.class,
+                    () -> ops(catalog, true, true, true, 
externalCatalogName).listDatabaseNames()));
+        }
+    }
+
+    @Test
+    public void listDatabaseNamesPropagatesOtherTraversalFailures() {
+        for (RuntimeException failure : Arrays.asList(
+                new ForbiddenException("Access denied"), new 
ServiceFailureException("Service unavailable"))) {
+            FailingNamespaceCatalog catalog = new FailingNamespaceCatalog();
+            Namespace child = Namespace.of("child");
+            catalog.childNamespaces.put(Namespace.empty(), 
Collections.singletonList(child));
+            catalog.failures.put(child, failure);
+
+            Assertions.assertSame(failure, 
Assertions.assertThrows(failure.getClass(),
+                    () -> ops(catalog, true, true, true, 
Optional.empty()).listDatabaseNames()));
+        }
+    }
+
     @Test
     public void listDatabaseNamesDoesNotRecurseWhenNestedFlagButNotRest() {
         // WHY: legacy gates recursion on `dorisCatalog instanceof 
IcebergRestExternalCatalog` AS WELL AS
@@ -403,4 +454,17 @@ public class CatalogBackedIcebergCatalogOpsTest {
         Assertions.assertEquals(TableIdentifier.of(Namespace.of("a", "b"), 
"t1"), catalog.lastTableExistsId,
                 "tableExists must build the identifier from the split 
namespace");
     }
+
+    private static class FailingNamespaceCatalog extends FakeIcebergCatalog {
+        private final Map<Namespace, RuntimeException> failures = new 
HashMap<>();
+
+        @Override
+        public List<Namespace> listNamespaces(Namespace ns) {
+            RuntimeException failure = failures.get(ns);
+            if (failure != null) {
+                throw failure;
+            }
+            return super.listNamespaces(ns);
+        }
+    }
 }
diff --git 
a/regression-test/suites/external_table_p0/iceberg/iceberg_and_internal_nested_namespace.groovy
 
b/regression-test/suites/external_table_p0/iceberg/iceberg_and_internal_nested_namespace.groovy
index effb1b2fc8c..abf060b85ce 100644
--- 
a/regression-test/suites/external_table_p0/iceberg/iceberg_and_internal_nested_namespace.groovy
+++ 
b/regression-test/suites/external_table_p0/iceberg/iceberg_and_internal_nested_namespace.groovy
@@ -57,8 +57,8 @@ suite("iceberg_and_internal_nested_namespace", "p0,external") 
{
                         owner.sql("""refresh catalog ${catalog_name}""")
                         sleep(500) // Sleep 500ms before retry
                     } else {
-                        // log but not throw exception
-                        logger.error("Query failed after ${maxRetries} 
attempts: ${errorMsg}")
+                        // Exhausted retries must fail the suite instead of 
hiding a namespace listing regression.
+                        throw e
                     }
                 } else {
                     throw e // Rethrow if it's a different exception


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to