Skip to content

Commit ea18076

Browse files
mpkorstanjemarcphilipp
authored andcommitted
Fix deadlock in NamespacedHierarchicalStore.computeIfAbsent() (#5348)
In #5231 evaluation of the default value creator was deferred until after an entry was created in the concurrent hash map backing the store. This enables recursive updates. However, by calling `evaluateIfNotNull` inside `compute` threads that lost the race would wait inside `Map::compute` for the value to be evaluated. Waiting here is a problem when the backing concurrent hashmap is reaching capacity. The thread waiting inside compute holds a lock while at the same time the thread that won the race will try to acquire a lock to resize the map. By making the threads that lost the race wait outside of `Map::compute` we can prevent this dead lock. This does require that each thread retries when another thread failed to insert a value. But eventually either one other thread either successfully inserts a value or the thread uses its own default value creator. Fixes: #5346 Co-authored-by: Marc Philipp <mail@marcphilipp.de> (cherry picked from commit f29de38)
1 parent 869e232 commit ea18076

File tree

3 files changed

+137
-35
lines changed

3 files changed

+137
-35
lines changed

documentation/modules/ROOT/partials/release-notes/release-notes-6.0.3.adoc

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@ repository on GitHub.
1616
[[v6.0.3-junit-platform-bug-fixes]]
1717
==== Bug Fixes
1818

19-
* ❓
19+
* A deadlock issue in `NamespacedHierarchicalStore.computeIfAbsent(N, K, Function)` has
20+
been fixed.
2021

2122
[[v6.0.3-junit-platform-deprecations-and-breaking-changes]]
2223
==== Deprecations and Breaking Changes

junit-platform-engine/src/main/java/org/junit/platform/engine/support/store/NamespacedHierarchicalStore.java

Lines changed: 87 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -223,22 +223,39 @@ public void close() {
223223
rejectIfClosed();
224224
return defaultCreator.apply(key);
225225
});
226-
var storedValue = storedValues.compute(compositeKey, //
227-
(__, oldStoredValue) -> {
228-
// guard against race conditions, repeated from getStoredValue
229-
// this filters out failures inserted by computeIfAbsent
230-
if (StoredValue.isNonNullAndPresent(oldStoredValue)) {
231-
return oldStoredValue;
232-
}
233-
rejectIfClosed();
234-
return candidateStoredValue;
235-
});
236226

237-
// Only the caller that created the candidateStoredValue may run it
238-
if (candidateStoredValue.equals(storedValue)) {
239-
return candidateStoredValue.execute();
227+
for (;;) {
228+
var storedValue = storedValues.compute(compositeKey, //
229+
(__, oldStoredValue) -> {
230+
// The old stored value remains if a) there is an old stored value and
231+
// b) the old stored value has not yet been evaluated or c) the old
232+
// stored value was evaluated to a present value.
233+
//
234+
// Condition b ensures that we do not evaluate or await the evaluation
235+
// inside `compute`, this would lead to recursive updates or deadlocks
236+
// respectively.
237+
// Condition c guards against race conditions (repeated from
238+
// getStoredValue) this filters out failures inserted by
239+
// computeIfAbsent.
240+
if (oldStoredValue != null && (!oldStoredValue.isDone() || oldStoredValue.isPresent())) {
241+
return oldStoredValue;
242+
}
243+
rejectIfClosed();
244+
return candidateStoredValue;
245+
});
246+
247+
// Only the caller that created the candidateStoredValue may run it
248+
if (candidateStoredValue.equals(storedValue)) {
249+
return candidateStoredValue.execute();
250+
}
251+
// Implicitly awaits evaluation and uses the result if it was present.
252+
// this filters out failures inserted by computeIfAbsent
253+
// Otherwise, another thread won the race but failed to insert a
254+
// present value so we try again.
255+
if (storedValue.isPresent()) {
256+
return storedValue.evaluate();
257+
}
240258
}
241-
return storedValue.evaluate();
242259
}
243260

244261
/**
@@ -269,29 +286,44 @@ public <K, V> Object computeIfAbsent(N namespace, K key, Function<? super K, ? e
269286
rejectIfClosed();
270287
return Preconditions.notNull(defaultCreator.apply(key), "defaultCreator must not return null");
271288
});
272-
var storedValue = storedValues.compute(compositeKey, (__, oldStoredValue) -> {
273-
// guard against race conditions
274-
// computeIfAbsent replaces both null and absent values
275-
if (StoredValue.evaluateIfNotNull(oldStoredValue) != null) {
276-
return oldStoredValue;
289+
290+
for (;;) {
291+
var storedValue = storedValues.compute(compositeKey, (__, oldStoredValue) -> {
292+
// The old stored value remains if a) there is an old stored value and
293+
// b) the old stored value has not yet been evaluated or c) the old
294+
// stored value evaluated to null.
295+
//
296+
// Condition b ensures that we do not evaluate or await the evaluation
297+
// inside `compute`, this would lead to recursive updates or deadlocks
298+
// respectively.
299+
// Condition c ensures we replace both null and absent values.
300+
if (oldStoredValue != null && (!oldStoredValue.isDone() || oldStoredValue.evaluate() != null)) {
301+
return oldStoredValue;
302+
}
303+
rejectIfClosed();
304+
return candidateStoredValue;
305+
});
306+
307+
// Only the caller that created the candidateStoredValue may run it
308+
// and see the exception.
309+
if (candidateStoredValue.equals(storedValue)) {
310+
Object newResult = candidateStoredValue.execute();
311+
// DeferredOptionalValue is quite heavy, replace with lighter container
312+
if (candidateStoredValue.isPresent()) {
313+
storedValues.computeIfPresent(compositeKey, compareAndPut(storedValue, newStoredValue(newResult)));
314+
}
315+
return newResult;
277316
}
278-
rejectIfClosed();
279-
return candidateStoredValue;
280-
});
281317

282-
// In a race condition either put, getOrComputeIfAbsent, or another
283-
// computeIfAbsent call put a non-null value in the store
284-
if (!candidateStoredValue.equals(storedValue)) {
285-
return requireNonNull(storedValue.evaluate());
286-
}
287-
// Only the caller that created the candidateStoredValue may run it
288-
// and see the exception.
289-
Object newResult = candidateStoredValue.execute();
290-
// DeferredOptionalValue is quite heavy, replace with lighter container
291-
if (candidateStoredValue.isPresent()) {
292-
storedValues.computeIfPresent(compositeKey, compareAndPut(storedValue, newStoredValue(newResult)));
318+
// Awaits evaluation and uses the stored value if it was not null
319+
// this ensures we replace both null and absent values.
320+
// Otherwise, another thread won the race but failed to insert a
321+
// non-null value so we try again.
322+
Object storedResult = storedValue.evaluate();
323+
if (storedResult != null) {
324+
return storedResult;
325+
}
293326
}
294-
return newResult;
295327
}
296328

297329
private static <N> BiFunction<CompositeKey<N>, StoredValue, StoredValue> compareAndPut(StoredValue expectedValue,
@@ -488,6 +520,8 @@ private interface StoredValue {
488520

489521
boolean isPresent();
490522

523+
boolean isDone();
524+
491525
static @Nullable Object evaluateIfNotNull(@Nullable StoredValue value) {
492526
return value != null ? value.evaluate() : null;
493527
}
@@ -518,6 +552,11 @@ public boolean isPresent() {
518552
return true;
519553
}
520554

555+
@Override
556+
public boolean isDone() {
557+
return true;
558+
}
559+
521560
@Override
522561
public int order() {
523562
return order;
@@ -546,6 +585,11 @@ public boolean isPresent() {
546585
return true;
547586
}
548587

588+
@Override
589+
public boolean isDone() {
590+
return delegate.isDone();
591+
}
592+
549593
@Nullable
550594
Object execute() {
551595
delegate.run();
@@ -580,6 +624,11 @@ public boolean isPresent() {
580624
return evaluate() != null;
581625
}
582626

627+
@Override
628+
public boolean isDone() {
629+
return delegate.isDone();
630+
}
631+
583632
Object execute() {
584633
delegate.run();
585634
// Delegate does not produce null
@@ -670,6 +719,10 @@ Object getOrThrow() {
670719
throw throwAsUncheckedException(cause);
671720
}
672721
}
722+
723+
boolean isDone() {
724+
return task.isDone();
725+
}
673726
}
674727

675728
/**

platform-tests/src/test/java/org/junit/platform/engine/support/store/NamespacedHierarchicalStoreTests.java

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333

3434
import org.junit.jupiter.api.BeforeEach;
3535
import org.junit.jupiter.api.Nested;
36+
import org.junit.jupiter.api.RepeatedTest;
3637
import org.junit.jupiter.api.Test;
3738

3839
/**
@@ -431,6 +432,30 @@ void simulateRaceConditionInGetOrComputeIfAbsent() throws Exception {
431432
assertThat(values).hasSize(threads).containsOnly(1);
432433
}
433434

435+
@SuppressWarnings("deprecation")
436+
@RepeatedTest(value = 10, failureThreshold = 1)
437+
void simulateRaceConditionInGetOrComputeIfAbsentWithResizingMap() throws Exception {
438+
int threads = 10;
439+
AtomicInteger counter = new AtomicInteger();
440+
List<Object> values;
441+
442+
try (var localStore = new NamespacedHierarchicalStore<>(null)) {
443+
values = executeConcurrently(threads, //
444+
() -> {
445+
// Simulate other extensions computing values,
446+
// this will trigger several resizes of the store
447+
for (int i = 0; i < 16; i++) {
448+
localStore.getOrComputeIfAbsent(namespace, i, __ -> value);
449+
}
450+
return requireNonNull(
451+
localStore.getOrComputeIfAbsent(namespace, key, it -> counter.incrementAndGet()));
452+
});
453+
}
454+
455+
assertEquals(1, counter.get());
456+
assertThat(values).hasSize(threads).containsOnly(1);
457+
}
458+
434459
@Test
435460
void simulateRaceConditionInComputeIfAbsent() throws Exception {
436461
int threads = 10;
@@ -446,6 +471,29 @@ void simulateRaceConditionInComputeIfAbsent() throws Exception {
446471
assertThat(values).hasSize(threads).containsOnly(1);
447472
}
448473

474+
@RepeatedTest(value = 10, failureThreshold = 1)
475+
void simulateRaceConditionInComputeIfAbsentWithResizingMap() throws Exception {
476+
int threads = 10;
477+
AtomicInteger counter = new AtomicInteger();
478+
List<Object> values;
479+
480+
try (var localStore = new NamespacedHierarchicalStore<>(null)) {
481+
values = executeConcurrently(threads, //
482+
() -> {
483+
// Simulate other extensions computing values,
484+
// this will trigger several resizes of the store
485+
for (int i = 0; i < 16; i++) {
486+
localStore.computeIfAbsent(namespace, i, s -> value);
487+
}
488+
return requireNonNull(
489+
localStore.computeIfAbsent(namespace, key, it -> counter.incrementAndGet()));
490+
});
491+
}
492+
493+
assertEquals(1, counter.get());
494+
assertThat(values).hasSize(threads).containsOnly(1);
495+
}
496+
449497
@SuppressWarnings("deprecation")
450498
@Test
451499
void updateRecursivelyGetOrComputeIfAbsent() {

0 commit comments

Comments
 (0)