Skip to content

Commit 16e900f

Browse files
committed
Avoid deadlocks in cache (#30461)
This commit avoids deadlocks in the cache by removing dangerous places where we try to take the LRU lock while completing a future. Instead, we block for the future to complete, and then execute the handling code under the LRU lock (for example, eviction).
1 parent 2734c6f commit 16e900f

File tree

2 files changed

+36
-44
lines changed

2 files changed

+36
-44
lines changed

server/src/main/java/org/elasticsearch/common/cache/Cache.java

Lines changed: 36 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -206,34 +206,33 @@ private static class CacheSegment<K, V> {
206206
*/
207207
Entry<K, V> get(K key, long now, Predicate<Entry<K, V>> isExpired, Consumer<Entry<K, V>> onExpiration) {
208208
CompletableFuture<Entry<K, V>> future;
209-
Entry<K, V> entry = null;
210209
try (ReleasableLock ignored = readLock.acquire()) {
211210
future = map.get(key);
212211
}
213212
if (future != null) {
213+
Entry<K, V> entry;
214214
try {
215-
entry = future.handle((ok, ex) -> {
216-
if (ok != null && !isExpired.test(ok)) {
217-
segmentStats.hit();
218-
ok.accessTime = now;
219-
return ok;
220-
} else {
221-
segmentStats.miss();
222-
if (ok != null) {
223-
assert isExpired.test(ok);
224-
onExpiration.accept(ok);
225-
}
226-
return null;
227-
}
228-
}).get();
229-
} catch (ExecutionException | InterruptedException e) {
215+
entry = future.get();
216+
} catch (ExecutionException e) {
217+
assert future.isCompletedExceptionally();
218+
segmentStats.miss();
219+
return null;
220+
} catch (InterruptedException e) {
230221
throw new IllegalStateException(e);
231222
}
232-
}
233-
else {
223+
if (isExpired.test(entry)) {
224+
segmentStats.miss();
225+
onExpiration.accept(entry);
226+
return null;
227+
} else {
228+
segmentStats.hit();
229+
entry.accessTime = now;
230+
return entry;
231+
}
232+
} else {
234233
segmentStats.miss();
234+
return null;
235235
}
236-
return entry;
237236
}
238237

239238
/**
@@ -269,30 +268,18 @@ Tuple<Entry<K, V>, Entry<K, V>> put(K key, V value, long now) {
269268
/**
270269
* remove an entry from the segment
271270
*
272-
* @param key the key of the entry to remove from the cache
273-
* @return the removed entry if there was one, otherwise null
271+
* @param key the key of the entry to remove from the cache
272+
* @param onRemoval a callback for the removed entry
274273
*/
275-
Entry<K, V> remove(K key) {
274+
void remove(K key, Consumer<CompletableFuture<Entry<K, V>>> onRemoval) {
276275
CompletableFuture<Entry<K, V>> future;
277-
Entry<K, V> entry = null;
278276
try (ReleasableLock ignored = writeLock.acquire()) {
279277
future = map.remove(key);
280278
}
281279
if (future != null) {
282-
try {
283-
entry = future.handle((ok, ex) -> {
284-
if (ok != null) {
285-
segmentStats.eviction();
286-
return ok;
287-
} else {
288-
return null;
289-
}
290-
}).get();
291-
} catch (ExecutionException | InterruptedException e) {
292-
throw new IllegalStateException(e);
293-
}
280+
segmentStats.eviction();
281+
onRemoval.accept(future);
294282
}
295-
return entry;
296283
}
297284

298285
private static class SegmentStats {
@@ -476,12 +463,18 @@ private void put(K key, V value, long now) {
476463
*/
477464
public void invalidate(K key) {
478465
CacheSegment<K, V> segment = getCacheSegment(key);
479-
Entry<K, V> entry = segment.remove(key);
480-
if (entry != null) {
481-
try (ReleasableLock ignored = lruLock.acquire()) {
482-
delete(entry, RemovalNotification.RemovalReason.INVALIDATED);
466+
segment.remove(key, f -> {
467+
try {
468+
Entry<K, V> entry = f.get();
469+
try (ReleasableLock ignored = lruLock.acquire()) {
470+
delete(entry, RemovalNotification.RemovalReason.INVALIDATED);
471+
}
472+
} catch (ExecutionException e) {
473+
// ok
474+
} catch (InterruptedException e) {
475+
throw new IllegalStateException(e);
483476
}
484-
}
477+
});
485478
}
486479

487480
/**
@@ -632,7 +625,7 @@ public void remove() {
632625
Entry<K, V> entry = current;
633626
if (entry != null) {
634627
CacheSegment<K, V> segment = getCacheSegment(entry.key);
635-
segment.remove(entry.key);
628+
segment.remove(entry.key, f -> {});
636629
try (ReleasableLock ignored = lruLock.acquire()) {
637630
current = null;
638631
delete(entry, RemovalNotification.RemovalReason.INVALIDATED);
@@ -717,7 +710,7 @@ private void evictEntry(Entry<K, V> entry) {
717710

718711
CacheSegment<K, V> segment = getCacheSegment(entry.key);
719712
if (segment != null) {
720-
segment.remove(entry.key);
713+
segment.remove(entry.key, f -> {});
721714
}
722715
delete(entry, RemovalNotification.RemovalReason.EVICTED);
723716
}

server/src/test/java/org/elasticsearch/common/cache/CacheTests.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -344,7 +344,6 @@ protected long now() {
344344
assertEquals(numberOfEntries, cache.stats().getEvictions());
345345
}
346346

347-
@AwaitsFix(bugUrl = "https://github.com/elastic/elasticsearch/issues/30428")
348347
public void testComputeIfAbsentDeadlock() throws BrokenBarrierException, InterruptedException {
349348
final int numberOfThreads = randomIntBetween(2, 32);
350349
final Cache<Integer, String> cache =

0 commit comments

Comments
 (0)