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


##########
core/src/main/java/org/apache/gravitino/storage/relational/service/DomainMetaService.java:
##########
@@ -216,32 +219,37 @@ public <E extends Entity & HasIdentifier> DomainEntity 
updateDomain(
             // name of its backing tag.
             DomainPO lockedDomainPO = 
mapper.selectDomainMetaById(observedDomainPO.domainId());
             DomainEntity oldDomainEntity =
-                DomainPO.fromDomainPO(lockedDomainPO, identifier.namespace());
+                fromDomainPOWithParent(lockedDomainPO, identifier.namespace());
             DomainEntity updatedDomainEntity = (DomainEntity) 
updater.apply((E) oldDomainEntity);
             Preconditions.checkArgument(
                 Objects.equals(oldDomainEntity.id(), updatedDomainEntity.id()),
                 "The updated domain entity id: %s must have the same id as the 
old entity id %s",
                 updatedDomainEntity.id(),
                 oldDomainEntity.id());
+            Preconditions.checkArgument(
+                Objects.equals(oldDomainEntity.parent(), 
updatedDomainEntity.parent()),
+                "The parent of domain %s cannot be changed by an update, use 
DomainRelService",
+                identifier);
 
             DomainPO newDomainPO = DomainPO.updateDomainPO(lockedDomainPO, 
updatedDomainEntity);
             updateDomainWithVersion(identifier, mapper, lockedDomainPO, 
newDomainPO);
             storedDomainPO[0] = 
mapper.selectDomainMetaById(lockedDomainPO.domainId());
           });
 
-      return DomainPO.fromDomainPO(storedDomainPO[0], identifier.namespace());
+      return fromDomainPOWithParent(storedDomainPO[0], identifier.namespace());

Review Comment:
   [Important] Materialize the complete update response before committing.
   
   `doWithCommit` has returned by this point, but `fromDomainPOWithParent` now 
performs another database query. If that parent lookup fails, `updateDomain` 
throws even though its metadata update and version increment have already 
committed. The pre-PR conversion only decoded the captured PO; the new helper 
adds database I/O after commit. A concurrent move can also make this return 
combine the captured row with a later parent.
   
   A temporary probe on H2, MySQL and PostgreSQL injected a failure into the 
parent lookup after the update. Before throwing, a separate SQL session could 
already read the new comment. The API call failed, while the stored comment was 
changed and the version was 2. Normal exception conversion cannot roll that 
commit back.
   
   Please construct/capture the complete `DomainEntity`, including its parent, 
inside the transaction while the child remains locked, then return that object 
after commit without another database lookup. Add a failure regression 
demonstrating rollback when response hydration fails before commit.
   
   Validation used actual database commit visibility and a controlled 
mapper-query failure, not a real network outage or an external client's retry.



##########
core/src/main/java/org/apache/gravitino/storage/relational/service/DomainMetaService.java:
##########
@@ -482,4 +506,18 @@ private DomainPO getDomainPOByMetalakeAndName(String 
metalakeName, String domain
     }
     return domainPO;
   }
+
+  private DomainEntity fromDomainPOWithParent(DomainPO domainPO, Namespace 
namespace) {
+    return DomainPO.fromDomainPO(
+        domainPO,
+        namespace,
+        
DomainRelService.getInstance().getParentDomainName(domainPO.domainId()).orElse(null));
+  }
+
+  private List<DomainEntity> fromDomainPOsWithParents(
+      List<DomainPO> domainPOs, Namespace namespace) {
+    List<Long> domainIds = 
domainPOs.stream().map(DomainPO::domainId).collect(Collectors.toList());
+    return DomainPO.fromDomainPOs(
+        domainPOs, namespace, 
DomainRelService.getInstance().batchGetParentDomainNames(domainIds));

Review Comment:
   [Important] Read the domain and its parent from one consistent database 
snapshot.
   
   The point/list/batch paths fetch domain rows first and resolve their parents 
in a separate statement, normally in a different SQL session. An atomic 
concurrent transaction can change a child's metadata and move it between these 
reads, producing a combination that was never committed. Simply wrapping two 
statements in a READ_COMMITTED transaction would not fix this.
   
   On H2, MySQL and PostgreSQL I paused after the root query and atomically 
changed `comment=old, parent=OldParent` to `comment=new, parent=NewParent` in 
an independent transaction. `backend.get`, service list and service batch each 
returned `comment=old, parent=NewParent`.
   
   The symmetric case exists in `DomainRelService.listSubDomains` (lines 
125–131): it reads the parent's old name first, then queries children and 
annotates them with that name. A probe atomically renamed the parent and 
changed the child metadata between these steps; the result contained new child 
data with the old parent name.
   
   Please join the live relation/parent into the same SELECT as the domain row 
for get/list/batch/sub-domain listing, and project the parent name from that 
statement (or provide a genuinely consistent snapshot strategy). Keep lock-only 
reads join-free. Add controlled interleaving regressions for these four read 
paths.
   
   This is a confirmed storage snapshot problem; I am not claiming an existing 
authorization bypass because the Domain manager/authorization consumers are not 
wired on this head.



##########
core/src/main/java/org/apache/gravitino/storage/relational/service/DomainMetaService.java:
##########
@@ -311,15 +332,18 @@ boolean deleteDomain(NameIdentifier identifier, DomainPO 
domainPO) {
 
     // The metalake lock comes first. Dropping the metalake soft-deletes the 
privileges before the
     // domains, while this delete changes the domain row before its 
privileges; without the lock
-    // the two could deadlock on a domain with privileges. The domain row is 
locked next, and a row
-    // that is gone or renamed makes the delete report the domain as missing 
before any dependent
-    // row is touched. The version-checked delete keeps the version as a 
backstop. The cleanups that
-    // follow are blanket soft deletes for this domain ID, so their row counts 
carry no information
-    // and are not checked.
+    // the two could deadlock on a domain with privileges. The domain row is 
locked next, which also
+    // keeps a sub-domain from being attached while the sub-domain check runs. 
A row that is gone or
+    // renamed makes the delete report the domain as missing before any 
dependent row is touched.
+    // The version-checked delete keeps the version as a backstop. The 
cleanups that follow are
+    // blanket soft deletes for this domain ID, so their row counts carry no 
information and are not
+    // checked.
     try {
       SessionUtils.doMultipleWithCommit(
           () -> lockMetalakeForDomainWrite(identifier.namespace().level(0), 
domainPO.metalakeId()),
           () -> deleteDomainWithVersion(identifier, 
lockAndGetDomainPO(identifier, domainPO)),
+          () ->
+              
DomainRelService.getInstance().deleteDomainRelsOfDeletedDomain(identifier, 
domainId),

Review Comment:
   [Important] Check for sub-domains before soft-deleting the domain row.
   
   The preceding operation already soft-deletes the domain before this method 
counts its children and throws `NonEmptyEntityException`. A standalone call 
rolls back, but nested transactions behave differently: `SessionUtils` supports 
nesting, and `SqlSessions.handleSessionClose` only performs the JDBC rollback 
when the outermost session count reaches zero.
   
   I reproduced this on H2, MySQL and PostgreSQL with an outer 
`doMultipleWithCommit` that catches the expected `NonEmptyEntityException` and 
then updates an unrelated child comment. The outer commit persists the refused 
parent deletion: the parent is missing, its child's active edge remains, and 
the parent resolver now returns empty.
   
   Please take the metalake/domain locks, run a read-only nonempty check before 
the first mutation, and then perform the domain/edge/owner/privilege cleanup in 
the transaction. Split validation from edge cleanup rather than moving a 
mutating cleanup ahead of the domain write. Add the nested-and-caught refusal 
case as a regression. This can be fixed locally without redesigning transaction 
management.
   
   Validation: real service calls and database transactions on all three 
backends. This is a storage-contract failure; no existing Domain REST request 
is claimed to exercise it, since the manager is still future work.



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