Shut down Reactor Schedulers for WAR deployments
Prior to this commit, Spring applications and libraries would call `Schedulers.boundedElastic()` or similar static methods at runtime. This would ininitialize and cache reactive schedulers for the Reactor core library. Those instances are cached for the lifetime of the JVM and are shared static instances. In a WAR deployment case, those schedulers would be initialized and tied to the web application servlet context class loader. When undeploying the web applications, schedulers would not be automatically shut down and this would keep the now useless application classloader, leaking resources. This commit ensures that Spring Boot shuts down shared schedulers if they were loaded and initiazed by the web application classloader, once the the Spring application context is closed. Closes gh-41548
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2012-2023 the original author or authors.
|
||||
* Copyright 2012-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -28,6 +28,7 @@ import jakarta.servlet.ServletContextEvent;
|
||||
import jakarta.servlet.ServletException;
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import reactor.core.scheduler.Schedulers;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.builder.ParentContextApplicationContextInitializer;
|
||||
@@ -44,6 +45,7 @@ import org.springframework.core.annotation.MergedAnnotations;
|
||||
import org.springframework.core.annotation.MergedAnnotations.SearchStrategy;
|
||||
import org.springframework.core.env.ConfigurableEnvironment;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ClassUtils;
|
||||
import org.springframework.web.WebApplicationInitializer;
|
||||
import org.springframework.web.context.ConfigurableWebEnvironment;
|
||||
import org.springframework.web.context.ContextLoaderListener;
|
||||
@@ -69,11 +71,15 @@ import org.springframework.web.context.WebApplicationContext;
|
||||
* @author Dave Syer
|
||||
* @author Phillip Webb
|
||||
* @author Andy Wilkinson
|
||||
* @author Brian Clozel
|
||||
* @since 2.0.0
|
||||
* @see #configure(SpringApplicationBuilder)
|
||||
*/
|
||||
public abstract class SpringBootServletInitializer implements WebApplicationInitializer {
|
||||
|
||||
private static final boolean REACTOR_PRESENT = ClassUtils.isPresent("reactor.core.scheduler.Schedulers",
|
||||
SpringBootServletInitializer.class.getClassLoader());
|
||||
|
||||
protected Log logger; // Don't initialize early
|
||||
|
||||
private boolean registerErrorPageFilter = true;
|
||||
@@ -125,6 +131,20 @@ public abstract class SpringBootServletInitializer implements WebApplicationInit
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Shuts down the reactor {@link Schedulers} that were initialized by
|
||||
* {@code Schedulers.boundedElastic()} (or similar). The default implementation
|
||||
* {@link Schedulers#shutdownNow()} schedulers if they were initialized on this web
|
||||
* application's class loader.
|
||||
* @param servletContext the web application's servlet context
|
||||
* @since 3.4.0
|
||||
*/
|
||||
protected void shutDownSharedReactorSchedulers(ServletContext servletContext) {
|
||||
if (Schedulers.class.getClassLoader() == servletContext.getClassLoader()) {
|
||||
Schedulers.shutdownNow();
|
||||
}
|
||||
}
|
||||
|
||||
protected WebApplicationContext createRootApplicationContext(ServletContext servletContext) {
|
||||
SpringApplicationBuilder builder = createSpringApplicationBuilder();
|
||||
builder.main(getClass());
|
||||
@@ -248,6 +268,10 @@ public abstract class SpringBootServletInitializer implements WebApplicationInit
|
||||
finally {
|
||||
// Use original context so that the classloader can be accessed
|
||||
deregisterJdbcDrivers(this.servletContext);
|
||||
// Shut down shared reactor schedulers tied to this classloader
|
||||
if (REACTOR_PRESENT) {
|
||||
shutDownSharedReactorSchedulers(this.servletContext);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -228,6 +228,32 @@ class SpringBootServletInitializerTests {
|
||||
assertThat(driversDeregistered).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenServletContextIsDestroyedThenReactorSchedulersAreShutDown() throws ServletException {
|
||||
ServletContext servletContext = mock(ServletContext.class);
|
||||
given(servletContext.addFilter(any(), any(Filter.class))).willReturn(mock(Dynamic.class));
|
||||
given(servletContext.getInitParameterNames()).willReturn(new Vector<String>().elements());
|
||||
given(servletContext.getAttributeNames()).willReturn(new Vector<String>().elements());
|
||||
AtomicBoolean schedulersShutDown = new AtomicBoolean();
|
||||
new SpringBootServletInitializer() {
|
||||
|
||||
@Override
|
||||
protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) {
|
||||
return builder.sources(Config.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void shutDownSharedReactorSchedulers(ServletContext servletContext) {
|
||||
schedulersShutDown.set(true);
|
||||
}
|
||||
|
||||
}.onStartup(servletContext);
|
||||
ArgumentCaptor<ServletContextListener> captor = ArgumentCaptor.forClass(ServletContextListener.class);
|
||||
then(servletContext).should().addListener(captor.capture());
|
||||
captor.getValue().contextDestroyed(new ServletContextEvent(servletContext));
|
||||
assertThat(schedulersShutDown).isTrue();
|
||||
}
|
||||
|
||||
static class PropertySourceVerifyingSpringBootServletInitializer extends SpringBootServletInitializer {
|
||||
|
||||
@Override
|
||||
|
||||
Reference in New Issue
Block a user