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

roryqi 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 3460999096 [#9799] improvement(core): Cache non-existence relational 
data for next access (#9800)
3460999096 is described below

commit 34609990967051ef8da2da23548c0cd83fc10733
Author: Qi Yu <[email protected]>
AuthorDate: Thu Feb 26 10:11:07 2026 +0800

    [#9799] improvement(core): Cache non-existence relational data for next 
access (#9800)
    
    ### What changes were proposed in this pull request?
    
    This pull request improves the handling of empty entity lists in the
    caching logic and adds comprehensive tests to verify cache behavior when
    entity relations change. The changes ensure that empty lists are now
    cached, and that cache invalidation works correctly when new relations
    are added between entities.
    
    ### Why are the changes needed?
    
    To improve load performance.
    
    Fix: #9799
    
    ### Does this PR introduce _any_ user-facing change?
    
    N/A
    
    ### How was this patch tested?
    
    UTs
---
 .../gravitino/cache/CaffeineEntityCache.java       |   6 -
 .../storage/TestEntityStorageRelationCache.java    | 226 +++++++++++++++++++++
 2 files changed, 226 insertions(+), 6 deletions(-)

diff --git 
a/core/src/main/java/org/apache/gravitino/cache/CaffeineEntityCache.java 
b/core/src/main/java/org/apache/gravitino/cache/CaffeineEntityCache.java
index 316d4a3e01..40036e1bc7 100644
--- a/core/src/main/java/org/apache/gravitino/cache/CaffeineEntityCache.java
+++ b/core/src/main/java/org/apache/gravitino/cache/CaffeineEntityCache.java
@@ -287,12 +287,6 @@ public class CaffeineEntityCache extends BaseEntityCache {
     segmentedLock.withLock(
         entityCacheKey,
         () -> {
-          // Return directly if entities are empty. No need to put an empty 
list to cache, we will
-          // use another PR to resolve the performance problem.
-          if (entities.isEmpty()) {
-            return;
-          }
-
           syncEntitiesToCache(
               entityCacheKey, entities.stream().map(e -> (Entity) 
e).collect(Collectors.toList()));
         });
diff --git 
a/core/src/test/java/org/apache/gravitino/storage/TestEntityStorageRelationCache.java
 
b/core/src/test/java/org/apache/gravitino/storage/TestEntityStorageRelationCache.java
index 2b73260790..3c3bb95045 100644
--- 
a/core/src/test/java/org/apache/gravitino/storage/TestEntityStorageRelationCache.java
+++ 
b/core/src/test/java/org/apache/gravitino/storage/TestEntityStorageRelationCache.java
@@ -842,4 +842,230 @@ public class TestEntityStorageRelationCache extends 
AbstractEntityStorageTest {
       destroy(type);
     }
   }
+
+  @ParameterizedTest
+  @MethodSource("storageProvider")
+  void testCacheInvalidationOnNewRelation(String type, boolean enableCache) 
throws Exception {
+    Config config = Mockito.mock(Config.class);
+    Mockito.when(config.get(Configs.CACHE_ENABLED)).thenReturn(enableCache);
+    init(type, config);
+
+    AuditInfo auditInfo =
+        
AuditInfo.builder().withCreator("creator").withCreateTime(Instant.now()).build();
+
+    try (EntityStore store = EntityStoreFactory.createEntityStore(config)) {
+      store.initialize(config);
+
+      BaseMetalake metalake =
+          createBaseMakeLake(RandomIdGenerator.INSTANCE.nextId(), "metalake", 
auditInfo);
+      store.put(metalake, false);
+
+      CatalogEntity catalog =
+          createCatalog(
+              RandomIdGenerator.INSTANCE.nextId(),
+              NamespaceUtil.ofCatalog("metalake"),
+              "catalog",
+              auditInfo);
+      store.put(catalog, false);
+
+      SupportsRelationOperations relationOperations = 
(SupportsRelationOperations) store;
+
+      // 1. Fetch relation, it should be empty
+      List<TagEntity> tags =
+          relationOperations.listEntitiesByRelation(
+              SupportsRelationOperations.Type.TAG_METADATA_OBJECT_REL,
+              catalog.nameIdentifier(),
+              Entity.EntityType.CATALOG,
+              true);
+      Assertions.assertTrue(tags.isEmpty());
+
+      // 2. Verify cache has empty list if cache is enabled
+      if (enableCache && store instanceof RelationalEntityStore) {
+        RelationalEntityStore relationalEntityStore = (RelationalEntityStore) 
store;
+        if (relationalEntityStore.getCache() instanceof CaffeineEntityCache) {
+          CaffeineEntityCache cache = (CaffeineEntityCache) 
relationalEntityStore.getCache();
+          List<Entity> cachedEntities =
+              cache
+                  .getCacheData()
+                  .getIfPresent(
+                      EntityCacheRelationKey.of(
+                          catalog.nameIdentifier(),
+                          Entity.EntityType.CATALOG,
+                          
SupportsRelationOperations.Type.TAG_METADATA_OBJECT_REL));
+          Assertions.assertNotNull(cachedEntities);
+          Assertions.assertTrue(cachedEntities.isEmpty());
+        }
+      }
+
+      // 3. Create a tag and add relation
+      Namespace tagNamespace = NameIdentifierUtil.ofTag("metalake", 
"tag1").namespace();
+      TagEntity tag1 =
+          TagEntity.builder()
+              .withId(RandomIdGenerator.INSTANCE.nextId())
+              .withNamespace(tagNamespace)
+              .withName("tag1")
+              .withAuditInfo(auditInfo)
+              .withProperties(Collections.emptyMap())
+              .build();
+      store.put(tag1, false);
+
+      relationOperations.updateEntityRelations(
+          SupportsRelationOperations.Type.TAG_METADATA_OBJECT_REL,
+          catalog.nameIdentifier(),
+          Entity.EntityType.CATALOG,
+          new NameIdentifier[] {tag1.nameIdentifier()},
+          new NameIdentifier[] {});
+
+      // 4. Fetch relation again, it should not be empty
+      tags =
+          relationOperations.listEntitiesByRelation(
+              SupportsRelationOperations.Type.TAG_METADATA_OBJECT_REL,
+              catalog.nameIdentifier(),
+              Entity.EntityType.CATALOG,
+              true);
+      Assertions.assertEquals(1, tags.size());
+      Assertions.assertEquals(tag1.name(), tags.get(0).name());
+
+      List<UserEntity> owners =
+          relationOperations.listEntitiesByRelation(
+              SupportsRelationOperations.Type.OWNER_REL,
+              catalog.nameIdentifier(),
+              Entity.EntityType.CATALOG,
+              true);
+      Assertions.assertTrue(owners.isEmpty());
+
+      if (enableCache && store instanceof RelationalEntityStore) {
+        RelationalEntityStore relationalEntityStore = (RelationalEntityStore) 
store;
+        if (relationalEntityStore.getCache() instanceof CaffeineEntityCache) {
+          CaffeineEntityCache cache = (CaffeineEntityCache) 
relationalEntityStore.getCache();
+          List<Entity> cachedOwners =
+              cache
+                  .getCacheData()
+                  .getIfPresent(
+                      EntityCacheRelationKey.of(
+                          catalog.nameIdentifier(),
+                          Entity.EntityType.CATALOG,
+                          SupportsRelationOperations.Type.OWNER_REL));
+          Assertions.assertNotNull(cachedOwners);
+          Assertions.assertTrue(cachedOwners.isEmpty());
+        }
+      }
+
+      UserEntity ownerUser =
+          createUserEntity(
+              RandomIdGenerator.INSTANCE.nextId(),
+              AuthorizationUtils.ofUserNamespace("metalake"),
+              "ownerUser",
+              auditInfo);
+      store.put(ownerUser, false);
+
+      relationOperations.insertRelation(
+          SupportsRelationOperations.Type.OWNER_REL,
+          catalog.nameIdentifier(),
+          Entity.EntityType.CATALOG,
+          ownerUser.nameIdentifier(),
+          Entity.EntityType.USER,
+          true);
+
+      owners =
+          relationOperations.listEntitiesByRelation(
+              SupportsRelationOperations.Type.OWNER_REL,
+              catalog.nameIdentifier(),
+              Entity.EntityType.CATALOG,
+              true);
+      Assertions.assertEquals(1, owners.size());
+      Assertions.assertEquals(ownerUser.name(), owners.get(0).name());
+
+      destroy(type);
+    }
+  }
+
+  @ParameterizedTest
+  @MethodSource("storageProvider")
+  void testCacheInvalidationOnNewRelationReverse(String type, boolean 
enableCache)
+      throws Exception {
+    Config config = Mockito.mock(Config.class);
+    Mockito.when(config.get(Configs.CACHE_ENABLED)).thenReturn(enableCache);
+    init(type, config);
+
+    AuditInfo auditInfo =
+        
AuditInfo.builder().withCreator("creator").withCreateTime(Instant.now()).build();
+
+    try (EntityStore store = EntityStoreFactory.createEntityStore(config)) {
+      store.initialize(config);
+
+      BaseMetalake metalake =
+          createBaseMakeLake(RandomIdGenerator.INSTANCE.nextId(), "metalake", 
auditInfo);
+      store.put(metalake, false);
+
+      CatalogEntity catalog =
+          createCatalog(
+              RandomIdGenerator.INSTANCE.nextId(),
+              NamespaceUtil.ofCatalog("metalake"),
+              "catalog",
+              auditInfo);
+      store.put(catalog, false);
+
+      Namespace tagNamespace = NameIdentifierUtil.ofTag("metalake", 
"tag1").namespace();
+      TagEntity tag1 =
+          TagEntity.builder()
+              .withId(RandomIdGenerator.INSTANCE.nextId())
+              .withNamespace(tagNamespace)
+              .withName("tag1")
+              .withAuditInfo(auditInfo)
+              .withProperties(Collections.emptyMap())
+              .build();
+      store.put(tag1, false);
+
+      SupportsRelationOperations relationOperations = 
(SupportsRelationOperations) store;
+
+      // 1. Fetch relation for Tag (Target side), it should be empty
+      // This populates the cache for (Tag, TAG, REL) with empty list
+      List<GenericEntity> entities =
+          relationOperations.listEntitiesByRelation(
+              SupportsRelationOperations.Type.TAG_METADATA_OBJECT_REL,
+              tag1.nameIdentifier(),
+              Entity.EntityType.TAG,
+              true);
+      Assertions.assertTrue(entities.isEmpty());
+
+      // 2. Verify cache has empty list if cache is enabled
+      if (enableCache && store instanceof RelationalEntityStore) {
+        RelationalEntityStore relationalEntityStore = (RelationalEntityStore) 
store;
+        if (relationalEntityStore.getCache() instanceof CaffeineEntityCache) {
+          CaffeineEntityCache cache = (CaffeineEntityCache) 
relationalEntityStore.getCache();
+          List<Entity> cachedEntities =
+              cache
+                  .getCacheData()
+                  .getIfPresent(
+                      EntityCacheRelationKey.of(
+                          tag1.nameIdentifier(),
+                          Entity.EntityType.TAG,
+                          
SupportsRelationOperations.Type.TAG_METADATA_OBJECT_REL));
+          Assertions.assertNotNull(cachedEntities);
+          Assertions.assertTrue(cachedEntities.isEmpty());
+        }
+      }
+
+      // 3. Add relation from Catalog (Source side)
+      relationOperations.updateEntityRelations(
+          SupportsRelationOperations.Type.TAG_METADATA_OBJECT_REL,
+          catalog.nameIdentifier(),
+          Entity.EntityType.CATALOG,
+          new NameIdentifier[] {tag1.nameIdentifier()},
+          new NameIdentifier[] {});
+
+      // 4. Fetch relation for Tag again, it should NOT be empty
+      entities =
+          relationOperations.listEntitiesByRelation(
+              SupportsRelationOperations.Type.TAG_METADATA_OBJECT_REL,
+              tag1.nameIdentifier(),
+              Entity.EntityType.TAG,
+              true);
+      Assertions.assertEquals(1, entities.size());
+      Assertions.assertEquals(catalog.name(), entities.get(0).name());
+
+      destroy(type);
+    }
+  }
 }

Reply via email to