Copilot commented on code in PR #11041:
URL: https://github.com/apache/gravitino/pull/11041#discussion_r3247897252


##########
core/src/main/java/org/apache/gravitino/catalog/TableOperationDispatcher.java:
##########
@@ -477,6 +477,22 @@ private EntityCombinedTable importTable(NameIdentifier 
identifier) {
     try {
       store.put(tableEntity, true);
     } catch (EntityAlreadyExistsException e) {
+      // HA race: another Gravitino node finished importing the same table 
between
+      // our internalLoadTable check above and this put. Reuse the existing 
entity
+      // instead of failing the caller's load with "managed by multiple 
catalogs".
+      TableEntity concurrentTableEntity = getEntity(identifier, TABLE, 
TableEntity.class);
+      if (concurrentTableEntity != null) {
+        LOG.info(
+            "Table {} was imported concurrently, reusing the existing entity 
in Gravitino.",
+            identifier);
+        return EntityCombinedTable.of(table.tableFromCatalog(), 
concurrentTableEntity)
+            .withHiddenProperties(
+                getHiddenPropertyNames(
+                    getCatalogIdentifier(identifier),
+                    HasPropertyMetadata::tablePropertiesMetadata,
+                    table.tableFromCatalog().properties()));
+      }

Review Comment:
   In the EntityAlreadyExistsException fallback, the code unconditionally 
treats the conflict as an HA race and reuses whatever entity is currently in 
the store. To avoid masking genuine inconsistencies/collisions (e.g., an 
existing entity with a different uid than the uid being imported), validate the 
fetched entity matches the expected uid (or the parsed StringIdentifier id when 
present) before reusing it; otherwise keep the current failure path.



##########
iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/iceberg/service/dispatcher/IcebergTableHookDispatcher.java:
##########
@@ -215,4 +206,47 @@ private void importTableAndSetOwner(
         context.userName(),
         GravitinoEnv.getInstance().ownerDispatcher());
   }
+
+  private void importTableEntity(String catalogName, Namespace namespace, 
String tableName) {
+    TableDispatcher tableDispatcher = 
GravitinoEnv.getInstance().tableDispatcher();
+    if (tableDispatcher != null) {
+      tableDispatcher.loadTable(
+          IcebergIdentifierUtils.toGravitinoTableIdentifier(
+              metalake, catalogName, TableIdentifier.of(namespace, 
tableName)));
+    }
+  }
+
+  private void reconcileTableEntity(
+      IcebergRequestContext context, TableIdentifier tableIdentifier) {
+    // IRC requests can be served by different Gravitino nodes. Without a 
distributed TreeLock,
+    // another node may drop or recreate the same Iceberg table between the 
backend operation and
+    // this hook's EntityStore mutation. Reconcile the local Gravitino entity 
with the Iceberg
+    // backend state to avoid leaving stale/orphan table metadata in 
multi-node deployments.
+    if (dispatcher.tableExists(context, tableIdentifier)) {
+      importTableEntity(context.catalogName(), tableIdentifier.namespace(), 
tableIdentifier.name());
+      return;
+    }
+
+    deleteTableEntity(context.catalogName(), tableIdentifier);
+
+    if (dispatcher.tableExists(context, tableIdentifier)) {
+      importTableEntity(context.catalogName(), tableIdentifier.namespace(), 
tableIdentifier.name());
+    }
+  }

Review Comment:
   reconcileTableEntity() can invoke tableDispatcher.loadTable() when the 
Iceberg backend reports the table exists. Any runtime failure from loadTable 
will now propagate out of dropTable/renameTable even though the backend 
operation already succeeded, increasing the chance of client-visible errors and 
retries while the system is already in the desired backend state. Consider 
making the reconciliation import best-effort here (log and continue) or 
otherwise ensuring drop/rename responses aren’t failed solely due to a post-op 
metadata reconciliation step.



##########
iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/iceberg/service/dispatcher/IcebergViewHookDispatcher.java:
##########
@@ -186,6 +156,12 @@ public void renameView(IcebergRequestContext context, 
RenameTableRequest renameV
       LOG.error("Failed to rename view entity in store from {} to {}", 
sourceIdent, destIdent, ioe);
       throw new RuntimeException("Failed to rename view entity in store", ioe);
     }
+
+    // IRC rename can race with another node's drop/create on either name.
+    // Reconcile both ends against the Iceberg backend so we don't leave a
+    // stale entity on the source or miss importing a re-created destination.
+    reconcileViewEntity(context, renameViewRequest.source());
+    reconcileViewEntity(context, renameViewRequest.destination());

Review Comment:
   reconcileViewEntity() is now called from renameView(). Since 
deleteViewEntity() throws RuntimeException on IOException, a transient 
EntityStore I/O failure during this post-op reconciliation can cause renameView 
to fail even after the Iceberg backend rename has succeeded. Consider treating 
reconciliation as best-effort for rename (log and continue) to avoid surfacing 
5xx solely due to metadata cleanup.
   



##########
core/src/main/java/org/apache/gravitino/catalog/SchemaOperationDispatcher.java:
##########
@@ -387,6 +387,18 @@ private void importSchema(NameIdentifier identifier) {
     try {
       store.put(schemaEntity, true);
     } catch (EntityAlreadyExistsException e) {
+      // HA race: another Gravitino node finished importing the same schema 
between
+      // our internalLoadSchema check above and this put. The entity is 
already in
+      // the store, so the import is effectively a no-op — let the caller's 
load
+      // request succeed instead of failing with "managed by multiple 
catalogs".
+      SchemaEntity concurrentSchemaEntity = getEntity(identifier, SCHEMA, 
SchemaEntity.class);
+      if (concurrentSchemaEntity != null) {
+        LOG.info(
+            "Schema {} was imported concurrently, reusing the existing entity 
in Gravitino.",
+            identifier);

Review Comment:
   The EntityAlreadyExistsException handler currently assumes the conflict is 
always an HA import race and returns success if any entity exists in the store. 
To prevent hiding real conflicts (e.g., an existing schema entity with a 
different id than the uid being imported), verify the fetched entity matches 
the expected uid / StringIdentifier id before treating it as a concurrent 
import; otherwise preserve the existing error path.
   



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