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

yuqi1129 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 933401bda9 [#12504] fix(core): Fix flaky 
TestCatalogManager.testCatalogCacheRemoveListener (#12514)
933401bda9 is described below

commit 933401bda9da09fcfa1a5bf4048a92bce1ab8460
Author: Qi Yu <[email protected]>
AuthorDate: Wed Aug 26 17:16:29 2026 +0800

    [#12504] fix(core): Fix flaky 
TestCatalogManager.testCatalogCacheRemoveListener (#12514)
    
    ### What changes were proposed in this pull request?
    
    1. `testCatalogCacheRemoveListener` now runs against its own
    `CatalogManager` and `InMemoryEntityStore` instead of the `static`
    instance shared by the whole test class.
    2. `reset()` (`@BeforeEach`/`@AfterEach`) additionally invalidates the
    shared catalog cache, so cache entries no longer leak from one test
    method to the next.
    3. `CatalogManager.removalListeners` becomes a `CopyOnWriteArrayList`.
    
    ### Why are the changes needed?
    
    The shared `CatalogManager` is created once in `@BeforeAll` and
    `reset()` only cleared `entityStore`, so its catalog cache accumulated
    entries across test methods. Instrumenting the test showed 4 leftover
    entries present when it starts:
    
    ```
    cache size at test start = 4 keys=[metalake.test1, metalake.catalog_rel, 
metalake.catalog_file, metalake.test51]
    ```
    
    Caffeine delivers removal notifications asynchronously, and
    `CatalogManager` has no API to unregister a listener, so those leftover
    removals could reach the listener this test registers, producing the
    reported `expected: <1> but was: <5>` (4 leftovers + its own).
    
    `removalListeners` was a plain `ArrayList` iterated on cache executor
    threads while `addCatalogCacheRemoveListener` may append concurrently —
    unsafe regardless of the flaky test.
    
    Fix: #12504
    
    ### Does this PR introduce _any_ user-facing change?
    
    No.
    
    ### How was this patch tested?
    
    - `./gradlew :core:test --tests
    "org.apache.gravitino.catalog.TestCatalogManager" -PskipITs` passes.
    - Temporarily annotated the class with
    `@TestMethodOrder(MethodOrderer.Random.class)` and re-ran it 3 times
    with `--rerun-tasks`; all green (annotation not included in the commit).
---
 .../apache/gravitino/catalog/CatalogManager.java   |  5 +-
 .../gravitino/catalog/TestCatalogManager.java      | 63 +++++++++++++---------
 2 files changed, 43 insertions(+), 25 deletions(-)

diff --git 
a/core/src/main/java/org/apache/gravitino/catalog/CatalogManager.java 
b/core/src/main/java/org/apache/gravitino/catalog/CatalogManager.java
index b6090c9671..c5a205fb24 100644
--- a/core/src/main/java/org/apache/gravitino/catalog/CatalogManager.java
+++ b/core/src/main/java/org/apache/gravitino/catalog/CatalogManager.java
@@ -53,6 +53,7 @@ import java.util.Properties;
 import java.util.ServiceLoader;
 import java.util.Set;
 import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.CopyOnWriteArrayList;
 import java.util.concurrent.ScheduledThreadPoolExecutor;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicInteger;
@@ -367,7 +368,9 @@ public class CatalogManager implements CatalogDispatcher, 
Closeable {
 
   private final SecretManager secretManager;
 
-  private final List<Consumer<NameIdentifier>> removalListeners = 
Lists.newArrayList();
+  // Copy-on-write: listeners may be registered while the cache's removal 
listener (running on a
+  // cache executor thread) is iterating this list.
+  private final List<Consumer<NameIdentifier>> removalListeners = new 
CopyOnWriteArrayList<>();
   private final ConcurrentHashMap<NameIdentifier, AtomicInteger> 
localMutationCounts =
       new ConcurrentHashMap<>();
 
diff --git 
a/core/src/test/java/org/apache/gravitino/catalog/TestCatalogManager.java 
b/core/src/test/java/org/apache/gravitino/catalog/TestCatalogManager.java
index 23b9140a36..b6caa8cbd9 100644
--- a/core/src/test/java/org/apache/gravitino/catalog/TestCatalogManager.java
+++ b/core/src/test/java/org/apache/gravitino/catalog/TestCatalogManager.java
@@ -145,6 +145,9 @@ public class TestCatalogManager {
   void reset() throws IOException {
     ((InMemoryEntityStore) entityStore).clear();
     entityStore.put(metalakeEntity, true);
+    // The shared CatalogManager is created once in @BeforeAll, so its cache 
would otherwise keep
+    // entries created by previously executed test methods and make tests 
order-dependent.
+    catalogManager.getCatalogCache().invalidateAll();
   }
 
   @AfterAll
@@ -1443,37 +1446,49 @@ public class TestCatalogManager {
   }
 
   @Test
-  public void testCatalogCacheRemoveListener() {
+  public void testCatalogCacheRemoveListener() throws IOException {
     NameIdentifier ident = NameIdentifier.of(metalake, "catalog");
     Map<String, String> props =
         ImmutableMap.of(
             PROPERTY_KEY1, "value1", PROPERTY_KEY2, "value2", 
PROPERTY_KEY5_PREFIX + "1", "value3");
 
-    // Create a catalog
-    catalogManager.createCatalog(ident, Catalog.Type.RELATIONAL, provider, 
"comment", props);
+    // Use a dedicated CatalogManager (and entity store) instead of the shared 
static one: the
+    // shared instance keeps cache entries and cache removal listeners 
registered by other test
+    // methods, which would make the assertions below depend on the test 
execution order.
+    EntityStore store = new InMemoryEntityStore();
+    store.initialize(config);
+    store.put(metalakeEntity, true);
 
-    // Load the catalog to add it to the cache
-    catalogManager.loadCatalog(ident);
-    
Assertions.assertNotNull(catalogManager.getCatalogCache().getIfPresent(ident));
+    try (CatalogManager manager =
+        new CatalogManager(config, store, new RandomIdGenerator(), new 
SecretManager(config))) {
+      // Create a catalog
+      manager.createCatalog(ident, Catalog.Type.RELATIONAL, provider, 
"comment", props);
+
+      // Load the catalog to add it to the cache
+      manager.loadCatalog(ident);
+      Assertions.assertNotNull(manager.getCatalogCache().getIfPresent(ident));
+
+      // Add a listener to track removed catalogs
+      Set<NameIdentifier> removedCatalogs = Sets.newConcurrentHashSet();
+      manager.addCatalogCacheRemoveListener(removedCatalogs::add);
 
-    // Add a listener to track removed catalogs
-    Set<NameIdentifier> removedCatalogs = Sets.newConcurrentHashSet();
-    catalogManager.addCatalogCacheRemoveListener(removedCatalogs::add);
-
-    // Invalidate the cache to trigger the removal listener
-    catalogManager.getCatalogCache().invalidate(ident);
-
-    // Wait for the async eviction to complete
-    await()
-        .atMost(Duration.ofSeconds(5))
-        .untilAsserted(
-            () -> {
-              Assertions.assertTrue(
-                  removedCatalogs.contains(ident),
-                  "Listener should be notified of catalog removal");
-              Assertions.assertEquals(
-                  1, removedCatalogs.size(), "Only one catalog should be 
removed");
-            });
+      // Invalidate the cache to trigger the removal listener
+      manager.getCatalogCache().invalidate(ident);
+
+      // Wait for the async eviction to complete
+      await()
+          .atMost(Duration.ofSeconds(5))
+          .untilAsserted(
+              () -> {
+                Assertions.assertTrue(
+                    removedCatalogs.contains(ident),
+                    "Listener should be notified of catalog removal");
+                Assertions.assertEquals(
+                    1, removedCatalogs.size(), "Only one catalog should be 
removed");
+              });
+    } finally {
+      store.close();
+    }
   }
 
   private void testProperties(Map<String, String> expectedProps, Map<String, 
String> testProps) {

Reply via email to