Skip to content

Conversation

marcphilipp
Copy link
Member

The new junit.jupiter.execution.parallel.config.interceptor.class
configuration parameter allows configuring the fully qualified class
name of a ParallelExecutionInterceptor implementation that gets called
for each executed TestTask. The FixedThreadPoolForTests sample
implementation uses a separate fixed thread pool to run all tasks of
type TEST with execution mode CONCURRENT in.

Resolves #3108.


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


Definition of Done

The new `junit.jupiter.execution.parallel.config.interceptor.class`
configuration parameter allows configuring the fully qualified class
name of a `ParallelExecutionInterceptor` implementation that gets called
for each executed `TestTask`. The `FixedThreadPoolForTests` sample
implementation uses a separate fixed thread pool to run all tasks of
type `TEST` with execution mode `CONCURRENT` in.

Resolves #3108.
@mpkorstanje
Copy link
Contributor

mpkorstanje commented Oct 6, 2025

The FixedThreadPoolForTests sample implementation uses a separate fixed thread pool to run all tasks of type TEST with execution mode CONCURRENT in.

While this solves the specific problem, the implementation feels incomplete. Is that intentional?

When using a fixed thread pool, for each subtrees that must be executed on the same thread, I would expect that a thread from the thread pool is used for that entire subtree. With tasks of the type tests as the trivial case.

Edit: wording for clarity.

@marcphilipp
Copy link
Member Author

marcphilipp commented Oct 6, 2025

When using a fixed thread pool, for each subtrees that must be executed on the same thread, I would expect that a thread from the thread pool is used for that entire subtree. With tasks of the type tests as the trivial case.

Since tests using ExecutionMode.CONCURRENT can be registered dynamically at runtime, we could only compute this value reliably if all TestDescriptors in the subtree returned false from their mayRegisterTests() methods. I guess we would restrict dynamic children to use SAME_THREAD regardless of what they return but we can't do so in general because that would be a regression. Thoughts?

@mpkorstanje
Copy link
Contributor

mpkorstanje commented Oct 6, 2025

I was under the impression that the NodeExecutionAdvisor would force the execution mode of all descendants to SAME_THREAD. So all dynamic children are already have the SAME_THREAD execution mode if any of their ancestors have SAME_THREAD regardless of their own execution mode.

@marcphilipp
Copy link
Member Author

I was under the impression that the NodeExecutionAdvisor would force the execution mode of all descendants to SAME_THREAD.

It only does so if exclusive resource / resource locks are used.

// If this method is called from outside the used ForkJoinPool,
// calls to fork() will schedule tasks in the commonPool
Preconditions.condition(isAlreadyRunningInForkJoinPool(),
"invokeAll() must be called from a thread in the ForkJoinPool");
Copy link
Contributor

Choose a reason for hiding this comment

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

Would it be possible to create a dummy task that calls invokeAll and submit that task instead?

Copy link
Contributor

Choose a reason for hiding this comment

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

Ah. No then SAME_THREAD won't work.

Copy link
Contributor

@mpkorstanje mpkorstanje Oct 6, 2025

Choose a reason for hiding this comment

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

But what if only the call to joinConcurrentTasksInReverseOrderToEnableWorkStealing(concurrentTasksInReverseOrder); is submitted?

@mpkorstanje
Copy link
Contributor

mpkorstanje commented Oct 6, 2025

I guess we would restrict dynamic children to use SAME_THREAD regardless of what they return

What if, upon encountering DYNAMIC children, execution goes back to the ForkJoinPoolHierarchicalTestExecutorService again, and then invokes the ParallelExecutionInterceptor for each task as needed?

@mpkorstanje
Copy link
Contributor

To clarify what I meant by incompleteness. When using junit.jupiter.execution.parallel.config.interceptor.class I would expect that it is possible to invoke all methods without a fork join pool. Not just the concurrent test methods methods but also the before all and same thread annotated methods.

Expressed as a test case:

@Execution(CONCURRENT)
static class ExampleTest {
	
	final static ThreadLocal<Integer> threadLocal = new ThreadLocal<>();

	@BeforeAll
	static void initialize() {
		assertThat(Thread.currentThread().getName()).doesNotStartWith("ForkJoinPool");
		threadLocal.set(42);
	}

	@Test
	@Execution(SAME_THREAD)
	void firstTest() throws Exception {
		assertThat(Thread.currentThread().getName()).doesNotStartWith("ForkJoinPool");
		assertEquals(42, threadLocal.get());
	}

	@Test
	void secondTest() throws Exception {
		assertThat(Thread.currentThread().getName()).doesNotStartWith("ForkJoinPool");
		assertNull(threadLocal.get());
	}
}

But I haven't quite got the time to try out my suggestions in code and see if they would actually work as expected.. Apologies for 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