This is an automated email from the ASF dual-hosted git repository.
yuqi1129 pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/gravitino.git
The following commit(s) were added to refs/heads/main by this push:
new 912302110a [#13557] fix(core): Invalidate the authorizer name-id
mapping on model, tag, policy and job template rename or delete (#13558)
912302110a is described below
commit 912302110a77c330640cf1fd74ce13d3e70ca1cf
Author: Qi Yu <[email protected]>
AuthorDate: Sat Oct 10 09:04:59 2026 +0800
[#13557] fix(core): Invalidate the authorizer name-id mapping on model,
tag, policy and job template rename or delete (#13558)
### What changes were proposed in this pull request?
Invalidate the built-in authorizer's old name-to-ID mapping after a
successful model, tag, policy or job template deletion or rename,
without calling catalog authorization plugins.
- Model, tag, policy and job template hook dispatchers notify the
authorizer after a successful delete or rename. The view hook dispatcher
now also notifies after a successful drop; view rename already did.
- `JobTemplateMetaService` writes `entity_change_log` records for job
template delete and rename. Job templates are not entity-cacheable, so
`JDBCBackend` did not log them before.
- `EntityChangeLogWriter.append` is now the single change-log writer,
replacing the copies in `JDBCBackend`, `ModelMetaService`,
`SemanticModelMetaService` and `JobTemplateMetaService`.
- The JCasbin change-log listener replays TAG, POLICY, JOB and
JOB_TEMPLATE records so peer nodes drop stale mappings.
### Why are the changes needed?
Deleting or renaming these entities and recreating their old names could
leave authorization using the previous object's ID. Tag and policy
change-log records were skipped on peer nodes, and job template changes
were never logged, so a peer could keep the old ID until the cache TTL
(default 3600s).
Fix: #13557
### Does this PR introduce _any_ user-facing change?
No API changes. After a rename or delete/recreate through the public
API, authorization resolves the current object's ID immediately on the
local node. Peer nodes, and internal paths that bypass the hook
dispatchers (such as built-in job template upgrades), pick it up within
one change-log poll interval.
### How was this patch tested?
- Hook tests for model, tag, policy and job template dispatchers,
including failed renames, and for view drop.
- `TestJobTemplateMetaService`: rename and delete write one change-log
record each; comment-only updates, missing deletes, deletes rejected for
active jobs and renames to an existing name write none.
- Real-cache regression tests for model deletion/rename, similarly named
models and model change-log replay.
- Change-log replay tests for TAG, POLICY, JOB and JOB_TEMPLATE records.
- All `org.apache.gravitino.server.authorization.jcasbin` tests, the
hook tests, `TestJobManager`, `TestJobManagerMultiNode`,
`TestJobMetaService`, `TestEntityChangeLogService`,
`TestEntityStorageChangeLog`, `TestEntityChangeLogPoller`,
`TestEntityChangeLogMapper` and the Model, SemanticModel, Fileset and
Table meta service tests pass. Spotless checks pass.
---
.../apache/gravitino/hook/JobHookDispatcher.java | 22 ++-
.../apache/gravitino/hook/ModelHookDispatcher.java | 15 +-
.../gravitino/hook/PolicyHookDispatcher.java | 17 ++-
.../apache/gravitino/hook/TagHookDispatcher.java | 17 ++-
.../apache/gravitino/hook/ViewHookDispatcher.java | 8 +-
.../storage/relational/EntityChangeLogWriter.java | 51 +++++++
.../gravitino/storage/relational/JDBCBackend.java | 20 +--
.../relational/service/JobTemplateMetaService.java | 16 ++-
.../relational/service/ModelMetaService.java | 41 ++----
.../service/SemanticModelMetaService.java | 46 ++----
.../gravitino/hook/TestJobHookDispatcher.java | 71 ++++++++++
.../gravitino/hook/TestModelHookDispatcher.java | 70 +++++++++
.../gravitino/hook/TestPolicyHookDispatcher.java | 71 ++++++++++
.../gravitino/hook/TestTagHookDispatcher.java | 66 +++++++++
.../gravitino/hook/TestViewHookDispatcher.java | 23 +++
.../service/TestJobTemplateMetaService.java | 85 +++++++++++
.../jcasbin/JcasbinChangeListener.java | 29 ++--
.../jcasbin/TestJcasbinAuthorizationCacheKeys.java | 15 ++
.../jcasbin/TestJcasbinChangePoller.java | 37 +++--
.../jcasbin/TestJcasbinModelCacheInvalidation.java | 157 +++++++++++++++++++++
20 files changed, 758 insertions(+), 119 deletions(-)
diff --git
a/core/src/main/java/org/apache/gravitino/hook/JobHookDispatcher.java
b/core/src/main/java/org/apache/gravitino/hook/JobHookDispatcher.java
index d6ca24eff2..320ab0ec0e 100644
--- a/core/src/main/java/org/apache/gravitino/hook/JobHookDispatcher.java
+++ b/core/src/main/java/org/apache/gravitino/hook/JobHookDispatcher.java
@@ -19,11 +19,13 @@
package org.apache.gravitino.hook;
import java.io.IOException;
+import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import org.apache.gravitino.Entity;
import org.apache.gravitino.GravitinoEnv;
+import org.apache.gravitino.authorization.AuthorizationUtils;
import org.apache.gravitino.authorization.Owner;
import org.apache.gravitino.authorization.OwnerDispatcher;
import org.apache.gravitino.exceptions.InUseException;
@@ -74,14 +76,30 @@ public class JobHookDispatcher implements
JobOperationDispatcher {
@Override
public boolean deleteJobTemplate(String metalake, String jobTemplateName)
throws InUseException {
- return jobOperationDispatcher.deleteJobTemplate(metalake, jobTemplateName);
+ boolean deleted = jobOperationDispatcher.deleteJobTemplate(metalake,
jobTemplateName);
+ if (deleted) {
+ // A job template registered later under the same name gets a new id, so
drop the cached
+ // mapping.
+ AuthorizationUtils.notifyEntityNameIdMappingChange(
+ NameIdentifierUtil.ofJobTemplate(metalake, jobTemplateName),
+ Entity.EntityType.JOB_TEMPLATE);
+ }
+ return deleted;
}
@Override
public JobTemplateEntity alterJobTemplate(
String metalake, String jobTemplateName, JobTemplateChange... changes)
throws NoSuchJobTemplateException, IllegalArgumentException {
- return jobOperationDispatcher.alterJobTemplate(metalake, jobTemplateName,
changes);
+ JobTemplateEntity alteredJobTemplate =
+ jobOperationDispatcher.alterJobTemplate(metalake, jobTemplateName,
changes);
+ if (Arrays.stream(changes)
+ .anyMatch(change -> change instanceof
JobTemplateChange.RenameJobTemplate)) {
+ AuthorizationUtils.notifyEntityNameIdMappingChange(
+ NameIdentifierUtil.ofJobTemplate(metalake, jobTemplateName),
+ Entity.EntityType.JOB_TEMPLATE);
+ }
+ return alteredJobTemplate;
}
@Override
diff --git
a/core/src/main/java/org/apache/gravitino/hook/ModelHookDispatcher.java
b/core/src/main/java/org/apache/gravitino/hook/ModelHookDispatcher.java
index facad41909..cb2b28f343 100644
--- a/core/src/main/java/org/apache/gravitino/hook/ModelHookDispatcher.java
+++ b/core/src/main/java/org/apache/gravitino/hook/ModelHookDispatcher.java
@@ -19,11 +19,13 @@
package org.apache.gravitino.hook;
+import java.util.Arrays;
import java.util.Map;
import org.apache.gravitino.Entity;
import org.apache.gravitino.GravitinoEnv;
import org.apache.gravitino.NameIdentifier;
import org.apache.gravitino.Namespace;
+import org.apache.gravitino.authorization.AuthorizationUtils;
import org.apache.gravitino.authorization.Owner;
import org.apache.gravitino.authorization.OwnerDispatcher;
import org.apache.gravitino.catalog.ModelDispatcher;
@@ -82,7 +84,12 @@ public class ModelHookDispatcher implements ModelDispatcher {
@Override
public boolean deleteModel(NameIdentifier ident) {
- return dispatcher.deleteModel(ident);
+ boolean deleted = dispatcher.deleteModel(ident);
+ if (deleted) {
+ // A model registered later under the same name gets a new id, so drop
the cached mapping.
+ AuthorizationUtils.notifyEntityNameIdMappingChange(ident,
Entity.EntityType.MODEL);
+ }
+ return deleted;
}
@Override
@@ -181,7 +188,11 @@ public class ModelHookDispatcher implements
ModelDispatcher {
@Override
public Model alterModel(NameIdentifier ident, ModelChange... changes)
throws NoSuchModelException, IllegalArgumentException {
- return dispatcher.alterModel(ident, changes);
+ Model alteredModel = dispatcher.alterModel(ident, changes);
+ if (Arrays.stream(changes).anyMatch(change -> change instanceof
ModelChange.RenameModel)) {
+ AuthorizationUtils.notifyEntityNameIdMappingChange(ident,
Entity.EntityType.MODEL);
+ }
+ return alteredModel;
}
@Override
diff --git
a/core/src/main/java/org/apache/gravitino/hook/PolicyHookDispatcher.java
b/core/src/main/java/org/apache/gravitino/hook/PolicyHookDispatcher.java
index 34394357b3..1d08d80f53 100644
--- a/core/src/main/java/org/apache/gravitino/hook/PolicyHookDispatcher.java
+++ b/core/src/main/java/org/apache/gravitino/hook/PolicyHookDispatcher.java
@@ -17,10 +17,12 @@
package org.apache.gravitino.hook;
+import java.util.Arrays;
import org.apache.gravitino.Entity;
import org.apache.gravitino.GravitinoEnv;
import org.apache.gravitino.MetadataObject;
import org.apache.gravitino.RelationalEntity;
+import org.apache.gravitino.authorization.AuthorizationUtils;
import org.apache.gravitino.authorization.Owner;
import org.apache.gravitino.authorization.OwnerDispatcher;
import org.apache.gravitino.exceptions.NoSuchPolicyException;
@@ -82,7 +84,12 @@ public class PolicyHookDispatcher implements
PolicyDispatcher {
@Override
public PolicyEntity alterPolicy(String metalake, String policyName,
PolicyChange... changes) {
- return dispatcher.alterPolicy(metalake, policyName, changes);
+ PolicyEntity alteredPolicy = dispatcher.alterPolicy(metalake, policyName,
changes);
+ if (Arrays.stream(changes).anyMatch(change -> change instanceof
PolicyChange.RenamePolicy)) {
+ AuthorizationUtils.notifyEntityNameIdMappingChange(
+ NameIdentifierUtil.ofPolicy(metalake, policyName),
Entity.EntityType.POLICY);
+ }
+ return alteredPolicy;
}
@Override
@@ -97,7 +104,13 @@ public class PolicyHookDispatcher implements
PolicyDispatcher {
@Override
public boolean deletePolicy(String metalake, String policyName) {
- return dispatcher.deletePolicy(metalake, policyName);
+ boolean deleted = dispatcher.deletePolicy(metalake, policyName);
+ if (deleted) {
+ // A policy created later under the same name gets a new id, so drop the
cached mapping.
+ AuthorizationUtils.notifyEntityNameIdMappingChange(
+ NameIdentifierUtil.ofPolicy(metalake, policyName),
Entity.EntityType.POLICY);
+ }
+ return deleted;
}
@Override
diff --git
a/core/src/main/java/org/apache/gravitino/hook/TagHookDispatcher.java
b/core/src/main/java/org/apache/gravitino/hook/TagHookDispatcher.java
index 0662767e3e..fcc470c17c 100644
--- a/core/src/main/java/org/apache/gravitino/hook/TagHookDispatcher.java
+++ b/core/src/main/java/org/apache/gravitino/hook/TagHookDispatcher.java
@@ -17,11 +17,13 @@
package org.apache.gravitino.hook;
+import java.util.Arrays;
import java.util.Map;
import org.apache.gravitino.Entity;
import org.apache.gravitino.GravitinoEnv;
import org.apache.gravitino.MetadataObject;
import org.apache.gravitino.RelationalEntity;
+import org.apache.gravitino.authorization.AuthorizationUtils;
import org.apache.gravitino.authorization.Owner;
import org.apache.gravitino.authorization.OwnerDispatcher;
import org.apache.gravitino.exceptions.NoSuchTagException;
@@ -90,12 +92,23 @@ public class TagHookDispatcher implements TagDispatcher {
@Override
public Tag alterTag(String metalake, String name, TagChange... changes)
throws IllegalArgumentException, TagAlreadyExistsException {
- return dispatcher.alterTag(metalake, name, changes);
+ Tag alteredTag = dispatcher.alterTag(metalake, name, changes);
+ if (Arrays.stream(changes).anyMatch(change -> change instanceof
TagChange.RenameTag)) {
+ AuthorizationUtils.notifyEntityNameIdMappingChange(
+ NameIdentifierUtil.ofTag(metalake, name), Entity.EntityType.TAG);
+ }
+ return alteredTag;
}
@Override
public boolean deleteTag(String metalake, String name) {
- return dispatcher.deleteTag(metalake, name);
+ boolean deleted = dispatcher.deleteTag(metalake, name);
+ if (deleted) {
+ // A tag created later under the same name gets a new id, so drop the
cached mapping.
+ AuthorizationUtils.notifyEntityNameIdMappingChange(
+ NameIdentifierUtil.ofTag(metalake, name), Entity.EntityType.TAG);
+ }
+ return deleted;
}
@Override
diff --git
a/core/src/main/java/org/apache/gravitino/hook/ViewHookDispatcher.java
b/core/src/main/java/org/apache/gravitino/hook/ViewHookDispatcher.java
index 1f871682e9..c24117ef5f 100644
--- a/core/src/main/java/org/apache/gravitino/hook/ViewHookDispatcher.java
+++ b/core/src/main/java/org/apache/gravitino/hook/ViewHookDispatcher.java
@@ -118,6 +118,12 @@ public class ViewHookDispatcher implements ViewDispatcher {
@Override
public boolean dropView(NameIdentifier ident) {
- return dispatcher.dropView(ident);
+ boolean dropped = dispatcher.dropView(ident);
+ if (dropped) {
+ // A view created later under the same name gets a new id, so drop the
cached mapping. Views
+ // have no catalog authorization plugin privileges to remove.
+ AuthorizationUtils.notifyEntityNameIdMappingChange(ident,
Entity.EntityType.VIEW);
+ }
+ return dropped;
}
}
diff --git
a/core/src/main/java/org/apache/gravitino/storage/relational/EntityChangeLogWriter.java
b/core/src/main/java/org/apache/gravitino/storage/relational/EntityChangeLogWriter.java
new file mode 100644
index 0000000000..54e750b69a
--- /dev/null
+++
b/core/src/main/java/org/apache/gravitino/storage/relational/EntityChangeLogWriter.java
@@ -0,0 +1,51 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.gravitino.storage.relational;
+
+import org.apache.gravitino.Entity;
+import org.apache.gravitino.NameIdentifier;
+import org.apache.gravitino.storage.relational.mapper.EntityChangeLogMapper;
+import org.apache.gravitino.storage.relational.po.cache.OperateType;
+import org.apache.gravitino.storage.relational.utils.SessionUtils;
+import org.apache.gravitino.utils.NameIdentifierUtil;
+
+/** Appends {@code entity_change_log} records that peer nodes replay to
invalidate their caches. */
+public final class EntityChangeLogWriter {
+
+ private EntityChangeLogWriter() {}
+
+ /**
+ * Appends a change record in the caller's transaction, so it commits or
rolls back with the
+ * change it describes.
+ *
+ * @param ident the name that peers may have cached: the pre-mutation name
for an alter and the
+ * current name for a drop
+ * @param entityType the entity type
+ * @param operateType the change operation
+ */
+ public static void append(
+ NameIdentifier ident, Entity.EntityType entityType, OperateType
operateType) {
+ String metalake = NameIdentifierUtil.getMetalake(ident);
+ String fullName = EntityChangeLogNameIdentifierCodec.encode(ident);
+ SessionUtils.doWithoutCommit(
+ EntityChangeLogMapper.class,
+ mapper -> mapper.insertEntityChange(metalake, entityType.name(),
fullName, operateType));
+ EntityChangeLogDiagnostics.logAppended(metalake, entityType.name(),
operateType, fullName);
+ }
+}
diff --git
a/core/src/main/java/org/apache/gravitino/storage/relational/JDBCBackend.java
b/core/src/main/java/org/apache/gravitino/storage/relational/JDBCBackend.java
index b6f184fa8c..995b385a4e 100644
---
a/core/src/main/java/org/apache/gravitino/storage/relational/JDBCBackend.java
+++
b/core/src/main/java/org/apache/gravitino/storage/relational/JDBCBackend.java
@@ -72,7 +72,6 @@ import org.apache.gravitino.meta.UserEntity;
import org.apache.gravitino.meta.ViewEntity;
import
org.apache.gravitino.storage.relational.converters.SQLExceptionConverterFactory;
import org.apache.gravitino.storage.relational.database.H2Database;
-import org.apache.gravitino.storage.relational.mapper.EntityChangeLogMapper;
import org.apache.gravitino.storage.relational.po.cache.OperateType;
import org.apache.gravitino.storage.relational.service.CatalogMetaService;
import org.apache.gravitino.storage.relational.service.FilesetMetaService;
@@ -100,7 +99,6 @@ import
org.apache.gravitino.storage.relational.service.ViewMetaService;
import org.apache.gravitino.storage.relational.session.SqlSessionFactoryHelper;
import org.apache.gravitino.storage.relational.utils.SessionUtils;
import org.apache.gravitino.tag.TagValue;
-import org.apache.gravitino.utils.NameIdentifierUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -207,7 +205,7 @@ public class JDBCBackend implements RelationalBackend,
SupportsOrphanedRelationC
boolean committed = false;
try {
insertEntity(e, true);
- insertEntityChange(e.nameIdentifier(), e.type(), OperateType.ALTER);
+ EntityChangeLogWriter.append(e.nameIdentifier(), e.type(),
OperateType.ALTER);
if (transactionOwner) {
SessionUtils.commitTransaction();
}
@@ -234,7 +232,7 @@ public class JDBCBackend implements RelationalBackend,
SupportsOrphanedRelationC
boolean committed = false;
try {
E updatedEntity = updateEntity(ident, entityType, updater);
- insertEntityChange(ident, entityType, OperateType.ALTER);
+ EntityChangeLogWriter.append(ident, entityType, OperateType.ALTER);
if (transactionOwner) {
SessionUtils.commitTransaction();
}
@@ -399,7 +397,7 @@ public class JDBCBackend implements RelationalBackend,
SupportsOrphanedRelationC
try {
boolean deleted = deleteEntity(ident, entityType, cascade);
if (deleted) {
- insertEntityChange(ident, entityType, OperateType.DROP);
+ EntityChangeLogWriter.append(ident, entityType, OperateType.DROP);
}
if (transactionOwner) {
SessionUtils.commitTransaction();
@@ -439,7 +437,7 @@ public class JDBCBackend implements RelationalBackend,
SupportsOrphanedRelationC
// reported to the caller as "there was nothing to delete".
return Optional.empty();
}
- insertEntityChange(ident, entityType, OperateType.DROP);
+ EntityChangeLogWriter.append(ident, entityType, OperateType.DROP);
E deletedEntity = clazz.cast(deletedFileset);
// Run external cleanup while the metadata delete can still be rolled
back. The callback uses
// the same snapshot whose OCC token won above, so it cannot act on
stale locations.
@@ -1119,16 +1117,6 @@ public class JDBCBackend implements RelationalBackend,
SupportsOrphanedRelationC
}
}
- private static void insertEntityChange(
- NameIdentifier ident, Entity.EntityType entityType, OperateType
operateType) {
- String metalake = NameIdentifierUtil.getMetalake(ident);
- String fullName = EntityChangeLogNameIdentifierCodec.encode(ident);
- SessionUtils.doWithoutCommit(
- EntityChangeLogMapper.class,
- mapper -> mapper.insertEntityChange(metalake, entityType.name(),
fullName, operateType));
- EntityChangeLogDiagnostics.logAppended(metalake, entityType.name(),
operateType, fullName);
- }
-
private static boolean shouldRecordEntityDrop(Entity.EntityType entityType) {
// Functions bypass the Entity Store cache, but their drops must still
invalidate JCasbin's
// name-to-ID cache on peer nodes.
diff --git
a/core/src/main/java/org/apache/gravitino/storage/relational/service/JobTemplateMetaService.java
b/core/src/main/java/org/apache/gravitino/storage/relational/service/JobTemplateMetaService.java
index 9f5dffbfa8..9628fa7f7e 100644
---
a/core/src/main/java/org/apache/gravitino/storage/relational/service/JobTemplateMetaService.java
+++
b/core/src/main/java/org/apache/gravitino/storage/relational/service/JobTemplateMetaService.java
@@ -35,9 +35,11 @@ import org.apache.gravitino.exceptions.NoSuchEntityException;
import org.apache.gravitino.exceptions.NonEmptyEntityException;
import org.apache.gravitino.meta.JobTemplateEntity;
import org.apache.gravitino.metrics.Monitored;
+import org.apache.gravitino.storage.relational.EntityChangeLogWriter;
import org.apache.gravitino.storage.relational.mapper.JobMetaMapper;
import org.apache.gravitino.storage.relational.mapper.JobTemplateMetaMapper;
import org.apache.gravitino.storage.relational.po.JobTemplatePO;
+import org.apache.gravitino.storage.relational.po.cache.OperateType;
import org.apache.gravitino.storage.relational.utils.ExceptionUtils;
import org.apache.gravitino.storage.relational.utils.SessionUtils;
@@ -161,7 +163,15 @@ public class JobTemplateMetaService {
JobTemplateMetaMapper.class,
mapper ->
mapper.updateJobTemplateMeta(newJobTemplatePO,
oldJobTemplatePO)),
- () -> writeFailure(jobTemplateIdent, oldJobTemplatePO)));
+ () -> writeFailure(jobTemplateIdent, oldJobTemplatePO)),
+ () -> {
+ // Job templates are not entity-cacheable, so JDBCBackend does not
log their changes.
+ // Peers still cache the old name's id for authorization, so log a
rename here.
+ if (!Objects.equals(oldJobTemplateEntity.name(),
newJobTemplateEntity.name())) {
+ EntityChangeLogWriter.append(
+ jobTemplateIdent, Entity.EntityType.JOB_TEMPLATE,
OperateType.ALTER);
+ }
+ });
} catch (RuntimeException e) {
ExceptionUtils.checkSQLException(e, Entity.EntityType.JOB_TEMPLATE,
jobTemplateIdent.name());
throw e;
@@ -248,7 +258,9 @@ public class JobTemplateMetaService {
() ->
SessionUtils.doWithoutCommit(
JobMetaMapper.class,
- mapper ->
mapper.softDeleteJobsByTemplateId(observed.jobTemplateId())));
+ mapper ->
mapper.softDeleteJobsByTemplateId(observed.jobTemplateId())),
+ () ->
+ EntityChangeLogWriter.append(ident,
Entity.EntityType.JOB_TEMPLATE, OperateType.DROP));
}
/** Locks the observed template while a job is inserted in the same
transaction. */
diff --git
a/core/src/main/java/org/apache/gravitino/storage/relational/service/ModelMetaService.java
b/core/src/main/java/org/apache/gravitino/storage/relational/service/ModelMetaService.java
index 49f5e7d260..1b209445f1 100644
---
a/core/src/main/java/org/apache/gravitino/storage/relational/service/ModelMetaService.java
+++
b/core/src/main/java/org/apache/gravitino/storage/relational/service/ModelMetaService.java
@@ -38,9 +38,7 @@ import org.apache.gravitino.exceptions.NoSuchEntityException;
import org.apache.gravitino.meta.ModelEntity;
import org.apache.gravitino.meta.NamespacedEntityId;
import org.apache.gravitino.metrics.Monitored;
-import org.apache.gravitino.storage.relational.EntityChangeLogDiagnostics;
-import
org.apache.gravitino.storage.relational.EntityChangeLogNameIdentifierCodec;
-import org.apache.gravitino.storage.relational.mapper.EntityChangeLogMapper;
+import org.apache.gravitino.storage.relational.EntityChangeLogWriter;
import org.apache.gravitino.storage.relational.mapper.ModelMetaMapper;
import
org.apache.gravitino.storage.relational.mapper.ModelVersionAliasRelMapper;
import org.apache.gravitino.storage.relational.mapper.ModelVersionMetaMapper;
@@ -132,9 +130,8 @@ public class ModelMetaService {
String metalakeName = ident.namespace().level(0);
String catalogName = ident.namespace().level(1);
String schemaName = ident.namespace().level(2);
- String modelFullName =
- EntityChangeLogNameIdentifierCodec.encode(
- NameIdentifierUtil.ofModel(metalakeName, catalogName, schemaName,
ident.name()));
+ NameIdentifier modelIdent =
+ NameIdentifierUtil.ofModel(metalakeName, catalogName, schemaName,
ident.name());
// Delete the model row first, and only when its concurrency version still
matches the value
// read above. If another writer changed the model, stop before removing
any related data.
@@ -142,18 +139,8 @@ public class ModelMetaService {
SessionUtils.doMultipleWithCommit(
() -> deleteModelWithVersion(ident, modelPO),
() -> deleteModelDependents(modelPO),
- () -> {
- SessionUtils.doWithoutCommit(
- EntityChangeLogMapper.class,
- mapper ->
- mapper.insertEntityChange(
- metalakeName,
- Entity.EntityType.MODEL.name(),
- modelFullName,
- OperateType.DROP));
- EntityChangeLogDiagnostics.logAppended(
- metalakeName, Entity.EntityType.MODEL.name(),
OperateType.DROP, modelFullName);
- });
+ () ->
+ EntityChangeLogWriter.append(modelIdent,
Entity.EntityType.MODEL, OperateType.DROP));
} catch (NoSuchEntityException e) {
// Another writer dropped the model between the read above and this
transaction. A drop that
// finds nothing to drop is reported the same way as the read above
reports it, so that a
@@ -338,10 +325,8 @@ public class ModelMetaService {
String metalakeName = identifier.namespace().level(0);
String catalogName = identifier.namespace().level(1);
String schemaName = identifier.namespace().level(2);
- String oldFullName =
- EntityChangeLogNameIdentifierCodec.encode(
- NameIdentifierUtil.ofModel(
- metalakeName, catalogName, schemaName, oldModelEntity.name()));
+ NameIdentifier oldModelIdent =
+ NameIdentifierUtil.ofModel(metalakeName, catalogName, schemaName,
oldModelEntity.name());
boolean isRenamed = !Objects.equals(oldModelEntity.name(),
newEntity.name());
try {
@@ -368,16 +353,8 @@ public class ModelMetaService {
},
() -> {
if (isRenamed) {
- SessionUtils.doWithoutCommit(
- EntityChangeLogMapper.class,
- mapper ->
- mapper.insertEntityChange(
- metalakeName,
- Entity.EntityType.MODEL.name(),
- oldFullName,
- OperateType.ALTER));
- EntityChangeLogDiagnostics.logAppended(
- metalakeName, Entity.EntityType.MODEL.name(),
OperateType.ALTER, oldFullName);
+ EntityChangeLogWriter.append(
+ oldModelIdent, Entity.EntityType.MODEL,
OperateType.ALTER);
}
});
} catch (RuntimeException re) {
diff --git
a/core/src/main/java/org/apache/gravitino/storage/relational/service/SemanticModelMetaService.java
b/core/src/main/java/org/apache/gravitino/storage/relational/service/SemanticModelMetaService.java
index 4baffc78ee..624d038866 100644
---
a/core/src/main/java/org/apache/gravitino/storage/relational/service/SemanticModelMetaService.java
+++
b/core/src/main/java/org/apache/gravitino/storage/relational/service/SemanticModelMetaService.java
@@ -40,8 +40,7 @@ import org.apache.gravitino.exceptions.NoSuchEntityException;
import org.apache.gravitino.exceptions.OptimisticLockException;
import org.apache.gravitino.meta.SemanticModelEntity;
import org.apache.gravitino.metrics.Monitored;
-import
org.apache.gravitino.storage.relational.EntityChangeLogNameIdentifierCodec;
-import org.apache.gravitino.storage.relational.mapper.EntityChangeLogMapper;
+import org.apache.gravitino.storage.relational.EntityChangeLogWriter;
import org.apache.gravitino.storage.relational.mapper.SemanticModelMetaMapper;
import
org.apache.gravitino.storage.relational.mapper.SemanticModelVersionInfoMapper;
import org.apache.gravitino.storage.relational.po.SemanticModelPO;
@@ -224,13 +223,9 @@ public class SemanticModelMetaService {
String metalakeName = identifier.namespace().level(0);
String catalogName = identifier.namespace().level(1);
String schemaName = identifier.namespace().level(2);
- String oldFullName =
- EntityChangeLogNameIdentifierCodec.encode(
- NameIdentifierUtil.ofSemanticModel(
- metalakeName,
- catalogName,
- schemaName,
- oldSemanticModelPO.getSemanticModelName()));
+ NameIdentifier oldSemanticModelIdent =
+ NameIdentifierUtil.ofSemanticModel(
+ metalakeName, catalogName, schemaName,
oldSemanticModelPO.getSemanticModelName());
boolean isRenamed =
!Objects.equals(
oldSemanticModelPO.getSemanticModelName(),
newSemanticModelPO.getSemanticModelName());
@@ -263,14 +258,8 @@ public class SemanticModelMetaService {
newSemanticModelPO.getSemanticModelVersionInfoPO())),
() -> {
if (isRenamed && updateResult.get() > 0) {
- SessionUtils.doWithoutCommit(
- EntityChangeLogMapper.class,
- mapper ->
- mapper.insertEntityChange(
- metalakeName,
- Entity.EntityType.SEMANTIC_MODEL.name(),
- oldFullName,
- OperateType.ALTER));
+ EntityChangeLogWriter.append(
+ oldSemanticModelIdent, Entity.EntityType.SEMANTIC_MODEL,
OperateType.ALTER);
}
});
return newEntity;
@@ -374,13 +363,12 @@ public class SemanticModelMetaService {
NameIdentifier identifier, SemanticModelPO observedSemanticModelPO) {
Long semanticModelId = observedSemanticModelPO.getSemanticModelId();
String metalakeName = identifier.namespace().level(0);
- String semanticModelFullName =
- EntityChangeLogNameIdentifierCodec.encode(
- NameIdentifierUtil.ofSemanticModel(
- metalakeName,
- identifier.namespace().level(1),
- identifier.namespace().level(2),
- observedSemanticModelPO.getSemanticModelName()));
+ NameIdentifier semanticModelIdent =
+ NameIdentifierUtil.ofSemanticModel(
+ metalakeName,
+ identifier.namespace().level(1),
+ identifier.namespace().level(2),
+ observedSemanticModelPO.getSemanticModelName());
// TODO: Soft-delete Semantic Model owner, tag, and securable-object
relations in this
// transaction and in the metalake/catalog/schema cascade delete paths.
SessionUtils.doMultipleWithCommit(
@@ -398,14 +386,8 @@ public class SemanticModelMetaService {
SemanticModelVersionInfoMapper.class,
mapper ->
mapper.softDeleteSemanticModelVersionsBySemanticModelId(semanticModelId)),
() ->
- SessionUtils.doWithoutCommit(
- EntityChangeLogMapper.class,
- mapper ->
- mapper.insertEntityChange(
- metalakeName,
- Entity.EntityType.SEMANTIC_MODEL.name(),
- semanticModelFullName,
- OperateType.DROP)));
+ EntityChangeLogWriter.append(
+ semanticModelIdent, Entity.EntityType.SEMANTIC_MODEL,
OperateType.DROP));
}
private SemanticModelPO updateSemanticModelPO(
diff --git
a/core/src/test/java/org/apache/gravitino/hook/TestJobHookDispatcher.java
b/core/src/test/java/org/apache/gravitino/hook/TestJobHookDispatcher.java
index baea025fc4..d1f95b1a0b 100644
--- a/core/src/test/java/org/apache/gravitino/hook/TestJobHookDispatcher.java
+++ b/core/src/test/java/org/apache/gravitino/hook/TestJobHookDispatcher.java
@@ -21,17 +21,22 @@ package org.apache.gravitino.hook;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import java.util.Collections;
import org.apache.commons.lang3.reflect.FieldUtils;
+import org.apache.gravitino.Entity;
import org.apache.gravitino.GravitinoEnv;
import org.apache.gravitino.NameIdentifier;
+import org.apache.gravitino.authorization.GravitinoAuthorizer;
import org.apache.gravitino.authorization.OwnerDispatcher;
import org.apache.gravitino.job.JobOperationDispatcher;
+import org.apache.gravitino.job.JobTemplateChange;
import org.apache.gravitino.meta.JobEntity;
import org.apache.gravitino.meta.JobTemplateEntity;
+import org.apache.gravitino.utils.NameIdentifierUtil;
import org.apache.gravitino.utils.NamespaceUtil;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Assertions;
@@ -46,6 +51,8 @@ public class TestJobHookDispatcher {
// Save the original ownerDispatcher before each test and restore it in
tearDown so we do not
// leak null state into the GravitinoEnv singleton across tests.
private OwnerDispatcher savedOwnerDispatcher;
+ private GravitinoAuthorizer mockAuthorizer;
+ private GravitinoAuthorizer savedAuthorizer;
@BeforeEach
public void setUp() throws IllegalAccessException {
@@ -54,6 +61,9 @@ public class TestJobHookDispatcher {
savedOwnerDispatcher =
GravitinoEnv.getInstance().internalOwnerDispatcher();
FieldUtils.writeField(
GravitinoEnv.getInstance(), "internalOwnerDispatcher",
mockOwnerDispatcher, true);
+ mockAuthorizer = mock(GravitinoAuthorizer.class);
+ savedAuthorizer = GravitinoEnv.getInstance().gravitinoAuthorizer();
+ GravitinoEnv.getInstance().setGravitinoAuthorizer(mockAuthorizer);
hookDispatcher = new JobHookDispatcher(mockDispatcher);
}
@@ -61,6 +71,7 @@ public class TestJobHookDispatcher {
public void tearDown() throws IllegalAccessException {
FieldUtils.writeField(
GravitinoEnv.getInstance(), "internalOwnerDispatcher",
savedOwnerDispatcher, true);
+ GravitinoEnv.getInstance().setGravitinoAuthorizer(savedAuthorizer);
}
@Test
@@ -104,4 +115,64 @@ public class TestJobHookDispatcher {
Assertions.assertEquals("Set owner failed", thrown.getMessage());
verify(mockDispatcher).runJob(any(), any(), any());
}
+
+ @Test
+ public void testDeleteJobTemplateInvalidatesNameIdMapping() {
+ when(mockDispatcher.deleteJobTemplate("test_metalake",
"test_template")).thenReturn(true);
+
+ Assertions.assertTrue(hookDispatcher.deleteJobTemplate("test_metalake",
"test_template"));
+ verify(mockAuthorizer)
+ .handleEntityNameIdMappingChange(
+ "test_metalake",
+ NameIdentifierUtil.ofJobTemplate("test_metalake", "test_template"),
+ Entity.EntityType.JOB_TEMPLATE);
+ }
+
+ @Test
+ public void testDeleteMissingJobTemplateDoesNotInvalidateNameIdMapping() {
+ when(mockDispatcher.deleteJobTemplate("test_metalake",
"test_template")).thenReturn(false);
+
+ Assertions.assertFalse(hookDispatcher.deleteJobTemplate("test_metalake",
"test_template"));
+ verify(mockAuthorizer, never()).handleEntityNameIdMappingChange(any(),
any(), any());
+ }
+
+ @Test
+ public void testRenameJobTemplateInvalidatesOldNameIdMapping() {
+ JobTemplateChange[] changes = {
+ JobTemplateChange.updateComment("new comment"),
JobTemplateChange.rename("new_template")
+ };
+ JobTemplateEntity mockTemplate = mock(JobTemplateEntity.class);
+ when(mockDispatcher.alterJobTemplate("test_metalake", "test_template",
changes))
+ .thenReturn(mockTemplate);
+
+ Assertions.assertSame(
+ mockTemplate, hookDispatcher.alterJobTemplate("test_metalake",
"test_template", changes));
+ verify(mockAuthorizer)
+ .handleEntityNameIdMappingChange(
+ "test_metalake",
+ NameIdentifierUtil.ofJobTemplate("test_metalake", "test_template"),
+ Entity.EntityType.JOB_TEMPLATE);
+ }
+
+ @Test
+ public void
testAlterJobTemplateWithoutRenameDoesNotInvalidateNameIdMapping() {
+ JobTemplateChange[] changes = {JobTemplateChange.updateComment("new
comment")};
+ when(mockDispatcher.alterJobTemplate("test_metalake", "test_template",
changes))
+ .thenReturn(mock(JobTemplateEntity.class));
+
+ hookDispatcher.alterJobTemplate("test_metalake", "test_template", changes);
+ verify(mockAuthorizer, never()).handleEntityNameIdMappingChange(any(),
any(), any());
+ }
+
+ @Test
+ public void testFailedRenameJobTemplateDoesNotInvalidateNameIdMapping() {
+ JobTemplateChange[] changes = {JobTemplateChange.rename("new_template")};
+ when(mockDispatcher.alterJobTemplate("test_metalake", "test_template",
changes))
+ .thenThrow(new RuntimeException("Alter failed"));
+
+ Assertions.assertThrows(
+ RuntimeException.class,
+ () -> hookDispatcher.alterJobTemplate("test_metalake",
"test_template", changes));
+ verify(mockAuthorizer, never()).handleEntityNameIdMappingChange(any(),
any(), any());
+ }
}
diff --git
a/core/src/test/java/org/apache/gravitino/hook/TestModelHookDispatcher.java
b/core/src/test/java/org/apache/gravitino/hook/TestModelHookDispatcher.java
index 25df575ca3..d2fc6b0090 100644
--- a/core/src/test/java/org/apache/gravitino/hook/TestModelHookDispatcher.java
+++ b/core/src/test/java/org/apache/gravitino/hook/TestModelHookDispatcher.java
@@ -22,14 +22,17 @@ import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import java.util.Collections;
import org.apache.commons.lang3.reflect.FieldUtils;
+import org.apache.gravitino.Entity;
import org.apache.gravitino.GravitinoEnv;
import org.apache.gravitino.MetadataObject;
import org.apache.gravitino.NameIdentifier;
+import org.apache.gravitino.authorization.GravitinoAuthorizer;
import org.apache.gravitino.authorization.Owner;
import org.apache.gravitino.authorization.OwnerDispatcher;
import org.apache.gravitino.catalog.CatalogManager;
@@ -40,6 +43,7 @@ import org.apache.gravitino.connector.BaseCatalog;
import org.apache.gravitino.connector.capability.Capability;
import org.apache.gravitino.connector.capability.CapabilityResult;
import org.apache.gravitino.model.Model;
+import org.apache.gravitino.model.ModelChange;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
@@ -57,6 +61,8 @@ public class TestModelHookDispatcher {
// state into the GravitinoEnv singleton across tests.
private OwnerDispatcher savedOwnerDispatcher;
private CatalogManager savedCatalogManager;
+ private GravitinoAuthorizer mockAuthorizer;
+ private GravitinoAuthorizer savedAuthorizer;
@BeforeEach
public void setUp() throws Exception {
@@ -75,6 +81,9 @@ public class TestModelHookDispatcher {
FieldUtils.writeField(
GravitinoEnv.getInstance(), "internalOwnerDispatcher",
mockOwnerDispatcher, true);
FieldUtils.writeField(GravitinoEnv.getInstance(), "catalogManager",
mockCatalogManager, true);
+ mockAuthorizer = mock(GravitinoAuthorizer.class);
+ savedAuthorizer = GravitinoEnv.getInstance().gravitinoAuthorizer();
+ GravitinoEnv.getInstance().setGravitinoAuthorizer(mockAuthorizer);
hookDispatcher =
new ModelNormalizeDispatcher(new ModelHookDispatcher(mockDispatcher),
mockCatalogManager);
}
@@ -84,6 +93,7 @@ public class TestModelHookDispatcher {
FieldUtils.writeField(
GravitinoEnv.getInstance(), "internalOwnerDispatcher",
savedOwnerDispatcher, true);
FieldUtils.writeField(GravitinoEnv.getInstance(), "catalogManager",
savedCatalogManager, true);
+ GravitinoEnv.getInstance().setGravitinoAuthorizer(savedAuthorizer);
}
@Test
@@ -166,6 +176,66 @@ public class TestModelHookDispatcher {
+ " ident.name() was used as the metalake arg");
}
+ @Test
+ public void testDeleteModelInvalidatesNormalizedNameIdMapping() {
+ when(mockCatalog.capability()).thenReturn(new CaseInsensitiveCapability());
+ NameIdentifier normalizedIdent =
+ NameIdentifier.of("test_metalake", "test_catalog", "test_schema",
"my_model");
+ when(mockDispatcher.deleteModel(normalizedIdent)).thenReturn(true);
+
+ Assertions.assertTrue(
+ hookDispatcher.deleteModel(
+ NameIdentifier.of("test_metalake", "test_catalog", "TEST_SCHEMA",
"MY_MODEL")));
+ verify(mockAuthorizer)
+ .handleEntityNameIdMappingChange("test_metalake", normalizedIdent,
Entity.EntityType.MODEL);
+ }
+
+ @Test
+ public void testDeleteMissingModelDoesNotInvalidateNameIdMapping() {
+ NameIdentifier ident =
+ NameIdentifier.of("test_metalake", "test_catalog", "test_schema",
"test_model");
+ when(mockDispatcher.deleteModel(ident)).thenReturn(false);
+
+ Assertions.assertFalse(hookDispatcher.deleteModel(ident));
+ verify(mockAuthorizer, never()).handleEntityNameIdMappingChange(any(),
any(), any());
+ }
+
+ @Test
+ public void testRenameModelInvalidatesOldNameIdMapping() {
+ NameIdentifier ident =
+ NameIdentifier.of("test_metalake", "test_catalog", "test_schema",
"test_model");
+ ModelChange[] changes = {ModelChange.updateComment("new comment"),
ModelChange.rename("new")};
+ Model mockModel = mock(Model.class);
+ when(mockDispatcher.alterModel(eq(ident),
any(ModelChange[].class))).thenReturn(mockModel);
+
+ Assertions.assertSame(mockModel, hookDispatcher.alterModel(ident,
changes));
+ verify(mockAuthorizer)
+ .handleEntityNameIdMappingChange("test_metalake", ident,
Entity.EntityType.MODEL);
+ }
+
+ @Test
+ public void testAlterModelWithoutRenameDoesNotInvalidateNameIdMapping() {
+ NameIdentifier ident =
+ NameIdentifier.of("test_metalake", "test_catalog", "test_schema",
"test_model");
+ when(mockDispatcher.alterModel(eq(ident), any(ModelChange[].class)))
+ .thenReturn(mock(Model.class));
+
+ hookDispatcher.alterModel(ident, ModelChange.updateComment("new comment"));
+ verify(mockAuthorizer, never()).handleEntityNameIdMappingChange(any(),
any(), any());
+ }
+
+ @Test
+ public void testFailedRenameModelDoesNotInvalidateNameIdMapping() {
+ NameIdentifier ident =
+ NameIdentifier.of("test_metalake", "test_catalog", "test_schema",
"test_model");
+ when(mockDispatcher.alterModel(eq(ident), any(ModelChange[].class)))
+ .thenThrow(new RuntimeException("Alter failed"));
+
+ Assertions.assertThrows(
+ RuntimeException.class, () -> hookDispatcher.alterModel(ident,
ModelChange.rename("new")));
+ verify(mockAuthorizer, never()).handleEntityNameIdMappingChange(any(),
any(), any());
+ }
+
private static class CaseInsensitiveCapability implements Capability {
@Override
public CapabilityResult caseSensitiveOnName(Scope scope) {
diff --git
a/core/src/test/java/org/apache/gravitino/hook/TestPolicyHookDispatcher.java
b/core/src/test/java/org/apache/gravitino/hook/TestPolicyHookDispatcher.java
index fd0ca523fd..a99ba3cd90 100644
--- a/core/src/test/java/org/apache/gravitino/hook/TestPolicyHookDispatcher.java
+++ b/core/src/test/java/org/apache/gravitino/hook/TestPolicyHookDispatcher.java
@@ -22,14 +22,19 @@ import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import org.apache.commons.lang3.reflect.FieldUtils;
+import org.apache.gravitino.Entity;
import org.apache.gravitino.GravitinoEnv;
+import org.apache.gravitino.authorization.GravitinoAuthorizer;
import org.apache.gravitino.authorization.OwnerDispatcher;
import org.apache.gravitino.meta.PolicyEntity;
+import org.apache.gravitino.policy.PolicyChange;
import org.apache.gravitino.policy.PolicyDispatcher;
+import org.apache.gravitino.utils.NameIdentifierUtil;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
@@ -43,6 +48,8 @@ public class TestPolicyHookDispatcher {
// Save the original ownerDispatcher before each test and restore it in
tearDown so we do not
// leak null state into the GravitinoEnv singleton across tests.
private OwnerDispatcher savedOwnerDispatcher;
+ private GravitinoAuthorizer mockAuthorizer;
+ private GravitinoAuthorizer savedAuthorizer;
@BeforeEach
public void setUp() throws IllegalAccessException {
@@ -51,6 +58,9 @@ public class TestPolicyHookDispatcher {
savedOwnerDispatcher =
GravitinoEnv.getInstance().internalOwnerDispatcher();
FieldUtils.writeField(
GravitinoEnv.getInstance(), "internalOwnerDispatcher",
mockOwnerDispatcher, true);
+ mockAuthorizer = mock(GravitinoAuthorizer.class);
+ savedAuthorizer = GravitinoEnv.getInstance().gravitinoAuthorizer();
+ GravitinoEnv.getInstance().setGravitinoAuthorizer(mockAuthorizer);
hookDispatcher = new PolicyHookDispatcher(mockDispatcher);
}
@@ -58,6 +68,7 @@ public class TestPolicyHookDispatcher {
public void tearDown() throws IllegalAccessException {
FieldUtils.writeField(
GravitinoEnv.getInstance(), "internalOwnerDispatcher",
savedOwnerDispatcher, true);
+ GravitinoEnv.getInstance().setGravitinoAuthorizer(savedAuthorizer);
}
@Test
@@ -79,4 +90,64 @@ public class TestPolicyHookDispatcher {
Assertions.assertEquals("Set owner failed", thrown.getMessage());
verify(mockDispatcher).createPolicy(any(), any(), any(), any(),
anyBoolean(), any());
}
+
+ @Test
+ public void testDeletePolicyInvalidatesNameIdMapping() {
+ when(mockDispatcher.deletePolicy("test_metalake",
"test_policy")).thenReturn(true);
+
+ Assertions.assertTrue(hookDispatcher.deletePolicy("test_metalake",
"test_policy"));
+ verify(mockAuthorizer)
+ .handleEntityNameIdMappingChange(
+ "test_metalake",
+ NameIdentifierUtil.ofPolicy("test_metalake", "test_policy"),
+ Entity.EntityType.POLICY);
+ }
+
+ @Test
+ public void testDeleteMissingPolicyDoesNotInvalidateNameIdMapping() {
+ when(mockDispatcher.deletePolicy("test_metalake",
"test_policy")).thenReturn(false);
+
+ Assertions.assertFalse(hookDispatcher.deletePolicy("test_metalake",
"test_policy"));
+ verify(mockAuthorizer, never()).handleEntityNameIdMappingChange(any(),
any(), any());
+ }
+
+ @Test
+ public void testRenamePolicyInvalidatesOldNameIdMapping() {
+ PolicyChange[] changes = {
+ PolicyChange.updateComment("new comment"),
PolicyChange.rename("new_policy")
+ };
+ PolicyEntity mockPolicy = mock(PolicyEntity.class);
+ when(mockDispatcher.alterPolicy("test_metalake", "test_policy", changes))
+ .thenReturn(mockPolicy);
+
+ Assertions.assertSame(
+ mockPolicy, hookDispatcher.alterPolicy("test_metalake", "test_policy",
changes));
+ verify(mockAuthorizer)
+ .handleEntityNameIdMappingChange(
+ "test_metalake",
+ NameIdentifierUtil.ofPolicy("test_metalake", "test_policy"),
+ Entity.EntityType.POLICY);
+ }
+
+ @Test
+ public void testAlterPolicyWithoutRenameDoesNotInvalidateNameIdMapping() {
+ PolicyChange[] changes = {PolicyChange.updateComment("new comment")};
+ when(mockDispatcher.alterPolicy("test_metalake", "test_policy", changes))
+ .thenReturn(mock(PolicyEntity.class));
+
+ hookDispatcher.alterPolicy("test_metalake", "test_policy", changes);
+ verify(mockAuthorizer, never()).handleEntityNameIdMappingChange(any(),
any(), any());
+ }
+
+ @Test
+ public void testFailedRenamePolicyDoesNotInvalidateNameIdMapping() {
+ PolicyChange[] changes = {PolicyChange.rename("new_policy")};
+ when(mockDispatcher.alterPolicy("test_metalake", "test_policy", changes))
+ .thenThrow(new RuntimeException("Alter failed"));
+
+ Assertions.assertThrows(
+ RuntimeException.class,
+ () -> hookDispatcher.alterPolicy("test_metalake", "test_policy",
changes));
+ verify(mockAuthorizer, never()).handleEntityNameIdMappingChange(any(),
any(), any());
+ }
}
diff --git
a/core/src/test/java/org/apache/gravitino/hook/TestTagHookDispatcher.java
b/core/src/test/java/org/apache/gravitino/hook/TestTagHookDispatcher.java
index 0e92cd403b..6189a4d122 100644
--- a/core/src/test/java/org/apache/gravitino/hook/TestTagHookDispatcher.java
+++ b/core/src/test/java/org/apache/gravitino/hook/TestTagHookDispatcher.java
@@ -21,15 +21,20 @@ package org.apache.gravitino.hook;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import java.util.Collections;
import org.apache.commons.lang3.reflect.FieldUtils;
+import org.apache.gravitino.Entity;
import org.apache.gravitino.GravitinoEnv;
+import org.apache.gravitino.authorization.GravitinoAuthorizer;
import org.apache.gravitino.authorization.OwnerDispatcher;
import org.apache.gravitino.tag.Tag;
+import org.apache.gravitino.tag.TagChange;
import org.apache.gravitino.tag.TagDispatcher;
+import org.apache.gravitino.utils.NameIdentifierUtil;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
@@ -43,6 +48,8 @@ public class TestTagHookDispatcher {
// Save the original ownerDispatcher before each test and restore it in
tearDown so we do not
// leak null state into the GravitinoEnv singleton across tests.
private OwnerDispatcher savedOwnerDispatcher;
+ private GravitinoAuthorizer mockAuthorizer;
+ private GravitinoAuthorizer savedAuthorizer;
@BeforeEach
public void setUp() throws IllegalAccessException {
@@ -51,6 +58,9 @@ public class TestTagHookDispatcher {
savedOwnerDispatcher =
GravitinoEnv.getInstance().internalOwnerDispatcher();
FieldUtils.writeField(
GravitinoEnv.getInstance(), "internalOwnerDispatcher",
mockOwnerDispatcher, true);
+ mockAuthorizer = mock(GravitinoAuthorizer.class);
+ savedAuthorizer = GravitinoEnv.getInstance().gravitinoAuthorizer();
+ GravitinoEnv.getInstance().setGravitinoAuthorizer(mockAuthorizer);
hookDispatcher = new TagHookDispatcher(mockDispatcher);
}
@@ -58,6 +68,7 @@ public class TestTagHookDispatcher {
public void tearDown() throws IllegalAccessException {
FieldUtils.writeField(
GravitinoEnv.getInstance(), "internalOwnerDispatcher",
savedOwnerDispatcher, true);
+ GravitinoEnv.getInstance().setGravitinoAuthorizer(savedAuthorizer);
}
@Test
@@ -78,4 +89,59 @@ public class TestTagHookDispatcher {
Assertions.assertEquals("Set owner failed", thrown.getMessage());
verify(mockDispatcher).createTag(any(), any(), any(), any(), any());
}
+
+ @Test
+ public void testDeleteTagInvalidatesNameIdMapping() {
+ when(mockDispatcher.deleteTag("test_metalake",
"test_tag")).thenReturn(true);
+
+ Assertions.assertTrue(hookDispatcher.deleteTag("test_metalake",
"test_tag"));
+ verify(mockAuthorizer)
+ .handleEntityNameIdMappingChange(
+ "test_metalake",
+ NameIdentifierUtil.ofTag("test_metalake", "test_tag"),
+ Entity.EntityType.TAG);
+ }
+
+ @Test
+ public void testDeleteMissingTagDoesNotInvalidateNameIdMapping() {
+ when(mockDispatcher.deleteTag("test_metalake",
"test_tag")).thenReturn(false);
+
+ Assertions.assertFalse(hookDispatcher.deleteTag("test_metalake",
"test_tag"));
+ verify(mockAuthorizer, never()).handleEntityNameIdMappingChange(any(),
any(), any());
+ }
+
+ @Test
+ public void testRenameTagInvalidatesOldNameIdMapping() {
+ TagChange[] changes = {TagChange.updateComment("new comment"),
TagChange.rename("new_tag")};
+ Tag mockTag = mock(Tag.class);
+ when(mockDispatcher.alterTag("test_metalake", "test_tag",
changes)).thenReturn(mockTag);
+
+ Assertions.assertSame(mockTag, hookDispatcher.alterTag("test_metalake",
"test_tag", changes));
+ verify(mockAuthorizer)
+ .handleEntityNameIdMappingChange(
+ "test_metalake",
+ NameIdentifierUtil.ofTag("test_metalake", "test_tag"),
+ Entity.EntityType.TAG);
+ }
+
+ @Test
+ public void testAlterTagWithoutRenameDoesNotInvalidateNameIdMapping() {
+ TagChange[] changes = {TagChange.updateComment("new comment")};
+ when(mockDispatcher.alterTag("test_metalake", "test_tag",
changes)).thenReturn(mock(Tag.class));
+
+ hookDispatcher.alterTag("test_metalake", "test_tag", changes);
+ verify(mockAuthorizer, never()).handleEntityNameIdMappingChange(any(),
any(), any());
+ }
+
+ @Test
+ public void testFailedRenameTagDoesNotInvalidateNameIdMapping() {
+ TagChange[] changes = {TagChange.rename("new_tag")};
+ when(mockDispatcher.alterTag("test_metalake", "test_tag", changes))
+ .thenThrow(new RuntimeException("Alter failed"));
+
+ Assertions.assertThrows(
+ RuntimeException.class,
+ () -> hookDispatcher.alterTag("test_metalake", "test_tag", changes));
+ verify(mockAuthorizer, never()).handleEntityNameIdMappingChange(any(),
any(), any());
+ }
}
diff --git
a/core/src/test/java/org/apache/gravitino/hook/TestViewHookDispatcher.java
b/core/src/test/java/org/apache/gravitino/hook/TestViewHookDispatcher.java
index 2041d9163e..d10954a151 100644
--- a/core/src/test/java/org/apache/gravitino/hook/TestViewHookDispatcher.java
+++ b/core/src/test/java/org/apache/gravitino/hook/TestViewHookDispatcher.java
@@ -19,8 +19,10 @@
package org.apache.gravitino.hook;
import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
@@ -148,6 +150,27 @@ public class TestViewHookDispatcher {
}
}
+ @Test
+ public void testDropViewInvalidatesNameIdMappingOnlyWhenDropped() {
+ ViewDispatcher dispatcher = Mockito.mock(ViewDispatcher.class);
+ ViewHookDispatcher hook = new ViewHookDispatcher(dispatcher, () -> null);
+ NameIdentifier dropped = NameIdentifier.of(METALAKE, CATALOG, "schema",
"view");
+ NameIdentifier missing = NameIdentifier.of(METALAKE, CATALOG, "schema",
"missing");
+ Mockito.when(dispatcher.dropView(dropped)).thenReturn(true);
+ Mockito.when(dispatcher.dropView(missing)).thenReturn(false);
+
+ try (MockedStatic<AuthorizationUtils> authorizationUtils =
+ Mockito.mockStatic(AuthorizationUtils.class)) {
+ assertFalse(hook.dropView(missing));
+ authorizationUtils.verifyNoInteractions();
+
+ assertTrue(hook.dropView(dropped));
+ authorizationUtils.verify(
+ () ->
+ AuthorizationUtils.notifyEntityNameIdMappingChange(dropped,
Entity.EntityType.VIEW));
+ }
+ }
+
private View createView(ViewDispatcher hook, NameIdentifier ident) {
Representation[] representations =
new Representation[] {
diff --git
a/core/src/test/java/org/apache/gravitino/storage/relational/service/TestJobTemplateMetaService.java
b/core/src/test/java/org/apache/gravitino/storage/relational/service/TestJobTemplateMetaService.java
index 7d3fec6f43..31ed1f9100 100644
---
a/core/src/test/java/org/apache/gravitino/storage/relational/service/TestJobTemplateMetaService.java
+++
b/core/src/test/java/org/apache/gravitino/storage/relational/service/TestJobTemplateMetaService.java
@@ -21,6 +21,8 @@ package org.apache.gravitino.storage.relational.service;
import java.io.IOException;
import java.time.Instant;
import java.util.List;
+import java.util.stream.Collectors;
+import org.apache.gravitino.Entity;
import org.apache.gravitino.EntityAlreadyExistsException;
import org.apache.gravitino.exceptions.NoSuchEntityException;
import org.apache.gravitino.exceptions.NonEmptyEntityException;
@@ -30,7 +32,12 @@ import org.apache.gravitino.meta.BaseMetalake;
import org.apache.gravitino.meta.JobEntity;
import org.apache.gravitino.meta.JobTemplateEntity;
import org.apache.gravitino.storage.RandomIdGenerator;
+import
org.apache.gravitino.storage.relational.EntityChangeLogNameIdentifierCodec;
import org.apache.gravitino.storage.relational.TestJDBCBackend;
+import org.apache.gravitino.storage.relational.mapper.EntityChangeLogMapper;
+import org.apache.gravitino.storage.relational.po.cache.EntityChangeRecord;
+import org.apache.gravitino.storage.relational.po.cache.OperateType;
+import org.apache.gravitino.storage.relational.utils.SessionUtils;
import org.apache.gravitino.utils.NameIdentifierUtil;
import org.apache.gravitino.utils.NamespaceUtil;
import org.junit.jupiter.api.Assertions;
@@ -197,11 +204,14 @@ public class TestJobTemplateMetaService extends
TestJDBCBackend {
newJobEntity("job_template_with_jobs", JobHandle.Status.SUCCEEDED,
METALAKE_NAME);
backend.insert(jobEntity2, false);
+ long lastChangeId = maxEntityChangeId();
Assertions.assertThrows(
NonEmptyEntityException.class,
() ->
jobTemplateMetaService.deleteJobTemplate(
NameIdentifierUtil.ofJobTemplate(METALAKE_NAME,
"job_template_with_jobs")));
+ // A delete rejected for active jobs logs no change, so peers keep a valid
mapping.
+ Assertions.assertTrue(jobTemplateChangesAfter(lastChangeId).isEmpty());
Assertions.assertEquals(
2,
JobMetaService.getInstance()
@@ -284,11 +294,86 @@ public class TestJobTemplateMetaService extends
TestJDBCBackend {
.withAuditInfo(updatedJobTemplateEntity.auditInfo())
.build();
+ long lastChangeId = maxEntityChangeId();
Assertions.assertThrows(
EntityAlreadyExistsException.class,
() ->
jobTemplateMetaService.updateJobTemplate(
updatedJobTemplateEntity.nameIdentifier(), e ->
duplicateNameJobTemplateEntity));
+ // A rename rejected for a duplicate name logs no change.
+ Assertions.assertTrue(jobTemplateChangesAfter(lastChangeId).isEmpty());
+ }
+
+ @TestTemplate
+ public void testRenameAndDeleteJobTemplateLogChanges() throws IOException {
+ BaseMetalake metalake =
+ createBaseMakeLake(RandomIdGenerator.INSTANCE.nextId(), METALAKE_NAME,
AUDIT_INFO);
+ backend.insert(metalake, false);
+
+ JobTemplateMetaService jobTemplateMetaService =
JobTemplateMetaService.getInstance();
+ JobTemplateEntity original =
+ newShellJobTemplateEntity("logged_template", "A logged job template",
METALAKE_NAME);
+ jobTemplateMetaService.insertJobTemplate(original, false);
+
+ // A change that keeps the name leaves the cached name-to-id mapping valid.
+ long lastChangeId = maxEntityChangeId();
+ JobTemplateEntity commented = renamedJobTemplate(original,
original.name(), "new comment");
+ jobTemplateMetaService.updateJobTemplate(original.nameIdentifier(), e ->
commented);
+ Assertions.assertTrue(jobTemplateChangesAfter(lastChangeId).isEmpty());
+
+ lastChangeId = maxEntityChangeId();
+ JobTemplateEntity renamed = renamedJobTemplate(commented,
"renamed_template", "renamed");
+ jobTemplateMetaService.updateJobTemplate(commented.nameIdentifier(), e ->
renamed);
+ assertJobTemplateChange(lastChangeId, "logged_template",
OperateType.ALTER);
+
+ lastChangeId = maxEntityChangeId();
+
Assertions.assertTrue(jobTemplateMetaService.deleteJobTemplate(renamed.nameIdentifier()));
+ assertJobTemplateChange(lastChangeId, "renamed_template",
OperateType.DROP);
+
+ // Deleting a missing template changes nothing, so it must not log a
change.
+ lastChangeId = maxEntityChangeId();
+
Assertions.assertFalse(jobTemplateMetaService.deleteJobTemplate(renamed.nameIdentifier()));
+ Assertions.assertTrue(jobTemplateChangesAfter(lastChangeId).isEmpty());
+ }
+
+ private static JobTemplateEntity renamedJobTemplate(
+ JobTemplateEntity entity, String name, String comment) {
+ return JobTemplateEntity.builder()
+ .withId(entity.id())
+ .withName(name)
+ .withNamespace(entity.namespace())
+ .withTemplateContent(entity.templateContent())
+ .withComment(comment)
+ .withAuditInfo(entity.auditInfo())
+ .build();
+ }
+
+ private static long maxEntityChangeId() {
+ Long maxId =
+ SessionUtils.doWithCommitAndFetchResult(
+ EntityChangeLogMapper.class,
EntityChangeLogMapper::selectMaxChangeId);
+ return maxId == null ? 0L : maxId;
+ }
+
+ private static List<EntityChangeRecord> jobTemplateChangesAfter(long
lastConsumedId) {
+ return SessionUtils.doWithCommitAndFetchResult(
+ EntityChangeLogMapper.class, mapper ->
mapper.selectEntityChanges(lastConsumedId, 100))
+ .stream()
+ .filter(change ->
Entity.EntityType.JOB_TEMPLATE.name().equals(change.getEntityType()))
+ .collect(Collectors.toList());
+ }
+
+ private static void assertJobTemplateChange(
+ long lastConsumedId, String jobTemplateName, OperateType operateType) {
+ List<EntityChangeRecord> changes = jobTemplateChangesAfter(lastConsumedId);
+ Assertions.assertEquals(1, changes.size());
+ EntityChangeRecord change = changes.get(0);
+ Assertions.assertEquals(METALAKE_NAME, change.getMetalakeName());
+ Assertions.assertEquals(
+ EntityChangeLogNameIdentifierCodec.encode(
+ NameIdentifierUtil.ofJobTemplate(METALAKE_NAME, jobTemplateName)),
+ change.getFullName());
+ Assertions.assertEquals(operateType, change.getOperateType());
}
static JobEntity newJobEntity(String templateName, JobHandle.Status status,
String metalake) {
diff --git
a/server-common/src/main/java/org/apache/gravitino/server/authorization/jcasbin/JcasbinChangeListener.java
b/server-common/src/main/java/org/apache/gravitino/server/authorization/jcasbin/JcasbinChangeListener.java
index 7ef37273e0..d245f3a95c 100644
---
a/server-common/src/main/java/org/apache/gravitino/server/authorization/jcasbin/JcasbinChangeListener.java
+++
b/server-common/src/main/java/org/apache/gravitino/server/authorization/jcasbin/JcasbinChangeListener.java
@@ -60,12 +60,11 @@ public class JcasbinChangeListener implements
EntityChangeLogListener, AutoClose
private static final Logger LOG =
LoggerFactory.getLogger(JcasbinChangeListener.class);
/**
- * Entity types that are cacheable in the entity store — and therefore
emitted into {@code
- * entity_change_log} — but that live in a virtual namespace ({@code
- * <metalake>.system.<kind>.<name>}) instead of under a catalog. Their
change-log identifier has
- * more levels than {@link MetadataObjects#of} accepts for the matching
type, and the JCasbin
- * {@code metadataIdCache} never keys on them, so they are dropped before
any mapping is
- * attempted.
+ * Entity types that are emitted into {@code entity_change_log} but live in
a virtual namespace
+ * ({@code <metalake>.system.<kind>.<name>}) instead of under a catalog.
Their change-log
+ * identifier has more levels than {@link MetadataObjects#of} accepts for
the matching type, so
+ * only the leaf name is mapped. Authorization resolves them by that name,
so the JCasbin {@code
+ * metadataIdCache} does key on them and peers must drop the mapping after a
delete or rename.
*/
private static final Set<MetadataObject.Type> VIRTUAL_NAMESPACE_TYPES =
ImmutableSet.of(
@@ -219,11 +218,12 @@ public class JcasbinChangeListener implements
EntityChangeLogListener, AutoClose
* Invalidates the affected {@code metadataIdCache} keys from an
entity-change batch.
*
* <p><b>Contract with the writer side:</b> {@code
entity_change_log.full_name} must be the
- * <i>pre-mutation</i> name (the name that consumers currently have cached).
{@code JDBCBackend}
- * emits the pre-mutation identifier on update and the current name on drop,
so the cacheKey we
- * build here resolves to the entry a peer node would have populated under
that name. If a future
- * change starts emitting the new post-rename name, this invalidation will
silently miss and stale
- * entries will only clear via LRU eviction.
+ * <i>pre-mutation</i> name (the name that consumers currently have cached).
Every writer ({@code
+ * JDBCBackend}, and the services that log types it skips, such as {@code
ModelMetaService} and
+ * {@code JobTemplateMetaService}) emits the pre-mutation identifier on
update and the current
+ * name on drop, so the cacheKey we build here resolves to the entry a peer
node would have
+ * populated under that name. If a future change starts emitting the new
post-rename name, this
+ * invalidation will silently miss and stale entries will only clear via LRU
eviction.
*
* <p><b>Bad rows:</b> a record this listener cannot understand is logged
and skipped instead of
* being thrown up. Such a row does not point at any cache key, so skipping
it leaves nothing
@@ -257,10 +257,6 @@ public class JcasbinChangeListener implements
EntityChangeLogListener, AutoClose
continue;
}
- if (VIRTUAL_NAMESPACE_TYPES.contains(mdType)) {
- continue;
- }
-
String cacheKey;
try {
MetadataObject mdObj = metadataObjectFromChangeLog(metalake, fullName,
mdType);
@@ -303,6 +299,9 @@ public class JcasbinChangeListener implements
EntityChangeLogListener, AutoClose
static MetadataObject metadataObjectFromChangeLog(
String metalake, String fullName, MetadataObject.Type type) {
NameIdentifier ident = EntityChangeLogNameIdentifierCodec.decode(fullName);
+ if (VIRTUAL_NAMESPACE_TYPES.contains(type)) {
+ return MetadataObjects.of(null, ident.name(), type);
+ }
List<String> names = new
ArrayList<>(Arrays.asList(ident.namespace().levels()));
names.add(ident.name());
if (type != MetadataObject.Type.METALAKE
diff --git
a/server-common/src/test/java/org/apache/gravitino/server/authorization/jcasbin/TestJcasbinAuthorizationCacheKeys.java
b/server-common/src/test/java/org/apache/gravitino/server/authorization/jcasbin/TestJcasbinAuthorizationCacheKeys.java
index be359f2977..d6c0d2efc0 100644
---
a/server-common/src/test/java/org/apache/gravitino/server/authorization/jcasbin/TestJcasbinAuthorizationCacheKeys.java
+++
b/server-common/src/test/java/org/apache/gravitino/server/authorization/jcasbin/TestJcasbinAuthorizationCacheKeys.java
@@ -60,6 +60,12 @@ public class TestJcasbinAuthorizationCacheKeys {
key("ml1", "CATALOG", "cat1", "SCHEMA", "sch1", "TABLE", "tbl1",
"COLUMN", "col1"),
columnKey);
+ MetadataObject model =
+ MetadataObjects.of(Arrays.asList("cat1", "sch1", "model1"),
MetadataObject.Type.MODEL);
+ String modelKey = JcasbinAuthorizationCacheKeys.metadataIdCacheKey("ml1",
model);
+ Assertions.assertEquals(
+ key("ml1", "CATALOG", "cat1", "SCHEMA", "sch1", "MODEL", "model1"),
modelKey);
+
MetadataObject view =
MetadataObjects.of(Arrays.asList("cat1", "sch1", "tbl1"),
MetadataObject.Type.VIEW);
String viewKey = JcasbinAuthorizationCacheKeys.metadataIdCacheKey("ml1",
view);
@@ -80,6 +86,12 @@ public class TestJcasbinAuthorizationCacheKeys {
Assertions.assertTrue(tableKey.startsWith(schemaKey));
Assertions.assertTrue(columnKey.startsWith(tableKey));
Assertions.assertFalse(viewKey.startsWith(tableKey));
+ Assertions.assertNotEquals(
+ modelKey,
+ JcasbinAuthorizationCacheKeys.metadataIdCacheKey(
+ "ml1",
+ MetadataObjects.of(
+ Arrays.asList("cat1", "sch1", "model10"),
MetadataObject.Type.MODEL)));
}
@Test
@@ -93,6 +105,9 @@ public class TestJcasbinAuthorizationCacheKeys {
Assertions.assertTrue(
JcasbinAuthorizationCacheKeys.hasNestedMetadataObjects(MetadataObject.Type.TABLE));
+ Assertions.assertFalse(
+
JcasbinAuthorizationCacheKeys.hasNestedMetadataObjects(MetadataObject.Type.MODEL));
+
Assertions.assertFalse(
JcasbinAuthorizationCacheKeys.hasNestedMetadataObjects(MetadataObject.Type.VIEW));
Assertions.assertFalse(
diff --git
a/server-common/src/test/java/org/apache/gravitino/server/authorization/jcasbin/TestJcasbinChangePoller.java
b/server-common/src/test/java/org/apache/gravitino/server/authorization/jcasbin/TestJcasbinChangePoller.java
index 3d4f6c1017..c8e815a926 100644
---
a/server-common/src/test/java/org/apache/gravitino/server/authorization/jcasbin/TestJcasbinChangePoller.java
+++
b/server-common/src/test/java/org/apache/gravitino/server/authorization/jcasbin/TestJcasbinChangePoller.java
@@ -24,6 +24,7 @@ import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import org.apache.gravitino.MetadataObject;
+import org.apache.gravitino.MetadataObjects;
import org.apache.gravitino.NameIdentifier;
import org.apache.gravitino.cache.GravitinoCache;
import
org.apache.gravitino.storage.relational.EntityChangeLogNameIdentifierCodec;
@@ -119,28 +120,38 @@ public class TestJcasbinChangePoller {
}
@Test
- void testVirtualNamespaceTypesAreSkippedWithoutFailingTheBatch() {
+ void testVirtualNamespaceTypesInvalidateLookupKeys() {
RecordingCache<String, Long> metadataIdCache = new RecordingCache<>();
RecordingCache<Long, Optional<OwnerInfo>> ownerRelCache = new
RecordingCache<>();
JcasbinChangeListener poller = new JcasbinChangeListener(metadataIdCache,
ownerRelCache, 1);
- // TAG/POLICY/JOB live in a virtual namespace, so their change-log full
name has more levels
- // than MetadataObjects.of() accepts. They must be skipped, not blow up
the whole batch.
- Assertions.assertDoesNotThrow(
- () ->
- poller.onEntityChange(
- List.of(
- change(1L, MetadataObject.Type.TAG, "ml1.system.tag.pii"),
- change(2L, MetadataObject.Type.POLICY,
"ml1.system.policy.retention"),
- change(3L, MetadataObject.Type.JOB, "ml1.system.job.job1"),
- change(4L, MetadataObject.Type.TABLE,
"ml1.cat1.sch1.tbl1"))));
+ // TAG/POLICY/JOB/JOB_TEMPLATE live in a virtual namespace
(<metalake>.system.<kind>.<name>).
+ // Authorization caches their ids by leaf name, so a peer must drop
exactly that key.
+ poller.onEntityChange(
+ List.of(
+ change(1L, MetadataObject.Type.TAG, "ml1.system.tag.pii"),
+ change(2L, MetadataObject.Type.POLICY,
"ml1.system.policy.retention"),
+ change(3L, MetadataObject.Type.JOB, "ml1.system.job.job1"),
+ change(4L, MetadataObject.Type.JOB_TEMPLATE,
"ml1.system.job_template.tpl1"),
+ change(5L, MetadataObject.Type.TABLE, "ml1.cat1.sch1.tbl1")));
- // The one mappable record in the batch is still applied.
Assertions.assertEquals(
List.of(key("ml1", "CATALOG", "cat1", "SCHEMA", "sch1", "TABLE",
"tbl1", "")),
metadataIdCache.invalidatedPrefixes);
- Assertions.assertEquals(List.of(), metadataIdCache.invalidatedKeys);
+ Assertions.assertEquals(
+ List.of(
+ lookupKey("pii", MetadataObject.Type.TAG),
+ lookupKey("retention", MetadataObject.Type.POLICY),
+ lookupKey("job1", MetadataObject.Type.JOB),
+ lookupKey("tpl1", MetadataObject.Type.JOB_TEMPLATE)),
+ metadataIdCache.invalidatedKeys);
+ }
+
+ // The key JcasbinAuthorizationLookups builds when authorizing a
metalake-level object by name.
+ private static String lookupKey(String name, MetadataObject.Type type) {
+ return JcasbinAuthorizationCacheKeys.metadataIdCacheKey(
+ "ml1", MetadataObjects.of(null, name, type));
}
@Test
diff --git
a/server-common/src/test/java/org/apache/gravitino/server/authorization/jcasbin/TestJcasbinModelCacheInvalidation.java
b/server-common/src/test/java/org/apache/gravitino/server/authorization/jcasbin/TestJcasbinModelCacheInvalidation.java
new file mode 100644
index 0000000000..7a6f67b607
--- /dev/null
+++
b/server-common/src/test/java/org/apache/gravitino/server/authorization/jcasbin/TestJcasbinModelCacheInvalidation.java
@@ -0,0 +1,157 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.gravitino.server.authorization.jcasbin;
+
+import static org.mockito.Answers.CALLS_REAL_METHODS;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.mockStatic;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.when;
+
+import java.util.List;
+import java.util.Optional;
+import org.apache.commons.lang3.reflect.FieldUtils;
+import org.apache.gravitino.GravitinoEnv;
+import org.apache.gravitino.MetadataObject;
+import org.apache.gravitino.MetadataObjects;
+import org.apache.gravitino.NameIdentifier;
+import org.apache.gravitino.authorization.AuthorizationRequestContext;
+import org.apache.gravitino.cache.CaffeineGravitinoCache;
+import org.apache.gravitino.catalog.CapabilityHelpers;
+import org.apache.gravitino.catalog.CatalogManager;
+import org.apache.gravitino.catalog.ModelDispatcher;
+import org.apache.gravitino.catalog.ModelNormalizeDispatcher;
+import org.apache.gravitino.connector.capability.Capability;
+import org.apache.gravitino.hook.ModelHookDispatcher;
+import org.apache.gravitino.model.Model;
+import org.apache.gravitino.model.ModelChange;
+import org.apache.gravitino.server.authorization.MetadataIdConverter;
+import
org.apache.gravitino.storage.relational.EntityChangeLogNameIdentifierCodec;
+import org.apache.gravitino.storage.relational.po.auth.OwnerInfo;
+import org.apache.gravitino.storage.relational.po.cache.EntityChangeRecord;
+import org.apache.gravitino.storage.relational.po.cache.OperateType;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.EnumSource;
+import org.junit.jupiter.params.provider.ValueSource;
+import org.mockito.MockedStatic;
+
+/** Regression tests for invalidating model IDs in the metadata id cache. */
+public class TestJcasbinModelCacheInvalidation {
+
+ private static final String METALAKE = "ml1";
+ private static final MetadataObject MODEL =
+ MetadataObjects.parse("cat1.sch1.model1", MetadataObject.Type.MODEL);
+ private static final MetadataObject OTHER_MODEL =
+ MetadataObjects.parse("cat1.sch1.model10", MetadataObject.Type.MODEL);
+
+ @ParameterizedTest
+ @ValueSource(booleans = {false, true})
+ void testModelMutationInvalidatesModel(boolean rename) throws Exception {
+ GravitinoEnv env = mock(GravitinoEnv.class);
+ CatalogManager catalogManager = mock(CatalogManager.class);
+ when(env.catalogManager()).thenReturn(catalogManager);
+ ModelDispatcher delegate = mock(ModelDispatcher.class);
+ NameIdentifier normalizedIdent = NameIdentifier.of(METALAKE, "cat1",
"sch1", "model1");
+ when(delegate.deleteModel(normalizedIdent)).thenReturn(true);
+ Model renamedModel = mock(Model.class);
+ when(delegate.alterModel(eq(normalizedIdent), any(ModelChange[].class)))
+ .thenReturn(renamedModel);
+
+ try (CaffeineGravitinoCache<String, Long> metadataIdCache =
+ new CaffeineGravitinoCache<>(60_000L, 100L);
+ CaffeineGravitinoCache<Long, Optional<OwnerInfo>> ownerRelCache =
+ new CaffeineGravitinoCache<>(60_000L, 100L);
+ MockedStatic<GravitinoEnv> environment =
mockStatic(GravitinoEnv.class);
+ MockedStatic<CapabilityHelpers> capabilities =
+ mockStatic(CapabilityHelpers.class, CALLS_REAL_METHODS);
+ MockedStatic<MetadataIdConverter> converter =
mockStatic(MetadataIdConverter.class)) {
+ environment.when(GravitinoEnv::getInstance).thenReturn(env);
+ capabilities
+ .when(() -> CapabilityHelpers.getCapability(any(),
eq(catalogManager)))
+ .thenReturn(Capability.DEFAULT);
+ converter
+ .when(() -> MetadataIdConverter.getID(MODEL, METALAKE))
+ .thenReturn(Optional.of(10L), Optional.of(30L));
+ // Use the real JCasbin hook and shared caches without starting its
background poller.
+ JcasbinAuthorizer authorizer = mock(JcasbinAuthorizer.class,
CALLS_REAL_METHODS);
+ FieldUtils.writeField(authorizer, "metadataIdCache", metadataIdCache,
true);
+ when(env.gravitinoAuthorizer()).thenReturn(authorizer);
+ JcasbinAuthorizationLookups lookups =
+ new JcasbinAuthorizationLookups(metadataIdCache, ownerRelCache);
+ Assertions.assertEquals(
+ Optional.of(10L),
+ lookups.resolveMetadataId(MODEL, METALAKE, new
AuthorizationRequestContext()));
+ String otherKey =
JcasbinAuthorizationCacheKeys.metadataIdCacheKey(METALAKE, OTHER_MODEL);
+ metadataIdCache.put(otherKey, 50L);
+
+ ModelDispatcher dispatcher =
+ new ModelNormalizeDispatcher(new ModelHookDispatcher(delegate),
catalogManager);
+ if (rename) {
+ Assertions.assertSame(
+ renamedModel, dispatcher.alterModel(normalizedIdent,
ModelChange.rename("renamed")));
+ } else {
+ Assertions.assertTrue(dispatcher.deleteModel(normalizedIdent));
+ }
+ Assertions.assertFalse(
+ metadataIdCache
+
.getIfPresent(JcasbinAuthorizationCacheKeys.metadataIdCacheKey(METALAKE, MODEL))
+ .isPresent());
+ Assertions.assertEquals(Optional.of(50L),
metadataIdCache.getIfPresent(otherKey));
+
+ // Simulate recreation under the old name with a fresh model ID.
+ Assertions.assertEquals(
+ Optional.of(30L),
+ lookups.resolveMetadataId(MODEL, METALAKE, new
AuthorizationRequestContext()));
+ converter.verify(() -> MetadataIdConverter.getID(MODEL, METALAKE),
times(2));
+ }
+ }
+
+ @ParameterizedTest
+ @EnumSource(
+ value = OperateType.class,
+ names = {"ALTER", "DROP"})
+ void testModelChangeLogInvalidatesModel(OperateType operation) {
+ try (CaffeineGravitinoCache<String, Long> metadataIdCache =
+ new CaffeineGravitinoCache<>(60_000L, 100L);
+ CaffeineGravitinoCache<Long, Optional<OwnerInfo>> ownerRelCache =
+ new CaffeineGravitinoCache<>(60_000L, 100L);
+ JcasbinChangeListener listener =
+ new JcasbinChangeListener(metadataIdCache, ownerRelCache, 1)) {
+ String modelKey =
JcasbinAuthorizationCacheKeys.metadataIdCacheKey(METALAKE, MODEL);
+ String otherKey =
JcasbinAuthorizationCacheKeys.metadataIdCacheKey(METALAKE, OTHER_MODEL);
+ metadataIdCache.put(modelKey, 10L);
+ metadataIdCache.put(otherKey, 50L);
+ EntityChangeRecord change =
+ new EntityChangeRecord(
+ 1L,
+ METALAKE,
+ "MODEL",
+ EntityChangeLogNameIdentifierCodec.encode(
+ NameIdentifier.of(METALAKE, "cat1", "sch1", "model1")),
+ operation,
+ 1L);
+ listener.onEntityChange(List.of(change));
+
Assertions.assertFalse(metadataIdCache.getIfPresent(modelKey).isPresent());
+ Assertions.assertEquals(Optional.of(50L),
metadataIdCache.getIfPresent(otherKey));
+ }
+ }
+}