Skip to content

Commit 6529d8d

Browse files
Allow for work stealing when only holding read locks (#4012)
This allows tasks with only read locks to be stolen by threads that are currently holding only read locks. --------- Co-authored-by: Leonard Brünings <leonard.bruenings@gradle.com>
1 parent 0d25a5a commit 6529d8d

File tree

12 files changed

+560
-150
lines changed

12 files changed

+560
-150
lines changed

junit-platform-engine/src/main/java/org/junit/platform/engine/support/hierarchical/CompositeLock.java

+27-1
Original file line numberDiff line numberDiff line change
@@ -10,22 +10,36 @@
1010

1111
package org.junit.platform.engine.support.hierarchical;
1212

13+
import static java.util.Collections.unmodifiableList;
14+
1315
import java.util.ArrayList;
1416
import java.util.List;
1517
import java.util.concurrent.ForkJoinPool;
1618
import java.util.concurrent.locks.Lock;
1719

1820
import org.junit.platform.commons.util.Preconditions;
21+
import org.junit.platform.commons.util.ToStringBuilder;
1922

2023
/**
2124
* @since 1.3
2225
*/
2326
class CompositeLock implements ResourceLock {
2427

28+
private final List<ExclusiveResource> resources;
2529
private final List<Lock> locks;
30+
private final boolean exclusive;
2631

27-
CompositeLock(List<Lock> locks) {
32+
CompositeLock(List<ExclusiveResource> resources, List<Lock> locks) {
33+
Preconditions.condition(resources.size() == locks.size(), "Resources and locks must have the same size");
34+
this.resources = unmodifiableList(resources);
2835
this.locks = Preconditions.notEmpty(locks, "Locks must not be empty");
36+
this.exclusive = resources.stream().anyMatch(
37+
resource -> resource.getLockMode() == ExclusiveResource.LockMode.READ_WRITE);
38+
}
39+
40+
@Override
41+
public List<ExclusiveResource> getResources() {
42+
return resources;
2943
}
3044

3145
// for tests only
@@ -64,6 +78,18 @@ private void release(List<Lock> acquiredLocks) {
6478
}
6579
}
6680

81+
@Override
82+
public boolean isExclusive() {
83+
return exclusive;
84+
}
85+
86+
@Override
87+
public String toString() {
88+
return new ToStringBuilder(this) //
89+
.append("resources", resources) //
90+
.toString();
91+
}
92+
6793
private class CompositeLockManagedBlocker implements ForkJoinPool.ManagedBlocker {
6894

6995
private volatile boolean acquired;

junit-platform-engine/src/main/java/org/junit/platform/engine/support/hierarchical/ExclusiveResource.java

+11
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,11 @@
1010

1111
package org.junit.platform.engine.support.hierarchical;
1212

13+
import static java.util.Comparator.comparing;
14+
import static java.util.Comparator.naturalOrder;
1315
import static org.apiguardian.api.API.Status.STABLE;
1416

17+
import java.util.Comparator;
1518
import java.util.Objects;
1619
import java.util.concurrent.locks.ReadWriteLock;
1720

@@ -50,6 +53,14 @@ public class ExclusiveResource {
5053
static final ExclusiveResource GLOBAL_READ = new ExclusiveResource(GLOBAL_KEY, LockMode.READ);
5154
static final ExclusiveResource GLOBAL_READ_WRITE = new ExclusiveResource(GLOBAL_KEY, LockMode.READ_WRITE);
5255

56+
static final Comparator<ExclusiveResource> COMPARATOR //
57+
= comparing(ExclusiveResource::getKey, globalKeyFirst().thenComparing(naturalOrder())) //
58+
.thenComparing(ExclusiveResource::getLockMode);
59+
60+
private static Comparator<String> globalKeyFirst() {
61+
return comparing(key -> !GLOBAL_KEY.equals(key));
62+
}
63+
5364
private final String key;
5465
private final LockMode lockMode;
5566
private int hash;

junit-platform-engine/src/main/java/org/junit/platform/engine/support/hierarchical/ForkJoinPoolHierarchicalTestExecutorService.java

+9-3
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212

1313
import static java.util.concurrent.CompletableFuture.completedFuture;
1414
import static org.apiguardian.api.API.Status.STABLE;
15+
import static org.junit.platform.engine.support.hierarchical.ExclusiveResource.GLOBAL_READ_WRITE;
1516
import static org.junit.platform.engine.support.hierarchical.Node.ExecutionMode.CONCURRENT;
1617
import static org.junit.platform.engine.support.hierarchical.Node.ExecutionMode.SAME_THREAD;
1718

@@ -39,7 +40,6 @@
3940
import org.junit.platform.commons.logging.LoggerFactory;
4041
import org.junit.platform.commons.util.ExceptionUtils;
4142
import org.junit.platform.engine.ConfigurationParameters;
42-
import org.junit.platform.engine.support.hierarchical.SingleLock.GlobalReadWriteLock;
4343

4444
/**
4545
* A {@link ForkJoinPool}-based
@@ -53,7 +53,9 @@
5353
@API(status = STABLE, since = "1.10")
5454
public class ForkJoinPoolHierarchicalTestExecutorService implements HierarchicalTestExecutorService {
5555

56-
private final ForkJoinPool forkJoinPool;
56+
// package-private for testing
57+
final ForkJoinPool forkJoinPool;
58+
5759
private final TaskEventListener taskEventListener;
5860
private final int parallelism;
5961
private final ThreadLocal<ThreadLock> threadLocks = ThreadLocal.withInitial(ThreadLock::new);
@@ -170,7 +172,7 @@ private void forkConcurrentTasks(List<? extends TestTask> tasks, Deque<Exclusive
170172
Deque<ExclusiveTask> sameThreadTasks, Deque<ExclusiveTask> concurrentTasksInReverseOrder) {
171173
for (TestTask testTask : tasks) {
172174
ExclusiveTask exclusiveTask = new ExclusiveTask(testTask);
173-
if (testTask.getResourceLock() instanceof GlobalReadWriteLock) {
175+
if (requiresGlobalReadWriteLock(testTask)) {
174176
isolatedTasks.add(exclusiveTask);
175177
}
176178
else if (testTask.getExecutionMode() == SAME_THREAD) {
@@ -183,6 +185,10 @@ else if (testTask.getExecutionMode() == SAME_THREAD) {
183185
}
184186
}
185187

188+
private static boolean requiresGlobalReadWriteLock(TestTask testTask) {
189+
return testTask.getResourceLock().getResources().contains(GLOBAL_READ_WRITE);
190+
}
191+
186192
private void executeSync(Deque<ExclusiveTask> tasks) {
187193
for (ExclusiveTask task : tasks) {
188194
task.execSync();

junit-platform-engine/src/main/java/org/junit/platform/engine/support/hierarchical/LockManager.java

+28-39
Original file line numberDiff line numberDiff line change
@@ -12,18 +12,15 @@
1212

1313
import static java.util.Collections.emptyList;
1414
import static java.util.Collections.singletonList;
15-
import static java.util.Comparator.comparing;
16-
import static java.util.Comparator.naturalOrder;
1715
import static java.util.stream.Collectors.groupingBy;
1816
import static java.util.stream.Collectors.toList;
1917
import static org.junit.platform.commons.util.CollectionUtils.getOnlyElement;
20-
import static org.junit.platform.engine.support.hierarchical.ExclusiveResource.GLOBAL_KEY;
18+
import static org.junit.platform.commons.util.CollectionUtils.toUnmodifiableList;
2119
import static org.junit.platform.engine.support.hierarchical.ExclusiveResource.GLOBAL_READ;
2220
import static org.junit.platform.engine.support.hierarchical.ExclusiveResource.GLOBAL_READ_WRITE;
2321
import static org.junit.platform.engine.support.hierarchical.ExclusiveResource.LockMode.READ;
2422

2523
import java.util.Collection;
26-
import java.util.Comparator;
2724
import java.util.LinkedHashMap;
2825
import java.util.List;
2926
import java.util.Map;
@@ -32,83 +29,75 @@
3229
import java.util.concurrent.locks.ReadWriteLock;
3330
import java.util.concurrent.locks.ReentrantReadWriteLock;
3431

35-
import org.junit.platform.engine.support.hierarchical.SingleLock.GlobalReadLock;
36-
import org.junit.platform.engine.support.hierarchical.SingleLock.GlobalReadWriteLock;
37-
3832
/**
3933
* @since 1.3
4034
*/
4135
class LockManager {
4236

43-
private static final Comparator<ExclusiveResource> COMPARATOR //
44-
= comparing(ExclusiveResource::getKey, globalKeyFirst().thenComparing(naturalOrder())) //
45-
.thenComparing(ExclusiveResource::getLockMode);
46-
47-
private static Comparator<String> globalKeyFirst() {
48-
return comparing(key -> !GLOBAL_KEY.equals(key));
49-
}
50-
5137
private final Map<String, ReadWriteLock> locksByKey = new ConcurrentHashMap<>();
52-
private final GlobalReadLock globalReadLock;
53-
private final GlobalReadWriteLock globalReadWriteLock;
38+
private final SingleLock globalReadLock;
39+
private final SingleLock globalReadWriteLock;
5440

5541
public LockManager() {
56-
globalReadLock = new GlobalReadLock(toLock(GLOBAL_READ));
57-
globalReadWriteLock = new GlobalReadWriteLock(toLock(GLOBAL_READ_WRITE));
42+
globalReadLock = new SingleLock(GLOBAL_READ, toLock(GLOBAL_READ));
43+
globalReadWriteLock = new SingleLock(GLOBAL_READ_WRITE, toLock(GLOBAL_READ_WRITE));
5844
}
5945

6046
ResourceLock getLockForResources(Collection<ExclusiveResource> resources) {
61-
return toResourceLock(toDistinctSortedLocks(resources));
47+
return toResourceLock(toDistinctSortedResources(resources));
6248
}
6349

6450
ResourceLock getLockForResource(ExclusiveResource resource) {
65-
return toResourceLock(toLock(resource));
51+
return toResourceLock(singletonList(resource));
6652
}
6753

68-
private List<Lock> toDistinctSortedLocks(Collection<ExclusiveResource> resources) {
54+
private List<ExclusiveResource> toDistinctSortedResources(Collection<ExclusiveResource> resources) {
6955
if (resources.isEmpty()) {
7056
return emptyList();
7157
}
7258
if (resources.size() == 1) {
73-
return singletonList(toLock(getOnlyElement(resources)));
59+
return singletonList(getOnlyElement(resources));
7460
}
7561
// @formatter:off
7662
Map<String, List<ExclusiveResource>> resourcesByKey = resources.stream()
77-
.sorted(COMPARATOR)
63+
.sorted(ExclusiveResource.COMPARATOR)
7864
.distinct()
7965
.collect(groupingBy(ExclusiveResource::getKey, LinkedHashMap::new, toList()));
8066

8167
return resourcesByKey.values().stream()
8268
.map(resourcesWithSameKey -> resourcesWithSameKey.get(0))
83-
.map(this::toLock)
84-
.collect(toList());
69+
.collect(toUnmodifiableList());
8570
// @formatter:on
8671
}
8772

88-
private Lock toLock(ExclusiveResource resource) {
89-
ReadWriteLock lock = this.locksByKey.computeIfAbsent(resource.getKey(), key -> new ReentrantReadWriteLock());
90-
return resource.getLockMode() == READ ? lock.readLock() : lock.writeLock();
91-
}
92-
93-
private ResourceLock toResourceLock(List<Lock> locks) {
94-
switch (locks.size()) {
73+
private ResourceLock toResourceLock(List<ExclusiveResource> resources) {
74+
switch (resources.size()) {
9575
case 0:
9676
return NopLock.INSTANCE;
9777
case 1:
98-
return toResourceLock(locks.get(0));
78+
return toSingleLock(getOnlyElement(resources));
9979
default:
100-
return new CompositeLock(locks);
80+
return new CompositeLock(resources, toLocks(resources));
10181
}
10282
}
10383

104-
private ResourceLock toResourceLock(Lock lock) {
105-
if (lock == toLock(GLOBAL_READ)) {
84+
private SingleLock toSingleLock(ExclusiveResource resource) {
85+
if (GLOBAL_READ.equals(resource)) {
10686
return globalReadLock;
10787
}
108-
if (lock == toLock(GLOBAL_READ_WRITE)) {
88+
if (GLOBAL_READ_WRITE.equals(resource)) {
10989
return globalReadWriteLock;
11090
}
111-
return new SingleLock(lock);
91+
return new SingleLock(resource, toLock(resource));
92+
}
93+
94+
private List<Lock> toLocks(List<ExclusiveResource> resources) {
95+
return resources.stream().map(this::toLock).collect(toUnmodifiableList());
96+
}
97+
98+
private Lock toLock(ExclusiveResource resource) {
99+
ReadWriteLock lock = this.locksByKey.computeIfAbsent(resource.getKey(), key -> new ReentrantReadWriteLock());
100+
return resource.getLockMode() == READ ? lock.readLock() : lock.writeLock();
112101
}
113102

114103
}

junit-platform-engine/src/main/java/org/junit/platform/engine/support/hierarchical/NopLock.java

+20
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,12 @@
1010

1111
package org.junit.platform.engine.support.hierarchical;
1212

13+
import static java.util.Collections.emptyList;
14+
15+
import java.util.List;
16+
17+
import org.junit.platform.commons.util.ToStringBuilder;
18+
1319
/**
1420
* No-op {@link ResourceLock} implementation.
1521
*
@@ -22,6 +28,11 @@ class NopLock implements ResourceLock {
2228
private NopLock() {
2329
}
2430

31+
@Override
32+
public List<ExclusiveResource> getResources() {
33+
return emptyList();
34+
}
35+
2536
@Override
2637
public ResourceLock acquire() {
2738
return this;
@@ -32,4 +43,13 @@ public void release() {
3243
// nothing to do
3344
}
3445

46+
@Override
47+
public boolean isExclusive() {
48+
return false;
49+
}
50+
51+
@Override
52+
public String toString() {
53+
return new ToStringBuilder(this).toString();
54+
}
3555
}

junit-platform-engine/src/main/java/org/junit/platform/engine/support/hierarchical/ResourceLock.java

+43-2
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@
1212

1313
import static org.apiguardian.api.API.Status.STABLE;
1414

15+
import java.util.List;
16+
import java.util.Optional;
17+
1518
import org.apiguardian.api.API;
1619

1720
/**
@@ -43,12 +46,50 @@ default void close() {
4346
release();
4447
}
4548

49+
/**
50+
* {@return the exclusive resources this lock represents}
51+
*/
52+
List<ExclusiveResource> getResources();
53+
54+
/**
55+
* {@return whether this lock requires exclusiveness}
56+
*/
57+
boolean isExclusive();
58+
4659
/**
4760
* {@return whether the given lock is compatible with this lock}
4861
* @param other the other lock to check for compatibility
4962
*/
5063
default boolean isCompatible(ResourceLock other) {
51-
return this instanceof NopLock || other instanceof NopLock;
52-
}
5364

65+
List<ExclusiveResource> ownResources = this.getResources();
66+
List<ExclusiveResource> otherResources = other.getResources();
67+
68+
if (ownResources.isEmpty() || otherResources.isEmpty()) {
69+
return true;
70+
}
71+
72+
// Whenever there's a READ_WRITE lock, it's incompatible with any other lock
73+
// because we guarantee that all children will have exclusive access to the
74+
// resource in question. In practice, whenever a READ_WRITE lock is present,
75+
// NodeTreeWalker will force all children to run in the same thread so that
76+
// it should never attempt to steal work from another thread, and we shouldn't
77+
// actually reach this point.
78+
// The global read lock (which is always on direct children of the engine node)
79+
// needs special treatment so that it is compatible with the first write lock
80+
// (which may be on a test method).
81+
boolean isGlobalReadLock = ownResources.size() == 1
82+
&& ExclusiveResource.GLOBAL_READ.equals(ownResources.get(0));
83+
if ((!isGlobalReadLock && other.isExclusive()) || this.isExclusive()) {
84+
return false;
85+
}
86+
87+
Optional<ExclusiveResource> potentiallyDeadlockCausingAdditionalResource = otherResources.stream() //
88+
.filter(resource -> !ownResources.contains(resource)) //
89+
.findFirst() //
90+
.filter(resource -> ExclusiveResource.COMPARATOR.compare(resource,
91+
ownResources.get(ownResources.size() - 1)) < 0);
92+
93+
return !(potentiallyDeadlockCausingAdditionalResource.isPresent());
94+
}
5495
}

0 commit comments

Comments
 (0)