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


##########
core/src/main/java/org/apache/gravitino/storage/relational/service/PolicyMetaService.java:
##########
@@ -440,6 +459,205 @@ public int deletePolicyVersionsByRetentionCount(Long 
versionRetentionCount, int
     return totalDeletedCount;
   }
 
+  /**
+   * Holds the parent metalake row for the rest of the transaction, so a 
policy cannot be created
+   * under a metalake that is going away.
+   *
+   * <p>The lock is shared, not exclusive: many policies can be created under 
the same metalake at
+   * the same time, while dropping the metalake takes an exclusive lock on 
this row, so a drop and a
+   * create cannot overlap.
+   *
+   * <p>The name is compared again because the ID alone cannot tell a rename 
apart: the caller
+   * looked the metalake up by name, so a renamed row means the name in the 
request no longer
+   * exists. The metalake version is deliberately not compared, matching 
{@code CatalogMetaService}:
+   * holding the row is what makes the create safe, and an unrelated metalake 
edit that commits in
+   * between would otherwise reject the create for no reason.
+   */
+  private void lockMetalakeForPolicyCreate(MetalakePO observedMetalakePO) {
+    OccWriteSupport.lockParentForChildWrite(
+        observedMetalakePO.getMetalakeName(),
+        Entity.EntityType.METALAKE,
+        () ->
+            SessionUtils.getWithoutCommit(
+                MetalakeMetaMapper.class,
+                mapper ->
+                    
mapper.selectMetalakeMetaByIdForShare(observedMetalakePO.getMetalakeId())),
+        null,
+        current -> Objects.equals(current.getMetalakeName(), 
observedMetalakePO.getMetalakeName()));
+  }
+
+  /**
+   * Writes the policy row and its content snapshot.
+   *
+   * <p>An overwrite is not an upsert any more: the existing row is located 
and locked first, and
+   * the replacement is written as the next version of that row, so the 
snapshot history survives
+   * the overwrite instead of being reset. When no row is there to replace, 
the overwrite inserts
+   * like a plain create; a create of the same name that commits in between 
then surfaces as an
+   * already-exists failure that the caller retries, which is the same outcome 
the caller would see
+   * had the two requests arrived in the other order.
+   *
+   * <p>An overwrite no longer revives a soft-deleted row that happens to 
carry the same policy ID.
+   * Such a row keeps the primary key, so the insert is rejected as an 
already-existing policy,
+   * which is the honest answer: the snapshots and relations of the deleted 
policy are gone with it.
+   */
+  private void insertPolicyWithoutCommit(
+      PolicyEntity policyEntity, PolicyPO initializedPolicyPO, boolean 
overwritten) {
+    if (!overwritten) {
+      insertNewPolicyWithoutCommit(initializedPolicyPO);
+      return;
+    }
+
+    PolicyPO existingPolicyPO = 
findAndLockPolicyForOverwrite(initializedPolicyPO);
+    if (existingPolicyPO == null) {
+      insertNewPolicyWithoutCommit(initializedPolicyPO);
+      return;
+    }
+
+    PolicyPO replacementPolicyPO =
+        POConverters.updatePolicyPOWithVersion(existingPolicyPO, policyEntity);
+    updatePolicyRootWithVersion(
+        policyEntity.nameIdentifier(), existingPolicyPO, replacementPolicyPO);
+    SessionUtils.doWithoutCommit(
+        PolicyVersionMapper.class,
+        mapper -> 
mapper.insertPolicyVersion(replacementPolicyPO.getPolicyVersionPO()));
+  }
+
+  private void insertNewPolicyWithoutCommit(PolicyPO policyPO) {
+    SessionUtils.doWithoutCommit(
+        PolicyMetaMapper.class, mapper -> mapper.insertPolicyMeta(policyPO));
+    SessionUtils.doWithoutCommit(
+        PolicyVersionMapper.class,
+        mapper -> mapper.insertPolicyVersion(policyPO.getPolicyVersionPO()));
+  }
+
+  private PolicyPO findAndLockPolicyForOverwrite(PolicyPO initializedPolicyPO) 
{

Review Comment:
   Fixed in 3a4f768. Overwrite now locks the target natural key directly with 
`SELECT ... FOR UPDATE`; if a concurrent rename wins, the locked lookup no 
longer returns that row and the overwrite cannot revert the rename. Added 
`testPolicyOverwriteByNameDoesNotRevertConcurrentRename`, which forces this 
ordering and passes on H2, MySQL, and PostgreSQL.



##########
core/src/main/java/org/apache/gravitino/storage/relational/service/PolicyMetaService.java:
##########
@@ -143,69 +141,82 @@ public <E extends Entity & HasIdentifier> PolicyEntity 
updatePolicy(
         updatedPolicyEntity.id(),
         oldPolicyEntity.id());
 
-    Integer updateResult;
     try {
-      boolean checkNeedUpdateVersion =
-          POConverters.checkPolicyVersionNeedUpdate(
-              oldPolicyPO.getPolicyVersionPO(), updatedPolicyEntity);
       PolicyPO newPolicyPO =
-          POConverters.updatePolicyPOWithVersion(
-              oldPolicyPO, updatedPolicyEntity, checkNeedUpdateVersion);
-      if (checkNeedUpdateVersion) {
-        SessionUtils.doMultipleWithCommit(
-            () ->
-                SessionUtils.doWithoutCommit(
-                    PolicyVersionMapper.class,
-                    mapper -> 
mapper.insertPolicyVersion(newPolicyPO.getPolicyVersionPO())),
-            () ->
-                SessionUtils.doWithoutCommit(
-                    PolicyMetaMapper.class,
-                    mapper -> mapper.updatePolicyMeta(newPolicyPO, 
oldPolicyPO)));
-        // we set the updateResult to 1 to indicate that the update is 
successful
-        updateResult = 1;
-      } else {
-        updateResult =
-            SessionUtils.doWithCommitAndFetchResult(
-                PolicyMetaMapper.class,
-                mapper -> mapper.updatePolicyMeta(newPolicyPO, oldPolicyPO));
-      }
+          POConverters.updatePolicyPOWithVersion(oldPolicyPO, 
updatedPolicyEntity);
+      SessionUtils.doMultipleWithCommit(
+          () -> updatePolicyRootWithVersion(ident, oldPolicyPO, newPolicyPO),
+          () ->
+              SessionUtils.doWithoutCommit(
+                  PolicyVersionMapper.class,
+                  mapper -> 
mapper.insertPolicyVersion(newPolicyPO.getPolicyVersionPO())));
     } catch (RuntimeException re) {
       ExceptionUtils.checkSQLException(
           re, Entity.EntityType.POLICY, 
updatedPolicyEntity.nameIdentifier().toString());
       throw re;
     }
 
-    if (updateResult > 0) {
-      return updatedPolicyEntity;
-    } else {
-      throw new IOException("Failed to update the entity: " + 
updatedPolicyEntity);
-    }
+    return updatedPolicyEntity;
   }
 
   @Monitored(
       metricsSource = GRAVITINO_RELATIONAL_STORE_METRIC_NAME,
       baseMetricName = "deletePolicy")
   public boolean deletePolicy(NameIdentifier ident) {

Review Comment:
   Fixed in 3a4f768. A CAS miss classified as missing is now converted to the 
idempotent `false` result, while a version mismatch still propagates 
`OptimisticLockException`. Added 
`testPolicyDeleteReturnsFalseWhenConcurrentDeleteWins` to exercise the 
read-before-winner-delete state.



##########
core/src/main/java/org/apache/gravitino/storage/relational/service/PolicyMetaService.java:
##########
@@ -440,6 +459,205 @@ public int deletePolicyVersionsByRetentionCount(Long 
versionRetentionCount, int
     return totalDeletedCount;
   }
 
+  /**
+   * Holds the parent metalake row for the rest of the transaction, so a 
policy cannot be created
+   * under a metalake that is going away.
+   *
+   * <p>The lock is shared, not exclusive: many policies can be created under 
the same metalake at
+   * the same time, while dropping the metalake takes an exclusive lock on 
this row, so a drop and a
+   * create cannot overlap.
+   *
+   * <p>The name is compared again because the ID alone cannot tell a rename 
apart: the caller
+   * looked the metalake up by name, so a renamed row means the name in the 
request no longer
+   * exists. The metalake version is deliberately not compared, matching 
{@code CatalogMetaService}:
+   * holding the row is what makes the create safe, and an unrelated metalake 
edit that commits in
+   * between would otherwise reject the create for no reason.
+   */
+  private void lockMetalakeForPolicyCreate(MetalakePO observedMetalakePO) {
+    OccWriteSupport.lockParentForChildWrite(
+        observedMetalakePO.getMetalakeName(),
+        Entity.EntityType.METALAKE,
+        () ->
+            SessionUtils.getWithoutCommit(
+                MetalakeMetaMapper.class,
+                mapper ->
+                    
mapper.selectMetalakeMetaByIdForShare(observedMetalakePO.getMetalakeId())),
+        null,
+        current -> Objects.equals(current.getMetalakeName(), 
observedMetalakePO.getMetalakeName()));
+  }
+
+  /**
+   * Writes the policy row and its content snapshot.
+   *
+   * <p>An overwrite is not an upsert any more: the existing row is located 
and locked first, and
+   * the replacement is written as the next version of that row, so the 
snapshot history survives
+   * the overwrite instead of being reset. When no row is there to replace, 
the overwrite inserts
+   * like a plain create; a create of the same name that commits in between 
then surfaces as an
+   * already-exists failure that the caller retries, which is the same outcome 
the caller would see
+   * had the two requests arrived in the other order.
+   *
+   * <p>An overwrite no longer revives a soft-deleted row that happens to 
carry the same policy ID.
+   * Such a row keeps the primary key, so the insert is rejected as an 
already-existing policy,
+   * which is the honest answer: the snapshots and relations of the deleted 
policy are gone with it.
+   */
+  private void insertPolicyWithoutCommit(
+      PolicyEntity policyEntity, PolicyPO initializedPolicyPO, boolean 
overwritten) {
+    if (!overwritten) {
+      insertNewPolicyWithoutCommit(initializedPolicyPO);
+      return;
+    }
+
+    PolicyPO existingPolicyPO = 
findAndLockPolicyForOverwrite(initializedPolicyPO);
+    if (existingPolicyPO == null) {
+      insertNewPolicyWithoutCommit(initializedPolicyPO);
+      return;
+    }
+
+    PolicyPO replacementPolicyPO =
+        POConverters.updatePolicyPOWithVersion(existingPolicyPO, policyEntity);
+    updatePolicyRootWithVersion(

Review Comment:
   Fixed in 3a4f768. The overwrite CAS failure now uses an identifier built 
from the locked row name, so same-ID rename overwrites report the observed 
identity rather than the requested replacement name.



##########
core/src/main/java/org/apache/gravitino/storage/relational/service/PolicyMetaService.java:
##########
@@ -440,6 +459,205 @@ public int deletePolicyVersionsByRetentionCount(Long 
versionRetentionCount, int
     return totalDeletedCount;
   }
 
+  /**
+   * Holds the parent metalake row for the rest of the transaction, so a 
policy cannot be created
+   * under a metalake that is going away.
+   *
+   * <p>The lock is shared, not exclusive: many policies can be created under 
the same metalake at
+   * the same time, while dropping the metalake takes an exclusive lock on 
this row, so a drop and a
+   * create cannot overlap.
+   *
+   * <p>The name is compared again because the ID alone cannot tell a rename 
apart: the caller
+   * looked the metalake up by name, so a renamed row means the name in the 
request no longer
+   * exists. The metalake version is deliberately not compared, matching 
{@code CatalogMetaService}:
+   * holding the row is what makes the create safe, and an unrelated metalake 
edit that commits in
+   * between would otherwise reject the create for no reason.
+   */
+  private void lockMetalakeForPolicyCreate(MetalakePO observedMetalakePO) {
+    OccWriteSupport.lockParentForChildWrite(
+        observedMetalakePO.getMetalakeName(),
+        Entity.EntityType.METALAKE,
+        () ->
+            SessionUtils.getWithoutCommit(
+                MetalakeMetaMapper.class,
+                mapper ->
+                    
mapper.selectMetalakeMetaByIdForShare(observedMetalakePO.getMetalakeId())),
+        null,
+        current -> Objects.equals(current.getMetalakeName(), 
observedMetalakePO.getMetalakeName()));
+  }
+
+  /**
+   * Writes the policy row and its content snapshot.
+   *
+   * <p>An overwrite is not an upsert any more: the existing row is located 
and locked first, and
+   * the replacement is written as the next version of that row, so the 
snapshot history survives
+   * the overwrite instead of being reset. When no row is there to replace, 
the overwrite inserts
+   * like a plain create; a create of the same name that commits in between 
then surfaces as an
+   * already-exists failure that the caller retries, which is the same outcome 
the caller would see
+   * had the two requests arrived in the other order.
+   *
+   * <p>An overwrite no longer revives a soft-deleted row that happens to 
carry the same policy ID.
+   * Such a row keeps the primary key, so the insert is rejected as an 
already-existing policy,
+   * which is the honest answer: the snapshots and relations of the deleted 
policy are gone with it.
+   */
+  private void insertPolicyWithoutCommit(
+      PolicyEntity policyEntity, PolicyPO initializedPolicyPO, boolean 
overwritten) {
+    if (!overwritten) {
+      insertNewPolicyWithoutCommit(initializedPolicyPO);
+      return;
+    }
+
+    PolicyPO existingPolicyPO = 
findAndLockPolicyForOverwrite(initializedPolicyPO);
+    if (existingPolicyPO == null) {
+      insertNewPolicyWithoutCommit(initializedPolicyPO);
+      return;
+    }
+
+    PolicyPO replacementPolicyPO =
+        POConverters.updatePolicyPOWithVersion(existingPolicyPO, policyEntity);
+    updatePolicyRootWithVersion(
+        policyEntity.nameIdentifier(), existingPolicyPO, replacementPolicyPO);
+    SessionUtils.doWithoutCommit(
+        PolicyVersionMapper.class,
+        mapper -> 
mapper.insertPolicyVersion(replacementPolicyPO.getPolicyVersionPO()));
+  }
+
+  private void insertNewPolicyWithoutCommit(PolicyPO policyPO) {
+    SessionUtils.doWithoutCommit(
+        PolicyMetaMapper.class, mapper -> mapper.insertPolicyMeta(policyPO));
+    SessionUtils.doWithoutCommit(
+        PolicyVersionMapper.class,
+        mapper -> mapper.insertPolicyVersion(policyPO.getPolicyVersionPO()));
+  }
+
+  private PolicyPO findAndLockPolicyForOverwrite(PolicyPO initializedPolicyPO) 
{
+    PolicyPO existingPolicyPO =
+        SessionUtils.getWithoutCommit(
+            PolicyMetaMapper.class,
+            mapper -> 
mapper.selectPolicyByPolicyIdForUpdate(initializedPolicyPO.getPolicyId()));
+    if (existingPolicyPO != null) {
+      return existingPolicyPO;
+    }
+
+    PolicyPO sameNamePolicyPO =
+        SessionUtils.getWithoutCommit(
+            PolicyMetaMapper.class,
+            mapper ->
+                mapper.selectPolicyMetaByMetalakeIdAndName(
+                    initializedPolicyPO.getMetalakeId(), 
initializedPolicyPO.getPolicyName()));
+    if (sameNamePolicyPO == null) {
+      return null;
+    }
+    return SessionUtils.getWithoutCommit(
+        PolicyMetaMapper.class,
+        mapper -> 
mapper.selectPolicyByPolicyIdForUpdate(sameNamePolicyPO.getPolicyId()));
+  }
+
+  /**
+   * Advances the policy row to the next version, keyed on the version the 
caller observed. A row
+   * that moved on, was renamed away, or was deleted in between matches 
nothing, and the failure is
+   * classified as a conflict or as a missing policy by {@link 
#policyWriteFailure}.
+   */
+  private void updatePolicyRootWithVersion(

Review Comment:
   Fixed in 3a4f768. The single-row update/check/classify idiom is now 
`OccWriteSupport.updateWithVersion`, with success and miss unit tests. 
`PolicyMetaService` delegates to it instead of adding another inline copy.



##########
core/src/main/java/org/apache/gravitino/storage/relational/service/PolicyMetaService.java:
##########
@@ -440,6 +459,205 @@ public int deletePolicyVersionsByRetentionCount(Long 
versionRetentionCount, int
     return totalDeletedCount;
   }
 
+  /**
+   * Holds the parent metalake row for the rest of the transaction, so a 
policy cannot be created
+   * under a metalake that is going away.
+   *
+   * <p>The lock is shared, not exclusive: many policies can be created under 
the same metalake at
+   * the same time, while dropping the metalake takes an exclusive lock on 
this row, so a drop and a
+   * create cannot overlap.
+   *
+   * <p>The name is compared again because the ID alone cannot tell a rename 
apart: the caller
+   * looked the metalake up by name, so a renamed row means the name in the 
request no longer
+   * exists. The metalake version is deliberately not compared, matching 
{@code CatalogMetaService}:
+   * holding the row is what makes the create safe, and an unrelated metalake 
edit that commits in
+   * between would otherwise reject the create for no reason.
+   */
+  private void lockMetalakeForPolicyCreate(MetalakePO observedMetalakePO) {

Review Comment:
   Kept as-is after checking the repo-wide pattern. The reusable mechanism is 
already centralized in `OccWriteSupport.lockParentForChildWrite`; this private 
adapter only supplies the policy service mapper lookup and identity predicate. 
Catalog, group, role, and user intentionally keep the same service-local 
adapter. Moving `MetalakeMetaMapper` and `SessionUtils` into the generic OCC 
helper would couple that helper to one parent type and lower its abstraction 
level.



##########
core/src/main/java/org/apache/gravitino/storage/relational/service/PolicyMetaService.java:
##########
@@ -440,6 +459,205 @@ public int deletePolicyVersionsByRetentionCount(Long 
versionRetentionCount, int
     return totalDeletedCount;
   }
 
+  /**
+   * Holds the parent metalake row for the rest of the transaction, so a 
policy cannot be created
+   * under a metalake that is going away.
+   *
+   * <p>The lock is shared, not exclusive: many policies can be created under 
the same metalake at
+   * the same time, while dropping the metalake takes an exclusive lock on 
this row, so a drop and a
+   * create cannot overlap.
+   *
+   * <p>The name is compared again because the ID alone cannot tell a rename 
apart: the caller
+   * looked the metalake up by name, so a renamed row means the name in the 
request no longer
+   * exists. The metalake version is deliberately not compared, matching 
{@code CatalogMetaService}:
+   * holding the row is what makes the create safe, and an unrelated metalake 
edit that commits in
+   * between would otherwise reject the create for no reason.
+   */
+  private void lockMetalakeForPolicyCreate(MetalakePO observedMetalakePO) {
+    OccWriteSupport.lockParentForChildWrite(
+        observedMetalakePO.getMetalakeName(),
+        Entity.EntityType.METALAKE,
+        () ->
+            SessionUtils.getWithoutCommit(
+                MetalakeMetaMapper.class,
+                mapper ->
+                    
mapper.selectMetalakeMetaByIdForShare(observedMetalakePO.getMetalakeId())),
+        null,
+        current -> Objects.equals(current.getMetalakeName(), 
observedMetalakePO.getMetalakeName()));
+  }
+
+  /**
+   * Writes the policy row and its content snapshot.
+   *
+   * <p>An overwrite is not an upsert any more: the existing row is located 
and locked first, and
+   * the replacement is written as the next version of that row, so the 
snapshot history survives
+   * the overwrite instead of being reset. When no row is there to replace, 
the overwrite inserts
+   * like a plain create; a create of the same name that commits in between 
then surfaces as an
+   * already-exists failure that the caller retries, which is the same outcome 
the caller would see
+   * had the two requests arrived in the other order.
+   *
+   * <p>An overwrite no longer revives a soft-deleted row that happens to 
carry the same policy ID.
+   * Such a row keeps the primary key, so the insert is rejected as an 
already-existing policy,
+   * which is the honest answer: the snapshots and relations of the deleted 
policy are gone with it.
+   */
+  private void insertPolicyWithoutCommit(
+      PolicyEntity policyEntity, PolicyPO initializedPolicyPO, boolean 
overwritten) {
+    if (!overwritten) {
+      insertNewPolicyWithoutCommit(initializedPolicyPO);
+      return;
+    }
+
+    PolicyPO existingPolicyPO = 
findAndLockPolicyForOverwrite(initializedPolicyPO);
+    if (existingPolicyPO == null) {
+      insertNewPolicyWithoutCommit(initializedPolicyPO);
+      return;
+    }
+
+    PolicyPO replacementPolicyPO =
+        POConverters.updatePolicyPOWithVersion(existingPolicyPO, policyEntity);

Review Comment:
   Fixed in 3a4f768. `insertPolicy` already creates an initialized `PolicyPO` 
before the transaction. Overwrite now advances the locked row from that 
pre-serialized PO through a new converter overload, so JSON serialization no 
longer occurs while the policy row lock is held.



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