JVM can't exit due to threads left if Tomcat throws exceptions during shutdown #16892
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.
Summary
If
TomcatWebServer#rethrowDeferredStartupExceptions
throws an exception, the embedded Tomcat instance may not allow the the JVM to terminate.Symptoms
We have been running into some sporadic issues in which startup errors will sometime leave the JVM running, although the main thread has since died (the one running the
@SpringBootApplication
main).Things like a failing Liquibase migration, that were being applied fairly early during the startup of the application, would leave the service hanging, and not truly failing (exiting with a status code != 0).
Analysis
The problem stems from the fact that some beans require early initialization in order for them to be injected into the Tomcat context.
Beans like Servlet Filters (and Servlet Filter Registrations) or Servlet Listeners (and Servlet Listener Registrations) are needed before the Tomcat context is started, so they (and their dependents) are created very early.
The remaining beans are created after the Tomcat context has been created.
If an exception occurs after the Tomcat context is started, the Tomcat is properly disposed of. However, if an error occurs early, the instance will not be fully created, and the instance will not be closed and destroyed.
This leaves some non daemon threads running and, those kind of threads do not allow the JVM to terminate, even if the exception propagates all the way to the main method.
Technical background
The method
org.springframework.boot.web.embedded.tomcat.TomcatStarter#onStartup
is called to run allServletContextInitializers
Spring Boot uses a
ServletContextInitializer
to initialize the Spring framework that comes fromorg.springframework.boot.web.servlet.context.ServletWebServerApplicationContext#getSelfInitializer
.This
ServletContextInitializer
callsorg.springframework.boot.web.servlet.context.ServletWebServerApplicationContext#getServletContextInitializerBeans
to fetch availableorg.springframework.boot.web.servlet.ServletContextInitializerBeans
org.springframework.boot.web.embedded.tomcat.TomcatStarter#onStartup
Back to
org.springframework.boot.web.embedded.tomcat.TomcatWebServer#initialize
, aftertomcat.start()
is done,org.springframework.boot.web.embedded.tomcat.TomcatWebServer#rethrowDeferredStartupExceptions
is called to rethrow any deferred exceptions, which is immediately caught and:org.springframework.boot.web.embedded.tomcat.TomcatWebServer#stopSilently
Exception
is wrapped in aWebServerException
andSince
initialize()
is called from the constructor ofTomcatWebServer
, a webServer instance is never stored inorg.springframework.boot.web.servlet.context.ServletWebServerApplicationContext#createWebServer
Down the stack,
org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext#refresh
will catch theWebServerException
and callorg.springframework.boot.web.servlet.context.ServletWebServerApplicationContext#stopAndReleaseWebServer
.However, since a
webServer
was never fully initialized, there is none to callstop()
upon.Notice that
stop()
is not the same asstopSilently()
.stop()
will stop and destroy and destroy the Tomcat instance.stopSilently()
does not destroy the Tomcat instance.Root cause
A Tomcat server (
org.apache.catalina.Server
) depends on destroy to release certain resources.One such resource is the
utilityExecutor
stored inorg.apache.catalina.core.StandardServer
.This executor creates threads that are not marked as daemon (
org.apache.catalina.core.StandardServer#utilityThreadsAsDaemon
is by defaultfalse
).Since these threads are not daemon, they do not allow the JVM to die after the main thread finishes.
The executor is stopped from
org.apache.catalina.core.StandardServer#destroyInternal
, which depends on Tomcat being destroyed.