Repository navigation
[improve][ci] Shorten CI - Unit - Other: start the jcloud tests first, skip JVM-wide threads in the leak detector - #27305
Merged
Conversation
…R unit test group `test` requests the test tasks by project path, and Gradle starts the ready tasks in that order, so :tiered-storage:tiered-storage-jcloud:test started last in every CI run (task 575 of 610). Most of its 2-4 minutes is BlobStoreManagedLedgerOffloaderTest on one fork, so the build ended with one test JVM busy and three lanes idle: a median of 75 s over the 58 scheduled master runs from 4 Oct to 2 Nov. Request it before `test`, so it is planned first (task 50 of the same 610) and runs alongside the other modules' tests.
CI sets THREAD_LEAK_DETECTOR_WAIT_MILLIS=10000, and under Gradle the listener checks once per test JVM, when its tests are done. Any thread started during the run that is still alive makes it wait the full 10 s and write a leak report. In CI - Unit - Other, 33-51 of the 125 test JVMs per run waited, and in 68-73% of them every reported thread lives as long as the JVM: - ForkJoinPool.commonPool-delayScheduler: since Java 25 it runs CompletableFuture.delayedExecutor and orTimeout, in place of the CompletableFutureDelayScheduler thread that was already skipped - Cleaner-N in the JDK's InnocuousThreadGroup: a java.lang.ref.Cleaner thread, e.g. from the static Cleaner in PulsarExecutors - pulsar-auth-blocking-shared: the process-wide pool of V5AuthContexts - JFR Recorder Thread, JFR Periodic Tasks, Java2D Disposer and VirtualThread-unblocker, started by the JVM on first use Skip them like the other JVM-wide threads, and test the rules on the threads the JDK actually starts.
lhotari
approved these changes
Nov 2, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Motivation
CI - Unit - Otherruns the tests of about 40 modules in one Gradle build on a 4-vCPU runner: 42 test tasks and 125test JVMs, with the CPU 80–96 % busy for most of the build. Two things waste time in every run.
The tiered-storage-jcloud tests run last, and alone.
testrequests the test tasks by project path, and Gradlestarts the ready tasks in that order, so
:tiered-storage:tiered-storage-jcloud:teststarts last in every run. Adry run of the group plans it as task 575 of 610. Most of its 2–4 minutes is
BlobStoreManagedLedgerOffloaderTest(81–201 s) on one fork, so the build ends with one test JVM busy and three lanes idle. Over 58 scheduled master runs
(4 Oct – 2 Nov), this tail had a median of 75 s, with 56 s of the four lanes' capacity unused.
Many test JVMs sleep 10 s at the end for threads that never end. CI sets
THREAD_LEAK_DETECTOR_WAIT_MILLIS=10000.Gradle runs a test JVM's classes in one TestNG context, so
ThreadLeakDetectorListenerchecks once, when the JVM'stests are done. If a thread started during the run is still alive, it polls for up to 10 s, then writes a leak
report. 33–51 of the 125 JVMs per run wait the full 10 s. In 68–73 % of those, every reported thread lives as long
as the JVM:
ForkJoinPool.commonPool-delayScheduler. Since Java 25,CompletableFuture.delayedExecutorandorTimeoutrunon the common pool's delay scheduler. Before, they ran on the
CompletableFutureDelaySchedulerthread, which thelistener already skips. 12–15 JVMs per JDK 25 run report it, none on JDK 21.
Cleaner-0: ajava.lang.ref.Cleanerthread.PulsarExecutors.AutoShutdownExecutorServicekeeps itsCleanerin a static field.
pulsar-auth-blocking-shared:V5AuthContexts.SharedBlockingExecutor, a pool that lives as long as the JVM andwhose idle threads retire after 60 s.
JFR Recorder Thread,JFR Periodic Tasks,Java2D Disposer,VirtualThread-unblocker.The same waits happen in the other unit test jobs.
For example, in the scheduled master run 41443581285
(2 Nov 12:18 UTC, JDK 25), OTHER's main Gradle build took 943 s:
The jcloud tests started at 749 s and ended at 943 s. From 875 s on they ran alone, and the runner's CPU dropped to
56 %, then 21 %.
41 test JVMs waited ~11.5 s after their last test class, while the other 84 took ~1.3 s to exit. In 30 of the 41,
the job log's leak report lists only threads from the list above, for example:
The thread was
ForkJoinPool.commonPool-delayScheduler.Locally (JDK 26,
THREAD_LEAK_DETECTOR_WAIT_MILLIS=10000), on current master::pulsar-client-auth-athenz:testpulsar-auth-blocking-shared:pulsar-transaction:pulsar-transaction-coordinator:testForkJoinPool.commonPool-delayScheduler:pulsar-functions:pulsar-functions-worker:testCleaner-0twice,function-worker-leader-*Modifications
run_unit_group_gradle.sh: the OTHER group requests:tiered-storage:tiered-storage-jcloud:testbeforetest.Gradle's plan then puts it before the other test tasks: a dry run shows the same 610 tasks, with the jcloud tests at
task 50, right after their compile tasks. They now run alongside the other modules' tests instead of after them.
ThreadLeakDetectorListener.shouldSkipThreadskips the threads listed above, the way it already skipsCompletableFutureDelayScheduler,process reaperand others. ACleaner-Nthread is skipped only in the JDK'sInnocuousThreadGroup. Real leak candidates are still reported, for examplefunction-worker-leader-*,WebSocket@…and HDFSLeaseRenewer.shouldSkipThreadbecomes package-private for the new test.Expected saving on the OTHER test step: about 1–1.5 min. Replaying each of the 58 runs' test JVM lanes on 4 lanes,
moving the jcloud tests saves 21–105 s (median 57 s), and skipping the singleton waits saves up to 41–62 s more. That
second figure is an upper bound, since the runner is CPU-bound. The leak reports in every unit job also get shorter.
The two changes are independent, one commit each. #27299 changes other parts of
run_unit_group_gradle.sh(not theOTHER group), and the two merge cleanly.
Verifying this change
Make sure that the change passes the CI checks.
New
ThreadLeakDetectorListenerTest. It starts aCleanerand aCompletableFuturedelayed task and checks that thelistener skips their threads. It also checks that an ordinary thread, and a thread named
Cleaner-0outside theInnocuousThreadGroup, are still reported. Without the change, both skip checks fail on JDK 26 (Cleaner-0andForkJoinPool.commonPool-delayScheduleraren't skipped). With it, all three pass.The three test tasks above, with the 10 s wait, after the change:
:pulsar-client-auth-athenz:test:pulsar-transaction:pulsar-transaction-coordinator:test:pulsar-functions:pulsar-functions-worker:testfunction-worker-leader-*, still reportedThe functions-worker task is barely faster, because the fork with the real leak report still waits 10 s.
./pulsar-build/run_unit_group_gradle.sh OTHER --dry-run --max-workers=1: the main build plans the same 610 tasks asbefore.
:tiered-storage:tiered-storage-jcloud:testmoves from task 575 to task 50 and is the first test task. TheDnsResolverTestbuild is unchanged.shellcheck pulsar-build/run_unit_group_gradle.sh: the same two findings as before (line 59, untouched)../gradlew quickCheckpasses.On CI, the OTHER job's leak summary should list only the real candidates.
Does this pull request potentially affect one of the following parts:
If the box was checked, please highlight the changes