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


##########
core/src/main/java/org/apache/gravitino/storage/relational/RelationalEntityStore.java:
##########
@@ -311,6 +312,25 @@ public boolean delete(NameIdentifier ident, 
Entity.EntityType entityType, boolea
     }
   }
 
+  @Override
+  public EntityVersion getVersion(NameIdentifier ident, Entity.EntityType 
entityType)
+      throws IOException {
+    // Always read through: a cached entity does not carry the store version, 
and a stale cache
+    // entry must never be the basis of a version check.
+    return backend.getVersion(ident, entityType);
+  }
+
+  @Override
+  public boolean delete(
+      NameIdentifier ident, Entity.EntityType entityType, boolean cascade, 
EntityVersion expected)
+      throws IOException {
+    try {
+      return backend.delete(ident, entityType, cascade, expected);
+    } finally {
+      cache.invalidate(ident, entityType);

Review Comment:
   Fixed in 8ac260668. The version-checked delete now calls invalidateCache(), 
so it advances the epoch before invalidating the entry. I added 
testBatchGetSkipsWriteBackWhenVersionCheckedDeleteInvalidatesDuringBackendRead 
to reproduce the late-fill interleaving. The targeted dispatcher and late-fill 
suites pass (33 + 9 tests).



##########
core/src/main/java/org/apache/gravitino/catalog/OperationDispatcher.java:
##########
@@ -244,6 +249,102 @@ protected StringIdentifier 
getStringIdFromProperties(Map<String, String> propert
     }
   }
 
+  /**
+   * Wraps an updater so the store rejects the update, before writing 
anything, when the row under
+   * the name is not the entity the external catalog reported.
+   *
+   * <p>The updater runs inside the store's update, after the current row is 
read and before the
+   * version-checked write. Throwing here therefore aborts the transaction 
with nothing written,
+   * which is what a post-write id comparison cannot do.
+   *
+   * @param expectedId the id read from the external catalog
+   * @param updater the update to apply when the ids match
+   * @param <E> the entity type
+   * @return the guarded updater
+   */
+  protected static <E extends Entity & HasIdentifier> Function<E, E> 
requireEntityId(
+      long expectedId, Function<E, E> updater) {
+    return entity -> {
+      if (entity.id() != expectedId) {
+        throw new EntityIdMismatchException(entity.id(), expectedId);
+      }
+      return updater.apply(entity);
+    };
+  }
+
+  /**
+   * Reads the id and store version of a registration before an 
external-catalog call, so a later
+   * store write can be fenced on it.
+   *
+   * @param ident the entity identifier
+   * @param type the entity type
+   * @return the observed id and version, or null when nothing is registered 
under the name or the
+   *     store cannot read versions
+   */
+  @Nullable
+  protected EntityVersion observeRegistration(NameIdentifier ident, 
Entity.EntityType type) {
+    try {
+      return store.getVersion(ident, type);
+    } catch (NoSuchEntityException | UnsupportedOperationException e) {
+      return null;

Review Comment:
   Fixed in 8ac260668. observeRegistration now treats only 
NoSuchEntityException as an absent registration. Unsupported getVersion fails 
before the external drop. I also removed the broad 
UnsupportedOperationException catch around the version-checked delete: if that 
operation is unsupported, the store registration is left in place and the 
failure is reported instead of deleting by name. Custom stores/backends used on 
these paths must implement both version operations. Tests cover both 
unsupported cases, including preservation of the external table when the 
version read fails.



##########
core/src/main/java/org/apache/gravitino/catalog/SchemaOperationDispatcher.java:
##########
@@ -556,33 +559,31 @@ public boolean dropSchema(NameIdentifier ident, boolean 
cascade) throws NonEmpty
             schemaProperties = new HashMap<>(schemaEntity.properties());
           }
 
+          // For managed schema, we don't need to drop the schema from the 
store again.
+          boolean isManagedSchema = isManagedEntity(catalogIdent, 
Capability.Scope.SCHEMA);
+          // Read the registration before the external call, so the store 
delete below can only
+          // remove the row this drop started with and never one re-created 
under the same name.
+          EntityVersion observed = isManagedSchema ? null : 
observeRegistration(ident, SCHEMA);
           boolean droppedFromCatalog =
               doWithCatalog(
                   catalogIdent,
                   c -> c.doWithSchemaOps(s -> s.dropSchema(ident, cascade)),
                   NonEmptySchemaException.class,
                   RuntimeException.class);
 
-          // For managed schema, we don't need to drop the schema from the 
store again.
-          boolean isManagedSchema = isManagedEntity(catalogIdent, 
Capability.Scope.SCHEMA);
           if (isManagedSchema) {
             if (droppedFromCatalog) {
               secretManager.deleteSecretsFromProperties(schemaProperties);
             }
             return droppedFromCatalog;
           }
 
-          // A non-cascading drop preserves a missing registration because the 
source schema
-          // may have been renamed. An explicit cascading drop also removes 
stale metadata.
+          // A non-cascading false result may mean the external schema was 
renamed, so preserve
+          // its registration. An explicit cascade also removes stale 
metadata, but only if the
+          // registration is still the one observed before the external call.
           boolean droppedFromStore = false;
           if (droppedFromCatalog || cascade) {
-            try {
-              droppedFromStore = store.delete(ident, SCHEMA, true);
-            } catch (NoSuchEntityException e) {
-              LOG.warn("The schema to be dropped does not exist in the store: 
{}", ident, e);
-            } catch (Exception e) {
-              throw new RuntimeException(e);
-            }
+            droppedFromStore = deleteObservedRegistration(ident, SCHEMA, true, 
observed);

Review Comment:
   Agreed. The schema fence protects the observed schema row and its identity; 
it does not snapshot or fence every descendant. A child registered after the 
observation can still be included in the later cascade, so I would not claim 
this PR solves that race. #13172 covers the external-backed entity incarnation 
ABA and wrong-id update; descendant creation during cascade is a separate issue 
that needs subtree-level coordination or per-child fencing. No code change for 
this question in 8ac260668.



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