Skip to content

Commit e2bd955

Browse files
committed
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
1 parent 397f879 commit e2bd955

File tree

2 files changed

+51
-1
lines changed

2 files changed

+51
-1
lines changed

spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/servlet/support/SpringBootServletInitializer.java

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2023 the original author or authors.
2+
* Copyright 2012-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -28,6 +28,7 @@
2828
import jakarta.servlet.ServletException;
2929
import org.apache.commons.logging.Log;
3030
import org.apache.commons.logging.LogFactory;
31+
import reactor.core.scheduler.Schedulers;
3132

3233
import org.springframework.boot.SpringApplication;
3334
import org.springframework.boot.builder.ParentContextApplicationContextInitializer;
@@ -44,6 +45,7 @@
4445
import org.springframework.core.annotation.MergedAnnotations.SearchStrategy;
4546
import org.springframework.core.env.ConfigurableEnvironment;
4647
import org.springframework.util.Assert;
48+
import org.springframework.util.ClassUtils;
4749
import org.springframework.web.WebApplicationInitializer;
4850
import org.springframework.web.context.ConfigurableWebEnvironment;
4951
import org.springframework.web.context.ContextLoaderListener;
@@ -69,11 +71,15 @@
6971
* @author Dave Syer
7072
* @author Phillip Webb
7173
* @author Andy Wilkinson
74+
* @author Brian Clozel
7275
* @since 2.0.0
7376
* @see #configure(SpringApplicationBuilder)
7477
*/
7578
public abstract class SpringBootServletInitializer implements WebApplicationInitializer {
7679

80+
private static final boolean REACTOR_PRESENT = ClassUtils.isPresent("reactor.core.scheduler.Schedulers",
81+
SpringBootServletInitializer.class.getClassLoader());
82+
7783
protected Log logger; // Don't initialize early
7884

7985
private boolean registerErrorPageFilter = true;
@@ -125,6 +131,20 @@ protected void deregisterJdbcDrivers(ServletContext servletContext) {
125131
}
126132
}
127133

134+
/**
135+
* Shuts down the reactor {@link Schedulers} that were initialized by
136+
* {@code Schedulers.boundedElastic()} (or similar). The default implementation
137+
* {@link Schedulers#shutdownNow()} schedulers if they were initialized on this web
138+
* application's class loader.
139+
* @param servletContext the web application's servlet context
140+
* @since 3.4.0
141+
*/
142+
protected void shutDownSharedReactorSchedulers(ServletContext servletContext) {
143+
if (Schedulers.class.getClassLoader() == servletContext.getClassLoader()) {
144+
Schedulers.shutdownNow();
145+
}
146+
}
147+
128148
protected WebApplicationContext createRootApplicationContext(ServletContext servletContext) {
129149
SpringApplicationBuilder builder = createSpringApplicationBuilder();
130150
builder.main(getClass());
@@ -248,6 +268,10 @@ public void contextDestroyed(ServletContextEvent event) {
248268
finally {
249269
// Use original context so that the classloader can be accessed
250270
deregisterJdbcDrivers(this.servletContext);
271+
// Shut down shared reactor schedulers tied to this classloader
272+
if (REACTOR_PRESENT) {
273+
shutDownSharedReactorSchedulers(this.servletContext);
274+
}
251275
}
252276
}
253277

spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/servlet/support/SpringBootServletInitializerTests.java

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,32 @@ protected void deregisterJdbcDrivers(ServletContext servletContext) {
228228
assertThat(driversDeregistered).isTrue();
229229
}
230230

231+
@Test
232+
void whenServletContextIsDestroyedThenReactorSchedulersAreShutDown() throws ServletException {
233+
ServletContext servletContext = mock(ServletContext.class);
234+
given(servletContext.addFilter(any(), any(Filter.class))).willReturn(mock(Dynamic.class));
235+
given(servletContext.getInitParameterNames()).willReturn(new Vector<String>().elements());
236+
given(servletContext.getAttributeNames()).willReturn(new Vector<String>().elements());
237+
AtomicBoolean schedulersShutDown = new AtomicBoolean();
238+
new SpringBootServletInitializer() {
239+
240+
@Override
241+
protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) {
242+
return builder.sources(Config.class);
243+
}
244+
245+
@Override
246+
protected void shutDownSharedReactorSchedulers(ServletContext servletContext) {
247+
schedulersShutDown.set(true);
248+
}
249+
250+
}.onStartup(servletContext);
251+
ArgumentCaptor<ServletContextListener> captor = ArgumentCaptor.forClass(ServletContextListener.class);
252+
then(servletContext).should().addListener(captor.capture());
253+
captor.getValue().contextDestroyed(new ServletContextEvent(servletContext));
254+
assertThat(schedulersShutDown).isTrue();
255+
}
256+
231257
static class PropertySourceVerifyingSpringBootServletInitializer extends SpringBootServletInitializer {
232258

233259
@Override

0 commit comments

Comments
 (0)