This is an automated email from the ASF dual-hosted git repository.
jerryshao 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 640ac60ae8 [#11032] feat(authz): add cache support infrastructure
(#11033)
640ac60ae8 is described below
commit 640ac60ae898af32fe50fab03adab6e2bde8e0d5
Author: Qi Yu <[email protected]>
AuthorDate: Thu May 14 15:41:43 2026 +0800
[#11032] feat(authz): add cache support infrastructure (#11033)
### What changes were proposed in this pull request?
This PR extracts the infrastructure part from #10996 to support the
follow-up JcasbinAuthorizer cache refactor.
It includes:
- Add a generic `GravitinoCache` abstraction with Caffeine and no-op
implementations.
- Add per-request lookup caches in `AuthorizationRequestContext` for
user, group, metadata id, and owner lookup deduplication.
- Thread `AuthorizationRequestContext` through authorization
filter/executor paths.
- Update `isMetalakeUser` to accept `AuthorizationRequestContext`.
- Switch entity/owner change polling mapper APIs to id-based cursors and
expose latest id lookup helpers.
This PR intentionally does not rewrite `JcasbinAuthorizer` cache
behavior. The JcasbinAuthorizer version-validated cache refactor will
stay in the follow-up PR.
### Why are the changes needed?
This reduces the size of #10996 and provides the reusable support layer
needed by the JcasbinAuthorizer cache refactor.
Fix: #11032
### Does this PR introduce _any_ user-facing change?
No.
### How was this patch tested?
- `./gradlew :core:spotlessApply :server-common:spotlessApply
:server:spotlessApply`
- `./gradlew :core:test --tests
org.apache.gravitino.authorization.TestAuthorizationRequestContext
--tests org.apache.gravitino.cache.TestGravitinoCache --tests
org.apache.gravitino.storage.relational.mapper.provider.base.TestAuthMappers
--tests
org.apache.gravitino.storage.relational.mapper.provider.base.TestEntityChangeLogMapper
--tests
org.apache.gravitino.storage.relational.service.TestEntityChangeLogService
--tests
org.apache.gravitino.storage.relational.service.TestTableMetaService
:server-common:test --tests
org.apache.gravitino.server.authorization.TestPassThroughAuthorizer
:server:test --tests
org.apache.gravitino.server.web.filter.TestGravitinoInterceptionService`
---
.../authorization/AuthorizationRequestContext.java | 160 ++++++-----
.../authorization/AuthorizationUtils.java | 7 +-
.../authorization/GravitinoAuthorizer.java | 6 +-
.../gravitino/cache/CaffeineGravitinoCache.java | 101 +++++++
.../org/apache/gravitino/cache/GravitinoCache.java | 74 +++++
.../gravitino/cache/NoOpsGravitinoCache.java | 66 +++++
.../relational/mapper/EntityChangeLogMapper.java | 5 +-
.../mapper/EntityChangeLogSQLProviderFactory.java | 8 +-
.../storage/relational/mapper/OwnerMetaMapper.java | 5 +-
.../mapper/OwnerMetaSQLProviderFactory.java | 8 +-
.../base/EntityChangeLogBaseSQLProvider.java | 27 +-
.../provider/base/OwnerMetaBaseSQLProvider.java | 21 +-
.../relational/po/auth/ChangedOwnerInfo.java | 1 +
.../TestAuthorizationRequestContext.java | 226 +++++++++++++++
.../apache/gravitino/cache/TestGravitinoCache.java | 319 +++++++++++++++++++++
.../mapper/provider/base/TestAuthMappers.java | 9 +-
.../provider/base/TestEntityChangeLogMapper.java | 3 +-
.../service/TestEntityChangeLogService.java | 81 +++---
.../relational/service/TestTableMetaService.java | 21 +-
.../authorization/PassThroughAuthorizer.java | 2 +-
.../AuthorizationExpressionConstants.java | 3 +-
.../AuthorizationExpressionConverter.java | 4 +-
.../authorization/jcasbin/JcasbinAuthorizer.java | 2 +-
.../authorization/MockGravitinoAuthorizer.java | 2 +-
.../authorization/TestPassThroughAuthorizer.java | 3 +-
.../web/filter/GravitinoInterceptionService.java | 8 +-
.../AssociatePolicyAuthorizationExecutor.java | 3 +-
.../AssociateTagAuthorizationExecutor.java | 3 +-
.../authorization/AuthorizationExecutor.java | 4 +-
.../authorization/CommonAuthorizerExecutor.java | 5 +-
.../authorization/RunJobAuthorizationExecutor.java | 6 +-
.../filter/TestGravitinoInterceptionService.java | 4 +-
32 files changed, 1039 insertions(+), 158 deletions(-)
diff --git
a/core/src/main/java/org/apache/gravitino/authorization/AuthorizationRequestContext.java
b/core/src/main/java/org/apache/gravitino/authorization/AuthorizationRequestContext.java
index 392eb13c27..554fed8b52 100644
---
a/core/src/main/java/org/apache/gravitino/authorization/AuthorizationRequestContext.java
+++
b/core/src/main/java/org/apache/gravitino/authorization/AuthorizationRequestContext.java
@@ -6,7 +6,9 @@
* 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
@@ -20,11 +22,35 @@ package org.apache.gravitino.authorization;
import java.security.Principal;
import java.util.Map;
import java.util.Objects;
+import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Function;
+import lombok.AllArgsConstructor;
+import lombok.EqualsAndHashCode;
+import lombok.Getter;
import org.apache.gravitino.MetadataObject;
-
+import org.apache.gravitino.storage.relational.po.auth.GroupUpdatedAt;
+import org.apache.gravitino.storage.relational.po.auth.OwnerInfo;
+import org.apache.gravitino.storage.relational.po.auth.UserUpdatedAt;
+
+/**
+ * Per-HTTP-request scratchpad shared by {@link GravitinoAuthorizer} calls. A
fresh instance is
+ * created for each request by the authorization filter and threaded through
{@code authorize},
+ * {@code isOwner}, {@code isMetalakeUser} etc., so that:
+ *
+ * <ul>
+ * <li>repeated authorization decisions for the same {@code (principal,
metalake, object,
+ * privilege)} short-circuit via {@link #allowAuthorizerCache} / {@link
#denyAuthorizerCache};
+ * <li>user identity, name→id and metadataId→owner lookups are de-duplicated
within the request
+ * (see the {@code computeXxxIfAbsent} helpers) so each underlying DB
query runs at most once;
+ * <li>per-request role loading happens at most once via {@link
#loadRole(Runnable)}.
+ * </ul>
+ *
+ * <p>Instances are not intended to outlive a request and are not reusable
across threads beyond the
+ * request handling thread; the internal maps are {@link ConcurrentHashMap}
purely to tolerate any
+ * incidental fan-out (e.g. async listeners) within the same request scope.
+ */
public class AuthorizationRequestContext {
/** Used to cache the results of metadata authorization. */
@@ -36,6 +62,18 @@ public class AuthorizationRequestContext {
/** Used to determine whether the role has already been loaded. */
private final AtomicBoolean hasLoadRole = new AtomicBoolean();
+ /** Per-request user identity cache. Key: {@code metalake::userName}. */
+ private final Map<String, Optional<UserUpdatedAt>> userInfoCache = new
ConcurrentHashMap<>();
+
+ /** Per-request group identity cache. Key: {@code metalake::groupName}. */
+ private final Map<String, Optional<GroupUpdatedAt>> groupInfoCache = new
ConcurrentHashMap<>();
+
+ /** Per-request name→id cache. Deduplicates resolveMetadataId within a
single request. */
+ private final Map<String, Long> metadataIdCache = new ConcurrentHashMap<>();
+
+ /** Per-request metadataId→owner cache. Deduplicates isOwner within a single
request. */
+ private final Map<Long, Optional<OwnerInfo>> ownerCache = new
ConcurrentHashMap<>();
+
private volatile String originalAuthorizationExpression;
/**
@@ -78,6 +116,11 @@ public class AuthorizationRequestContext {
return denyAuthorizerCache.computeIfAbsent(context, authorizer);
}
+ /**
+ * Runs {@code runnable} at most once per request. The double-checked guard
plus {@code
+ * synchronized(this)} prevents two concurrent authorize calls in the same
request from both
+ * triggering the (potentially expensive) role load.
+ */
public void loadRole(Runnable runnable) {
if (hasLoadRole.get()) {
return;
@@ -95,6 +138,45 @@ public class AuthorizationRequestContext {
}
}
+ /**
+ * Per-request {@link UserUpdatedAt} dedup. Loader may return {@link
Optional#empty()} to cache
+ * the "user not found" outcome and avoid repeated DB lookups within a
single request.
+ */
+ public Optional<UserUpdatedAt> computeUserInfoIfAbsent(
+ String key, Function<String, Optional<UserUpdatedAt>> loader) {
+ return userInfoCache.computeIfAbsent(
+ key, k -> Objects.requireNonNull(loader.apply(k), "User info loader
must not return null"));
+ }
+
+ /**
+ * Per-request {@link GroupUpdatedAt} dedup. Loader may return {@link
Optional#empty()} to cache
+ * the "group not found" outcome and avoid repeated DB lookups within a
single request.
+ */
+ public Optional<GroupUpdatedAt> computeGroupInfoIfAbsent(
+ String key, Function<String, Optional<GroupUpdatedAt>> loader) {
+ return groupInfoCache.computeIfAbsent(
+ key,
+ k -> Objects.requireNonNull(loader.apply(k), "Group info loader must
not return null"));
+ }
+
+ /** Per-request name→id dedup. Loader must return a non-null id or throw. */
+ public Long computeMetadataIdIfAbsent(String key, Function<String, Long>
loader) {
+ return metadataIdCache.computeIfAbsent(
+ key,
+ k -> Objects.requireNonNull(loader.apply(k), "Metadata id loader must
not return null"));
+ }
+
+ /**
+ * Per-request metadataId→owner dedup. Loader returns {@link
Optional#empty()} when the object has
+ * no owner; the absent result is cached as well.
+ */
+ public Optional<OwnerInfo> computeOwnerIfAbsent(
+ Long metadataId, Function<Long, Optional<OwnerInfo>> loader) {
+ return ownerCache.computeIfAbsent(
+ metadataId,
+ id -> Objects.requireNonNull(loader.apply(id), "Owner loader must not
return null"));
+ }
+
public String getOriginalAuthorizationExpression() {
return originalAuthorizationExpression;
}
@@ -103,70 +185,18 @@ public class AuthorizationRequestContext {
this.originalAuthorizationExpression = originalAuthorizationExpression;
}
+ /**
+ * Composite key for {@link #allowAuthorizerCache} / {@link
#denyAuthorizerCache}. Immutable —
+ * mutating any field after construction would silently corrupt the {@link
+ * java.util.Objects#hashCode} used by the backing {@link ConcurrentHashMap}.
+ */
+ @Getter
+ @AllArgsConstructor
+ @EqualsAndHashCode
public static class AuthorizationKey {
- private Principal principal;
- private String metalake;
- private MetadataObject metadataObject;
- private Privilege.Name privilege;
-
- public AuthorizationKey(
- Principal principal,
- String metalake,
- MetadataObject metadataObject,
- Privilege.Name privilege) {
- this.principal = principal;
- this.metalake = metalake;
- this.metadataObject = metadataObject;
- this.privilege = privilege;
- }
-
- @Override
- public boolean equals(Object o) {
- if (!(o instanceof AuthorizationKey)) {
- return false;
- }
- AuthorizationKey that = (AuthorizationKey) o;
- return Objects.equals(principal, that.principal)
- && Objects.equals(metalake, that.metalake)
- && Objects.equals(metadataObject, that.metadataObject)
- && Objects.equals(privilege, that.privilege);
- }
-
- @Override
- public int hashCode() {
- return Objects.hash(principal, metalake, metadataObject, privilege);
- }
-
- public Principal getPrincipal() {
- return principal;
- }
-
- public void setPrincipal(Principal principal) {
- this.principal = principal;
- }
-
- public String getMetalake() {
- return metalake;
- }
-
- public void setMetalake(String metalake) {
- this.metalake = metalake;
- }
-
- public MetadataObject getMetadataObject() {
- return metadataObject;
- }
-
- public void setMetadataObject(MetadataObject metadataObject) {
- this.metadataObject = metadataObject;
- }
-
- public Privilege.Name getPrivilege() {
- return privilege;
- }
-
- public void setPrivilege(Privilege.Name privilege) {
- this.privilege = privilege;
- }
+ private final Principal principal;
+ private final String metalake;
+ private final MetadataObject metadataObject;
+ private final Privilege.Name privilege;
}
}
diff --git
a/core/src/main/java/org/apache/gravitino/authorization/AuthorizationUtils.java
b/core/src/main/java/org/apache/gravitino/authorization/AuthorizationUtils.java
index 4938ff6034..16642a5855 100644
---
a/core/src/main/java/org/apache/gravitino/authorization/AuthorizationUtils.java
+++
b/core/src/main/java/org/apache/gravitino/authorization/AuthorizationUtils.java
@@ -117,8 +117,13 @@ public class AuthorizationUtils {
private AuthorizationUtils() {}
public static void checkCurrentUser(String metalake, String user) {
+ checkCurrentUser(metalake, user, new AuthorizationRequestContext());
+ }
+
+ public static void checkCurrentUser(
+ String metalake, String user, AuthorizationRequestContext
requestContext) {
GravitinoAuthorizer authorizer =
GravitinoEnv.getInstance().gravitinoAuthorizer();
- if (authorizer != null && !authorizer.isMetalakeUser(metalake)) {
+ if (authorizer != null && !authorizer.isMetalakeUser(metalake,
requestContext)) {
throw new ForbiddenException(
"Current user %s doesn't exist in the metalake %s, you should add
the user to the metalake first",
user, metalake);
diff --git
a/core/src/main/java/org/apache/gravitino/authorization/GravitinoAuthorizer.java
b/core/src/main/java/org/apache/gravitino/authorization/GravitinoAuthorizer.java
index 7965f173ad..6ab74382ad 100644
---
a/core/src/main/java/org/apache/gravitino/authorization/GravitinoAuthorizer.java
+++
b/core/src/main/java/org/apache/gravitino/authorization/GravitinoAuthorizer.java
@@ -92,12 +92,14 @@ public interface GravitinoAuthorizer extends Closeable {
boolean isSelf(Entity.EntityType type, NameIdentifier nameIdentifier);
/**
- * Determine whether the user is the metalake user
+ * Determine whether the user is the metalake user.
*
* @param metalake metalake
+ * @param requestContext authorization request context; enables per-request
dedup with other
+ * authorizer calls (e.g. {@code authorize}/{@code isOwner}) that look
up the same user.
* @return authorization result
*/
- boolean isMetalakeUser(String metalake);
+ boolean isMetalakeUser(String metalake, AuthorizationRequestContext
requestContext);
/**
* Determine whether the user can set owner
diff --git
a/core/src/main/java/org/apache/gravitino/cache/CaffeineGravitinoCache.java
b/core/src/main/java/org/apache/gravitino/cache/CaffeineGravitinoCache.java
new file mode 100644
index 0000000000..f1ed54df75
--- /dev/null
+++ b/core/src/main/java/org/apache/gravitino/cache/CaffeineGravitinoCache.java
@@ -0,0 +1,101 @@
+/*
+ * 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.cache;
+
+import com.github.benmanes.caffeine.cache.Cache;
+import com.github.benmanes.caffeine.cache.Caffeine;
+import com.github.benmanes.caffeine.cache.Ticker;
+import com.google.common.annotations.VisibleForTesting;
+import java.util.Optional;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * A Caffeine-backed implementation of {@link GravitinoCache}. Supports
configurable TTL and maximum
+ * size.
+ *
+ * @param <K> the key type
+ * @param <V> the value type
+ */
+public class CaffeineGravitinoCache<K, V> implements GravitinoCache<K, V> {
+
+ private final Cache<K, V> cache;
+
+ /**
+ * Creates a new CaffeineGravitinoCache with the given TTL and maximum size.
+ *
+ * @param ttlMs the time-to-live in milliseconds for cache entries
(safety-net TTL)
+ * @param maxSize the maximum number of entries in the cache
+ */
+ public CaffeineGravitinoCache(long ttlMs, long maxSize) {
+ this(ttlMs, maxSize, Ticker.systemTicker());
+ }
+
+ CaffeineGravitinoCache(long ttlMs, long maxSize, Ticker ticker) {
+ this.cache =
+ Caffeine.newBuilder()
+ .expireAfterWrite(ttlMs, TimeUnit.MILLISECONDS)
+ .maximumSize(maxSize)
+ .ticker(ticker)
+ .build();
+ }
+
+ @Override
+ public Optional<V> getIfPresent(K key) {
+ V value = cache.getIfPresent(key);
+ return Optional.ofNullable(value);
+ }
+
+ @Override
+ public void put(K key, V value) {
+ cache.put(key, value);
+ }
+
+ @Override
+ public void invalidate(K key) {
+ cache.invalidate(key);
+ }
+
+ @Override
+ public void invalidateAll() {
+ cache.invalidateAll();
+ }
+
+ @Override
+ public void invalidateByPrefix(String prefix) {
+ // Prefix invalidation scans all keys. It is intended for infrequent
structural invalidations
+ // such as dropping or renaming an entity hierarchy, not for per-request
hot paths.
+ cache.asMap().keySet().removeIf(k -> k instanceof String && ((String)
k).startsWith(prefix));
+ }
+
+ @Override
+ public long size() {
+ return cache.estimatedSize();
+ }
+
+ @VisibleForTesting
+ void cleanUp() {
+ cache.cleanUp();
+ }
+
+ @Override
+ public void close() {
+ cache.invalidateAll();
+ cache.cleanUp();
+ }
+}
diff --git a/core/src/main/java/org/apache/gravitino/cache/GravitinoCache.java
b/core/src/main/java/org/apache/gravitino/cache/GravitinoCache.java
new file mode 100644
index 0000000000..3771608e3c
--- /dev/null
+++ b/core/src/main/java/org/apache/gravitino/cache/GravitinoCache.java
@@ -0,0 +1,74 @@
+/*
+ * 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.cache;
+
+import java.io.Closeable;
+import java.util.Optional;
+
+/**
+ * A general-purpose cache interface used by the authorization subsystem.
Implementations include a
+ * Caffeine-backed cache and a no-op cache for testing.
+ *
+ * @param <K> the key type
+ * @param <V> the value type
+ */
+public interface GravitinoCache<K, V> extends Closeable {
+
+ /**
+ * Returns the value associated with the key, or empty if not present.
+ *
+ * @param key the cache key
+ * @return an Optional containing the cached value, or empty if absent
+ */
+ Optional<V> getIfPresent(K key);
+
+ /**
+ * Associates the value with the key in the cache.
+ *
+ * @param key the cache key
+ * @param value the value to cache
+ */
+ void put(K key, V value);
+
+ /**
+ * Removes the entry for the given key.
+ *
+ * @param key the cache key to invalidate
+ */
+ void invalidate(K key);
+
+ /** Removes all entries from the cache. */
+ void invalidateAll();
+
+ /**
+ * Evicts all entries whose key is a String and starts with the given
prefix. Only meaningful when
+ * K = String. Used by metadataIdCache for hierarchical cascade
invalidation: dropping a catalog
+ * evicts the catalog entry plus all schema/table/fileset/... entries
beneath it.
+ *
+ * @param prefix the prefix to match against key strings
+ */
+ void invalidateByPrefix(String prefix);
+
+ /**
+ * Returns the approximate number of entries in the cache.
+ *
+ * @return the cache size
+ */
+ long size();
+}
diff --git
a/core/src/main/java/org/apache/gravitino/cache/NoOpsGravitinoCache.java
b/core/src/main/java/org/apache/gravitino/cache/NoOpsGravitinoCache.java
new file mode 100644
index 0000000000..8c41eabeef
--- /dev/null
+++ b/core/src/main/java/org/apache/gravitino/cache/NoOpsGravitinoCache.java
@@ -0,0 +1,66 @@
+/*
+ * 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.cache;
+
+import java.util.Optional;
+
+/**
+ * A no-op implementation of {@link GravitinoCache} that never caches
anything. Useful for testing
+ * and for environments where caching is disabled.
+ *
+ * @param <K> the key type
+ * @param <V> the value type
+ */
+public class NoOpsGravitinoCache<K, V> implements GravitinoCache<K, V> {
+
+ @Override
+ public Optional<V> getIfPresent(K key) {
+ return Optional.empty();
+ }
+
+ @Override
+ public void put(K key, V value) {
+ // no-op
+ }
+
+ @Override
+ public void invalidate(K key) {
+ // no-op
+ }
+
+ @Override
+ public void invalidateAll() {
+ // no-op
+ }
+
+ @Override
+ public void invalidateByPrefix(String prefix) {
+ // no-op
+ }
+
+ @Override
+ public long size() {
+ return 0;
+ }
+
+ @Override
+ public void close() {
+ // no-op
+ }
+}
diff --git
a/core/src/main/java/org/apache/gravitino/storage/relational/mapper/EntityChangeLogMapper.java
b/core/src/main/java/org/apache/gravitino/storage/relational/mapper/EntityChangeLogMapper.java
index ada263b359..d82754947c 100644
---
a/core/src/main/java/org/apache/gravitino/storage/relational/mapper/EntityChangeLogMapper.java
+++
b/core/src/main/java/org/apache/gravitino/storage/relational/mapper/EntityChangeLogMapper.java
@@ -38,7 +38,10 @@ public interface EntityChangeLogMapper {
@SelectProvider(type = EntityChangeLogSQLProviderFactory.class, method =
"selectEntityChanges")
List<EntityChangeRecord> selectEntityChanges(
- @Param("createdAtFrom") long createdAtFrom, @Param("maxRows") int
maxRows);
+ @Param("lastConsumedId") long lastConsumedId, @Param("maxRows") int
maxRows);
+
+ @SelectProvider(type = EntityChangeLogSQLProviderFactory.class, method =
"selectMaxChangeId")
+ Long selectMaxChangeId();
@InsertProvider(type = EntityChangeLogSQLProviderFactory.class, method =
"insertEntityChange")
void insertEntityChange(
diff --git
a/core/src/main/java/org/apache/gravitino/storage/relational/mapper/EntityChangeLogSQLProviderFactory.java
b/core/src/main/java/org/apache/gravitino/storage/relational/mapper/EntityChangeLogSQLProviderFactory.java
index 4ee8e43156..c48f2c0a1a 100644
---
a/core/src/main/java/org/apache/gravitino/storage/relational/mapper/EntityChangeLogSQLProviderFactory.java
+++
b/core/src/main/java/org/apache/gravitino/storage/relational/mapper/EntityChangeLogSQLProviderFactory.java
@@ -51,8 +51,12 @@ public class EntityChangeLogSQLProviderFactory {
static class EntityChangeLogH2Provider extends
EntityChangeLogBaseSQLProvider {}
public static String selectEntityChanges(
- @Param("createdAtFrom") long createdAtFrom, @Param("maxRows") int
maxRows) {
- return getProvider().selectEntityChanges(createdAtFrom, maxRows);
+ @Param("lastConsumedId") long lastConsumedId, @Param("maxRows") int
maxRows) {
+ return getProvider().selectEntityChanges(lastConsumedId, maxRows);
+ }
+
+ public static String selectMaxChangeId() {
+ return getProvider().selectMaxChangeId();
}
public static String insertEntityChange(
diff --git
a/core/src/main/java/org/apache/gravitino/storage/relational/mapper/OwnerMetaMapper.java
b/core/src/main/java/org/apache/gravitino/storage/relational/mapper/OwnerMetaMapper.java
index b2872ac3b2..7a74310184 100644
---
a/core/src/main/java/org/apache/gravitino/storage/relational/mapper/OwnerMetaMapper.java
+++
b/core/src/main/java/org/apache/gravitino/storage/relational/mapper/OwnerMetaMapper.java
@@ -116,5 +116,8 @@ public interface OwnerMetaMapper {
@Param("metadataObjectType") String metadataObjectType);
@SelectProvider(type = OwnerMetaSQLProviderFactory.class, method =
"selectChangedOwners")
- List<ChangedOwnerInfo> selectChangedOwners(@Param("updatedAtFrom") long
updatedAtFrom);
+ List<ChangedOwnerInfo> selectChangedOwners(@Param("lastConsumedId") long
lastConsumedId);
+
+ @SelectProvider(type = OwnerMetaSQLProviderFactory.class, method =
"selectMaxChangeId")
+ Long selectMaxChangeId();
}
diff --git
a/core/src/main/java/org/apache/gravitino/storage/relational/mapper/OwnerMetaSQLProviderFactory.java
b/core/src/main/java/org/apache/gravitino/storage/relational/mapper/OwnerMetaSQLProviderFactory.java
index 3d805459c8..bcf2795789 100644
---
a/core/src/main/java/org/apache/gravitino/storage/relational/mapper/OwnerMetaSQLProviderFactory.java
+++
b/core/src/main/java/org/apache/gravitino/storage/relational/mapper/OwnerMetaSQLProviderFactory.java
@@ -121,7 +121,11 @@ public class OwnerMetaSQLProviderFactory {
return
getProvider().selectOwnerByMetadataObjectIdAndType(metadataObjectId,
metadataObjectType);
}
- public static String selectChangedOwners(@Param("updatedAtFrom") long
updatedAtFrom) {
- return getProvider().selectChangedOwners(updatedAtFrom);
+ public static String selectChangedOwners(@Param("lastConsumedId") long
lastConsumedId) {
+ return getProvider().selectChangedOwners(lastConsumedId);
+ }
+
+ public static String selectMaxChangeId() {
+ return getProvider().selectMaxChangeId();
}
}
diff --git
a/core/src/main/java/org/apache/gravitino/storage/relational/mapper/provider/base/EntityChangeLogBaseSQLProvider.java
b/core/src/main/java/org/apache/gravitino/storage/relational/mapper/provider/base/EntityChangeLogBaseSQLProvider.java
index 6193263c5e..78695830cc 100644
---
a/core/src/main/java/org/apache/gravitino/storage/relational/mapper/provider/base/EntityChangeLogBaseSQLProvider.java
+++
b/core/src/main/java/org/apache/gravitino/storage/relational/mapper/provider/base/EntityChangeLogBaseSQLProvider.java
@@ -26,23 +26,27 @@ import org.apache.ibatis.annotations.Param;
public class EntityChangeLogBaseSQLProvider {
/**
- * Cursor-advance contract for the entity change poller:
+ * Cursor-advance contract for the entity change poller: {@code id} is
monotonic and unique, so
+ * callers only need to remember the last consumed id.
*
- * <p>The {@code created_at >= #{createdAtFrom}} predicate is
<b>inclusive</b>. Combined with
- * {@code ORDER BY created_at, id}, callers must remember the {@code
(lastCreatedAt, lastId)} of
- * the last consumed row and on the next poll: pass {@code createdAtFrom =
lastCreatedAt} and
- * client-side skip rows whose {@code id <= lastId} until they encounter a
row with {@code
- * created_at > lastCreatedAt}. Naively advancing by {@code lastCreatedAt +
1} would miss rows
- * sharing the same millisecond boundary; advancing by {@code lastCreatedAt}
re-reads the boundary
- * row and relies on the client-side id filter.
+ * <p>This table is a short-lived broadcast log for local cache
invalidation, not a queue. In a
+ * multi-node deployment every server instance has its own local cache and
should independently
+ * consume the same change rows. A new instance may initialize its cursor
from {@link
+ * #selectMaxChangeId()} because its cache starts empty and it does not need
historical
+ * invalidations. Re-consuming a row on an existing instance is acceptable:
entity DROP/ALTER
+ * handling only invalidates cache keys, and invalidation is idempotent.
*/
public String selectEntityChanges(
- @Param("createdAtFrom") long createdAtFrom, @Param("maxRows") int
maxRows) {
+ @Param("lastConsumedId") long lastConsumedId, @Param("maxRows") int
maxRows) {
return "SELECT id, metalake_name as metalakeName, entity_type as
entityType,"
+ " entity_full_name as fullName, operate_type as operateType,
created_at as createdAt"
+ " FROM "
+ ENTITY_CHANGE_LOG_TABLE_NAME
- + " WHERE created_at >= #{createdAtFrom} ORDER BY created_at, id LIMIT
#{maxRows}";
+ + " WHERE id > #{lastConsumedId} ORDER BY id LIMIT #{maxRows}";
+ }
+
+ public String selectMaxChangeId() {
+ return "SELECT COALESCE(MAX(id), 0) FROM " + ENTITY_CHANGE_LOG_TABLE_NAME;
}
/**
@@ -68,6 +72,9 @@ public class EntityChangeLogBaseSQLProvider {
}
public String pruneOldEntityChanges(@Param("before") long before) {
+ // Keep the retention window conservative. A running server can be delayed
by long GC pauses,
+ // network isolation, scheduler stalls, or clock skew between nodes;
pruning too aggressively
+ // can let that server miss an invalidation while its local cache is still
warm.
return "DELETE FROM "
+ ENTITY_CHANGE_LOG_TABLE_NAME
+ " WHERE created_at < #{before} LIMIT 1000";
diff --git
a/core/src/main/java/org/apache/gravitino/storage/relational/mapper/provider/base/OwnerMetaBaseSQLProvider.java
b/core/src/main/java/org/apache/gravitino/storage/relational/mapper/provider/base/OwnerMetaBaseSQLProvider.java
index 5ec2fbdd89..2ac79dbab7 100644
---
a/core/src/main/java/org/apache/gravitino/storage/relational/mapper/provider/base/OwnerMetaBaseSQLProvider.java
+++
b/core/src/main/java/org/apache/gravitino/storage/relational/mapper/provider/base/OwnerMetaBaseSQLProvider.java
@@ -274,6 +274,9 @@ public class OwnerMetaBaseSQLProvider {
public String deleteOwnerMetasByLegacyTimeline(
@Param("legacyTimeline") Long legacyTimeline, @Param("limit") int limit)
{
+ // Keep this cutoff comfortably behind the present time. These deleted
owner rows are also used
+ // as short-lived cache-invalidation signals; a running server that is
delayed by long GC,
+ // network isolation, scheduler stalls, or clock skew must still have time
to consume them.
return "DELETE FROM "
+ OWNER_TABLE_NAME
+ " WHERE deleted_at > 0 AND deleted_at < #{legacyTimeline} LIMIT
#{limit}";
@@ -290,13 +293,23 @@ public class OwnerMetaBaseSQLProvider {
+ " ORDER BY updated_at DESC, id DESC LIMIT 1";
}
- public String selectChangedOwners(@Param("updatedAtFrom") long
updatedAtFrom) {
- return "SELECT metadata_object_id as metadataObjectId,"
+ public String selectChangedOwners(@Param("lastConsumedId") long
lastConsumedId) {
+ // Owner changes are broadcast to every server instance because owner
caches are local. Each
+ // instance tracks its own last consumed id; re-reading a row is harmless
because cache
+ // invalidation is idempotent.
+ return "SELECT id,"
+ + " metadata_object_id as metadataObjectId,"
+ " metadata_object_type as metadataObjectType,"
+ " updated_at as updatedAt"
+ " FROM "
+ OWNER_TABLE_NAME
- + " WHERE deleted_at = 0 AND updated_at >= #{updatedAtFrom}"
- + " ORDER BY updated_at, id LIMIT 1000";
+ + " WHERE deleted_at = 0 AND id > #{lastConsumedId}"
+ + " ORDER BY id LIMIT 1000";
+ }
+
+ public String selectMaxChangeId() {
+ // A newly started server has an empty local owner cache. It can start
from the current max id
+ // and consume only owner changes that happen after startup.
+ return "SELECT COALESCE(MAX(id), 0) FROM " + OWNER_TABLE_NAME + " WHERE
deleted_at = 0";
}
}
diff --git
a/core/src/main/java/org/apache/gravitino/storage/relational/po/auth/ChangedOwnerInfo.java
b/core/src/main/java/org/apache/gravitino/storage/relational/po/auth/ChangedOwnerInfo.java
index 16f93d5fd8..04b9b32204 100644
---
a/core/src/main/java/org/apache/gravitino/storage/relational/po/auth/ChangedOwnerInfo.java
+++
b/core/src/main/java/org/apache/gravitino/storage/relational/po/auth/ChangedOwnerInfo.java
@@ -29,6 +29,7 @@ import lombok.Setter;
@NoArgsConstructor
@AllArgsConstructor
public class ChangedOwnerInfo {
+ private long id;
private long metadataObjectId;
private String metadataObjectType;
private long updatedAt;
diff --git
a/core/src/test/java/org/apache/gravitino/authorization/TestAuthorizationRequestContext.java
b/core/src/test/java/org/apache/gravitino/authorization/TestAuthorizationRequestContext.java
index 2216a7dcfe..e4ef672bcc 100644
---
a/core/src/test/java/org/apache/gravitino/authorization/TestAuthorizationRequestContext.java
+++
b/core/src/test/java/org/apache/gravitino/authorization/TestAuthorizationRequestContext.java
@@ -18,12 +18,18 @@
package org.apache.gravitino.authorization;
import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
+import java.util.Optional;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
+import org.apache.gravitino.storage.relational.po.auth.GroupUpdatedAt;
+import org.apache.gravitino.storage.relational.po.auth.OwnerInfo;
+import org.apache.gravitino.storage.relational.po.auth.UserUpdatedAt;
import org.junit.jupiter.api.Test;
public class TestAuthorizationRequestContext {
@@ -101,4 +107,224 @@ public class TestAuthorizationRequestContext {
context.loadRole(counter::incrementAndGet);
assertEquals(2, counter.get(), "After a successful loadRole, further calls
must be ignored.");
}
+
+ @Test
+ public void testComputeUserInfoIfAbsentDedupesLoaderInvocation() {
+ AuthorizationRequestContext context = new AuthorizationRequestContext();
+ AtomicInteger loaderCalls = new AtomicInteger();
+ UserUpdatedAt expected = new UserUpdatedAt(42L, 1234L);
+
+ Optional<UserUpdatedAt> first =
+ context.computeUserInfoIfAbsent(
+ "ml::alice",
+ k -> {
+ loaderCalls.incrementAndGet();
+ return Optional.of(expected);
+ });
+ Optional<UserUpdatedAt> second =
+ context.computeUserInfoIfAbsent(
+ "ml::alice",
+ k -> {
+ loaderCalls.incrementAndGet();
+ return Optional.empty();
+ });
+
+ assertTrue(first.isPresent());
+ assertEquals(expected, first.get());
+ assertTrue(second.isPresent());
+ assertEquals(expected, second.get());
+ assertEquals(1, loaderCalls.get(), "Loader must run only once for the same
key");
+ }
+
+ @Test
+ public void testComputeUserInfoIfAbsentCachesEmptyResult() {
+ AuthorizationRequestContext context = new AuthorizationRequestContext();
+ AtomicInteger loaderCalls = new AtomicInteger();
+
+ Optional<UserUpdatedAt> first =
+ context.computeUserInfoIfAbsent(
+ "ml::ghost",
+ k -> {
+ loaderCalls.incrementAndGet();
+ return Optional.empty();
+ });
+ Optional<UserUpdatedAt> second =
+ context.computeUserInfoIfAbsent(
+ "ml::ghost",
+ k -> {
+ loaderCalls.incrementAndGet();
+ return Optional.of(new UserUpdatedAt(1L, 1L));
+ });
+
+ assertFalse(first.isPresent());
+ assertFalse(second.isPresent(), "Empty optional should be cached and
reused");
+ assertEquals(1, loaderCalls.get());
+ }
+
+ @Test
+ public void testComputeUserInfoIfAbsentDifferentKeysRunLoaderEachTime() {
+ AuthorizationRequestContext context = new AuthorizationRequestContext();
+ AtomicInteger loaderCalls = new AtomicInteger();
+
+ context.computeUserInfoIfAbsent(
+ "ml::alice",
+ k -> {
+ loaderCalls.incrementAndGet();
+ return Optional.of(new UserUpdatedAt(1L, 1L));
+ });
+ context.computeUserInfoIfAbsent(
+ "ml::bob",
+ k -> {
+ loaderCalls.incrementAndGet();
+ return Optional.of(new UserUpdatedAt(2L, 2L));
+ });
+
+ assertEquals(2, loaderCalls.get());
+ }
+
+ @Test
+ public void testComputeGroupInfoIfAbsentCachesPresentAndAbsentResults() {
+ AuthorizationRequestContext context = new AuthorizationRequestContext();
+ AtomicInteger loaderCalls = new AtomicInteger();
+
+ GroupUpdatedAt groupInfo = new GroupUpdatedAt(42L, 1234L);
+ Optional<GroupUpdatedAt> presentFirst =
+ context.computeGroupInfoIfAbsent(
+ "ml::group1",
+ k -> {
+ loaderCalls.incrementAndGet();
+ return Optional.of(groupInfo);
+ });
+ Optional<GroupUpdatedAt> presentSecond =
+ context.computeGroupInfoIfAbsent(
+ "ml::group1",
+ k -> {
+ loaderCalls.incrementAndGet();
+ return Optional.empty();
+ });
+
+ Optional<GroupUpdatedAt> absentFirst =
+ context.computeGroupInfoIfAbsent(
+ "ml::missing-group",
+ k -> {
+ loaderCalls.incrementAndGet();
+ return Optional.empty();
+ });
+ Optional<GroupUpdatedAt> absentSecond =
+ context.computeGroupInfoIfAbsent(
+ "ml::missing-group",
+ k -> {
+ loaderCalls.incrementAndGet();
+ return Optional.of(new GroupUpdatedAt(99L, 9999L));
+ });
+
+ assertEquals(Optional.of(groupInfo), presentFirst);
+ assertEquals(Optional.of(groupInfo), presentSecond);
+ assertFalse(absentFirst.isPresent());
+ assertFalse(absentSecond.isPresent(), "Absent group result must also be
cached");
+ assertEquals(2, loaderCalls.get(), "Loader must fire once per distinct
group key");
+ }
+
+ @Test
+ public void testComputeMetadataIdIfAbsentDedupesLoaderInvocation() {
+ AuthorizationRequestContext context = new AuthorizationRequestContext();
+ AtomicInteger loaderCalls = new AtomicInteger();
+
+ Long first =
+ context.computeMetadataIdIfAbsent(
+ "ml::cat::TABLE",
+ k -> {
+ loaderCalls.incrementAndGet();
+ return 1001L;
+ });
+ Long second =
+ context.computeMetadataIdIfAbsent(
+ "ml::cat::TABLE",
+ k -> {
+ loaderCalls.incrementAndGet();
+ return 9999L;
+ });
+
+ assertEquals(1001L, first);
+ assertEquals(1001L, second);
+ assertEquals(1, loaderCalls.get());
+ }
+
+ @Test
+ public void testComputeOwnerIfAbsentCachesPresentAndAbsentResults() {
+ AuthorizationRequestContext context = new AuthorizationRequestContext();
+ AtomicInteger loaderCalls = new AtomicInteger();
+
+ OwnerInfo ownerInfo = new OwnerInfo(99L, "USER");
+ Optional<OwnerInfo> presentFirst =
+ context.computeOwnerIfAbsent(
+ 10L,
+ id -> {
+ loaderCalls.incrementAndGet();
+ return Optional.of(ownerInfo);
+ });
+ Optional<OwnerInfo> presentSecond =
+ context.computeOwnerIfAbsent(
+ 10L,
+ id -> {
+ loaderCalls.incrementAndGet();
+ return Optional.empty();
+ });
+
+ Optional<OwnerInfo> absentFirst =
+ context.computeOwnerIfAbsent(
+ 20L,
+ id -> {
+ loaderCalls.incrementAndGet();
+ return Optional.empty();
+ });
+ Optional<OwnerInfo> absentSecond =
+ context.computeOwnerIfAbsent(
+ 20L,
+ id -> {
+ loaderCalls.incrementAndGet();
+ return Optional.of(new OwnerInfo(123L, "USER"));
+ });
+
+ assertEquals(Optional.of(ownerInfo), presentFirst);
+ assertEquals(Optional.of(ownerInfo), presentSecond);
+ assertFalse(absentFirst.isPresent());
+ assertFalse(absentSecond.isPresent(), "Absent owner result must also be
cached");
+ assertEquals(2, loaderCalls.get(), "Loader must fire once per distinct
metadataId");
+ }
+
+ @Test
+ public void testComputeHelpersRejectNullLoaderResults() {
+ AuthorizationRequestContext context = new AuthorizationRequestContext();
+
+ NullPointerException userError =
+ assertThrows(
+ NullPointerException.class,
+ () -> context.computeUserInfoIfAbsent("ml::user", key -> null));
+ assertTrue(userError.getMessage().contains("User info loader must not
return null"));
+
+ NullPointerException groupError =
+ assertThrows(
+ NullPointerException.class,
+ () -> context.computeGroupInfoIfAbsent("ml::group", key -> null));
+ assertTrue(groupError.getMessage().contains("Group info loader must not
return null"));
+
+ NullPointerException metadataError =
+ assertThrows(
+ NullPointerException.class,
+ () -> context.computeMetadataIdIfAbsent("ml::catalog", key ->
null));
+ assertTrue(metadataError.getMessage().contains("Metadata id loader must
not return null"));
+
+ NullPointerException ownerError =
+ assertThrows(
+ NullPointerException.class, () -> context.computeOwnerIfAbsent(1L,
id -> null));
+ assertTrue(ownerError.getMessage().contains("Owner loader must not return
null"));
+ }
+
+ @Test
+ public void testOriginalAuthorizationExpressionRoundTrip() {
+ AuthorizationRequestContext context = new AuthorizationRequestContext();
+ context.setOriginalAuthorizationExpression("OWNER && HAS_PRIVILEGE");
+ assertEquals("OWNER && HAS_PRIVILEGE",
context.getOriginalAuthorizationExpression());
+ }
}
diff --git
a/core/src/test/java/org/apache/gravitino/cache/TestGravitinoCache.java
b/core/src/test/java/org/apache/gravitino/cache/TestGravitinoCache.java
new file mode 100644
index 0000000000..132e142002
--- /dev/null
+++ b/core/src/test/java/org/apache/gravitino/cache/TestGravitinoCache.java
@@ -0,0 +1,319 @@
+/*
+ * 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.cache;
+
+import com.github.benmanes.caffeine.cache.Ticker;
+import java.util.Optional;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicLong;
+import org.awaitility.Awaitility;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+/** Tests for {@link CaffeineGravitinoCache} and {@link NoOpsGravitinoCache}.
*/
+public class TestGravitinoCache {
+
+ @Test
+ void testCaffeinePutAndGet() {
+ CaffeineGravitinoCache<String, Long> cache = new
CaffeineGravitinoCache<>(60_000L, 1000L);
+ try {
+ cache.put("key1", 100L);
+ cache.put("key2", 200L);
+
+ Optional<Long> val1 = cache.getIfPresent("key1");
+ Assertions.assertTrue(val1.isPresent());
+ Assertions.assertEquals(100L, val1.get());
+
+ Optional<Long> val2 = cache.getIfPresent("key2");
+ Assertions.assertTrue(val2.isPresent());
+ Assertions.assertEquals(200L, val2.get());
+
+ Optional<Long> missing = cache.getIfPresent("nonexistent");
+ Assertions.assertFalse(missing.isPresent());
+
+ Assertions.assertEquals(2, cache.size());
+ } finally {
+ cache.close();
+ }
+ }
+
+ @Test
+ void testCaffeineInvalidate() {
+ CaffeineGravitinoCache<String, String> cache = new
CaffeineGravitinoCache<>(60_000L, 1000L);
+ try {
+ cache.put("a", "val-a");
+ cache.put("b", "val-b");
+
+ cache.invalidate("a");
+ Assertions.assertFalse(cache.getIfPresent("a").isPresent());
+ Assertions.assertTrue(cache.getIfPresent("b").isPresent());
+
+ Assertions.assertEquals(1, cache.size());
+ } finally {
+ cache.close();
+ }
+ }
+
+ @Test
+ void testCaffeineInvalidateAll() {
+ CaffeineGravitinoCache<String, Integer> cache = new
CaffeineGravitinoCache<>(60_000L, 1000L);
+ try {
+ cache.put("x", 1);
+ cache.put("y", 2);
+ cache.put("z", 3);
+
+ cache.invalidateAll();
+ Assertions.assertEquals(0, cache.size());
+ Assertions.assertFalse(cache.getIfPresent("x").isPresent());
+ } finally {
+ cache.close();
+ }
+ }
+
+ @Test
+ void testCaffeineInvalidateByPrefix() {
+ CaffeineGravitinoCache<String, Long> cache = new
CaffeineGravitinoCache<>(60_000L, 1000L);
+ try {
+ // Simulate hierarchical keys: metalake::catalog::schema::
+ cache.put("lake1::cat1::", 1L);
+ cache.put("lake1::cat1::s1::", 2L);
+ cache.put("lake1::cat1::s1::t1::TABLE", 3L);
+ cache.put("lake1::cat1::s1::t2::TABLE", 4L);
+ cache.put("lake1::cat1::s2::", 5L);
+ cache.put("lake1::cat2::", 6L);
+ cache.put("lake2::cat3::", 7L);
+
+ Assertions.assertEquals(7, cache.size());
+
+ // Drop catalog cat1 — should invalidate cat1 and all children
+ cache.invalidateByPrefix("lake1::cat1::");
+
+ Assertions.assertEquals(2, cache.size());
+ Assertions.assertFalse(cache.getIfPresent("lake1::cat1::").isPresent());
+
Assertions.assertFalse(cache.getIfPresent("lake1::cat1::s1::").isPresent());
+
Assertions.assertFalse(cache.getIfPresent("lake1::cat1::s1::t1::TABLE").isPresent());
+
Assertions.assertFalse(cache.getIfPresent("lake1::cat1::s1::t2::TABLE").isPresent());
+
Assertions.assertFalse(cache.getIfPresent("lake1::cat1::s2::").isPresent());
+
+ // cat2 and lake2 should be unaffected
+ Assertions.assertTrue(cache.getIfPresent("lake1::cat2::").isPresent());
+ Assertions.assertTrue(cache.getIfPresent("lake2::cat3::").isPresent());
+ } finally {
+ cache.close();
+ }
+ }
+
+ @Test
+ void testCaffeineInvalidateByPrefixLeaf() {
+ CaffeineGravitinoCache<String, Long> cache = new
CaffeineGravitinoCache<>(60_000L, 1000L);
+ try {
+ cache.put("lake1::cat1::s1::t1::TABLE", 1L);
+ cache.put("lake1::cat1::s1::t2::TABLE", 2L);
+ cache.put("lake1::cat1::s1::f1::FILESET", 3L);
+
+ // Drop specific table — only t1 should be invalidated
+ cache.invalidateByPrefix("lake1::cat1::s1::t1::TABLE");
+
+ Assertions.assertEquals(2, cache.size());
+
Assertions.assertFalse(cache.getIfPresent("lake1::cat1::s1::t1::TABLE").isPresent());
+
Assertions.assertTrue(cache.getIfPresent("lake1::cat1::s1::t2::TABLE").isPresent());
+
Assertions.assertTrue(cache.getIfPresent("lake1::cat1::s1::f1::FILESET").isPresent());
+ } finally {
+ cache.close();
+ }
+ }
+
+ @Test
+ void testCaffeineOverwrite() {
+ CaffeineGravitinoCache<String, Long> cache = new
CaffeineGravitinoCache<>(60_000L, 1000L);
+ try {
+ cache.put("k", 1L);
+ Assertions.assertEquals(1L, cache.getIfPresent("k").get());
+
+ cache.put("k", 2L);
+ Assertions.assertEquals(2L, cache.getIfPresent("k").get());
+
+ Assertions.assertEquals(1, cache.size());
+ } finally {
+ cache.close();
+ }
+ }
+
+ @Test
+ void testNoOpsCache() {
+ NoOpsGravitinoCache<String, Long> cache = new NoOpsGravitinoCache<>();
+ try {
+ cache.put("key1", 100L);
+ Assertions.assertFalse(cache.getIfPresent("key1").isPresent());
+ Assertions.assertEquals(0, cache.size());
+
+ // All operations are no-ops, should not throw
+ cache.invalidate("key1");
+ cache.invalidateAll();
+ cache.invalidateByPrefix("any");
+ } finally {
+ cache.close();
+ }
+ }
+
+ @Test
+ void testCaffeineWithNonStringKeys() {
+ CaffeineGravitinoCache<Long, String> cache = new
CaffeineGravitinoCache<>(60_000L, 1000L);
+ try {
+ cache.put(1L, "role1");
+ cache.put(2L, "role2");
+ cache.put(3L, "role3");
+
+ Assertions.assertEquals("role1", cache.getIfPresent(1L).get());
+ Assertions.assertEquals(3, cache.size());
+
+ cache.invalidate(2L);
+ Assertions.assertFalse(cache.getIfPresent(2L).isPresent());
+ Assertions.assertEquals(2, cache.size());
+ } finally {
+ cache.close();
+ }
+ }
+
+ @Test
+ void testCaffeineInvalidateByPrefixIgnoresNonStringKeys() {
+ CaffeineGravitinoCache<Long, String> cache = new
CaffeineGravitinoCache<>(60_000L, 1000L);
+ try {
+ cache.put(10L, "role10");
+ cache.put(11L, "role11");
+
+ cache.invalidateByPrefix("1");
+
+ Assertions.assertEquals(2, cache.size());
+ Assertions.assertEquals("role10", cache.getIfPresent(10L).get());
+ Assertions.assertEquals("role11", cache.getIfPresent(11L).get());
+ } finally {
+ cache.close();
+ }
+ }
+
+ @Test
+ void testCaffeineExpiresAfterWriteTtl() {
+ ManualTicker ticker = new ManualTicker();
+ CaffeineGravitinoCache<String, Long> cache = new
CaffeineGravitinoCache<>(50L, 1000L, ticker);
+ try {
+ cache.put("k", 1L);
+ Assertions.assertTrue(cache.getIfPresent("k").isPresent());
+
+ ticker.advance(51L, TimeUnit.MILLISECONDS);
+ Optional<Long> afterTtl = cache.getIfPresent("k");
+ Assertions.assertFalse(afterTtl.isPresent(), "Entry should have expired
after write TTL");
+ } finally {
+ cache.close();
+ }
+ }
+
+ @Test
+ void testCaffeineEvictsBeyondMaxSize() {
+ CaffeineGravitinoCache<Long, Long> cache = new
CaffeineGravitinoCache<>(60_000L, 5L);
+ try {
+ for (long i = 0; i < 50L; i++) {
+ cache.put(i, i);
+ }
+
+ Awaitility.await()
+ .atMost(2, TimeUnit.SECONDS)
+ .untilAsserted(
+ () -> {
+ cache.cleanUp();
+ Assertions.assertTrue(
+ cache.size() <= 10L,
+ "Eviction should trim entries close to maxSize=5;
observed: " + cache.size());
+ });
+ } finally {
+ cache.close();
+ }
+ }
+
+ @Test
+ void testCaffeineInvalidateByPrefixWithEmptyPrefixDropsAll() {
+ CaffeineGravitinoCache<String, Long> cache = new
CaffeineGravitinoCache<>(60_000L, 1000L);
+ try {
+ cache.put("a", 1L);
+ cache.put("b", 2L);
+ cache.put("c", 3L);
+
+ cache.invalidateByPrefix("");
+ Assertions.assertEquals(0, cache.size());
+ } finally {
+ cache.close();
+ }
+ }
+
+ @Test
+ void testCaffeineInvalidateByPrefixNoMatch() {
+ CaffeineGravitinoCache<String, Long> cache = new
CaffeineGravitinoCache<>(60_000L, 1000L);
+ try {
+ cache.put("lake1::cat1::", 1L);
+ cache.put("lake2::cat2::", 2L);
+
+ cache.invalidateByPrefix("missing-prefix::");
+
+ Assertions.assertEquals(
+ 2, cache.size(), "No keys should be removed when prefix has no
match");
+ } finally {
+ cache.close();
+ }
+ }
+
+ @Test
+ void testCaffeineInvalidateNonExistentKey() {
+ CaffeineGravitinoCache<String, Long> cache = new
CaffeineGravitinoCache<>(60_000L, 1000L);
+ try {
+ cache.put("present", 1L);
+ // Should not throw or affect existing entries
+ cache.invalidate("absent");
+ Assertions.assertEquals(1, cache.size());
+ Assertions.assertTrue(cache.getIfPresent("present").isPresent());
+ } finally {
+ cache.close();
+ }
+ }
+
+ @Test
+ void testNoOpsCacheSizeAlwaysZero() {
+ NoOpsGravitinoCache<String, Long> cache = new NoOpsGravitinoCache<>();
+ try {
+ for (long i = 0; i < 100; i++) {
+ cache.put("k" + i, i);
+ }
+ Assertions.assertEquals(0, cache.size());
+ } finally {
+ cache.close();
+ }
+ }
+
+ private static class ManualTicker implements Ticker {
+ private final AtomicLong nanos = new AtomicLong();
+
+ @Override
+ public long read() {
+ return nanos.get();
+ }
+
+ private void advance(long time, TimeUnit unit) {
+ nanos.addAndGet(unit.toNanos(time));
+ }
+ }
+}
diff --git
a/core/src/test/java/org/apache/gravitino/storage/relational/mapper/provider/base/TestAuthMappers.java
b/core/src/test/java/org/apache/gravitino/storage/relational/mapper/provider/base/TestAuthMappers.java
index 439a5523fd..cb02de4aa1 100644
---
a/core/src/test/java/org/apache/gravitino/storage/relational/mapper/provider/base/TestAuthMappers.java
+++
b/core/src/test/java/org/apache/gravitino/storage/relational/mapper/provider/base/TestAuthMappers.java
@@ -351,15 +351,16 @@ public class TestAuthMappers {
throw new RuntimeException("Update failed", e);
}
- List<ChangedOwnerInfo> changed = ownerMetaMapper.selectChangedOwners(50L);
+ List<ChangedOwnerInfo> changed = ownerMetaMapper.selectChangedOwners(0L);
Assertions.assertEquals(1, changed.size());
Assertions.assertEquals(200L, changed.get(0).getMetadataObjectId());
Assertions.assertEquals("SCHEMA", changed.get(0).getMetadataObjectType());
Assertions.assertEquals(100L, changed.get(0).getUpdatedAt());
- // With the same timestamp, the row is returned again for timestamp-only
polling.
- List<ChangedOwnerInfo> sameTimestamp =
ownerMetaMapper.selectChangedOwners(100L);
- Assertions.assertEquals(1, sameTimestamp.size());
+ // Polling after the last seen id should not return the same row again.
+ List<ChangedOwnerInfo> sameTimestamp =
+ ownerMetaMapper.selectChangedOwners(changed.get(0).getId());
+ Assertions.assertTrue(sameTimestamp.isEmpty());
}
private AuditInfo buildAuditInfo() {
diff --git
a/core/src/test/java/org/apache/gravitino/storage/relational/mapper/provider/base/TestEntityChangeLogMapper.java
b/core/src/test/java/org/apache/gravitino/storage/relational/mapper/provider/base/TestEntityChangeLogMapper.java
index 8b4a3be8c2..8893a74116 100644
---
a/core/src/test/java/org/apache/gravitino/storage/relational/mapper/provider/base/TestEntityChangeLogMapper.java
+++
b/core/src/test/java/org/apache/gravitino/storage/relational/mapper/provider/base/TestEntityChangeLogMapper.java
@@ -106,8 +106,7 @@ public class TestEntityChangeLogMapper {
"metalake1", "TABLE", "metalake1.cat.schema.tbl", OperateType.ALTER);
long jvmAfter = System.currentTimeMillis();
- List<EntityChangeRecord> records =
- entityChangeLogMapper.selectEntityChanges(jvmBefore - 1000L, 10);
+ List<EntityChangeRecord> records =
entityChangeLogMapper.selectEntityChanges(0L, 10);
Assertions.assertEquals(1, records.size());
EntityChangeRecord record = records.get(0);
Assertions.assertEquals("metalake1", record.getMetalakeName());
diff --git
a/core/src/test/java/org/apache/gravitino/storage/relational/service/TestEntityChangeLogService.java
b/core/src/test/java/org/apache/gravitino/storage/relational/service/TestEntityChangeLogService.java
index d605400f41..7d85ea7f99 100644
---
a/core/src/test/java/org/apache/gravitino/storage/relational/service/TestEntityChangeLogService.java
+++
b/core/src/test/java/org/apache/gravitino/storage/relational/service/TestEntityChangeLogService.java
@@ -47,19 +47,24 @@ public class TestEntityChangeLogService extends
TestJDBCBackend {
private static final String CATALOG_NAME =
"catalog_for_entity_change_log_test";
private static final String SCHEMA_NAME =
"schema_for_entity_change_log_test";
- private List<EntityChangeRecord> listEntityChanges(long createdAtFrom) {
+ private long maxEntityChangeId() {
return SessionUtils.doWithCommitAndFetchResult(
- EntityChangeLogMapper.class, mapper ->
mapper.selectEntityChanges(createdAtFrom, 100));
+ EntityChangeLogMapper.class, EntityChangeLogMapper::selectMaxChangeId);
+ }
+
+ private List<EntityChangeRecord> listEntityChanges(long lastConsumedId) {
+ return SessionUtils.doWithCommitAndFetchResult(
+ EntityChangeLogMapper.class, mapper ->
mapper.selectEntityChanges(lastConsumedId, 100));
}
private void assertEntityChange(
- long createdAtFrom,
+ long lastConsumedId,
String metalakeName,
Entity.EntityType entityType,
String fullName,
OperateType operateType) {
Assertions.assertTrue(
- listEntityChanges(createdAtFrom).stream()
+ listEntityChanges(lastConsumedId).stream()
.anyMatch(
record ->
record.getMetalakeName().equals(metalakeName)
@@ -73,7 +78,7 @@ public class TestEntityChangeLogService extends
TestJDBCBackend {
void testMetalakeChangeLogOnRenameAndDrop() throws IOException {
BaseMetalake metalake = createAndInsertMakeLake(METALAKE_NAME);
- long beforeRename = System.currentTimeMillis() - 1;
+ long maxIdBeforeRename = maxEntityChangeId();
BaseMetalake renamedMetalake =
backend.update(
metalake.nameIdentifier(),
@@ -82,13 +87,17 @@ public class TestEntityChangeLogService extends
TestJDBCBackend {
createBaseMakeLake(
metalake.id(), METALAKE_NAME + "_renamed",
metalake.auditInfo()));
assertEntityChange(
- beforeRename, METALAKE_NAME, Entity.EntityType.METALAKE,
METALAKE_NAME, OperateType.ALTER);
+ maxIdBeforeRename,
+ METALAKE_NAME,
+ Entity.EntityType.METALAKE,
+ METALAKE_NAME,
+ OperateType.ALTER);
- long beforeDrop = System.currentTimeMillis() - 1;
+ long maxIdBeforeDrop = maxEntityChangeId();
Assertions.assertTrue(
MetalakeMetaService.getInstance().deleteMetalake(renamedMetalake.nameIdentifier(),
false));
assertEntityChange(
- beforeDrop,
+ maxIdBeforeDrop,
renamedMetalake.name(),
Entity.EntityType.METALAKE,
renamedMetalake.name(),
@@ -100,7 +109,7 @@ public class TestEntityChangeLogService extends
TestJDBCBackend {
createAndInsertMakeLake(METALAKE_NAME);
CatalogEntity catalog = createAndInsertCatalog(METALAKE_NAME,
CATALOG_NAME);
- long beforeCatalogRename = System.currentTimeMillis() - 1;
+ long maxIdBeforeCatalogRename = maxEntityChangeId();
CatalogEntity renamedCatalog =
backend.update(
catalog.nameIdentifier(),
@@ -109,17 +118,17 @@ public class TestEntityChangeLogService extends
TestJDBCBackend {
createCatalog(
catalog.id(), catalog.namespace(), CATALOG_NAME +
"_renamed", AUDIT_INFO));
assertEntityChange(
- beforeCatalogRename,
+ maxIdBeforeCatalogRename,
METALAKE_NAME,
Entity.EntityType.CATALOG,
NameIdentifierUtil.ofCatalog(METALAKE_NAME, CATALOG_NAME).toString(),
OperateType.ALTER);
- long beforeCatalogDrop = System.currentTimeMillis() - 1;
+ long maxIdBeforeCatalogDrop = maxEntityChangeId();
Assertions.assertTrue(
CatalogMetaService.getInstance().deleteCatalog(renamedCatalog.nameIdentifier(),
false));
assertEntityChange(
- beforeCatalogDrop,
+ maxIdBeforeCatalogDrop,
METALAKE_NAME,
Entity.EntityType.CATALOG,
NameIdentifierUtil.ofCatalog(METALAKE_NAME,
renamedCatalog.name()).toString(),
@@ -127,7 +136,7 @@ public class TestEntityChangeLogService extends
TestJDBCBackend {
CatalogEntity schemaCatalog = createAndInsertCatalog(METALAKE_NAME,
CATALOG_NAME + "_schema");
SchemaEntity schema = createAndInsertSchema(METALAKE_NAME,
schemaCatalog.name(), SCHEMA_NAME);
- long beforeSchemaRename = System.currentTimeMillis() - 1;
+ long maxIdBeforeSchemaRename = maxEntityChangeId();
SchemaEntity renamedSchema =
backend.update(
schema.nameIdentifier(),
@@ -136,17 +145,17 @@ public class TestEntityChangeLogService extends
TestJDBCBackend {
createSchemaEntity(
schema.id(), schema.namespace(), SCHEMA_NAME + "_renamed",
AUDIT_INFO));
assertEntityChange(
- beforeSchemaRename,
+ maxIdBeforeSchemaRename,
METALAKE_NAME,
Entity.EntityType.SCHEMA,
NameIdentifierUtil.ofSchema(METALAKE_NAME, schemaCatalog.name(),
SCHEMA_NAME).toString(),
OperateType.ALTER);
- long beforeSchemaDrop = System.currentTimeMillis() - 1;
+ long maxIdBeforeSchemaDrop = maxEntityChangeId();
Assertions.assertTrue(
SchemaMetaService.getInstance().deleteSchema(renamedSchema.nameIdentifier(),
false));
assertEntityChange(
- beforeSchemaDrop,
+ maxIdBeforeSchemaDrop,
METALAKE_NAME,
Entity.EntityType.SCHEMA,
NameIdentifierUtil.ofSchema(METALAKE_NAME, schemaCatalog.name(),
renamedSchema.name())
@@ -162,7 +171,7 @@ public class TestEntityChangeLogService extends
TestJDBCBackend {
TableEntity table =
createTableEntity(RandomIdGenerator.INSTANCE.nextId(), namespace,
"table1", AUDIT_INFO);
backend.insert(table, false);
- long beforeTableRename = System.currentTimeMillis() - 1;
+ long maxIdBeforeTableRename = maxEntityChangeId();
TableEntity renamedTable =
TableMetaService.getInstance()
.updateTable(
@@ -170,17 +179,17 @@ public class TestEntityChangeLogService extends
TestJDBCBackend {
entity ->
createTableEntity(table.id(), table.namespace(), "table2",
table.auditInfo()));
assertEntityChange(
- beforeTableRename,
+ maxIdBeforeTableRename,
METALAKE_NAME,
Entity.EntityType.TABLE,
NameIdentifierUtil.ofTable(METALAKE_NAME, CATALOG_NAME, SCHEMA_NAME,
"table1").toString(),
OperateType.ALTER);
- long beforeTableDrop = System.currentTimeMillis() - 1;
+ long maxIdBeforeTableDrop = maxEntityChangeId();
Assertions.assertTrue(
TableMetaService.getInstance().deleteTable(renamedTable.nameIdentifier()));
assertEntityChange(
- beforeTableDrop,
+ maxIdBeforeTableDrop,
METALAKE_NAME,
Entity.EntityType.TABLE,
NameIdentifierUtil.ofTable(METALAKE_NAME, CATALOG_NAME, SCHEMA_NAME,
"table2").toString(),
@@ -193,24 +202,24 @@ public class TestEntityChangeLogService extends
TestJDBCBackend {
"topic1",
AUDIT_INFO);
backend.insert(topic, false);
- long beforeTopicRename = System.currentTimeMillis() - 1;
+ long maxIdBeforeTopicRename = maxEntityChangeId();
TopicEntity renamedTopic =
backend.update(
topic.nameIdentifier(),
Entity.EntityType.TOPIC,
entity -> createTopicEntity(topic.id(), topic.namespace(),
"topic2", AUDIT_INFO));
assertEntityChange(
- beforeTopicRename,
+ maxIdBeforeTopicRename,
METALAKE_NAME,
Entity.EntityType.TOPIC,
NameIdentifierUtil.ofTopic(METALAKE_NAME, CATALOG_NAME, SCHEMA_NAME,
"topic1").toString(),
OperateType.ALTER);
- long beforeTopicDrop = System.currentTimeMillis() - 1;
+ long maxIdBeforeTopicDrop = maxEntityChangeId();
Assertions.assertTrue(
TopicMetaService.getInstance().deleteTopic(renamedTopic.nameIdentifier()));
assertEntityChange(
- beforeTopicDrop,
+ maxIdBeforeTopicDrop,
METALAKE_NAME,
Entity.EntityType.TOPIC,
NameIdentifierUtil.ofTopic(METALAKE_NAME, CATALOG_NAME, SCHEMA_NAME,
"topic2").toString(),
@@ -222,7 +231,7 @@ public class TestEntityChangeLogService extends
TestJDBCBackend {
NamespaceUtil.ofView(METALAKE_NAME, CATALOG_NAME, SCHEMA_NAME),
"view1");
ViewMetaService.getInstance().insertView(view, false);
- long beforeViewRename = System.currentTimeMillis() - 1;
+ long maxIdBeforeViewRename = maxEntityChangeId();
ViewEntity renamedView =
ViewMetaService.getInstance()
.updateView(
@@ -237,16 +246,16 @@ public class TestEntityChangeLogService extends
TestJDBCBackend {
.withAuditInfo(view.auditInfo())
.build());
assertEntityChange(
- beforeViewRename,
+ maxIdBeforeViewRename,
METALAKE_NAME,
Entity.EntityType.VIEW,
NameIdentifierUtil.ofView(METALAKE_NAME, CATALOG_NAME, SCHEMA_NAME,
"view1").toString(),
OperateType.ALTER);
- long beforeViewDrop = System.currentTimeMillis() - 1;
+ long maxIdBeforeViewDrop = maxEntityChangeId();
Assertions.assertTrue(ViewMetaService.getInstance().deleteView(renamedView.nameIdentifier()));
assertEntityChange(
- beforeViewDrop,
+ maxIdBeforeViewDrop,
METALAKE_NAME,
Entity.EntityType.VIEW,
NameIdentifierUtil.ofView(METALAKE_NAME, CATALOG_NAME, SCHEMA_NAME,
"view2").toString(),
@@ -259,7 +268,7 @@ public class TestEntityChangeLogService extends
TestJDBCBackend {
"fileset1",
AUDIT_INFO);
FilesetMetaService.getInstance().insertFileset(fileset, false);
- long beforeFilesetRename = System.currentTimeMillis() - 1;
+ long maxIdBeforeFilesetRename = maxEntityChangeId();
FilesetEntity renamedFileset =
FilesetMetaService.getInstance()
.updateFileset(
@@ -267,18 +276,18 @@ public class TestEntityChangeLogService extends
TestJDBCBackend {
entity ->
createFilesetEntity(fileset.id(), fileset.namespace(),
"fileset2", AUDIT_INFO));
assertEntityChange(
- beforeFilesetRename,
+ maxIdBeforeFilesetRename,
METALAKE_NAME,
Entity.EntityType.FILESET,
NameIdentifierUtil.ofFileset(METALAKE_NAME, CATALOG_NAME, SCHEMA_NAME,
"fileset1")
.toString(),
OperateType.ALTER);
- long beforeFilesetDrop = System.currentTimeMillis() - 1;
+ long maxIdBeforeFilesetDrop = maxEntityChangeId();
Assertions.assertTrue(
FilesetMetaService.getInstance().deleteFileset(renamedFileset.nameIdentifier()));
assertEntityChange(
- beforeFilesetDrop,
+ maxIdBeforeFilesetDrop,
METALAKE_NAME,
Entity.EntityType.FILESET,
NameIdentifierUtil.ofFileset(METALAKE_NAME, CATALOG_NAME, SCHEMA_NAME,
"fileset2")
@@ -295,7 +304,7 @@ public class TestEntityChangeLogService extends
TestJDBCBackend {
Map.of("k1", "v1"),
AUDIT_INFO);
ModelMetaService.getInstance().insertModel(model, false);
- long beforeModelRename = System.currentTimeMillis() - 1;
+ long maxIdBeforeModelRename = maxEntityChangeId();
ModelEntity renamedModel =
ModelMetaService.getInstance()
.updateModel(
@@ -310,17 +319,17 @@ public class TestEntityChangeLogService extends
TestJDBCBackend {
model.properties(),
AUDIT_INFO));
assertEntityChange(
- beforeModelRename,
+ maxIdBeforeModelRename,
METALAKE_NAME,
Entity.EntityType.MODEL,
NameIdentifierUtil.ofModel(METALAKE_NAME, CATALOG_NAME, SCHEMA_NAME,
"model1").toString(),
OperateType.ALTER);
- long beforeModelDrop = System.currentTimeMillis() - 1;
+ long maxIdBeforeModelDrop = maxEntityChangeId();
Assertions.assertTrue(
ModelMetaService.getInstance().deleteModel(renamedModel.nameIdentifier()));
assertEntityChange(
- beforeModelDrop,
+ maxIdBeforeModelDrop,
METALAKE_NAME,
Entity.EntityType.MODEL,
NameIdentifierUtil.ofModel(METALAKE_NAME, CATALOG_NAME, SCHEMA_NAME,
"model2").toString(),
diff --git
a/core/src/test/java/org/apache/gravitino/storage/relational/service/TestTableMetaService.java
b/core/src/test/java/org/apache/gravitino/storage/relational/service/TestTableMetaService.java
index 6596b4af2c..08d874f295 100644
---
a/core/src/test/java/org/apache/gravitino/storage/relational/service/TestTableMetaService.java
+++
b/core/src/test/java/org/apache/gravitino/storage/relational/service/TestTableMetaService.java
@@ -69,9 +69,14 @@ public class TestTableMetaService extends TestJDBCBackend {
private final String catalogName = "catalog_for_table_test";
private final String schemaName = "schema_for_table_test";
- private List<EntityChangeRecord> listEntityChanges(long createdAtFrom) {
+ private long maxEntityChangeId() {
return SessionUtils.doWithCommitAndFetchResult(
- EntityChangeLogMapper.class, mapper ->
mapper.selectEntityChanges(createdAtFrom, 100));
+ EntityChangeLogMapper.class, EntityChangeLogMapper::selectMaxChangeId);
+ }
+
+ private List<EntityChangeRecord> listEntityChanges(long lastConsumedId) {
+ return SessionUtils.doWithCommitAndFetchResult(
+ EntityChangeLogMapper.class, mapper ->
mapper.selectEntityChanges(lastConsumedId, 100));
}
@TestTemplate
@@ -201,7 +206,7 @@ public class TestTableMetaService extends TestJDBCBackend {
TableMetaService.getInstance().insertTable(createdTable, false);
// test update table without changing schema name
- long beforeRename = System.currentTimeMillis() - 1;
+ long maxIdBeforeRename = maxEntityChangeId();
TableEntity updatedTable =
TableEntity.builder()
.withId(createdTable.id())
@@ -222,7 +227,7 @@ public class TestTableMetaService extends TestJDBCBackend {
compareTwoColumns(updatedTable.columns(), retrievedTable.columns());
compareTwoColumns(updatedTable.columns(), retrievedTable.columns());
Assertions.assertTrue(
- listEntityChanges(beforeRename).stream()
+ listEntityChanges(maxIdBeforeRename).stream()
.anyMatch(
record ->
record.getMetalakeName().equals(metalakeName)
@@ -263,7 +268,7 @@ public class TestTableMetaService extends TestJDBCBackend {
AUDIT_INFO);
backend.insert(newSchema, false);
- long beforeSchemaMove = System.currentTimeMillis() - 1;
+ long maxIdBeforeSchemaMove = maxEntityChangeId();
TableEntity movedTable =
TableEntity.builder()
.withId(updatedTable.id())
@@ -275,7 +280,7 @@ public class TestTableMetaService extends TestJDBCBackend {
TableMetaService.getInstance()
.updateTable(updatedTable.nameIdentifier(), oldTable -> movedTable);
Assertions.assertTrue(
- listEntityChanges(beforeSchemaMove).stream()
+ listEntityChanges(maxIdBeforeSchemaMove).stream()
.anyMatch(
record ->
record.getMetalakeName().equals(metalakeName)
@@ -298,11 +303,11 @@ public class TestTableMetaService extends TestJDBCBackend
{
Assertions.assertEquals(updatedTable2.auditInfo(),
retrievedTable2.auditInfo());
compareTwoColumns(updatedTable2.columns(), retrievedTable2.columns());
- long beforeDelete = System.currentTimeMillis() - 1;
+ long maxIdBeforeDelete = maxEntityChangeId();
Assertions.assertTrue(
TableMetaService.getInstance().deleteTable(updatedTable2.nameIdentifier()));
Assertions.assertTrue(
- listEntityChanges(beforeDelete).stream()
+ listEntityChanges(maxIdBeforeDelete).stream()
.anyMatch(
record ->
record.getMetalakeName().equals(metalakeName)
diff --git
a/server-common/src/main/java/org/apache/gravitino/server/authorization/PassThroughAuthorizer.java
b/server-common/src/main/java/org/apache/gravitino/server/authorization/PassThroughAuthorizer.java
index 06e20b8ada..76de95b0d3 100644
---
a/server-common/src/main/java/org/apache/gravitino/server/authorization/PassThroughAuthorizer.java
+++
b/server-common/src/main/java/org/apache/gravitino/server/authorization/PassThroughAuthorizer.java
@@ -79,7 +79,7 @@ public class PassThroughAuthorizer implements
GravitinoAuthorizer {
}
@Override
- public boolean isMetalakeUser(String metalake) {
+ public boolean isMetalakeUser(String metalake, AuthorizationRequestContext
requestContext) {
AccessControlDispatcher dispatcher =
GravitinoEnv.getInstance().accessControlDispatcher();
if (dispatcher != null) {
try {
diff --git
a/server-common/src/main/java/org/apache/gravitino/server/authorization/expression/AuthorizationExpressionConstants.java
b/server-common/src/main/java/org/apache/gravitino/server/authorization/expression/AuthorizationExpressionConstants.java
index 6e72401720..bdb13b8ba8 100644
---
a/server-common/src/main/java/org/apache/gravitino/server/authorization/expression/AuthorizationExpressionConstants.java
+++
b/server-common/src/main/java/org/apache/gravitino/server/authorization/expression/AuthorizationExpressionConstants.java
@@ -177,7 +177,8 @@ public class AuthorizationExpressionConstants {
* Special case: "METALAKE_USER" is used here as a unique authorization
token, not a logical
* expression like other constants. This is intentional and required for
metalake-level user
* authorization checks {@link
- *
org.apache.gravitino.authorization.GravitinoAuthorizer#isMetalakeUser(String)}.
+ *
org.apache.gravitino.authorization.GravitinoAuthorizer#isMetalakeUser(String,
+ * org.apache.gravitino.authorization.AuthorizationRequestContext)}.
*/
public static final String LOAD_METALAKE_AUTHORIZATION_EXPRESSION =
"METALAKE_USER";
diff --git
a/server-common/src/main/java/org/apache/gravitino/server/authorization/expression/AuthorizationExpressionConverter.java
b/server-common/src/main/java/org/apache/gravitino/server/authorization/expression/AuthorizationExpressionConverter.java
index 99123a162f..4507ba0a50 100644
---
a/server-common/src/main/java/org/apache/gravitino/server/authorization/expression/AuthorizationExpressionConverter.java
+++
b/server-common/src/main/java/org/apache/gravitino/server/authorization/expression/AuthorizationExpressionConverter.java
@@ -212,7 +212,9 @@ public class AuthorizationExpressionConverter {
*/
public static String replaceAnyPrivilege(String expression) {
expression = expression.replaceAll("SERVICE_ADMIN",
"authorizer.isServiceAdmin()");
- expression = expression.replaceAll("METALAKE_USER",
"authorizer.isMetalakeUser(METALAKE_NAME)");
+ expression =
+ expression.replaceAll(
+ "METALAKE_USER",
"authorizer.isMetalakeUser(METALAKE_NAME,authorizationContext)");
// A single privilege (e.g., SELECT_TABLE) can be granted or denied at
multiple namespace
// levels: metalake, catalog, schema, and table.
diff --git
a/server-common/src/main/java/org/apache/gravitino/server/authorization/jcasbin/JcasbinAuthorizer.java
b/server-common/src/main/java/org/apache/gravitino/server/authorization/jcasbin/JcasbinAuthorizer.java
index 2e1f669b8f..3778f8b19f 100644
---
a/server-common/src/main/java/org/apache/gravitino/server/authorization/jcasbin/JcasbinAuthorizer.java
+++
b/server-common/src/main/java/org/apache/gravitino/server/authorization/jcasbin/JcasbinAuthorizer.java
@@ -252,7 +252,7 @@ public class JcasbinAuthorizer implements
GravitinoAuthorizer {
}
@Override
- public boolean isMetalakeUser(String metalake) {
+ public boolean isMetalakeUser(String metalake, AuthorizationRequestContext
requestContext) {
String currentUserName = PrincipalUtils.getCurrentUserName();
if (StringUtils.isBlank(currentUserName)) {
return false;
diff --git
a/server-common/src/test/java/org/apache/gravitino/server/authorization/MockGravitinoAuthorizer.java
b/server-common/src/test/java/org/apache/gravitino/server/authorization/MockGravitinoAuthorizer.java
index f9ef64bab0..51c55ed797 100644
---
a/server-common/src/test/java/org/apache/gravitino/server/authorization/MockGravitinoAuthorizer.java
+++
b/server-common/src/test/java/org/apache/gravitino/server/authorization/MockGravitinoAuthorizer.java
@@ -93,7 +93,7 @@ public class MockGravitinoAuthorizer implements
GravitinoAuthorizer {
}
@Override
- public boolean isMetalakeUser(String metalake) {
+ public boolean isMetalakeUser(String metalake, AuthorizationRequestContext
requestContext) {
return true;
}
diff --git
a/server-common/src/test/java/org/apache/gravitino/server/authorization/TestPassThroughAuthorizer.java
b/server-common/src/test/java/org/apache/gravitino/server/authorization/TestPassThroughAuthorizer.java
index f5290d01fa..e173491d6a 100644
---
a/server-common/src/test/java/org/apache/gravitino/server/authorization/TestPassThroughAuthorizer.java
+++
b/server-common/src/test/java/org/apache/gravitino/server/authorization/TestPassThroughAuthorizer.java
@@ -60,7 +60,8 @@ public class TestPassThroughAuthorizer {
passThroughAuthorizer.isOwner(
principal, "metalake", metadataObject, new
AuthorizationRequestContext()));
Assertions.assertTrue(passThroughAuthorizer.isServiceAdmin());
- Assertions.assertTrue(passThroughAuthorizer.isMetalakeUser("metalake"));
+ Assertions.assertTrue(
+ passThroughAuthorizer.isMetalakeUser("metalake", new
AuthorizationRequestContext()));
Assertions.assertTrue(passThroughAuthorizer.isSelf(Entity.EntityType.USER,
null));
Assertions.assertTrue(
passThroughAuthorizer.hasSetOwnerPermission(
diff --git
a/server/src/main/java/org/apache/gravitino/server/web/filter/GravitinoInterceptionService.java
b/server/src/main/java/org/apache/gravitino/server/web/filter/GravitinoInterceptionService.java
index 5dc0057fcd..0f542bdeb9 100644
---
a/server/src/main/java/org/apache/gravitino/server/web/filter/GravitinoInterceptionService.java
+++
b/server/src/main/java/org/apache/gravitino/server/web/filter/GravitinoInterceptionService.java
@@ -40,6 +40,7 @@ import org.apache.commons.lang3.StringUtils;
import org.apache.gravitino.Entity;
import org.apache.gravitino.MetadataObject;
import org.apache.gravitino.NameIdentifier;
+import org.apache.gravitino.authorization.AuthorizationRequestContext;
import org.apache.gravitino.authorization.AuthorizationUtils;
import org.apache.gravitino.exceptions.ForbiddenException;
import org.apache.gravitino.exceptions.NoSuchMetalakeException;
@@ -153,13 +154,16 @@ public class GravitinoInterceptionService implements
InterceptionService {
extractNameIdentifierFromParameters(parameters, args);
Map<String, Object> pathParams =
Utils.extractPathParamsFromParameters(parameters, args);
+ AuthorizationRequestContext authorizationRequestContext =
+ new AuthorizationRequestContext();
// Check metalake and user existence before authorization
NameIdentifier metalakeIdent =
metadataContext.get(Entity.EntityType.METALAKE);
if (metalakeIdent != null) {
String currentUser = PrincipalUtils.getCurrentUserName();
try {
- AuthorizationUtils.checkCurrentUser(metalakeIdent.name(),
currentUser);
+ AuthorizationUtils.checkCurrentUser(
+ metalakeIdent.name(), currentUser,
authorizationRequestContext);
} catch (NoSuchMetalakeException e) {
LOG.warn(
"Metalake {} does not exist when validating user {}",
metalakeIdent, currentUser);
@@ -199,7 +203,7 @@ public class GravitinoInterceptionService implements
InterceptionService {
args,
secondaryExpression,
secondaryExpressionCondition);
- boolean authorizeResult = executor.execute();
+ boolean authorizeResult =
executor.execute(authorizationRequestContext);
if (!authorizeResult) {
return buildNoAuthResponse(expressionAnnotation,
metadataContext, method, expression);
}
diff --git
a/server/src/main/java/org/apache/gravitino/server/web/filter/authorization/AssociatePolicyAuthorizationExecutor.java
b/server/src/main/java/org/apache/gravitino/server/web/filter/authorization/AssociatePolicyAuthorizationExecutor.java
index d9f4de8805..59b2e83305 100644
---
a/server/src/main/java/org/apache/gravitino/server/web/filter/authorization/AssociatePolicyAuthorizationExecutor.java
+++
b/server/src/main/java/org/apache/gravitino/server/web/filter/authorization/AssociatePolicyAuthorizationExecutor.java
@@ -54,13 +54,12 @@ public class AssociatePolicyAuthorizationExecutor extends
CommonAuthorizerExecut
}
@Override
- public boolean execute() throws Exception {
+ public boolean execute(AuthorizationRequestContext context) throws Exception
{
Object request = extractFromParameters(parameters, args);
if (request == null) {
return false;
}
- AuthorizationRequestContext context = new AuthorizationRequestContext();
context.setOriginalAuthorizationExpression(expression);
Entity.EntityType targetType =
Entity.EntityType.POLICY; // policies are the only supported batch
target here
diff --git
a/server/src/main/java/org/apache/gravitino/server/web/filter/authorization/AssociateTagAuthorizationExecutor.java
b/server/src/main/java/org/apache/gravitino/server/web/filter/authorization/AssociateTagAuthorizationExecutor.java
index 8d5080e800..2fef4e9ee3 100644
---
a/server/src/main/java/org/apache/gravitino/server/web/filter/authorization/AssociateTagAuthorizationExecutor.java
+++
b/server/src/main/java/org/apache/gravitino/server/web/filter/authorization/AssociateTagAuthorizationExecutor.java
@@ -54,13 +54,12 @@ public class AssociateTagAuthorizationExecutor extends
CommonAuthorizerExecutor
}
@Override
- public boolean execute() throws Exception {
+ public boolean execute(AuthorizationRequestContext context) throws Exception
{
Object request = extractFromParameters(parameters, args);
if (request == null) {
return false;
}
- AuthorizationRequestContext context = new AuthorizationRequestContext();
context.setOriginalAuthorizationExpression(expression);
Entity.EntityType targetType =
Entity.EntityType.TAG; // Tags are the only supported batch target here
diff --git
a/server/src/main/java/org/apache/gravitino/server/web/filter/authorization/AuthorizationExecutor.java
b/server/src/main/java/org/apache/gravitino/server/web/filter/authorization/AuthorizationExecutor.java
index be9c78f96e..d98d448a10 100644
---
a/server/src/main/java/org/apache/gravitino/server/web/filter/authorization/AuthorizationExecutor.java
+++
b/server/src/main/java/org/apache/gravitino/server/web/filter/authorization/AuthorizationExecutor.java
@@ -17,7 +17,9 @@
package org.apache.gravitino.server.web.filter.authorization;
+import org.apache.gravitino.authorization.AuthorizationRequestContext;
+
public interface AuthorizationExecutor {
- boolean execute() throws Exception;
+ boolean execute(AuthorizationRequestContext authorizationRequestContext)
throws Exception;
}
diff --git
a/server/src/main/java/org/apache/gravitino/server/web/filter/authorization/CommonAuthorizerExecutor.java
b/server/src/main/java/org/apache/gravitino/server/web/filter/authorization/CommonAuthorizerExecutor.java
index b03bdeaa58..59d9bcc5eb 100644
---
a/server/src/main/java/org/apache/gravitino/server/web/filter/authorization/CommonAuthorizerExecutor.java
+++
b/server/src/main/java/org/apache/gravitino/server/web/filter/authorization/CommonAuthorizerExecutor.java
@@ -45,10 +45,9 @@ public class CommonAuthorizerExecutor implements
AuthorizationExecutor {
}
@Override
- public boolean execute() throws Exception {
- AuthorizationRequestContext authorizationRequestContext = new
AuthorizationRequestContext();
+ public boolean execute(AuthorizationRequestContext
authorizationRequestContext) throws Exception {
authorizationRequestContext.setOriginalAuthorizationExpression(expression);
return authorizationExpressionEvaluator.evaluate(
- metadataContext, pathParams, new AuthorizationRequestContext(),
entityType);
+ metadataContext, pathParams, authorizationRequestContext, entityType);
}
}
diff --git
a/server/src/main/java/org/apache/gravitino/server/web/filter/authorization/RunJobAuthorizationExecutor.java
b/server/src/main/java/org/apache/gravitino/server/web/filter/authorization/RunJobAuthorizationExecutor.java
index 55a15835d0..7e592408ed 100644
---
a/server/src/main/java/org/apache/gravitino/server/web/filter/authorization/RunJobAuthorizationExecutor.java
+++
b/server/src/main/java/org/apache/gravitino/server/web/filter/authorization/RunJobAuthorizationExecutor.java
@@ -36,6 +36,7 @@ public class RunJobAuthorizationExecutor implements
AuthorizationExecutor {
private final AuthorizationExpressionEvaluator
authorizationExpressionEvaluator;
private final Map<String, Object> pathParams;
private final Optional<String> entityType;
+ private final String expression;
public RunJobAuthorizationExecutor(
Parameter[] parameters,
@@ -46,6 +47,7 @@ public class RunJobAuthorizationExecutor implements
AuthorizationExecutor {
Optional<String> entityType) {
this.parameters = parameters;
this.args = args;
+ this.expression = expression;
this.metadataContext = metadataContext;
this.authorizationExpressionEvaluator = new
AuthorizationExpressionEvaluator(expression);
this.pathParams = pathParams;
@@ -53,13 +55,13 @@ public class RunJobAuthorizationExecutor implements
AuthorizationExecutor {
}
@Override
- public boolean execute() throws Exception {
+ public boolean execute(AuthorizationRequestContext context) throws Exception
{
Object request = extractFromParameters(parameters, args);
if (request == null) {
return false;
}
- AuthorizationRequestContext context = new AuthorizationRequestContext();
+ context.setOriginalAuthorizationExpression(expression);
Preconditions.checkArgument(
request instanceof JobRunRequest,
"Expected JobRunRequest but found %s",
diff --git
a/server/src/test/java/org/apache/gravitino/server/web/filter/TestGravitinoInterceptionService.java
b/server/src/test/java/org/apache/gravitino/server/web/filter/TestGravitinoInterceptionService.java
index a96160da84..02d6048c95 100644
---
a/server/src/test/java/org/apache/gravitino/server/web/filter/TestGravitinoInterceptionService.java
+++
b/server/src/test/java/org/apache/gravitino/server/web/filter/TestGravitinoInterceptionService.java
@@ -170,7 +170,7 @@ public class TestGravitinoInterceptionService {
.when(
() ->
AuthorizationUtils.checkCurrentUser(
- ArgumentMatchers.any(), ArgumentMatchers.any()))
+ ArgumentMatchers.any(), ArgumentMatchers.any(),
ArgumentMatchers.any()))
.thenThrow(new NoSuchMetalakeException("Metalake nonExistentMetalake
does not exist"));
GravitinoInterceptionService gravitinoInterceptionService =
@@ -318,7 +318,7 @@ public class TestGravitinoInterceptionService {
}
@Override
- public boolean isMetalakeUser(String metalake) {
+ public boolean isMetalakeUser(String metalake, AuthorizationRequestContext
requestContext) {
return true;
}