Skip to content

Commit ed1e8e4

Browse files
committed
Fix compatibility between the cache compute methods and a load
The asMap().compute implementation did not take into account that the present value may be loading. A load does not block other writes to that entry and takes into account that it may be clobbered, causing it to automatically discard itself. This is a known design choice that breaks linearizability assumptions (#1881). The compute should check if a load is in progress and call the appropriate internal removal method. Because a zombie entry remained in the cache and still is marked as loading, the loader could discover entry and try to wait for it to materialize. When the computation is a removal, indicated by a null value, the loader would see this as the zombie's result. Since a cache loader may not return null it would throw an exception to indicate a user bug. A new ComputingValueReference resolves both issues by indicating that the load has completed. The compute's removeEntry will then actually remove this entry and the loader will not wait on the zombie. Instead if it observes the entry, it will neither receive a non-null value or wait for it to load, but rather try to load anew under the lock. This piggybacks on the reference collection support where an entry is present but its value was garbage collected, causing the load to proceed. By the time the lock is obtained the compute method's entry was removed and the load proceeds as normal (so no unnecessary notification is produced). fixes #5342 fixes #2827 resolves underlying cause of #2108
1 parent c86fa8f commit ed1e8e4

File tree

2 files changed

+80
-3
lines changed

2 files changed

+80
-3
lines changed

guava-tests/test/com/google/common/cache/LocalCacheMapComputeTest.java

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,17 @@
1818

1919
import static com.google.common.truth.Truth.assertThat;
2020

21+
import java.util.ArrayList;
22+
import java.util.List;
23+
import java.util.Queue;
24+
import java.util.concurrent.ConcurrentLinkedQueue;
25+
import java.util.concurrent.ExecutionException;
2126
import java.util.concurrent.TimeUnit;
2227
import java.util.function.IntConsumer;
2328
import java.util.stream.IntStream;
29+
30+
import com.google.common.util.concurrent.UncheckedExecutionException;
31+
2432
import junit.framework.TestCase;
2533

2634
/** Test Java8 map.compute in concurrent cache context. */
@@ -91,6 +99,27 @@ public void testComputeIfPresent() {
9199
assertThat(cache.getIfPresent(key).split(delimiter)).hasLength(count + 1);
92100
}
93101

102+
public void testComputeIfPresentRemove() {
103+
List<RemovalNotification<Integer, Integer>> notifications = new ArrayList<>();
104+
Cache<Integer, Integer> cache = CacheBuilder.newBuilder()
105+
.removalListener(new RemovalListener<Integer, Integer>() {
106+
@Override public void onRemoval(RemovalNotification<Integer, Integer> notification) {
107+
notifications.add(notification);
108+
}
109+
}).build();
110+
cache.put(1, 2);
111+
112+
// explicitly remove the existing value
113+
cache.asMap().computeIfPresent(1, (key, value) -> null);
114+
assertThat(notifications).hasSize(1);
115+
CacheTesting.checkEmpty(cache);
116+
117+
// ensure no zombie entry remains
118+
cache.asMap().computeIfPresent(1, (key, value) -> null);
119+
assertThat(notifications).hasSize(1);
120+
CacheTesting.checkEmpty(cache);
121+
}
122+
94123
public void testUpdates() {
95124
cache.put(key, "1");
96125
// simultaneous update for same key, some null, some non-null
@@ -113,6 +142,39 @@ public void testCompute() {
113142
assertEquals(0, cache.size());
114143
}
115144

145+
//
146+
public void testComputeWithLoad() {
147+
Queue<RemovalNotification<String, String>> notifications = new ConcurrentLinkedQueue<>();
148+
cache = CacheBuilder.newBuilder()
149+
.removalListener(new RemovalListener<String, String>() {
150+
@Override public void onRemoval(RemovalNotification<String, String> notification) {
151+
notifications.add(notification);
152+
}
153+
})
154+
.expireAfterAccess(500000, TimeUnit.MILLISECONDS)
155+
.maximumSize(count)
156+
.build();
157+
158+
cache.put(key, "1");
159+
// simultaneous load and deletion
160+
doParallelCacheOp(
161+
count,
162+
n -> {
163+
try {
164+
cache.get(key, () -> key);
165+
cache.asMap().compute(key, (k, v) -> null);
166+
} catch (ExecutionException e) {
167+
throw new UncheckedExecutionException(e);
168+
}
169+
});
170+
171+
CacheTesting.checkEmpty(cache);
172+
for (RemovalNotification<String, String> entry : notifications) {
173+
assertThat(entry.getKey()).isNotNull();
174+
assertThat(entry.getValue()).isNotNull();
175+
}
176+
}
177+
116178
public void testComputeExceptionally() {
117179
try {
118180
doParallelCacheOp(

guava/src/com/google/common/cache/LocalCache.java

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2188,7 +2188,7 @@ V waitForLoadingValue(ReferenceEntry<K, V> e, K key, ValueReference<K, V> valueR
21882188
V compute(K key, int hash, BiFunction<? super K, ? super V, ? extends V> function) {
21892189
ReferenceEntry<K, V> e;
21902190
ValueReference<K, V> valueReference = null;
2191-
LoadingValueReference<K, V> loadingValueReference = null;
2191+
ComputingValueReference<K, V> loadingValueReference = null;
21922192
boolean createNewEntry = true;
21932193
V newValue;
21942194

@@ -2229,7 +2229,7 @@ V compute(K key, int hash, BiFunction<? super K, ? super V, ? extends V> functio
22292229

22302230
// note valueReference can be an existing value or even itself another loading value if
22312231
// the value for the key is already being computed.
2232-
loadingValueReference = new LoadingValueReference<>(valueReference);
2232+
loadingValueReference = new ComputingValueReference<>(valueReference);
22332233

22342234
if (e == null) {
22352235
createNewEntry = true;
@@ -2257,6 +2257,9 @@ V compute(K key, int hash, BiFunction<? super K, ? super V, ? extends V> functio
22572257
} else if (createNewEntry) {
22582258
removeLoadingValue(key, hash, loadingValueReference);
22592259
return null;
2260+
} else if (valueReference.isLoading()) {
2261+
removeLoadingValue(key, hash, loadingValueReference);
2262+
return null;
22602263
} else {
22612264
removeEntry(e, hash, RemovalCause.EXPLICIT);
22622265
return null;
@@ -3465,6 +3468,18 @@ void runUnlockedCleanup() {
34653468
}
34663469
}
34673470

3471+
static class ComputingValueReference<K, V> extends LoadingValueReference<K, V> {
3472+
public ComputingValueReference() {
3473+
super();
3474+
}
3475+
public ComputingValueReference(ValueReference<K, V> oldValue) {
3476+
super(oldValue);
3477+
}
3478+
@Override public boolean isLoading() {
3479+
return false;
3480+
}
3481+
}
3482+
34683483
static class LoadingValueReference<K, V> implements ValueReference<K, V> {
34693484
volatile ValueReference<K, V> oldValue;
34703485

@@ -3927,7 +3942,7 @@ long longSize() {
39273942
Segment<K, V>[] segments = this.segments;
39283943
long sum = 0;
39293944
for (int i = 0; i < segments.length; ++i) {
3930-
sum += Math.max(0, segments[i].count); // see https://github.com/google/guava/issues/2108
3945+
sum += segments[i].count;
39313946
}
39323947
return sum;
39333948
}

0 commit comments

Comments
 (0)