jerryshao commented on code in PR #12782:
URL: https://github.com/apache/gravitino/pull/12782#discussion_r3949912799
##########
core/src/main/java/org/apache/gravitino/storage/relational/service/PolicyMetaService.java:
##########
@@ -440,6 +475,217 @@ 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. If another create wins the unique key after the
locking lookup misses, the
+ * caller receives a retryable optimistic-lock failure after the transaction
is rolled back.
+ *
+ * <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) {
+ if (SessionUtils.getWithoutCommit(
+ PolicyMetaMapper.class,
+ mapper ->
mapper.countDeletedPolicyMetasById(initializedPolicyPO.getPolicyId()))
+ > 0) {
+ throw new EntityAlreadyExistsException(
+ "The policy ID %s is reserved by a deleted policy; use a new ID",
+ initializedPolicyPO.getPolicyId());
+ }
+ insertNewPolicyWithoutCommit(initializedPolicyPO);
+ return;
+ }
+
+ PolicyPO replacementPolicyPO =
+ POConverters.updatePolicyPOWithVersion(existingPolicyPO,
initializedPolicyPO);
+ NameIdentifier observedIdentifier =
+ NameIdentifier.of(policyEntity.namespace(),
existingPolicyPO.getPolicyName());
+ updatePolicyRootWithVersion(observedIdentifier, 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:
**[test-coverage, PLAUSIBLE] Overwrite can clobber unrelated policy by name
collision**
`findAndLockPolicyForOverwrite` resolves the overwrite target by policy name
alone; it never checks that the caller-supplied policy ID matches the row found
by name, so `overwrite(entity)` can silently clobber a different, unrelated
existing policy that happens to hold the target name.
Concretely: two independent active policies exist in the same metalake,
A(id=1, name="alpha") and B(id=2, name="beta"). A caller invokes
`insertPolicy(entity, overwrite=true)` where `entity.id()==1` but
`entity.name()=="beta"`. This method finds B by name and returns it immediately
(the by-ID branch only runs on a name-miss). `insertPolicyWithoutCommit` then
overwrites B's content/audit-info with the payload intended for A, keeping B's
own `policy_id` — B's data is silently clobbered, A is untouched, no exception
is raised.
Verified this is an intentional repo-wide convention (the identical
by-name-first pattern exists in `TagMetaService.findAndLockTagForOverwrite`)
and currently latent: `overwrite=true` for policies is only exercised by unit
tests today, not reachable via `PolicyManager`'s REST path (always
`overwritten=false`). The existing test
`testPolicyOverwriteReplacesTheRowHoldingTheName` only covers overwriting with
a fresh, never-used ID — not the cross-ID case where the name is already owned
by a different real policy. Worth an explicit Javadoc callout and a test for
the cross-ID case before any future caller passes `overwrite=true`.
##########
core/src/main/java/org/apache/gravitino/storage/relational/service/OccWriteSupport.java:
##########
@@ -115,6 +115,20 @@ public static void deleteWithVersion(
}
}
+ /**
+ * Executes a single-row compare-and-set update for an entity guarded by
version.
+ *
+ * @param updateOps an operation supplying the number of rows affected by
the update
+ * @param onMissSupplier a supplier providing the RuntimeException when zero
rows are updated
+ */
+ public static void updateWithVersion(
Review Comment:
**[altitude, CONFIRMED] Update-CAS helper not backfilled to other services**
The new shared `updateWithVersion(IntSupplier, Supplier<RuntimeException>)`
helper — a genuine, well-built counterpart to the existing `deleteWithVersion`
— is used only by `PolicyMetaService`; `TableMetaService`, `TopicMetaService`,
`CatalogMetaService`, and `ModelVersionMetaService` still hand-roll the
identical "issue UPDATE, check affected-rows==0, classify via re-lookup"
pattern inline, unmigrated.
`TableMetaService.java:241`, `TopicMetaService.java:135`,
`CatalogMetaService.java:253`, and `ModelVersionMetaService.java:392/243` all
keep their own copy of the exact idiom this PR just centralized for Policy. Not
a functional bug today (both forms are currently semantically identical), but
it leaves five copies of the same CAS-classification logic in the codebase
instead of one — a future fix to the CAS-retry/error-classification behavior
applied only to `OccWriteSupport.updateWithVersion` will silently miss
Table/Topic/Catalog/ModelVersion, reintroducing the exact class of bug this PR
set out to eliminate.
##########
core/src/main/java/org/apache/gravitino/storage/relational/service/PolicyMetaService.java:
##########
@@ -93,36 +103,35 @@ public void insertPolicy(PolicyEntity policyEntity,
boolean overwritten) throws
String metalakeName = ns.level(0);
try {
- Long metalakeId =
- EntityIdService.getEntityId(NameIdentifier.of(metalakeName),
Entity.EntityType.METALAKE);
+ MetalakePO metalakePO =
Review Comment:
**[reuse, PLAUSIBLE] Metalake-lookup-by-name block duplicated across
services**
The "look up metalake by name, throw `NoSuchEntityException` if missing"
block in `insertPolicy` duplicates the identical block in
`CatalogMetaService.insertCatalog`.
Both blocks select via `MetalakeMetaMapper.selectMetalakeMetaByName`,
null-check, and throw the identical `NoSuchEntityException` with
`Entity.EntityType.METALAKE`. Combined with the lock-method duplication noted
separately, a single shared helper (e.g. `requireMetalakeByName(String)`)
returning a validated `MetalakePO` would cut ~9 duplicated lines per call site,
currently duplicated at least twice in this codebase.
##########
core/src/main/java/org/apache/gravitino/storage/relational/service/PolicyMetaService.java:
##########
@@ -440,6 +475,217 @@ 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:
**[reuse, CONFIRMED] lockMetalakeForPolicyCreate duplicates catalog's
version**
`lockMetalakeForPolicyCreate` remains a byte-for-byte duplicate of
`CatalogMetaService.lockMetalakeForCatalogCreate` (identical body and Javadoc)
— unaddressed since the prior review round on this same PR.
Both methods call `OccWriteSupport.lockParentForChildWrite` with the same
lookup and identity predicate, word-for-word. A third near-identical copy
(`lockCatalogForSchemaCreate`) exists in `SchemaMetaService`. Any future fix to
the "lock parent for child create" pattern must be applied in multiple places
by hand.
##########
core/src/main/java/org/apache/gravitino/storage/relational/service/PolicyMetaService.java:
##########
@@ -143,69 +152,87 @@ 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:
**[efficiency, PLAUSIBLE] Delete pulls full policy content just for 2
scalars**
`deletePolicy` now routes through `getPolicyPOByMetalakeAndName`, a 3-table
JOIN (policy_meta + metalake_meta + policy_version_info) that pulls the full
JSON `content` column, just to obtain the `policyId` + `currentVersion` the CAS
soft-delete actually needs.
Every policy delete now transfers a full policy-content JSON blob and joins
two extra tables that the old single-UPDATE-by-name implementation never
touched, for a delete path that only needs two scalar columns to perform its
CAS. On a policy with large content, this is pure wasted I/O and network
transfer on every delete call.
##########
core/src/main/java/org/apache/gravitino/storage/relational/mapper/provider/base/PolicyMetaBaseSQLProvider.java:
##########
@@ -185,6 +160,34 @@ public String selectPolicyByPolicyId(@Param("policyId")
Long policyId) {
+ " AND pm.deleted_at = 0 ";
}
+ /** Returns SQL that selects and exclusively locks an active policy by ID. */
+ public String selectPolicyByPolicyIdForUpdate(@Param("policyId") Long
policyId) {
+ return selectPolicyByPolicyId(policyId) + " FOR UPDATE";
+ }
+
+ /**
+ * Returns SQL that selects and exclusively locks several active policies,
ordered by policy ID so
+ * that concurrent callers take the row locks in the same order.
+ */
+ public String listPolicyPOsByPolicyIdsForUpdate(@Param("policyIds")
List<Long> policyIds) {
Review Comment:
**[simplification, PLAUSIBLE] listPolicyPOsByPolicyIdsForUpdate copy-pastes
base SQL**
`listPolicyPOsByPolicyIdsForUpdate` re-implements the full 19-line SQL body
of `listPolicyPOsByPolicyIds` instead of delegating to it and appending `FOR
UPDATE`, unlike the sibling method `selectPolicyByPolicyIdForUpdate` which
correctly builds on its base query (`return selectPolicyByPolicyId(id) + " FOR
UPDATE"`).
`listPolicyPOsByPolicyIdsForUpdate` instead copy-pastes the entire
`<script>...SELECT...FOR UPDATE</script>` block from
`listPolicyPOsByPolicyIds`, so any future column change to the base query must
be edited in two places, and the two can silently drift out of sync.
##########
core/src/main/java/org/apache/gravitino/storage/relational/service/PolicyMetaService.java:
##########
@@ -440,6 +475,217 @@ 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. If another create wins the unique key after the
locking lookup misses, the
+ * caller receives a retryable optimistic-lock failure after the transaction
is rolled back.
+ *
+ * <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) {
+ if (SessionUtils.getWithoutCommit(
+ PolicyMetaMapper.class,
+ mapper ->
mapper.countDeletedPolicyMetasById(initializedPolicyPO.getPolicyId()))
+ > 0) {
+ throw new EntityAlreadyExistsException(
Review Comment:
**[altitude, PLAUSIBLE] Overwrite conflict checks hand-rolled instead of
shared**
The overwrite path's pre-insert conflict classification (reserved-deleted-ID
reuse here, cross-metalake ID reuse elsewhere in this method) is hand-rolled as
ad-hoc inline `EntityAlreadyExistsException` throws rather than routed through
`OccWriteSupport`'s existing "lookup + identity predicate + classify" mechanism.
These checks were added piecemeal across the "polish" and "classify
conflicts safely" review-round commits, specific to the policy overwrite-by-ID
path. A future entity type needing the same "ID reuse after delete" or
"cross-parent ID reuse" checks will re-derive the same throw-inline idiom from
scratch instead of calling a generalized version of
`OccWriteSupport.writeFailure`/`lockParentForChildWrite` extended to cover
pre-insert conflict classification.
##########
core/src/main/java/org/apache/gravitino/storage/relational/service/PolicyMetaService.java:
##########
@@ -440,6 +475,217 @@ 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. If another create wins the unique key after the
locking lookup misses, the
+ * caller receives a retryable optimistic-lock failure after the transaction
is rolled back.
+ *
+ * <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) {
+ if (SessionUtils.getWithoutCommit(
+ PolicyMetaMapper.class,
+ mapper ->
mapper.countDeletedPolicyMetasById(initializedPolicyPO.getPolicyId()))
Review Comment:
**[efficiency, PLAUSIBLE] Overwrite-miss path does 3 sequential queries**
The overwrite-miss path in
`insertPolicyWithoutCommit`/`findAndLockPolicyForOverwrite` issues 3 sequential
SELECTs (locked-by-name miss, locked-by-id lookup, then this
`countDeletedPolicyMetasById` reservation check) where folding the `deleted_at`
check into the second query's predicate would need only 2.
This third query exists only because the second query filters `deleted_at =
0`, hiding a soft-deleted row with the same ID. Checking `deletedAt` in Java
instead of filtering it out in SQL would fold the "reserved by a deleted
policy" check into the same round trip, saving one query per
overwrite-of-a-new-name.
##########
core/src/main/java/org/apache/gravitino/storage/relational/mapper/PolicyMetaMapper.java:
##########
@@ -146,6 +161,29 @@ Integer deletePolicyMetasByLegacyTimeline(
PolicyPO selectPolicyMetaByMetalakeIdAndName(
@Param("metalakeId") long metalakeId, @Param("policyName") String
policyName);
+ /**
+ * Selects and exclusively locks an active policy by its natural key.
+ *
+ * @param metalakeId The metalake ID.
+ * @param policyName The policy name.
+ * @return The locked policy, or null if the natural key is not active.
+ */
+ @Results({
Review Comment:
**[simplification, PLAUSIBLE] Repeated @Results blocks instead of shared
@ResultMap**
Ten identical 8-line `@Results({...})` blocks mapping the same 8 `PolicyPO`
columns are repeated verbatim across the mapper (3 of them newly added by this
PR for the `*ForUpdate` query variants), instead of using this codebase's own
`@ResultMap(id=...)` reuse convention.
`FunctionMetaMapper.java` and `ViewMetaMapper.java` elsewhere in this
codebase already declare one `@Results(id = "...")` and reference it from other
methods via `@ResultMap("...")`. `PolicyMetaMapper` doesn't follow that
convention, so every new "for update" variant this PR adds pays another 8-line
copy instead of one annotation, and a future column addition/rename must be
replicated across all 10 call sites by hand.
##########
core/src/main/java/org/apache/gravitino/storage/relational/service/PolicyMetaService.java:
##########
@@ -313,81 +340,89 @@ public List<PolicyEntity>
associatePoliciesWithMetadataObject(
NameIdentifier[] policiesToAdd,
NameIdentifier[] policiesToRemove)
throws NoSuchEntityException, EntityAlreadyExistsException, IOException {
- MetadataObject metadataObject =
NameIdentifierUtil.toMetadataObject(objectIdent, objectType);
- String metalake = objectIdent.namespace().level(0);
-
try {
- Long metadataObjectId = EntityIdService.getEntityId(objectIdent,
objectType);
-
- // Fetch all the policies need to associate with the metadata object.
- List<String> policyNamesToAdd =
-
Arrays.stream(policiesToAdd).map(NameIdentifier::name).collect(Collectors.toList());
- List<PolicyPO> policyPOsToAdd =
- policyNamesToAdd.isEmpty()
- ? Collections.emptyList()
- : getPolicyPOsByMetalakeAndNames(metalake, policyNamesToAdd);
-
- // Fetch all the policies need to remove from the metadata object.
- List<String> policyNamesToRemove =
-
Arrays.stream(policiesToRemove).map(NameIdentifier::name).collect(Collectors.toList());
- List<PolicyPO> policyPOsToRemove =
- policyNamesToRemove.isEmpty()
- ? Collections.emptyList()
- : getPolicyPOsByMetalakeAndNames(metalake, policyNamesToRemove);
-
- SessionUtils.doMultipleWithCommit(
- () -> {
- // Insert the policy metadata object relations.
- if (policyPOsToAdd.isEmpty()) {
- return;
- }
-
- List<PolicyMetadataObjectRelPO> policyRelsToAdd =
- policyPOsToAdd.stream()
- .map(
- policyPO ->
-
POConverters.initializePolicyMetadataObjectRelPOWithVersion(
- policyPO.getPolicyId(),
- metadataObjectId,
- metadataObject.type().toString()))
- .collect(Collectors.toList());
- SessionUtils.doWithoutCommit(
- PolicyMetadataObjectRelMapper.class,
- mapper ->
mapper.batchInsertPolicyMetadataObjectRels(policyRelsToAdd));
- },
- () -> {
- // Remove the policy metadata object relations.
- if (policyPOsToRemove.isEmpty()) {
- return;
- }
-
- List<Long> policyIdsToRemove =
-
policyPOsToRemove.stream().map(PolicyPO::getPolicyId).collect(Collectors.toList());
- SessionUtils.doWithoutCommit(
- PolicyMetadataObjectRelMapper.class,
- mapper ->
-
mapper.batchDeletePolicyMetadataObjectRelsByPolicyIdsAndMetadataObject(
- metadataObjectId, metadataObject.type().toString(),
policyIdsToRemove));
- });
-
- // Fetch all the policies associated with the metadata object after the
operation.
- List<PolicyPO> policyPOs =
- SessionUtils.getWithoutCommit(
- PolicyMetadataObjectRelMapper.class,
- mapper ->
- mapper.listPolicyPOsByMetadataObjectIdAndType(
- metadataObjectId, metadataObject.type().toString()));
-
- return policyPOs.stream()
- .map(policyPO -> POConverters.fromPolicyPO(policyPO,
NamespaceUtil.ofPolicy(metalake)))
- .collect(Collectors.toList());
-
+ // One transaction for the whole association change: the policy rows
stay locked from the
+ // moment they are read until the relation rows are rewritten and read
back, so a conflict
+ // rolls the whole change back instead of leaving a half-applied
association set behind. The
+ // mapper handed to the callback is unused; the call only opens and
closes the transaction.
+ return SessionUtils.doWithCommitAndFetchResult(
+ PolicyMetaMapper.class,
+ ignored ->
+ associatePoliciesWithMetadataObjectWithoutCommit(
+ objectIdent, objectType, policiesToAdd, policiesToRemove));
} catch (RuntimeException e) {
ExceptionUtils.checkSQLException(e, Entity.EntityType.POLICY,
objectIdent.toString());
throw e;
}
}
+ private List<PolicyEntity> associatePoliciesWithMetadataObjectWithoutCommit(
Review Comment:
**[conventions, CONFIRMED] New private helpers still misplaced among public
methods**
A new private helper `associatePoliciesWithMetadataObjectWithoutCommit` is
inserted between two public methods, and a ~230-line block of new
private/package-private helpers is inserted before the pre-existing public
methods `getPolicyIdByPolicyName` and `batchGetPolicyByIdentifier` at the tail
of the class — the same class-member-ordering issue flagged in the prior review
round on this PR, still unaddressed.
Per this repo's CLAUDE.md: "Methods (Group by visibility, putting private
methods at the end)." The new private helper sits between
`associatePoliciesWithMetadataObject` (public) and
`deletePolicyAndVersionMetasByLegacyTimeline` (public), and the large new
private-helpers block precedes two pre-existing public methods at the class
tail — private methods are not grouped at the end as the rule requires, exactly
as in the prior review round.
--
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]