jerryshao commented on code in PR #12551:
URL: https://github.com/apache/gravitino/pull/12551#discussion_r3850762007


##########
core/src/main/java/org/apache/gravitino/catalog/OperationDispatcher.java:
##########
@@ -219,6 +220,9 @@ protected <R extends HasIdentifier> R operateOnEntity(
     R ret = null;
     try {
       ret = fn.apply(ident);
+    } catch (OptimisticLockException e) {

Review Comment:
   `operateOnEntity` now rethrows `OptimisticLockException` instead of 
tolerating it (previously caught by the generic `catch (Exception e)` below and 
just logged). For unmanaged/external tables, `alterTable` mutates the real 
catalog (e.g. Hive) first and only afterward syncs Gravitino's internal entity 
here. A lost CAS race on this internal sync now fails the whole `alterTable` 
call even though the external system was already changed — and a client that 
retries on error could re-apply a non-idempotent op (e.g. AddColumn) against 
the external system. Was this behavior change intentional, or should this 
particular sync failure still be tolerated for unmanaged catalogs?



##########
core/src/main/java/org/apache/gravitino/storage/relational/service/TableMetaService.java:
##########
@@ -374,4 +370,87 @@ private void 
fillTablePOBuilderParentEntityId(TablePO.Builder builder, Namespace
     builder.withCatalogId(namespacedEntityId.namespaceIds()[1]);
     builder.withSchemaId(namespacedEntityId.entityId());
   }
+
+  private TablePO tablePOWithPersistedVersions(TablePO incomingPO, TablePO 
persistedPO) {
+    return TablePO.builder()
+        .withTableId(incomingPO.getTableId())
+        .withTableName(incomingPO.getTableName())
+        .withMetalakeId(incomingPO.getMetalakeId())
+        .withCatalogId(incomingPO.getCatalogId())
+        .withSchemaId(incomingPO.getSchemaId())
+        .withAuditInfo(incomingPO.getAuditInfo())
+        .withCurrentVersion(persistedPO.getCurrentVersion())
+        .withLastVersion(persistedPO.getLastVersion())
+        .withDeletedAt(incomingPO.getDeletedAt())
+        .withFormat(incomingPO.getFormat())
+        .withProperties(incomingPO.getProperties())
+        .withPartitions(incomingPO.getPartitions())
+        .withSortOrders(incomingPO.getSortOrders())
+        .withDistribution(incomingPO.getDistribution())
+        .withIndexes(incomingPO.getIndexes())
+        .withComment(incomingPO.getComment())
+        .build();
+  }
+
+  private void deleteTableDependents(TablePO tablePO) {
+    // The table row has already passed its version check. All cleanup below 
uses the same database
+    // transaction, so either the table and every related row are deleted 
together, or none are.
+    SessionUtils.doWithoutCommit(
+        OwnerMetaMapper.class,
+        mapper ->
+            mapper.softDeleteOwnerRelByMetadataObjectIdAndType(
+                tablePO.getTableId(), MetadataObject.Type.TABLE.name()));
+    
TableColumnMetaService.getInstance().deleteColumnsByTableId(tablePO.getTableId());
+    SessionUtils.doWithoutCommit(
+        SecurableObjectMapper.class,
+        mapper ->
+            mapper.softDeleteObjectRelsByMetadataObject(
+                tablePO.getTableId(), MetadataObject.Type.TABLE.name()));
+    SessionUtils.doWithoutCommit(
+        TagMetadataObjectRelMapper.class,
+        mapper ->
+            mapper.softDeleteTagMetadataObjectRelsByMetadataObject(
+                tablePO.getTableId(), MetadataObject.Type.TABLE.name()));
+    SessionUtils.doWithoutCommit(
+        TagMetadataObjectRelMapper.class,
+        mapper -> 
mapper.softDeleteTagMetadataObjectRelsByTableId(tablePO.getTableId()));
+    SessionUtils.doWithoutCommit(
+        StatisticMetaMapper.class,
+        mapper -> mapper.softDeleteStatisticsByEntityId(tablePO.getTableId()));
+    SessionUtils.doWithoutCommit(
+        PolicyMetadataObjectRelMapper.class,
+        mapper -> 
mapper.softDeletePolicyMetadataObjectRelsByTableId(tablePO.getTableId()));
+    SessionUtils.doWithoutCommit(
+        TableVersionMapper.class,
+        mapper ->
+            mapper.softDeleteTableVersionByTableIdAndVersion(
+                tablePO.getTableId(), tablePO.getCurrentVersion()));
+  }
+
+  private RuntimeException tableWriteFailure(NameIdentifier identifier, 
TablePO observedTablePO) {

Review Comment:
   `tableWriteFailure`'s zero-row-CAS classification (locking read + 
identity-field comparison to distinguish `OptimisticLockException` from 
`NoSuchEntityException`) duplicates `SchemaMetaService.schemaWriteFailure`. As 
more entity types (catalog/fileset/topic/model/view) pick up OCC, might be 
worth factoring this into a shared helper so future fixes to the classification 
logic don't need to be re-applied in every service class.



##########
core/src/main/java/org/apache/gravitino/storage/relational/service/TableMetaService.java:
##########
@@ -139,15 +131,27 @@ public void insertTable(TableEntity tableEntity, boolean 
overwrite) throws IOExc
               SessionUtils.doWithoutCommit(
                   TableMetaMapper.class,
                   mapper -> {
-                    tablePORef.set(po);
                     ops.insertPO(mapper, po, overwrite);
+                    if (overwrite) {
+                      TablePO storedPO = 
mapper.selectTableMetaByIdForUpdate(po.getTableId());

Review Comment:
   `insertTable`'s overwrite branch re-selects the row by the caller's 
freshly-generated `table_id` after the upsert. But MySQL's `INSERT ... ON 
DUPLICATE KEY UPDATE` can instead conflict on the `(schema_id, table_name, 
deleted_at)` unique key rather than the `table_id` PK, in which case the 
existing row is updated in place and its `table_id` is left unchanged. The 
follow-up `selectTableMetaByIdForUpdate(po.getTableId())` then finds nothing, 
and `Preconditions.checkState(storedPO != null, ...)` throws 
`IllegalStateException`. This looks reachable when two callers concurrently 
import/upsert the same not-yet-tracked external table with different generated 
ids. Worth selecting by the natural key instead of (or in addition to) the 
generated id here?
   
   Separately: this locking `SELECT` now runs unconditionally on every 
overwrite insert, even on the common uncontested path (e.g. routine 
external-table import/refresh), adding a round trip that wasn't there before — 
is that acceptable, or could the version be derived without the extra read?



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