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


##########
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:
   Done. Added `testPurgeMissingTableCleansUpSchemas`, mirroring the 
`dropTable` out-of-band case: the catalog has already removed the table and 
auto-dropped the empty namespaces, so `purgeTable` returns false but the 
orphaned schema entities must still be cleaned up. It also guards against a 
future regression that re-guards the cleanup behind the catalog drop result. 
(Required adding a `purgeTable` override to `TestCatalogOperations`, since the 
default interface method throws `UnsupportedOperationException`.)



##########
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:
   Fixed. `dropNamespace` now routes through 
`IcebergOrphanSchemaCleanup.bestEffortCleanUp`, the same guarded entry point as 
the table and view paths, so all three Iceberg hook drop paths share one 
implementation and the cleanup can never surface an error after the backend 
drop has already succeeded.



##########
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:
   Addressed by unifying instead of removing: `dropNamespace` now also goes 
through `bestEffortCleanUp`, so all three Iceberg hook paths share this single 
helper rather than each inlining the setup + cleanup. That gives the wrapper a 
clear reason to exist (one guarded place that builds the identifier and 
swallows the unlikely setup-call failures) and removes the inconsistency in the 
namespace path.



##########
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:
   Done. Extracted the duplicated logic into a shared package-private helper 
`OrphanedSchemaCleanup` in the `catalog` package; both dispatchers now call 
`OrphanedSchemaCleanup.cleanUp(this, ...)`. I kept it out of 
`OperationDispatcher` itself, but the helper reuses the dispatcher's 
`protected` `store` and `doWithCatalog`, so there's now a single implementation.



-- 
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