From 217afb9276ed70357798f3be766d0b1a9041cd18 Mon Sep 17 00:00:00 2001 From: Nickita Khylkouski <90287684+nickita-khylkouski@users.noreply.github.com> Date: Mon, 26 Jan 2026 10:41:59 -0800 Subject: [PATCH 1/2] LocalCache: Fix compute() to report COLLECTED removal cause for GC'd entries When compute() returns null for an entry whose value has been garbage collected, the removal cause should be COLLECTED, not EXPLICIT. The fix checks if the original value was null and the reference is still active (not loading), which indicates the value was garbage collected rather than explicitly removed by the user. Fixes #7985 --- .../google/common/cache/LocalCacheTest.java | 38 +++++++++++++++++++ .../com/google/common/cache/LocalCache.java | 8 +++- 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/guava-tests/test/com/google/common/cache/LocalCacheTest.java b/guava-tests/test/com/google/common/cache/LocalCacheTest.java index 15ceacaae4bf..b54832b70094 100644 --- a/guava-tests/test/com/google/common/cache/LocalCacheTest.java +++ b/guava-tests/test/com/google/common/cache/LocalCacheTest.java @@ -1077,6 +1077,44 @@ public void testRemovalListener_collected() { assertThat(listener.isEmpty()).isTrue(); } + // Test for https://github.com/google/guava/issues/7985 + // When compute() returns null for a collected entry, the removal cause should be COLLECTED. + public void testComputeRemovalCause_collected() { + QueuingRemovalListener listener = queuingRemovalListener(); + LocalCache map = + makeLocalCache( + createCacheBuilder().concurrencyLevel(1).softValues().removalListener(listener)); + Segment segment = map.segments[0]; + assertThat(listener.isEmpty()).isTrue(); + + Object key = new Object(); + Object value = new Object(); + + // Put an entry into the cache + map.put(key, value); + assertThat(listener.isEmpty()).isTrue(); + + // Get the entry and clear its value reference to simulate garbage collection + int hash = map.hash(key); + ReferenceEntry entry = segment.getEntry(key, hash); + ValueReference valueReference = entry.getValueReference(); + + // Simulate garbage collection by clearing the value reference + ((java.lang.ref.Reference) valueReference).clear(); + + // Now call compute() with a function that returns null + // Since the value was collected, the removal cause should be COLLECTED, not EXPLICIT + map.compute(key, (k, v) -> { + // The value should be null since it was collected + assertThat(v).isNull(); + return null; + }); + + // Verify the removal notification has COLLECTED cause + assertNotified(listener, key, null, RemovalCause.COLLECTED); + assertThat(listener.isEmpty()).isTrue(); + } + public void testRemovalListener_expired() { FakeTicker ticker = new FakeTicker(); QueuingRemovalListener listener = queuingRemovalListener(); diff --git a/guava/src/com/google/common/cache/LocalCache.java b/guava/src/com/google/common/cache/LocalCache.java index 3fa8b8a599e6..bce4c9ba71c2 100644 --- a/guava/src/com/google/common/cache/LocalCache.java +++ b/guava/src/com/google/common/cache/LocalCache.java @@ -2287,7 +2287,13 @@ V waitForLoadingValue(ReferenceEntry e, K key, ValueReference valueR removeLoadingValue(key, hash, computingValueReference); return null; } else { - removeEntry(e, hash, RemovalCause.EXPLICIT); + // If the value was garbage collected, report COLLECTED; otherwise EXPLICIT. + V oldValue = valueReference.get(); + RemovalCause cause = + (oldValue == null && valueReference.isActive()) + ? RemovalCause.COLLECTED + : RemovalCause.EXPLICIT; + removeEntry(e, hash, cause); return null; } } finally { From 3386500fa75417d36cac5b950e8a528b79eda437 Mon Sep 17 00:00:00 2001 From: Nickita Khylkouski <90287684+nickita-khylkouski@users.noreply.github.com> Date: Wed, 28 Jan 2026 08:23:57 -0800 Subject: [PATCH 2/2] Fix CheckReturnValue error: assign compute() result to unused variable Error Prone requires compute() return value to be used. Using Object unused to satisfy the check (var not available with source 1.8). --- guava-tests/test/com/google/common/cache/LocalCacheTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/guava-tests/test/com/google/common/cache/LocalCacheTest.java b/guava-tests/test/com/google/common/cache/LocalCacheTest.java index b54832b70094..47258e33e8ef 100644 --- a/guava-tests/test/com/google/common/cache/LocalCacheTest.java +++ b/guava-tests/test/com/google/common/cache/LocalCacheTest.java @@ -1104,7 +1104,7 @@ public void testComputeRemovalCause_collected() { // Now call compute() with a function that returns null // Since the value was collected, the removal cause should be COLLECTED, not EXPLICIT - map.compute(key, (k, v) -> { + Object unused = map.compute(key, (k, v) -> { // The value should be null since it was collected assertThat(v).isNull(); return null;