Skip to content

Conversation

@marcphilipp
Copy link
Member

@marcphilipp marcphilipp commented Oct 13, 2025

This PR introduces a new implementation of HierarchicalTestExecutorService that runs rests in parallel and has limited work stealing capabilities but is not based on ForkJoinPool. It avoids its pitfalls (such as #3945) and #3108 but may require additional threads because its work stealing is limited to direct children. Contrary to the ForkJoinPool implementation, the new executor service guarantees that no more than parallelism test nodes are executed in parallel.

My intention is to initially ship this implementation as an opt-in feature (via the new junit.jupiter.execution.parallel.executor configuration parameter) in 6.1, make it an opt-out feature in 6.2, and drop support for the ForkJoinPool-based implementation in a later to-be-determined release.

The PR is not yet finished but feedback is already welcome! If you use parallel test execution in your projects (or other test engines), it would be great if you could try out the new implementation and report your observations.

Resolves #3108.


I hereby agree to the terms of the JUnit Contributor License Agreement.


Definition of Done

@marcphilipp marcphilipp marked this pull request as ready for review October 29, 2025 14:41
@marcphilipp
Copy link
Member Author

I changed how to opt-in to the feature and have updated the docs.

@mpkorstanje I'd appreciate your feedback! 🙂

+ PARALLEL_CONFIG_EXECUTOR_SERVICE_PROPERTY_NAME + "' configuration parameter to '"
+ WORKER_THREAD_POOL + "' and report any issues to the JUnit team. "
+ "Alternatively, set the configuration parameter to '" + FORK_JOIN_POOL
+ "' to hide this message.");
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
+ "' to hide this message.");
+ "' to hide this message and use the original implementation.");


* Support for creating a `ModuleSelector` from a `java.lang.Module` and using
its classloader for test discovery.
* New `WorkerThreadPoolHierarchicalTestExecutorService` implementation of parallel test
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* New `WorkerThreadPoolHierarchicalTestExecutorService` implementation of parallel test
* New `WorkerThreadPoolHierarchicalTestExecutorService` implementation used for parallel test

itself prior to waiting for its children to finish. In case it does need to block, it temporarily gives up its
worker lease and starts another worker thread to compensate for the reduced `parallelism`. If the max pool size
does not permit starting another thread, that is ignored in case there are still other active worker threads.
The same happens in case a resource lock needs to be acquired.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This reads like a flow of consciousness.

I'd suggest rewriting it to <objective> <action> format.

		This implementation is based on a regular thread pool and a work queue shared among all worker threads.

		Each worker thread scans the shared work queue for tasks to run. Since the tasks represent hierarchically
		structured tests, container tasks will call `submit(TestTask)` or `invokeAll(List<TestTask>)` for their
		children, recursively.
		
		To maintain the desired parallelism -- regardless whether the user code performs any blocking operations --
		a fixed number of worker leases is configured. Whenever a task is submitted to the work queue to be executed
		concurrently, an attempt is made to acquire a worker lease. If a worker lease was acquired, a worker thread is
		started. Each worker thread attempts to "steal" queue entries for its children and execute them itself prior to waiting
		for its children to finish.
		 
		To optimize CPU utilization, whenever a worker thread does need to block, it temporarily gives up its worker
		lease and attempts to start another worker thread to compensate for the reduced `parallelism`. If the max pool size does 
		not permit starting another thread, the attempt is ignored in case there are still other active worker threads.
		
		The same happens in case a resource lock needs to be acquired.
		
		To minimize the number of idle workers, worker threads will prefer to steal top level tasks, while working
		through their own task hierarchy in a depth first fashion. Furthermore, child tasks with execution mode
		`CONCURRENT` are submitted to the shared queue be prior to executing those with execution mode `SAME_THREAD`
		directly.

}

private static class WorkQueue implements Iterable<WorkQueue.Entry> {
private final AtomicLong index = new AtomicLong();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm having some second thoughts about this. This is an exhaustible resource, especially compared to ConcurrentSkipListSet. Not that I think any one could reasonably exhaust it, but those are famous last words.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've had a look at replacing the ConcurrentSkipListSet with a PriorityBlockingQueue. Because we depend on both queue and iteration order, the PriorityBlockingQueue with its "messy" iteration order can't be used because it is not guaranteed that the executing thread and work stealer will approach each other from opposite directions..

Resolved the potential exhaustion in 1f9ffde by using the UniqueId to create an arbitrary ordering. This however meant that invokeAll order wasn't used any more. 592a05e solves that.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

JUnit parallel executor running too many tests with a fixed policy

3 participants