yuqi1129 commented on code in PR #12417:
URL: https://github.com/apache/gravitino/pull/12417#discussion_r3763039733


##########
core/src/main/java/org/apache/gravitino/cache/CaffeineEntityCache.java:
##########
@@ -83,6 +85,13 @@ public class CaffeineEntityCache extends BaseEntityCache {
 
   private static final Logger LOG = 
LoggerFactory.getLogger(CaffeineEntityCache.class.getName());
 
+  /**
+   * Characters that can separate a cached entity's identifier from a 
descendant's. See {@link
+   * #invalidateHierarchy(EntityCacheKey)} for why both are needed.
+   */
+  private static final List<String> CHILD_KEY_BOUNDARIES =
+      ImmutableList.of(".", HierarchicalSchemaUtil.physicalSeparator());

Review Comment:
   Yes — you are right, and it was a real defect, not a style point.
   
   The cache lives in `RelationalEntityStore`, above the PO layer where 
`SchemaMetaService` runs the logical<->physical conversion 
(`HierarchicalConversionPOStorageOps`), so cached identifiers still carry the 
configured external separator. `\u0001` only exists in backend rows. Scanning 
for the physical separator matched nothing in production; the unit tests only 
passed because they built their names with the physical separator too.
   
   These are the real cache keys dumped from an H2-backed 
`RelationalEntityStore` (default separator, then `|`):
   
   ```
   metalake.catalog.raw:events:SCHEMA            
metalake.catalog.raw|events:SCHEMA
   metalake.catalog.raw:events:2024:SCHEMA       
metalake.catalog.raw|events|2024:SCHEMA
   metalake.catalog.raw:events:2024.t_child:TABLE  
metalake.catalog.raw|events|2024.t_child:TABLE
   ```
   
   Fixed to use `HierarchicalSchemaUtil.schemaSeparator()`. The static 
`CHILD_KEY_BOUNDARIES` constant is gone as well, since the separator is 
config-derived and must be read per call.
   
   One consequence found while fixing this: `EntityCacheKey.toString()` is 
`"<identifier>:<type>"`, and `":"` is also the default separator, so an 
unguarded second scan over a table key would also evict the topic/fileset of 
the same name. Only a schema can nest, and a catalog reaches its nested schemas 
through the `"."` pass, so the second pass now runs for schema keys only, with 
a regression test (`testInvalidateLeafDoesNotEvictSameNameEntityOfAnotherType`).



##########
core/src/main/java/org/apache/gravitino/cache/CaffeineEntityCache.java:
##########
@@ -267,22 +276,36 @@ protected void invalidateExpiredItem(EntityCacheKey key) {
 
   /**
    * Removes the entry for the given key and all cached descendant entries. 
Descendants are found
-   * through the prefix index: every child identifier starts with {@code 
parent identifier + "."},
-   * so the scan is exact for children and never matches siblings sharing a 
name prefix (e.g. {@code
-   * catalog1} vs {@code catalog10}).
+   * through the prefix index, scanning once per child boundary:
+   *
+   * <ul>
+   *   <li>{@code "."} separates {@link org.apache.gravitino.NameIdentifier} 
levels, so it matches

Review Comment:
   Fixed: `{@link org.apache.gravitino.NameIdentifier}` -> `{@link 
NameIdentifier}`, the class is already imported.



##########
core/src/test/java/org/apache/gravitino/cache/TestCaffeineEntityCacheInvalidation.java:
##########
@@ -155,6 +167,108 @@ void testGetIfPresentReturnsCachedEntity() {
         cache.getIfPresent(catalog.nameIdentifier(), 
Entity.EntityType.SCHEMA).isEmpty());
   }
 
+  @Test
+  void testInvalidateHierarchicalSchemaCascadesToNestedSchemas() {
+    // A HierarchicalSchema nests inside a single name level, joined by the 
physical separator,
+    // so "raw:events:2024" is a child of "raw:events" without adding a 
NameIdentifier level.
+    String parentName = hierarchicalName("raw", "events");
+    String childName = hierarchicalName("raw", "events", "2024");
+
+    SchemaEntity parent = TestUtil.getTestSchemaEntity(2L, parentName, 
CATALOG_NS, "cmt");
+    SchemaEntity child = TestUtil.getTestSchemaEntity(3L, childName, 
CATALOG_NS, "cmt");
+    TableEntity tableInParent =
+        TestUtil.getTestTableEntity(4L, "t_parent", 
schemaNamespace(parentName));
+    TableEntity tableInChild =
+        TestUtil.getTestTableEntity(5L, "t_child", schemaNamespace(childName));
+
+    cache.put(parent);
+    cache.put(child);
+    cache.put(tableInParent);
+    cache.put(tableInChild);
+    Assertions.assertEquals(4, cache.size());
+
+    cache.invalidate(parent.nameIdentifier(), Entity.EntityType.SCHEMA);
+
+    Assertions.assertFalse(cache.contains(parent.nameIdentifier(), 
Entity.EntityType.SCHEMA));
+    Assertions.assertFalse(cache.contains(tableInParent.nameIdentifier(), 
Entity.EntityType.TABLE));
+    Assertions.assertFalse(cache.contains(child.nameIdentifier(), 
Entity.EntityType.SCHEMA));
+    Assertions.assertFalse(cache.contains(tableInChild.nameIdentifier(), 
Entity.EntityType.TABLE));
+    Assertions.assertEquals(0, cache.size());
+  }
+
+  @Test
+  void testInvalidateHierarchicalSchemaCascadesToAnyDepth() {
+    String level1 = hierarchicalName("raw");
+    String level2 = hierarchicalName("raw", "events");
+    String level3 = hierarchicalName("raw", "events", "2024");
+    String level4 = hierarchicalName("raw", "events", "2024", "q1");
+
+    cache.put(TestUtil.getTestSchemaEntity(2L, level1, CATALOG_NS, "cmt"));
+    cache.put(TestUtil.getTestSchemaEntity(3L, level2, CATALOG_NS, "cmt"));
+    cache.put(TestUtil.getTestSchemaEntity(4L, level3, CATALOG_NS, "cmt"));
+    SchemaEntity deepest = TestUtil.getTestSchemaEntity(5L, level4, 
CATALOG_NS, "cmt");
+    cache.put(deepest);
+    TableEntity deepestTable = TestUtil.getTestTableEntity(6L, "t_deep", 
schemaNamespace(level4));
+    cache.put(deepestTable);
+    Assertions.assertEquals(5, cache.size());
+
+    cache.invalidate(
+        TestUtil.getTestSchemaEntity(2L, level1, CATALOG_NS, 
"cmt").nameIdentifier(),
+        Entity.EntityType.SCHEMA);
+
+    Assertions.assertFalse(cache.contains(deepest.nameIdentifier(), 
Entity.EntityType.SCHEMA));
+    Assertions.assertFalse(cache.contains(deepestTable.nameIdentifier(), 
Entity.EntityType.TABLE));
+    Assertions.assertEquals(0, cache.size());
+  }
+
+  @Test
+  void testInvalidateHierarchicalSchemaDoesNotTouchSiblings() {
+    // Guards against over-matching: "raw:events2" is a sibling of 
"raw:events", not a descendant,
+    // exactly like the catalog1 / catalog10 case the "." boundary already 
protects against.
+    String target = hierarchicalName("raw", "events");
+    String sibling = hierarchicalName("raw", "events2");
+    String siblingOfParent = hierarchicalName("raw2", "events");
+
+    SchemaEntity targetSchema = TestUtil.getTestSchemaEntity(2L, target, 
CATALOG_NS, "cmt");
+    SchemaEntity siblingSchema = TestUtil.getTestSchemaEntity(3L, sibling, 
CATALOG_NS, "cmt");
+    SchemaEntity otherBranch = TestUtil.getTestSchemaEntity(4L, 
siblingOfParent, CATALOG_NS, "cmt");
+    TableEntity siblingTable =
+        TestUtil.getTestTableEntity(5L, "t_sibling", schemaNamespace(sibling));
+
+    cache.put(targetSchema);
+    cache.put(siblingSchema);
+    cache.put(otherBranch);
+    cache.put(siblingTable);
+
+    cache.invalidate(targetSchema.nameIdentifier(), Entity.EntityType.SCHEMA);
+
+    Assertions.assertFalse(cache.contains(targetSchema.nameIdentifier(), 
Entity.EntityType.SCHEMA));
+    Assertions.assertTrue(cache.contains(siblingSchema.nameIdentifier(), 
Entity.EntityType.SCHEMA));
+    Assertions.assertTrue(cache.contains(otherBranch.nameIdentifier(), 
Entity.EntityType.SCHEMA));
+    Assertions.assertTrue(cache.contains(siblingTable.nameIdentifier(), 
Entity.EntityType.TABLE));
+    Assertions.assertEquals(3, cache.size());
+  }
+
+  @Test
+  void testInvalidateCatalogCascadesToHierarchicalSchemas() {
+    CatalogEntity catalog =
+        TestUtil.getTestCatalogEntity(1L, "catalog1", 
Namespace.of("metalake"), "hive", "cmt");
+    String nested = hierarchicalName("raw", "events", "2024");
+    SchemaEntity schema = TestUtil.getTestSchemaEntity(2L, nested, CATALOG_NS, 
"cmt");
+    TableEntity table = TestUtil.getTestTableEntity(3L, "t1", 
schemaNamespace(nested));
+
+    cache.put(catalog);
+    cache.put(schema);
+    cache.put(table);
+    Assertions.assertEquals(3, cache.size());
+
+    cache.invalidate(catalog.nameIdentifier(), Entity.EntityType.CATALOG);
+
+    Assertions.assertFalse(cache.contains(schema.nameIdentifier(), 
Entity.EntityType.SCHEMA));
+    Assertions.assertFalse(cache.contains(table.nameIdentifier(), 
Entity.EntityType.TABLE));
+    Assertions.assertEquals(0, cache.size());
+  }
+

Review Comment:
   Both added.
   
   `TestRelationalEntityStoreHierarchicalCache` drives a real H2-backed 
`RelationalEntityStore` with the cache enabled, parameterized over the default 
separator `:` and a non-default `|`. It writes `raw:events`, the nested 
`raw:events:2024`, a table inside the nested schema and the sibling 
`raw:events2`, reads them back through the store, drops `raw:events` with 
cascade, then asserts the nested schema and its table are evicted while the 
sibling survives. It also asserts that no cache key contains the physical 
separator, which is what pinned down the wrong boundary in the first place. 
Both parameter sets fail before the fix.
   
   `TestCaffeineEntityCacheInvalidation` gets the cache-level counterpart, 
`testInvalidateHierarchicalSchemaCascadesWithNonDefaultSeparator` (separator 
`|`), which also fails before the fix.
   
   The store-level test runs against H2 by default; MySQL/PostgreSQL come from 
CI.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to