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

clintropolis pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/druid.git


The following commit(s) were added to refs/heads/master by this push:
     new 80d6991eadd fix: acquire cached unmount race (#19873)
80d6991eadd is described below

commit 80d6991eadd1daeaaa11f037e1f5ea0e0c76e186
Author: Clint Wylie <[email protected]>
AuthorDate: Tue Aug 4 13:33:19 2026 -0700

    fix: acquire cached unmount race (#19873)
---
 .../segment/loading/SegmentLocalCacheManager.java  | 14 +++++++++-
 .../loading/SegmentLocalCacheManagerTest.java      | 31 ++++++++++++++++++++++
 2 files changed, 44 insertions(+), 1 deletion(-)

diff --git 
a/server/src/main/java/org/apache/druid/segment/loading/SegmentLocalCacheManager.java
 
b/server/src/main/java/org/apache/druid/segment/loading/SegmentLocalCacheManager.java
index 8ee13001f23..7764c62a153 100644
--- 
a/server/src/main/java/org/apache/druid/segment/loading/SegmentLocalCacheManager.java
+++ 
b/server/src/main/java/org/apache/druid/segment/loading/SegmentLocalCacheManager.java
@@ -1299,8 +1299,20 @@ public class SegmentLocalCacheManager implements 
SegmentCacheManager
                                 );
           }
           if (complete.isMounted()) {
+            // the entry is already mounted, so hand back its cached reference 
provider. Read the volatile
+            // referenceProvider exactly once, inside the supplier, rather 
than trusting the isMounted() check above and
+            // reading the field again when the supplier runs later: for a 
static (non-virtual-storage) entry a
+            // concurrent drop (release() -> unmount()) can null it in 
between. The reservation hold does not prevent
+            // this (it only guards weak entries against reclaim), so a weak 
entry would stay mounted here, but a
+            // static entry can be unmounted out from under us. A dropped 
entry is reported as absent (empty) rather
+            // than reloaded.
             return new AcquireSegmentAction(
-                () -> 
Futures.immediateFuture(AcquireSegmentResult.cached(complete.referenceProvider)),
+                () -> {
+                  final ReferenceCountedSegmentProvider provider = 
complete.referenceProvider;
+                  return Futures.immediateFuture(
+                      provider != null ? AcquireSegmentResult.cached(provider) 
: AcquireSegmentResult.empty()
+                  );
+                },
                 hold
             );
           } else {
diff --git 
a/server/src/test/java/org/apache/druid/segment/loading/SegmentLocalCacheManagerTest.java
 
b/server/src/test/java/org/apache/druid/segment/loading/SegmentLocalCacheManagerTest.java
index 458da17f219..d0ec702926b 100644
--- 
a/server/src/test/java/org/apache/druid/segment/loading/SegmentLocalCacheManagerTest.java
+++ 
b/server/src/test/java/org/apache/druid/segment/loading/SegmentLocalCacheManagerTest.java
@@ -918,6 +918,37 @@ public class SegmentLocalCacheManagerTest extends 
InitializedNullHandlingTest
     segmentActionAfterDrop.close();
   }
 
+  @Test
+  public void 
testAcquireExistingSegmentDroppedBeforeSupplierRunsReportsAbsent() throws 
Exception
+  {
+    final DataSegment segmentToLoad = 
makeTestDataSegment(segmentDeepStorageDir);
+    final File localSegmentFile = new File(segmentDeepStorageDir, 
TEST_DATA_RELATIVE_PATH);
+    makeSegmentZip(
+        localSegmentFile,
+        new File(segmentDeepStorageDir.getCanonicalPath() + "/" + 
TEST_DATA_RELATIVE_PATH + "/index.zip")
+    );
+
+    manager.load(segmentToLoad);
+    Assert.assertTrue("segment should be cached (static, mounted) after load", 
manager.isSegmentCached(segmentToLoad));
+
+    // Take the already-loaded fast path, but do NOT invoke the supplier yet 
(getSegmentFuture() is what runs it).
+    final AcquireSegmentAction action = manager.acquireSegment(segmentToLoad, 
AcquireMode.FULL);
+
+    // Drop the segment: for a static entry release() unmounts it immediately, 
nulling referenceProvider out from
+    // under the still-outstanding (no-op) hold.
+    manager.drop(segmentToLoad);
+
+    // Invoking the supplier now must not NPE; the segment is reported absent 
(empty) instead.
+    final AcquireSegmentResult result = action.getSegmentFuture().get();
+    Assert.assertNotNull("reference provider must never be null", 
result.getReferenceProvider());
+    Assert.assertFalse(
+        "a segment dropped before the supplier ran should be reported absent",
+        result.getReferenceProvider().acquireReference().isPresent()
+    );
+
+    action.close();
+  }
+
   @Test
   public void testVirtualStorageRejectsNonPositiveLoadThreads()
   {


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to