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 7c367db7ded fix: separate storage location accounting for internal 
cache holds (#20150)
7c367db7ded is described below

commit 7c367db7dedd4597f6d04103ed246201f87ca9a6
Author: Clint Wylie <[email protected]>
AuthorDate: Thu Aug 27 12:19:57 2026 -0700

    fix: separate storage location accounting for internal cache holds (#20150)
---
 .../loading/PartialSegmentBundleCacheEntry.java    |   4 +-
 .../loading/PartialSegmentMetadataCacheEntry.java  |  10 +-
 .../druid/segment/loading/StorageLocation.java     | 146 +++++++++++++++++----
 .../loading/VirtualStorageLocationStats.java       |  11 ++
 .../druid/server/metrics/StorageMonitor.java       |  14 ++
 .../loading/PartialSegmentRestoreFromDiskTest.java |   2 +-
 .../druid/segment/loading/StorageLocationTest.java | 101 +++++++++++++-
 7 files changed, 248 insertions(+), 40 deletions(-)

diff --git 
a/server/src/main/java/org/apache/druid/segment/loading/PartialSegmentBundleCacheEntry.java
 
b/server/src/main/java/org/apache/druid/segment/loading/PartialSegmentBundleCacheEntry.java
index a5fe0499828..00d7f90170d 100644
--- 
a/server/src/main/java/org/apache/druid/segment/loading/PartialSegmentBundleCacheEntry.java
+++ 
b/server/src/main/java/org/apache/druid/segment/loading/PartialSegmentBundleCacheEntry.java
@@ -362,7 +362,7 @@ public class PartialSegmentBundleCacheEntry implements 
CacheEntry
     try {
       // 1. Cache holds on metadata + parents (prevents cache eviction of weak 
dependencies)
       final StorageLocation.ReservationHold<?> metadataHold =
-          mountLocation.addWeakReservationHoldIfExists(metadataEntry.getId());
+          
mountLocation.addInternalWeakReservationHoldIfExists(metadataEntry.getId());
       if (metadataHold == null) {
         throw DruidException.defensive(
             "Cannot acquire metadata hold for [%s]; metadata entry not 
registered with location[%s]",
@@ -374,7 +374,7 @@ public class PartialSegmentBundleCacheEntry implements 
CacheEntry
 
       for (PartialSegmentBundleCacheEntryIdentifier parentId : parentEntryIds) 
{
         final StorageLocation.ReservationHold<?> parentHold =
-            mountLocation.addWeakReservationHoldIfExists(parentId);
+            mountLocation.addInternalWeakReservationHoldIfExists(parentId);
         if (parentHold == null) {
           throw DruidException.defensive(
               "Cannot acquire parent hold for [%s]; parent entry not 
registered with location[%s]",
diff --git 
a/server/src/main/java/org/apache/druid/segment/loading/PartialSegmentMetadataCacheEntry.java
 
b/server/src/main/java/org/apache/druid/segment/loading/PartialSegmentMetadataCacheEntry.java
index 8c4d827fb69..1e11d950fe1 100644
--- 
a/server/src/main/java/org/apache/druid/segment/loading/PartialSegmentMetadataCacheEntry.java
+++ 
b/server/src/main/java/org/apache/druid/segment/loading/PartialSegmentMetadataCacheEntry.java
@@ -362,7 +362,7 @@ public class PartialSegmentMetadataCacheEntry implements 
SegmentCacheEntry, Resi
     try {
       final StorageLocation.ReservationHold<PartialSegmentMetadataCacheEntry> 
newSelfHold;
       if (needsSelfHold) {
-        newSelfHold = loc.addWeakReservationHoldIfExists(id);
+        newSelfHold = loc.addInternalWeakReservationHoldIfExists(id);
         if (newSelfHold == null) {
           throw DruidException.defensive(
               "Failed to acquire self-referential rule-hold on partial 
metadata entry[%s]; entry is not weak-reserved",
@@ -376,7 +376,7 @@ public class PartialSegmentMetadataCacheEntry implements 
SegmentCacheEntry, Resi
       final Map<String, 
StorageLocation.ReservationHold<PartialSegmentBundleCacheEntry>> acquired = new 
HashMap<>();
       for (String name : namesToAcquire) {
         final StorageLocation.ReservationHold<PartialSegmentBundleCacheEntry> 
h =
-            loc.addWeakReservationHoldIfExists(new 
PartialSegmentBundleCacheEntryIdentifier(segmentId, name));
+            loc.addInternalWeakReservationHoldIfExists(new 
PartialSegmentBundleCacheEntryIdentifier(segmentId, name));
         if (h != null) {
           acquired.put(name, h);
           uncommittedHolds.add(h);
@@ -463,7 +463,7 @@ public class PartialSegmentMetadataCacheEntry implements 
SegmentCacheEntry, Resi
     final Map<String, 
StorageLocation.ReservationHold<PartialSegmentBundleCacheEntry>> acquired = new 
HashMap<>();
     for (String name : reconcileNames) {
       final StorageLocation.ReservationHold<PartialSegmentBundleCacheEntry> h =
-          loc.addWeakReservationHoldIfExists(new 
PartialSegmentBundleCacheEntryIdentifier(segmentId, name));
+          loc.addInternalWeakReservationHoldIfExists(new 
PartialSegmentBundleCacheEntryIdentifier(segmentId, name));
       if (h != null) {
         acquired.put(name, h);
         uncommittedHolds.add(h);
@@ -967,7 +967,7 @@ public class PartialSegmentMetadataCacheEntry implements 
SegmentCacheEntry, Resi
         // restore hold immediately after, if the entry should remain alive 
for query-side access, the runtime hold
         // chain (transitive parents from aggregates, segment-level holds from 
acquire APIs) keeps it pinned.
         try (StorageLocation.ReservationHold<?> restoreHold =
-                 location.addWeakReservationHold(bundle.getId(), () -> 
bundle)) {
+                 location.addInternalWeakReservationHold(bundle.getId(), () -> 
bundle)) {
           if (restoreHold == null) {
             throw DruidException.defensive(
                 "Failed to reserve bundle entry[%s] in location[%s] while 
restoring from disk",
@@ -1618,7 +1618,7 @@ public class PartialSegmentMetadataCacheEntry implements 
SegmentCacheEntry, Resi
     // ReservationHold on this bundle, so the weak entry is guaranteed present 
and this acquire cannot return null
     // for a "just evicted" reason.
     final StorageLocation.ReservationHold<PartialSegmentBundleCacheEntry> hold 
=
-        loc.addWeakReservationHoldIfExists(new 
PartialSegmentBundleCacheEntryIdentifier(segmentId, bundleName));
+        loc.addInternalWeakReservationHoldIfExists(new 
PartialSegmentBundleCacheEntryIdentifier(segmentId, bundleName));
     if (hold == null) {
       return;
     }
diff --git 
a/server/src/main/java/org/apache/druid/segment/loading/StorageLocation.java 
b/server/src/main/java/org/apache/druid/segment/loading/StorageLocation.java
index 5236aa19689..758d957a014 100644
--- a/server/src/main/java/org/apache/druid/segment/loading/StorageLocation.java
+++ b/server/src/main/java/org/apache/druid/segment/loading/StorageLocation.java
@@ -123,6 +123,14 @@ public class StorageLocation
   private final AtomicLong currWeakSizeBytes = new AtomicLong(0);
   private final AtomicLong currHoldCount = new AtomicLong(0);
   private final AtomicLong currHoldBytes = new AtomicLong(0);
+  /**
+   * The subset of {@link #currHoldCount}/{@link #currHoldBytes} that is 
structural rather than demand (a bundle
+   * pinning its metadata entry or a parent bundle, a partial-load rule 
pinning what it selected, bootstrap restoring
+   * a bundle). These pin an entry against {@link #reclaim} exactly as a query 
hold does, so they belong in the totals,
+   * but they are not somebody waiting on an entry, broken out so the two can 
be told apart.
+   */
+  private final AtomicLong currInternalHoldCount = new AtomicLong(0);
+  private final AtomicLong currInternalHoldBytes = new AtomicLong(0);
 
   private final AtomicReference<StaticStats> staticStats = new 
AtomicReference<>();
   private final AtomicReference<WeakStats> weakStats = new AtomicReference<>();
@@ -295,6 +303,25 @@ public class StorageLocation
    */
   @Nullable
   public <T extends CacheEntry> ReservationHold<T> 
addWeakReservationHoldIfExists(CacheEntryIdentifier entryId)
+  {
+    return addWeakReservationHoldIfExists(entryId, false);
+  }
+
+  /**
+   * Effectively the same as {@link 
#addWeakReservationHoldIfExists(CacheEntryIdentifier)} only accounted 
differently,
+   * for internal use to protect an entry from {@link #reclaim}.
+   */
+  @Nullable
+  public <T extends CacheEntry> ReservationHold<T> 
addInternalWeakReservationHoldIfExists(CacheEntryIdentifier entryId)
+  {
+    return addWeakReservationHoldIfExists(entryId, true);
+  }
+
+  @Nullable
+  private <T extends CacheEntry> ReservationHold<T> 
addWeakReservationHoldIfExists(
+      CacheEntryIdentifier entryId,
+      boolean internal
+  )
   {
     lock.readLock().lock();
     try {
@@ -304,12 +331,17 @@ public class StorageLocation
 
       WeakCacheEntry existingEntry = weakCacheEntries.get(entryId);
       if (existingEntry != null && existingEntry.hold()) {
+        // visited is set for internal holds too: an entry a live dependency 
is pinning genuinely is in use, and that
+        // is what this flag tells the reclaim scan.
         existingEntry.visited = true;
-        trackWeakHold(existingEntry);
-        weakStats.getAndUpdate(s -> s.hit(existingEntry.cacheEntry.getSize()));
+        final long heldBytes = existingEntry.cacheEntry.getSize();
+        trackWeakHold(heldBytes, internal);
+        if (!internal) {
+          weakStats.getAndUpdate(s -> s.hit(heldBytes));
+        }
         return new ReservationHold<>(
             (T) existingEntry.cacheEntry,
-            createWeakEntryReleaseRunnable(existingEntry, false)
+            createWeakEntryReleaseRunnable(existingEntry, false, internal, 
heldBytes)
         );
       }
       return null;
@@ -333,7 +365,30 @@ public class StorageLocation
       Supplier<? extends CacheEntry> entrySupplier
   )
   {
-    final ReservationHold<T> existingEntry = 
addWeakReservationHoldIfExists(entryId);
+    return addWeakReservationHold(entryId, entrySupplier, false);
+  }
+
+  /**
+   * Internal version of {@link #addWeakReservationHold(CacheEntryIdentifier, 
Supplier)}, see
+   * {@link #addInternalWeakReservationHoldIfExists}.
+   */
+  @Nullable
+  public <T extends CacheEntry> ReservationHold<T> 
addInternalWeakReservationHold(
+      CacheEntryIdentifier entryId,
+      Supplier<? extends CacheEntry> entrySupplier
+  )
+  {
+    return addWeakReservationHold(entryId, entrySupplier, true);
+  }
+
+  @Nullable
+  private <T extends CacheEntry> ReservationHold<T> addWeakReservationHold(
+      CacheEntryIdentifier entryId,
+      Supplier<? extends CacheEntry> entrySupplier,
+      boolean internal
+  )
+  {
+    final ReservationHold<T> existingEntry = 
addWeakReservationHoldIfExists(entryId, internal);
     if (existingEntry != null) {
       return existingEntry;
     }
@@ -343,11 +398,14 @@ public class StorageLocation
       WeakCacheEntry retryExistingEntry = weakCacheEntries.get(entryId);
       if (retryExistingEntry != null && retryExistingEntry.hold()) {
         retryExistingEntry.visited = true;
-        trackWeakHold(retryExistingEntry);
-        weakStats.getAndUpdate(s -> 
s.hit(retryExistingEntry.cacheEntry.getSize()));
+        final long heldBytes = retryExistingEntry.cacheEntry.getSize();
+        trackWeakHold(heldBytes, internal);
+        if (!internal) {
+          weakStats.getAndUpdate(s -> s.hit(heldBytes));
+        }
         return new ReservationHold<>(
             (T) retryExistingEntry.cacheEntry,
-            createWeakEntryReleaseRunnable(retryExistingEntry, false)
+            createWeakEntryReleaseRunnable(retryExistingEntry, false, 
internal, heldBytes)
         );
       }
       final CacheEntry newEntry = entrySupplier.get();
@@ -359,11 +417,12 @@ public class StorageLocation
         newWeakEntry.hold();
         linkNewWeakEntry(newWeakEntry);
         weakCacheEntries.put(newEntry.getId(), newWeakEntry);
-        trackWeakHold(newWeakEntry);
-        weakStats.getAndUpdate(s -> s.loadBegin(newEntry.getSize()));
+        final long heldBytes = newEntry.getSize();
+        trackWeakHold(heldBytes, internal);
+        weakStats.getAndUpdate(s -> s.loadBegin(heldBytes));
         hold = new ReservationHold<>(
             (T) newEntry,
-            createWeakEntryReleaseRunnable(newWeakEntry, true)
+            createWeakEntryReleaseRunnable(newWeakEntry, true, internal, 
heldBytes)
         );
       } else {
         weakStats.getAndUpdate(WeakStats::reject);
@@ -437,14 +496,6 @@ public class StorageLocation
         currWeakSizeBytes.getAndAdd(-delta);
         // The reservation (loadBegin) was recorded at the pre-shrink size; 
correct its byte total to match.
         weakStats.getAndUpdate(s -> s.shrinkLoadBegin(delta));
-        // Each active hold contributed entry.getSize() to currHoldBytes via 
trackWeakHold; shrink each hold's
-        // contribution by the same delta so a future trackWeakRelease (which 
subtracts the new smaller size) lands
-        // on the correct total. Clamp at 0 defensively against any 
pre-existing drift.
-        final long activeHolds = weak.holdReferents.getRegisteredParties() - 
1L;
-        if (activeHolds > 0) {
-          final long holdDelta = delta * activeHolds;
-          currHoldBytes.updateAndGet(v -> Math.max(0L, v - holdDelta));
-        }
       }
     }
     finally {
@@ -538,12 +589,14 @@ public class StorageLocation
    */
   private Runnable createWeakEntryReleaseRunnable(
       final WeakCacheEntry weakEntry,
-      final boolean isNewEntry
+      final boolean isNewEntry,
+      final boolean internal,
+      final long heldBytes
   )
   {
     return () -> {
       weakEntry.release();
-      trackWeakRelease(weakEntry);
+      trackWeakRelease(heldBytes, internal);
 
       if (!isNewEntry && !areWeakEntriesEphemeral) {
         // No need to consider removal from weakCacheEntries on hold release.
@@ -786,16 +839,31 @@ public class StorageLocation
     weakStats.getAndUpdate(s -> s.rangeRead(bytes, nanos));
   }
 
-  private void trackWeakHold(WeakCacheEntry entry)
+  /**
+   * {@code heldBytes} is the entry's size as of when the hold was taken, and 
the matching
+   * {@link #trackWeakRelease} subtracts that same number rather than 
re-reading the entry. The size can change under
+   * a live hold ({@link #adjustReservation} shrinks a partial segment's 
pessimistic estimate once its real size is
+   * known), and re-reading it would leave the totals permanently skewed by 
the difference. Pairing each add with an
+   * identical subtract keeps them balanced without either side taking a lock.
+   */
+  private void trackWeakHold(long heldBytes, boolean internal)
   {
     currHoldCount.getAndIncrement();
-    currHoldBytes.getAndAdd(entry.cacheEntry.getSize());
+    currHoldBytes.getAndAdd(heldBytes);
+    if (internal) {
+      currInternalHoldCount.getAndIncrement();
+      currInternalHoldBytes.getAndAdd(heldBytes);
+    }
   }
 
-  private void trackWeakRelease(WeakCacheEntry entry)
+  private void trackWeakRelease(long heldBytes, boolean internal)
   {
     currHoldCount.getAndDecrement();
-    currHoldBytes.getAndAdd(-entry.cacheEntry.getSize());
+    currHoldBytes.getAndAdd(-heldBytes);
+    if (internal) {
+      currInternalHoldCount.getAndDecrement();
+      currInternalHoldBytes.getAndAdd(-heldBytes);
+    }
   }
 
   @VisibleForTesting
@@ -842,6 +910,8 @@ public class StorageLocation
     currStaticSizeBytes.set(0);
     currHoldCount.set(0);
     currHoldBytes.set(0);
+    currInternalHoldCount.set(0);
+    currInternalHoldBytes.set(0);
     resetStaticStats();
     resetWeakStats();
   }
@@ -858,7 +928,9 @@ public class StorageLocation
 
   public WeakStats resetWeakStats()
   {
-    return weakStats.getAndSet(new WeakStats(currWeakSizeBytes, currHoldCount, 
currHoldBytes));
+    return weakStats.getAndSet(
+        new WeakStats(currWeakSizeBytes, currHoldCount, currHoldBytes, 
currInternalHoldCount, currInternalHoldBytes)
+    );
   }
 
   /**
@@ -1222,6 +1294,8 @@ public class StorageLocation
     private final AtomicLong sizeUsed;
     private final AtomicLong holdCount;
     private final AtomicLong holdBytes;
+    private final AtomicLong internalHoldCount;
+    private final AtomicLong internalHoldBytes;
     private final AtomicLong loadBeginCount = new AtomicLong(0);
     private final AtomicLong loadBeginBytes = new AtomicLong(0);
     private final AtomicLong loadCount = new AtomicLong(0);
@@ -1236,11 +1310,19 @@ public class StorageLocation
     private final AtomicLong readBytes = new AtomicLong(0);
     private final AtomicLong readTimeNanos = new AtomicLong(0);
 
-    public WeakStats(AtomicLong sizeUsed, AtomicLong holdCount, AtomicLong 
holdBytes)
+    public WeakStats(
+        AtomicLong sizeUsed,
+        AtomicLong holdCount,
+        AtomicLong holdBytes,
+        AtomicLong internalHoldCount,
+        AtomicLong internalHoldBytes
+    )
     {
       this.sizeUsed = sizeUsed;
       this.holdCount = holdCount;
       this.holdBytes = holdBytes;
+      this.internalHoldCount = internalHoldCount;
+      this.internalHoldBytes = internalHoldBytes;
     }
 
     public WeakStats hit(long size)
@@ -1320,6 +1402,18 @@ public class StorageLocation
       return holdBytes.get();
     }
 
+    @Override
+    public long getInternalHoldCount()
+    {
+      return internalHoldCount.get();
+    }
+
+    @Override
+    public long getInternalHoldBytes()
+    {
+      return internalHoldBytes.get();
+    }
+
     @Override
     public long getHitCount()
     {
diff --git 
a/server/src/main/java/org/apache/druid/segment/loading/VirtualStorageLocationStats.java
 
b/server/src/main/java/org/apache/druid/segment/loading/VirtualStorageLocationStats.java
index 035d30cafff..a68516e94f1 100644
--- 
a/server/src/main/java/org/apache/druid/segment/loading/VirtualStorageLocationStats.java
+++ 
b/server/src/main/java/org/apache/druid/segment/loading/VirtualStorageLocationStats.java
@@ -39,6 +39,17 @@ public interface VirtualStorageLocationStats
    */
   long getHoldBytes();
 
+  /**
+   * internal holds, such as those which one cache entry places on another it 
depends on, or that a partial-load rule
+   * places on what it selected, rather than a caller waiting on the entry.
+   */
+  long getInternalHoldCount();
+
+  /**
+   * Total bytes from the holds counted by {@link #getInternalHoldCount()}.
+   */
+  long getInternalHoldBytes();
+
   /**
    * Number of operations for which an entry was already present during the 
measurement period
    */
diff --git 
a/server/src/main/java/org/apache/druid/server/metrics/StorageMonitor.java 
b/server/src/main/java/org/apache/druid/server/metrics/StorageMonitor.java
index 23a8bd4bbf4..c44f9364355 100644
--- a/server/src/main/java/org/apache/druid/server/metrics/StorageMonitor.java
+++ b/server/src/main/java/org/apache/druid/server/metrics/StorageMonitor.java
@@ -100,6 +100,18 @@ public class StorageMonitor extends AbstractMonitor
    */
   public static final String VSF_HOLD_BYTES = "storage/virtual/hold/bytes";
 
+  /**
+   * internal holds, such as one cache entry places on another it depends on, 
or that a partial-load rule places on
+   * what it selected, rather than a caller waiting on the entry. Counted in 
the totals above because they pin an
+   * entry against reclaim identically.
+   */
+  public static final String VSF_INTERNAL_HOLD_COUNT = 
"storage/virtual/hold/internal/count";
+
+  /**
+   * Total bytes from the holds counted by {@link #VSF_INTERNAL_HOLD_COUNT}.
+   */
+  public static final String VSF_INTERNAL_HOLD_BYTES = 
"storage/virtual/hold/internal/bytes";
+
   /**
    * Number of acquire operations during the measurement period that found an 
existing weakly-held entry already in
    * virtual storage.
@@ -215,6 +227,8 @@ public class StorageMonitor extends AbstractMonitor
         emitter.emit(builder.setMetric(VSF_USED_BYTES, 
weakStats.getUsedBytes()));
         emitter.emit(builder.setMetric(VSF_HOLD_COUNT, 
weakStats.getHoldCount()));
         emitter.emit(builder.setMetric(VSF_HOLD_BYTES, 
weakStats.getHoldBytes()));
+        emitter.emit(builder.setMetric(VSF_INTERNAL_HOLD_COUNT, 
weakStats.getInternalHoldCount()));
+        emitter.emit(builder.setMetric(VSF_INTERNAL_HOLD_BYTES, 
weakStats.getInternalHoldBytes()));
         emitter.emit(builder.setMetric(VSF_HIT_COUNT, 
weakStats.getHitCount()));
         emitter.emit(builder.setMetric(VSF_HIT_BYTES, 
weakStats.getHitBytes()));
         emitter.emit(builder.setMetric(VSF_LOAD_BEGIN_COUNT, 
weakStats.getLoadBeginCount()));
diff --git 
a/server/src/test/java/org/apache/druid/segment/loading/PartialSegmentRestoreFromDiskTest.java
 
b/server/src/test/java/org/apache/druid/segment/loading/PartialSegmentRestoreFromDiskTest.java
index bb8b4e9f66f..52611f30519 100644
--- 
a/server/src/test/java/org/apache/druid/segment/loading/PartialSegmentRestoreFromDiskTest.java
+++ 
b/server/src/test/java/org/apache/druid/segment/loading/PartialSegmentRestoreFromDiskTest.java
@@ -336,7 +336,7 @@ class PartialSegmentRestoreFromDiskTest
     // Refuse only the aggregate bundle's reservation, so the restore fails 
with __base already mounted.
     Mockito.doReturn(null)
            .when(location)
-           .addWeakReservationHold(ArgumentMatchers.eq(aggId), 
ArgumentMatchers.any());
+           .addInternalWeakReservationHold(ArgumentMatchers.eq(aggId), 
ArgumentMatchers.any());
 
     Assertions.assertThrows(Throwable.class, () -> restoreFromDisk(location));
 
diff --git 
a/server/src/test/java/org/apache/druid/segment/loading/StorageLocationTest.java
 
b/server/src/test/java/org/apache/druid/segment/loading/StorageLocationTest.java
index b4b01f6dd21..23b89108e08 100644
--- 
a/server/src/test/java/org/apache/druid/segment/loading/StorageLocationTest.java
+++ 
b/server/src/test/java/org/apache/druid/segment/loading/StorageLocationTest.java
@@ -41,10 +41,12 @@ import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
 import java.util.Objects;
+import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Future;
 import java.util.concurrent.ThreadLocalRandom;
+import java.util.concurrent.TimeUnit;
 
 class StorageLocationTest
 {
@@ -549,12 +551,12 @@ class StorageLocationTest
     Assertions.assertEquals(1, location.getWeakStats().getHoldCount());
     Assertions.assertEquals(80, location.getWeakStats().getHoldBytes());
 
-    // Shrink to 30: hold-bytes contribution from the active hold must shrink 
in lockstep so the eventual
-    // trackWeakRelease (which subtracts the new smaller size) leaves 
currHoldBytes at 0.
+    // Shrink to 30. The live hold keeps contributing the 80 it was taken at - 
it subtracts that same 80 on release,
+    // which is what keeps the total balanced without the resize having to 
reach into it.
     location.adjustReservation(entry.getId(), 30);
     Assertions.assertEquals(30, entry.getSize());
     Assertions.assertEquals(30, location.currentWeakSizeBytes());
-    Assertions.assertEquals(30, location.getWeakStats().getHoldBytes());
+    Assertions.assertEquals(80, location.getWeakStats().getHoldBytes());
 
     hold.close();
     Assertions.assertEquals(0, location.getWeakStats().getHoldCount());
@@ -574,16 +576,103 @@ class StorageLocationTest
     Assertions.assertEquals(2, location.getWeakStats().getHoldCount());
     Assertions.assertEquals(100, location.getWeakStats().getHoldBytes());
 
-    // Shrink by 30 (50 → 20): each of the two active holds contributes -30, 
so currHoldBytes drops by 60.
+    // Shrink by 30 (50 -> 20). Both holds were taken at 50 and keep 
contributing it, so the total is untouched here
+    // and each release takes its own 50 back off.
     location.adjustReservation(entry.getId(), 20);
-    Assertions.assertEquals(40, location.getWeakStats().getHoldBytes());
+    Assertions.assertEquals(100, location.getWeakStats().getHoldBytes());
 
     hold1.close();
-    Assertions.assertEquals(20, location.getWeakStats().getHoldBytes());
+    Assertions.assertEquals(50, location.getWeakStats().getHoldBytes());
     hold2.close();
     Assertions.assertEquals(0, location.getWeakStats().getHoldBytes());
   }
 
+  @Test
+  public void testConcurrentResizeAndReleaseLeavesHoldBytesBalanced() throws 
Exception
+  {
+    // A resize racing a release: the release runs outside the location lock, 
so it can land either side of the
+    // resize. Whichever way it interleaves, a hold subtracts exactly what it 
added, so the totals return to zero.
+    for (int i = 0; i < 500; i++) {
+      final StorageLocation location = new StorageLocation(tempDir, 1000L, 
null);
+      final TestResizableCacheEntry entry = new TestResizableCacheEntry("a" + 
i, 80);
+      final StorageLocation.ReservationHold<?> reserver =
+          location.addWeakReservationHold(entry.getId(), () -> entry);
+      Assertions.assertNotNull(reserver);
+      final StorageLocation.ReservationHold<?> queryHold =
+          location.addWeakReservationHoldIfExists(entry.getId());
+      final StorageLocation.ReservationHold<?> internalHold =
+          location.addInternalWeakReservationHoldIfExists(entry.getId());
+      Assertions.assertNotNull(queryHold);
+      Assertions.assertNotNull(internalHold);
+
+      final CountDownLatch start = new CountDownLatch(1);
+      final Future<?> resizer = executorService.submit(() -> {
+        awaitUninterruptibly(start);
+        location.adjustReservation(entry.getId(), 30);
+      });
+      final Future<?> releaser = executorService.submit(() -> {
+        awaitUninterruptibly(start);
+        queryHold.close();
+        internalHold.close();
+      });
+      start.countDown();
+      resizer.get();
+      releaser.get();
+      reserver.close();
+
+      Assertions.assertEquals(0, location.getWeakStats().getHoldCount(), 
"iteration " + i);
+      Assertions.assertEquals(0, location.getWeakStats().getHoldBytes(), 
"iteration " + i);
+      Assertions.assertEquals(0, 
location.getWeakStats().getInternalHoldCount(), "iteration " + i);
+      Assertions.assertEquals(0, 
location.getWeakStats().getInternalHoldBytes(), "iteration " + i);
+    }
+  }
+
+  private static void awaitUninterruptibly(CountDownLatch latch)
+  {
+    try {
+      Assertions.assertTrue(latch.await(30, TimeUnit.SECONDS));
+    }
+    catch (InterruptedException e) {
+      Thread.currentThread().interrupt();
+      throw new RuntimeException(e);
+    }
+  }
+
+  @Test
+  public void testInternalHoldsAreNotCountedAsHitsButAreCountedAsPinned()
+  {
+    final StorageLocation location = new StorageLocation(tempDir, 100L, null);
+    final UnmountTrackingCacheEntry entry = new UnmountTrackingCacheEntry("a", 
10);
+    final StorageLocation.ReservationHold<?> reserver =
+        location.addWeakReservationHold(entry.getId(), () -> entry);
+    Assertions.assertNotNull(reserver);
+
+    // A demand hold: somebody is waiting on this entry, so it is a cache hit.
+    final StorageLocation.ReservationHold<?> query = 
location.addWeakReservationHoldIfExists(entry.getId());
+    Assertions.assertNotNull(query);
+    Assertions.assertEquals(1, location.getWeakStats().getHitCount());
+    Assertions.assertEquals(0, location.getWeakStats().getInternalHoldCount());
+
+    // A structural hold: another entry pinning this one. It pins against 
reclaim just the same, so it counts in the
+    // totals, but it is not demand and must not move the hit rate.
+    final StorageLocation.ReservationHold<?> internal =
+        location.addInternalWeakReservationHoldIfExists(entry.getId());
+    Assertions.assertNotNull(internal);
+    Assertions.assertEquals(1, location.getWeakStats().getHitCount(), "a 
structural hold is not a cache hit");
+    Assertions.assertEquals(3, location.getWeakStats().getHoldCount(), "but it 
does pin the entry");
+    Assertions.assertEquals(1, location.getWeakStats().getInternalHoldCount());
+    Assertions.assertEquals(10, 
location.getWeakStats().getInternalHoldBytes());
+
+    internal.close();
+    Assertions.assertEquals(0, location.getWeakStats().getInternalHoldCount());
+    Assertions.assertEquals(0, location.getWeakStats().getInternalHoldBytes());
+    Assertions.assertEquals(2, location.getWeakStats().getHoldCount());
+
+    query.close();
+    reserver.close();
+    Assertions.assertEquals(0, location.getWeakStats().getHoldCount());
+  }
+
   @Test
   public void 
testReleasingReserversHoldDoesNotEvictAnEntryAnotherHolderIsStillUsing()
   {


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

Reply via email to