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:
Brian Clozel
2024-07-25 16:06:59 +02:00
parent 397f87964b
commit e2bd95596d
2 changed files with 51 additions and 1 deletions

View File

@@ -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);
}
}
}

View File

@@ -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