yuqi1129 commented on code in PR #11362:
URL: https://github.com/apache/gravitino/pull/11362#discussion_r3356571351


##########
iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/iceberg/service/dispatcher/IcebergNamespaceHookDispatcher.java:
##########
@@ -147,49 +144,26 @@ public void dropNamespace(IcebergRequestContext context, 
Namespace namespace) {
         () -> {
           dispatcher.dropNamespace(context, namespace);
 
-          // Only the leaf namespace is dropped from the Iceberg catalog 
above. getAncestorNames
-          // returns ancestors outermost-to-innermost, so we walk it in 
reverse (innermost outward),
-          // advancing outermostStale past every ancestor whose Iceberg 
namespace no longer exists
-          // and stopping at the first ancestor that still exists. 
outermostStale therefore ends up
-          // as the outermost missing ancestor; everything from there down to 
the leaf is now stale
-          // in Gravitino.
-          //
-          // An inner Iceberg namespace cannot exist unless all of its outer 
ancestors do, so an
-          // ancestor missing from the catalog implies none of its descendants 
exist there
-          // either. A single cascade delete of the outermost empty namespace 
therefore removes
-          // it and every descendant Gravitino entity (the leaf plus the 
intermediate ancestors)
-          // in one batched operation, rather than issuing one delete per 
target.
+          // Only the leaf namespace is dropped from the Iceberg catalog 
above. Dropping it may have
+          // also emptied its ancestors, so clean up every Gravitino schema 
entity whose Iceberg
+          // namespace no longer exists. An inner Iceberg namespace cannot 
exist unless all of its
+          // outer ancestors do, so the cleaner cascade-deletes the outermost 
stale namespace and
+          // every descendant entity in one batched operation.
           //
           // Ancestor entities are still only cleaned up when the underlying 
Iceberg catalog
           // removes the empty parents on leaf-drop, which is 
catalog-implementation-dependent.
           // For catalogs that keep empty parents, operators may need to drop 
them manually.
           String separator = HierarchicalSchemaUtil.schemaSeparator();
-          Namespace outermostStale = namespace;
-          String namespaceName = String.join(separator, namespace.levels());
-          List<String> ancestorNames =
-              HierarchicalSchemaUtil.getAncestorNames(namespaceName, 
separator);
-          for (int i = ancestorNames.size() - 1; i >= 0; i--) {
-            Namespace ancestor = 
Namespace.of(ancestorNames.get(i).split(Pattern.quote(separator)));
-            if (dispatcher.namespaceExists(context, ancestor)) {
-              break;
-            }
-            outermostStale = ancestor;
-          }
-
-          EntityStore store = GravitinoEnv.getInstance().entityStore();
-          if (store != null) {
-            try {
-              store.delete(
-                  IcebergIdentifierUtils.toGravitinoSchemaIdentifier(
-                      metalake, catalogName, outermostStale, separator),
-                  Entity.EntityType.SCHEMA,
-                  true);
-            } catch (NoSuchEntityException ignore) {
-              // Already gone.
-            } catch (IOException ioe) {
-              throw new RuntimeException("io exception when deleting schema 
entity", ioe);
-            }
-          }
+          SchemaEntityCleaner.deleteOrphanedSchemaEntities(

Review Comment:
   The table and view drop paths wrap their orphan-cleanup call in 
`IcebergOrphanSchemaCleanup.bestEffortCleanUp`, which adds a 
`try/catch(RuntimeException)` so any failure in the two setup calls 
(`schemaSeparator()`, `toGravitinoSchemaIdentifier()`) does not propagate back 
to the caller.
   
   Here in `dropNamespace`, `SchemaEntityCleaner.deleteOrphanedSchemaEntities` 
is called directly inside the `doWithTreeLock` lambda without that guard. Those 
setup calls are unlikely to throw in a running server, but for consistency — 
and to ensure `dropNamespace` never returns an error when the actual drop 
already succeeded — consider wrapping in a try/catch or routing through 
`IcebergOrphanSchemaCleanup.bestEffortCleanUp` the same way the table and view 
paths do.



##########
core/src/main/java/org/apache/gravitino/catalog/TableOperationDispatcher.java:
##########
@@ -420,10 +424,33 @@ public boolean purgeTable(NameIdentifier ident) throws 
UnsupportedOperationExcep
           } catch (Exception e) {
             throw new RuntimeException(e);
           }
+          // Run unconditionally: an out-of-band purge may have left orphaned 
schema entities. The
+          // cleanup is best-effort and stops as soon as a schema still exists.
+          cleanUpOrphanedSchemaEntities(catalogIdent, schemaIdentifier);

Review Comment:
   `purgeTable` now runs the orphan-schema cleanup unconditionally (good), but 
the PR only adds regression tests for `dropTable`. Could you add a 
corresponding test for `purgeTable`? The cleanup path is structurally the same, 
but a separate test ensures a future regression (e.g. someone re-guarding 
`cleanUpOrphanedSchemaEntities` behind `if (droppedFromCatalog)`) would be 
caught.



##########
core/src/main/java/org/apache/gravitino/catalog/ViewOperationDispatcher.java:
##########
@@ -292,10 +293,33 @@ public boolean dropView(NameIdentifier ident) {
           } catch (Exception e) {
             throw new RuntimeException(e);
           }
+          // Run unconditionally: an out-of-band drop may have left orphaned 
schema entities. The
+          // cleanup is best-effort and stops as soon as a schema still exists.
+          cleanUpOrphanedSchemaEntities(catalogIdent, schemaIdentifier);
           return droppedFromCatalog;
         });
   }
 
+  /**
+   * Removes Gravitino schema entities whose backing catalog schema no longer 
exists, starting from
+   * {@code schemaIdentifier} and walking toward its outermost ancestor.
+   *
+   * @param catalogIdent the identifier of the catalog owning the schema
+   * @param schemaIdentifier the schema identifier to start the orphan check 
from
+   */
+  private void cleanUpOrphanedSchemaEntities(

Review Comment:
   The private `cleanUpOrphanedSchemaEntities` here is identical to the one in 
`TableOperationDispatcher` (same parameters, same body, same semantics). 
Duplicated logic diverges silently — e.g. if a future PR adjusts `includeSelf` 
semantics or error handling in one class and misses the other.
   
   Consider extracting it as a package-private static helper in 
`SchemaEntityCleaner` (or promoting the `doWithCatalog` pattern into a shared 
base) so both dispatchers share a single implementation.



##########
iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/iceberg/service/dispatcher/IcebergOrphanSchemaCleanup.java:
##########
@@ -0,0 +1,79 @@
+/*
+ * 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.gravitino.iceberg.service.dispatcher;
+
+import org.apache.gravitino.GravitinoEnv;
+import org.apache.gravitino.iceberg.common.utils.IcebergIdentifierUtils;
+import org.apache.gravitino.listener.api.event.IcebergRequestContext;
+import org.apache.gravitino.utils.HierarchicalSchemaUtil;
+import org.apache.gravitino.utils.SchemaEntityCleaner;
+import org.apache.iceberg.catalog.Namespace;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Shared best-effort cleanup of Gravitino schema entities that become 
orphaned when an Iceberg
+ * table or view drop empties their backing namespaces.
+ */
+final class IcebergOrphanSchemaCleanup {
+
+  private static final Logger LOG = 
LoggerFactory.getLogger(IcebergOrphanSchemaCleanup.class);
+
+  private IcebergOrphanSchemaCleanup() {}
+
+  /**
+   * Removes Gravitino schema entities whose backing Iceberg namespace no 
longer exists after a
+   * table or view drop.
+   *
+   * <p>Best-effort: the primary drop has already succeeded, so any failure 
here is logged and
+   * swallowed rather than propagated to the caller.
+   *
+   * @param metalake the metalake the catalog belongs to
+   * @param namespaceDispatcher dispatcher used to probe whether a namespace 
still exists in the
+   *     catalog
+   * @param context the Iceberg request context carrying the catalog name
+   * @param namespace the namespace of the dropped table or view
+   */
+  static void bestEffortCleanUp(
+      String metalake,
+      IcebergNamespaceOperationDispatcher namespaceDispatcher,
+      IcebergRequestContext context,
+      Namespace namespace) {
+    try {
+      String separator = HierarchicalSchemaUtil.schemaSeparator();
+      SchemaEntityCleaner.deleteOrphanedSchemaEntities(
+          GravitinoEnv.getInstance().entityStore(),
+          IcebergIdentifierUtils.toGravitinoSchemaIdentifier(
+              metalake, context.catalogName(), namespace, separator),
+          true,
+          schemaIdent ->
+              namespaceDispatcher.namespaceExists(
+                  context,
+                  Namespace.of(
+                      
HierarchicalSchemaUtil.splitSchemaName(schemaIdent.name(), separator))));
+    } catch (RuntimeException e) {

Review Comment:
   `SchemaEntityCleaner.deleteOrphanedSchemaEntities` already wraps its entire 
body in `catch(Exception)` and swallows all failures, so nothing can escape it. 
The outer `catch(RuntimeException)` here can only be reached by the two setup 
lines above it (`schemaSeparator()` and `toGravitinoSchemaIdentifier()`), which 
are extremely unlikely to throw in a live server.
   
   Given that, this class is mostly a thin wrapper that adds one extra log 
entry. Alternatively: move the try/catch directly around the two setup calls, 
then let both `IcebergTableHookDispatcher` and `IcebergViewHookDispatcher` call 
`SchemaEntityCleaner.deleteOrphanedSchemaEntities` directly — the same pattern 
`IcebergNamespaceHookDispatcher.dropNamespace` now uses — and remove this 
indirection entirely.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to