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

jerryshao 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 c49205be57 [#13462] fix(cache): reject nested clear that hangs core 
tests (#13463)
c49205be57 is described below

commit c49205be572f62149222e00cd45167f1ff95a451
Author: Qi Yu <[email protected]>
AuthorDate: Wed Sep 23 14:26:31 2026 +0800

    [#13462] fix(cache): reject nested clear that hangs core tests (#13463)
    
    ### What changes were proposed in this pull request?
    
    Replace the batch-get test's nested cache clear with an unrelated-key
    invalidation, which still exercises the post-put epoch check. Make
    `SegmentedLock.withGlobalLock` reject a call from a thread holding its
    segment read lock, add a regression test, and correct the write-back
    comments.
    
    ### Why are the changes needed?
    
    Since #13400, a segment operation holds a read lock until it finishes. A
    clear invoked from that operation cannot upgrade to the global write
    lock and hangs indefinitely. The test introduced in #13374 made exactly
    that call. Production code now fails fast if the same invalid call is
    made in the future; the revised test covers an invalidation that can
    occur during write-back.
    
    Fix: #13462
    
    ### Does this PR introduce _any_ user-facing change?
    
    No new API or configuration. An invalid nested cache clear now throws
    `IllegalStateException` instead of hanging.
    
    ### How was this patch tested?
    
    - `./gradlew :core:test --tests 'org.apache.gravitino.cache.*' --tests
    'org.apache.gravitino.storage.relational.TestRelationalEntityStore*'
    --tests 'org.apache.gravitino.storage.relational.TestEntityCache*'
    --tests 'org.apache.gravitino.storage.relational.TestEntityChangeLog*'
    -PskipITs` — 142 tests passed.
    - `./gradlew :core:spotlessCheck -PskipITs` — passed.
---
 .../org/apache/gravitino/cache/SegmentedLock.java  |  9 ++++++++-
 .../storage/relational/RelationalEntityStore.java  |  4 ++--
 .../apache/gravitino/cache/TestSegmentedLock.java  | 22 ++++++++++++++++++++++
 .../TestRelationalEntityStoreBatchGetLateFill.java | 17 +++++++++++++++--
 4 files changed, 47 insertions(+), 5 deletions(-)

diff --git a/core/src/main/java/org/apache/gravitino/cache/SegmentedLock.java 
b/core/src/main/java/org/apache/gravitino/cache/SegmentedLock.java
index b6b2d5a8dc..fa4f52daa2 100644
--- a/core/src/main/java/org/apache/gravitino/cache/SegmentedLock.java
+++ b/core/src/main/java/org/apache/gravitino/cache/SegmentedLock.java
@@ -231,11 +231,18 @@ public class SegmentedLock {
    * across their whole critical section, and the write lock acquired here 
waits for all of them.
    *
    * <p>Must not be called from inside a {@code withLock} action on the same 
instance: the read lock
-   * cannot upgrade to the write lock, so such a call would deadlock.
+   * cannot upgrade to the write lock. Such a call fails immediately instead 
of deadlocking.
    *
    * @param action The clearing action to execute
+   * @throws IllegalStateException if the current thread holds this lock's 
read lock or another
+   *     global operation is in progress
    */
   public void withGlobalLock(Runnable action) {
+    if (globalGate.getReadHoldCount() > 0) {
+      throw new IllegalStateException(
+          "Cannot start a global operation while holding a segment lock");
+    }
+
     // Mark the global operation in progress, fail if another one is already 
running
     if (!clearing.compareAndSet(false, true)) {
       throw new IllegalStateException("Global operation already in progress");
diff --git 
a/core/src/main/java/org/apache/gravitino/storage/relational/RelationalEntityStore.java
 
b/core/src/main/java/org/apache/gravitino/storage/relational/RelationalEntityStore.java
index eda8be4d96..748518fbda 100644
--- 
a/core/src/main/java/org/apache/gravitino/storage/relational/RelationalEntityStore.java
+++ 
b/core/src/main/java/org/apache/gravitino/storage/relational/RelationalEntityStore.java
@@ -286,8 +286,8 @@ public class RelationalEntityStore
           () -> {
             if (cacheInvalidationEpoch.get() == epochBeforeRead) {
               cache.put(entity);
-              // A whole-cache clear can run while this key lock is held. If 
it happened during
-              // put, remove the value we may have written after the clear.
+              // Invalidation of another key or an ancestor can advance the 
epoch while this key
+              // lock is held. Remove the value if that happened during the 
put.
               if (cacheInvalidationEpoch.get() != epochBeforeRead) {
                 cache.invalidate(entity.nameIdentifier(), entity.type());
               }
diff --git 
a/core/src/test/java/org/apache/gravitino/cache/TestSegmentedLock.java 
b/core/src/test/java/org/apache/gravitino/cache/TestSegmentedLock.java
index edce98096a..483520176e 100644
--- a/core/src/test/java/org/apache/gravitino/cache/TestSegmentedLock.java
+++ b/core/src/test/java/org/apache/gravitino/cache/TestSegmentedLock.java
@@ -382,6 +382,28 @@ public class TestSegmentedLock {
         });
   }
 
+  @Test
+  @Timeout(5)
+  void testGlobalClearingFromSegmentOperationFailsWithoutPoisoningLock() {
+    SegmentedLock lock = new SegmentedLock(4);
+    AtomicBoolean globalActionRan = new AtomicBoolean(false);
+
+    lock.withLock(
+        "key",
+        () -> {
+          IllegalStateException exception =
+              assertThrows(
+                  IllegalStateException.class,
+                  () -> lock.withGlobalLock(() -> globalActionRan.set(true)));
+          assertTrue(exception.getMessage().contains("segment lock"));
+          assertFalse(lock.isClearing());
+        });
+
+    assertFalse(globalActionRan.get());
+    assertDoesNotThrow(() -> lock.withGlobalLock(() -> 
globalActionRan.set(true)));
+    assertTrue(globalActionRan.get());
+  }
+
   @Test
   @Timeout(30)
   void testGlobalClearingWaitsForInFlightOperations() throws 
InterruptedException {
diff --git 
a/core/src/test/java/org/apache/gravitino/storage/relational/TestRelationalEntityStoreBatchGetLateFill.java
 
b/core/src/test/java/org/apache/gravitino/storage/relational/TestRelationalEntityStoreBatchGetLateFill.java
index 8ac5da1689..0a0b8892ab 100644
--- 
a/core/src/test/java/org/apache/gravitino/storage/relational/TestRelationalEntityStoreBatchGetLateFill.java
+++ 
b/core/src/test/java/org/apache/gravitino/storage/relational/TestRelationalEntityStoreBatchGetLateFill.java
@@ -21,6 +21,8 @@ package org.apache.gravitino.storage.relational;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.eq;
 
+import java.io.IOException;
+import java.io.UncheckedIOException;
 import java.time.Instant;
 import java.util.List;
 import org.apache.commons.lang3.reflect.FieldUtils;
@@ -159,10 +161,21 @@ public class TestRelationalEntityStoreBatchGetLateFill {
   }
 
   @Test
-  void testBatchGetRemovesValueWrittenAfterClear() throws 
IllegalAccessException {
+  void testBatchGetRemovesValueWrittenAfterInvalidationDuringPut() throws 
IllegalAccessException {
     TableEntity table = TestUtil.getTestTableEntity(1L, "t1", SCHEMA_NS);
     RecordingCache recordingCache = new RecordingCache();
-    recordingCache.beforePut = store::clearCache;
+    // Advance the epoch from inside the write-back, while this key's cache 
lock is held. Invalidate
+    // an unrelated entity so this entry is removed only by the post-put epoch 
check. A whole-cache
+    // clear cannot run from inside the segment operation.
+    NameIdentifier unrelatedIdent = NameIdentifier.of(SCHEMA_NS, "t2");
+    recordingCache.beforePut =
+        () -> {
+          try {
+            store.delete(unrelatedIdent, Entity.EntityType.TABLE, false);
+          } catch (IOException e) {
+            throw new UncheckedIOException(e);
+          }
+        };
     FieldUtils.writeField(store, "cache", recordingCache, true);
     Mockito.when(backend.batchGet(any(), 
eq(Entity.EntityType.TABLE))).thenReturn(List.of(table));
 

Reply via email to