Skip to content

Check parent done condition before rejecting extraneous worker#5238

Merged
mpkorstanje merged 3 commits intomainfrom
rien/check-done-condition-before-rejection
Dec 22, 2025
Merged

Check parent done condition before rejecting extraneous worker#5238
mpkorstanje merged 3 commits intomainfrom
rien/check-done-condition-before-rejection

Conversation

@mpkorstanje
Copy link
Copy Markdown
Contributor

@mpkorstanje mpkorstanje commented Dec 19, 2025

The WorkerThreadPoolHierarchicalTestExecutorServiceTests.limitsWorkerThreadsToMaxPoolSize is flaky with a rather puzzling exception. While there are 3 permits available, 1 worker active thread. And yet the task rejected from the pool.

Caused by: java.util.concurrent.RejectedExecutionException: Task with WorkerLeaseManager [parallelism = 3, semaphore = java.util.concurrent.Semaphore@5af97070[Permits = 3]] rejected from java.util.concurrent.ThreadPoolExecutor@f5237bd2[Running, pool size = 3, active threads = 1, queued tasks = 0, completed tasks = 2]
	at org.junit.platform.engine.support.hierarchical.WorkerThreadPoolHierarchicalTestExecutorService$LeaseAwareRejectedExecutionHandler.rejectedExecution(WorkerThreadPoolHierarchicalTestExecutorService.java:922)

This happens because, given the following test tree with a max of 3 workers, worker 1 starts execution at the root node:

root  <- worker 1
 |- child1
 | |- leaf1a
 | |- leaf1b
 |- child2
 | |- leaf2a
 | |- leaf2b

When the child nodes are executed, there are 2 worker threads active and 1 available. The state at this point looks like this:

root
 |- child1 <- worker 1
 | |- leaf1a 
 | |- leaf1b 
 |- child2  <- worker 2
 | |- leaf2a
 | |- leaf2b

Both workers will then race each other trying to start 1 more. The winner of that race, child1 for the sake of argument, will then execute the leaf1a, while the other thread will start working on the second leaf1b node.

The state at this point looks like this:

root
 |- child1
 | |- leaf1a <- worker 1
 | |- leaf1b <- worker 3
 |- child2
 | |- leaf2a <- worker 2
 | |- leaf2b

Then a second race condition happens. After leaf1a is done, the thread starts to wait for leaf1b to be finished. While it is waiting, it returns its worker lease and tries to compensate. At the time is tries to compensate, both worker 2 and worker 3 are still busy and the executor rejects the task.

The state at this point looks like this:

root
 |- child1  <- worker 1 (awaiting leaf1b)
 | |- leaf1a
 | |- leaf1b <- worker 3
 |- child2
 | |- leaf2a <- worker 2
 | |- leaf2b

Then a second race condition happens. After leaf1a is done, the thread starts to wait for leaf1b to be finished. While it is waiting, it returns its worker lease and tries to compensate. At the time is tries to compensate, both worker 2 and worker 3 are still busy and the executor rejects the task.

By the time the RejectedExecutionHandler is invoked, worker 2 and 3 have finished their work and returned their leases. The state at this point looks like this:

root
 |- child1 <- worker 1 (awaiting leaf1b)
 | |- leaf1a
 | |- leaf1b 
 |- child2
 | |- leaf2a 
 | |- leaf2b

At this point the executor.isShutdown() || workerLeaseManager.isAtLeastOneLeaseTaken() condition evaluates to false because 1) the executor is not shutdown and 2) there are no workers with active leases so an exception is thrown. By also checking if the doneCondition of the worker that tried to start the new worker we can see that it was waiting in vain.


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


Definition of Done

@mpkorstanje mpkorstanje force-pushed the rien/check-done-condition-before-rejection branch 2 times, most recently from a30c5d1 to 8acb708 Compare December 19, 2025 01:41
@mpkorstanje mpkorstanje changed the title Check done condition before rejecting extraneous worker Check parent done condition before rejecting extraneous worker Dec 19, 2025
@mpkorstanje mpkorstanje force-pushed the rien/check-done-condition-before-rejection branch from 8acb708 to 270fe55 Compare December 19, 2025 01:45
@testlens-app
Copy link
Copy Markdown

testlens-app Bot commented Dec 19, 2025

🔎 No tests executed 🔎

🏷️ Commit: 6966ccb
▶️ Tests: 0 executed
🟡 Checks: 0/12 completed


Learn more about TestLens at testlens.app.

@mpkorstanje mpkorstanje marked this pull request as ready for review December 19, 2025 12:54
@mpkorstanje mpkorstanje added this to the 6.0.2 milestone Dec 20, 2025
Copy link
Copy Markdown
Member

@marcphilipp marcphilipp left a comment

Choose a reason for hiding this comment

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

Thanks for the detailed analysis! 👍

@mpkorstanje mpkorstanje merged commit 436e420 into main Dec 22, 2025
13 checks passed
@mpkorstanje mpkorstanje deleted the rien/check-done-condition-before-rejection branch December 22, 2025 19:55
@marcphilipp marcphilipp modified the milestones: 6.0.2, 6.1.0-M2 Dec 28, 2025
marcphilipp added a commit that referenced this pull request Jan 6, 2026
marcphilipp added a commit that referenced this pull request Jan 6, 2026
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.

2 participants