This is an automated email from the ASF dual-hosted git repository.

yuqi4733 pushed a commit to branch feat/cache-jcasbin-id-mapping
in repository https://gitbox.apache.org/repos/asf/gravitino.git

commit edaa7bd6fef3eb7c3d1b619e458b0ea1eb298e3d
Author: yuqi <[email protected]>
AuthorDate: Thu Apr 16 23:26:27 2026 +0800

    feat(cache): add GravitinoCache interface with Caffeine and NoOps 
implementations
    
    - GravitinoCache<K,V>: general-purpose cache interface for auth subsystem
      - getIfPresent, put, invalidate, invalidateAll, invalidateByPrefix, size
      - invalidateByPrefix enables hierarchical cascade invalidation for 
metadataIdCache
    - CaffeineGravitinoCache<K,V>: Caffeine-backed implementation with 
configurable TTL and max size
    - NoOpsGravitinoCache<K,V>: no-op implementation for testing and 
disabled-cache environments
    - TestGravitinoCache: comprehensive tests covering put/get, invalidate, 
invalidateAll,
      invalidateByPrefix (hierarchical cascade and leaf), overwrite, non-string 
keys, and NoOps
    
    Co-Authored-By: Claude Opus 4.6 <[email protected]>
---
 .../gravitino/cache/CaffeineGravitinoCache.java    |  88 ++++++++++
 .../org/apache/gravitino/cache/GravitinoCache.java |  74 ++++++++
 .../gravitino/cache/NoOpsGravitinoCache.java       |  66 +++++++
 .../apache/gravitino/cache/TestGravitinoCache.java | 189 +++++++++++++++++++++
 4 files changed, 417 insertions(+)

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..5d52aafd8f
--- /dev/null
+++ b/core/src/main/java/org/apache/gravitino/cache/CaffeineGravitinoCache.java
@@ -0,0 +1,88 @@
+/*
+ * 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 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.cache =
+        Caffeine.newBuilder()
+            .expireAfterWrite(ttlMs, TimeUnit.MILLISECONDS)
+            .maximumSize(maxSize)
+            .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) {
+    cache.asMap().keySet().removeIf(k -> k.toString().startsWith(prefix));
+  }
+
+  @Override
+  public long size() {
+    cache.cleanUp();
+    return cache.estimatedSize();
+  }
+
+  @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..3beaad2e5d
--- /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 (as a String) 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/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..11304ef6e3
--- /dev/null
+++ b/core/src/test/java/org/apache/gravitino/cache/TestGravitinoCache.java
@@ -0,0 +1,189 @@
+/*
+ * 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;
+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();
+    }
+  }
+}

Reply via email to