Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
ede4958
Wait for deferred task before shutting down ForkJoinPool
marcphilipp Sep 19, 2024
7f90fe3
Add failing test case for work-stealing with compatible locks
marcphilipp Sep 19, 2024
8109abb
Allow work stealing when more locks can be acquired without deadlock
marcphilipp Sep 19, 2024
9803fce
Remove special classes for global locks
marcphilipp Sep 19, 2024
16f7f4c
Only check first additional resource as both sets are sorted
marcphilipp Sep 19, 2024
1fb9268
Use List to avoid overhead of TreeSet
marcphilipp Sep 19, 2024
627babc
Remove unused equals/hashCode methods
marcphilipp Sep 19, 2024
3698e1f
Make locks incompatible as soon as one of them requires exclusiveness
marcphilipp Sep 19, 2024
dc3135d
Remove unhelpful lock output from toString() methods
marcphilipp Sep 20, 2024
5a88d21
Add more/better test cases
marcphilipp Sep 20, 2024
f18f048
Polish ResourceLockTests
marcphilipp Sep 20, 2024
ec11802
Add explanatory comment
marcphilipp Sep 20, 2024
cd93fdb
Include tests for compatibility of global read lock with first read-w…
marcphilipp Sep 20, 2024
ed03140
Improve parameter names after extracting method
marcphilipp Sep 20, 2024
b6309e3
Simplify generating locks
marcphilipp Sep 20, 2024
395861f
Improve test naming
marcphilipp Sep 20, 2024
c205e5e
Add reasons for incompatibility and improve assertion messages
marcphilipp Sep 20, 2024
7c654e4
Add test for deferring multiple times
marcphilipp Sep 20, 2024
3bce266
Merge branch 'main' into marc/lock-compatibility-checking
marcphilipp Sep 20, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -10,22 +10,36 @@

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

import static java.util.Collections.unmodifiableList;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.locks.Lock;

import org.junit.platform.commons.util.Preconditions;
import org.junit.platform.commons.util.ToStringBuilder;

/**
* @since 1.3
*/
class CompositeLock implements ResourceLock {

private final List<ExclusiveResource> resources;
private final List<Lock> locks;
private final boolean exclusive;

CompositeLock(List<Lock> locks) {
CompositeLock(List<ExclusiveResource> resources, List<Lock> locks) {
Preconditions.condition(resources.size() == locks.size(), "Resources and locks must have the same size");
this.resources = unmodifiableList(resources);
this.locks = Preconditions.notEmpty(locks, "Locks must not be empty");
this.exclusive = resources.stream().anyMatch(
resource -> resource.getLockMode() == ExclusiveResource.LockMode.READ_WRITE);
}

@Override
public List<ExclusiveResource> getResources() {
return resources;
}

// for tests only
Expand Down Expand Up @@ -64,6 +78,18 @@ private void release(List<Lock> acquiredLocks) {
}
}

@Override
public boolean isExclusive() {
return exclusive;
}

@Override
public String toString() {
return new ToStringBuilder(this) //
.append("resources", resources) //
.toString();
}

private class CompositeLockManagedBlocker implements ForkJoinPool.ManagedBlocker {

private volatile boolean acquired;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,11 @@

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

import static java.util.Comparator.comparing;
import static java.util.Comparator.naturalOrder;
import static org.apiguardian.api.API.Status.STABLE;

import java.util.Comparator;
import java.util.Objects;
import java.util.concurrent.locks.ReadWriteLock;

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

static final Comparator<ExclusiveResource> COMPARATOR //
= comparing(ExclusiveResource::getKey, globalKeyFirst().thenComparing(naturalOrder())) //
.thenComparing(ExclusiveResource::getLockMode);

private static Comparator<String> globalKeyFirst() {
return comparing(key -> !GLOBAL_KEY.equals(key));
}

private final String key;
private final LockMode lockMode;
private int hash;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

import static java.util.concurrent.CompletableFuture.completedFuture;
import static org.apiguardian.api.API.Status.STABLE;
import static org.junit.platform.engine.support.hierarchical.ExclusiveResource.GLOBAL_READ_WRITE;
import static org.junit.platform.engine.support.hierarchical.Node.ExecutionMode.CONCURRENT;
import static org.junit.platform.engine.support.hierarchical.Node.ExecutionMode.SAME_THREAD;

Expand Down Expand Up @@ -39,7 +40,6 @@
import org.junit.platform.commons.logging.LoggerFactory;
import org.junit.platform.commons.util.ExceptionUtils;
import org.junit.platform.engine.ConfigurationParameters;
import org.junit.platform.engine.support.hierarchical.SingleLock.GlobalReadWriteLock;

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

private final ForkJoinPool forkJoinPool;
// package-private for testing
final ForkJoinPool forkJoinPool;

private final TaskEventListener taskEventListener;
private final int parallelism;
private final ThreadLocal<ThreadLock> threadLocks = ThreadLocal.withInitial(ThreadLock::new);
Expand Down Expand Up @@ -170,7 +172,7 @@ private void forkConcurrentTasks(List<? extends TestTask> tasks, Deque<Exclusive
Deque<ExclusiveTask> sameThreadTasks, Deque<ExclusiveTask> concurrentTasksInReverseOrder) {
for (TestTask testTask : tasks) {
ExclusiveTask exclusiveTask = new ExclusiveTask(testTask);
if (testTask.getResourceLock() instanceof GlobalReadWriteLock) {
if (requiresGlobalReadWriteLock(testTask)) {
isolatedTasks.add(exclusiveTask);
}
else if (testTask.getExecutionMode() == SAME_THREAD) {
Expand All @@ -183,6 +185,10 @@ else if (testTask.getExecutionMode() == SAME_THREAD) {
}
}

private static boolean requiresGlobalReadWriteLock(TestTask testTask) {
return testTask.getResourceLock().getResources().contains(GLOBAL_READ_WRITE);
}

private void executeSync(Deque<ExclusiveTask> tasks) {
for (ExclusiveTask task : tasks) {
task.execSync();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,18 +12,15 @@

import static java.util.Collections.emptyList;
import static java.util.Collections.singletonList;
import static java.util.Comparator.comparing;
import static java.util.Comparator.naturalOrder;
import static java.util.stream.Collectors.groupingBy;
import static java.util.stream.Collectors.toList;
import static org.junit.platform.commons.util.CollectionUtils.getOnlyElement;
import static org.junit.platform.engine.support.hierarchical.ExclusiveResource.GLOBAL_KEY;
import static org.junit.platform.commons.util.CollectionUtils.toUnmodifiableList;
import static org.junit.platform.engine.support.hierarchical.ExclusiveResource.GLOBAL_READ;
import static org.junit.platform.engine.support.hierarchical.ExclusiveResource.GLOBAL_READ_WRITE;
import static org.junit.platform.engine.support.hierarchical.ExclusiveResource.LockMode.READ;

import java.util.Collection;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
Expand All @@ -32,83 +29,75 @@
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

import org.junit.platform.engine.support.hierarchical.SingleLock.GlobalReadLock;
import org.junit.platform.engine.support.hierarchical.SingleLock.GlobalReadWriteLock;

/**
* @since 1.3
*/
class LockManager {

private static final Comparator<ExclusiveResource> COMPARATOR //
= comparing(ExclusiveResource::getKey, globalKeyFirst().thenComparing(naturalOrder())) //
.thenComparing(ExclusiveResource::getLockMode);

private static Comparator<String> globalKeyFirst() {
return comparing(key -> !GLOBAL_KEY.equals(key));
}

private final Map<String, ReadWriteLock> locksByKey = new ConcurrentHashMap<>();
private final GlobalReadLock globalReadLock;
private final GlobalReadWriteLock globalReadWriteLock;
private final SingleLock globalReadLock;
private final SingleLock globalReadWriteLock;

public LockManager() {
globalReadLock = new GlobalReadLock(toLock(GLOBAL_READ));
globalReadWriteLock = new GlobalReadWriteLock(toLock(GLOBAL_READ_WRITE));
globalReadLock = new SingleLock(GLOBAL_READ, toLock(GLOBAL_READ));
globalReadWriteLock = new SingleLock(GLOBAL_READ_WRITE, toLock(GLOBAL_READ_WRITE));
}

ResourceLock getLockForResources(Collection<ExclusiveResource> resources) {
return toResourceLock(toDistinctSortedLocks(resources));
return toResourceLock(toDistinctSortedResources(resources));
}

ResourceLock getLockForResource(ExclusiveResource resource) {
return toResourceLock(toLock(resource));
return toResourceLock(singletonList(resource));
}

private List<Lock> toDistinctSortedLocks(Collection<ExclusiveResource> resources) {
private List<ExclusiveResource> toDistinctSortedResources(Collection<ExclusiveResource> resources) {
if (resources.isEmpty()) {
return emptyList();
}
if (resources.size() == 1) {
return singletonList(toLock(getOnlyElement(resources)));
return singletonList(getOnlyElement(resources));
}
// @formatter:off
Map<String, List<ExclusiveResource>> resourcesByKey = resources.stream()
.sorted(COMPARATOR)
.sorted(ExclusiveResource.COMPARATOR)
.distinct()
.collect(groupingBy(ExclusiveResource::getKey, LinkedHashMap::new, toList()));

return resourcesByKey.values().stream()
.map(resourcesWithSameKey -> resourcesWithSameKey.get(0))
.map(this::toLock)
.collect(toList());
.collect(toUnmodifiableList());
// @formatter:on
}

private Lock toLock(ExclusiveResource resource) {
ReadWriteLock lock = this.locksByKey.computeIfAbsent(resource.getKey(), key -> new ReentrantReadWriteLock());
return resource.getLockMode() == READ ? lock.readLock() : lock.writeLock();
}

private ResourceLock toResourceLock(List<Lock> locks) {
switch (locks.size()) {
private ResourceLock toResourceLock(List<ExclusiveResource> resources) {
switch (resources.size()) {
case 0:
return NopLock.INSTANCE;
case 1:
return toResourceLock(locks.get(0));
return toSingleLock(getOnlyElement(resources));
default:
return new CompositeLock(locks);
return new CompositeLock(resources, toLocks(resources));
}
}

private ResourceLock toResourceLock(Lock lock) {
if (lock == toLock(GLOBAL_READ)) {
private SingleLock toSingleLock(ExclusiveResource resource) {
if (GLOBAL_READ.equals(resource)) {
return globalReadLock;
}
if (lock == toLock(GLOBAL_READ_WRITE)) {
if (GLOBAL_READ_WRITE.equals(resource)) {
return globalReadWriteLock;
}
return new SingleLock(lock);
return new SingleLock(resource, toLock(resource));
}

private List<Lock> toLocks(List<ExclusiveResource> resources) {
return resources.stream().map(this::toLock).collect(toUnmodifiableList());
}

private Lock toLock(ExclusiveResource resource) {
ReadWriteLock lock = this.locksByKey.computeIfAbsent(resource.getKey(), key -> new ReentrantReadWriteLock());
return resource.getLockMode() == READ ? lock.readLock() : lock.writeLock();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,12 @@

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

import static java.util.Collections.emptyList;

import java.util.List;

import org.junit.platform.commons.util.ToStringBuilder;

/**
* No-op {@link ResourceLock} implementation.
*
Expand All @@ -22,6 +28,11 @@ class NopLock implements ResourceLock {
private NopLock() {
}

@Override
public List<ExclusiveResource> getResources() {
return emptyList();
}

@Override
public ResourceLock acquire() {
return this;
Expand All @@ -32,4 +43,13 @@ public void release() {
// nothing to do
}

@Override
public boolean isExclusive() {
return false;
}

@Override
public String toString() {
return new ToStringBuilder(this).toString();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@

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

import java.util.List;
import java.util.Optional;

import org.apiguardian.api.API;

/**
Expand Down Expand Up @@ -43,12 +46,50 @@ default void close() {
release();
}

/**
* {@return the exclusive resources this lock represents}
*/
List<ExclusiveResource> getResources();

/**
* {@return whether this lock requires exclusiveness}
*/
boolean isExclusive();

/**
* {@return whether the given lock is compatible with this lock}
* @param other the other lock to check for compatibility
*/
default boolean isCompatible(ResourceLock other) {
return this instanceof NopLock || other instanceof NopLock;
}

List<ExclusiveResource> ownResources = this.getResources();
List<ExclusiveResource> otherResources = other.getResources();

if (ownResources.isEmpty() || otherResources.isEmpty()) {
return true;
}

// Whenever there's a READ_WRITE lock, it's incompatible with any other lock
// because we guarantee that all children will have exclusive access to the
// resource in question. In practice, whenever a READ_WRITE lock is present,
// NodeTreeWalker will force all children to run in the same thread so that
// it should never attempt to steal work from another thread, and we shouldn't
// actually reach this point.
// The global read lock (which is always on direct children of the engine node)
// needs special treatment so that it is compatible with the first write lock
// (which may be on a test method).
boolean isGlobalReadLock = ownResources.size() == 1
&& ExclusiveResource.GLOBAL_READ.equals(ownResources.get(0));
if ((!isGlobalReadLock && other.isExclusive()) || this.isExclusive()) {
return false;
}

Optional<ExclusiveResource> potentiallyDeadlockCausingAdditionalResource = otherResources.stream() //
.filter(resource -> !ownResources.contains(resource)) //
.findFirst() //
.filter(resource -> ExclusiveResource.COMPARATOR.compare(resource,
ownResources.get(ownResources.size() - 1)) < 0);

return !(potentiallyDeadlockCausingAdditionalResource.isPresent());
}
}
Loading