JmsHealthIndicator leaks watchdog threads if start fails immediately - #51412
Merged
mhalbritter merged 1 commit intoAug 25, 2026
Conversation
See spring-projectsgh-51412 Signed-off-by: 2heunxun <seapeon@naver.com>
mhalbritter
force-pushed
the
fix/jms-health-indicator-watchdog-thread-leak-4.0.x
branch
from
August 25, 2026 07:13
782388c to
d00566a
Compare
Contributor
|
Thank you very much and congratulations on your first contribution 🎉! |
Contributor
Author
|
@mhalbritter Thank you so much! I’m really happy to have made my first contribution. 🎉 |
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.
What happened?
JmsHealthIndicator.MonitoredConnection.start()spawns a watchdogthread that waits on a
CountDownLatchfor up to 5 seconds to detecta hanging
connection.start()call. The latch is only counted downafter
connection.start()returns:If connection.start() throws a JMSException immediately (a common case, e.g. when the broker rejects authentication or refuses the connection outright), the countDown() call is skipped.
The watchdog thread then blocks for the full 5 seconds for no reason. After that, it logs a misleading "Connection failed to start within 5 seconds and will be closed" warning—the connection did not time out, it failed immediately—and calls close() on a connection that has already been closed by the enclosing try-with-resources in doHealthCheck.
Under frequent health polling against a broker that fails start() immediately (for example, a Kubernetes liveness probe hitting /actuator/health every 1–2 seconds during an outage), a new watchdog thread is spawned on every poll and each one lingers for the full 5 seconds. As a result, threads accumulate for the duration of the outage.
The watchdog thread was also not a daemon thread, so in extreme cases it could delay JVM shutdown.
What should happen?
The watchdog should be released immediately whether connection.start() succeeds or throws, and it should not delay JVM shutdown.
How to reproduce?
Added JmsHealthIndicatorTests#whenConnectionStartThrowsWatchdogThreadDoesNotAlsoCloseConnection.
The test mocks Connection.start() to throw immediately, then asserts that Connection.close() is invoked exactly once (by the try-with-resources) after waiting past the 5-second watchdog window.
On the current code, this test fails with TooManyActualInvocations because the watchdog performs a second, redundant close() call once the timeout elapses.
What does this PR change?
MonitoredConnection.start() now wraps connection.start() in a try/finally block so latch.countDown() always runs.
The watchdog thread is also marked as a daemon thread.
No public API changes are introduced. Behavior for the success path and the genuine-hang path remains unchanged.
Impact / risk
close() is idempotent per the JMS specification, so the previous double-close was not unsafe, but it was unnecessary.
Tests
JmsHealthIndicatorTests(6 tests) continue to pass.checkstyleMain,checkstyleTest,checkFormatMain,checkFormatTest, and:module:spring-boot-jms:checkall pass.