This is an automated email from the ASF dual-hosted git repository.
yuqi4733 pushed a commit to branch branch-1.1
in repository https://gitbox.apache.org/repos/asf/gravitino.git
The following commit(s) were added to refs/heads/branch-1.1 by this push:
new a7f1b20202 [#9799] improvement(core): Cache non-existence relational
data for next access (#9800)(cherry-pick) (#10186)
a7f1b20202 is described below
commit a7f1b202026e0a3399d8c829374fea4d4608687b
Author: Qi Yu <[email protected]>
AuthorDate: Wed Mar 4 16:04:42 2026 +0800
[#9799] improvement(core): Cache non-existence relational data for next
access (#9800)(cherry-pick) (#10186)
### 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 b645d8126a..457c7baedf 100644
---
a/core/src/test/java/org/apache/gravitino/storage/TestEntityStorageRelationCache.java
+++
b/core/src/test/java/org/apache/gravitino/storage/TestEntityStorageRelationCache.java
@@ -773,4 +773,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);
+ }
+ }
}