From e2bd95596d6ba0951b9cb6d76be8458056874308 Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Thu, 25 Jul 2024 16:06:59 +0200 Subject: [PATCH] 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 --- .../support/SpringBootServletInitializer.java | 26 ++++++++++++++++++- .../SpringBootServletInitializerTests.java | 26 +++++++++++++++++++ 2 files changed, 51 insertions(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/servlet/support/SpringBootServletInitializer.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/servlet/support/SpringBootServletInitializer.java index 9ffab2263a..e21e486cab 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/servlet/support/SpringBootServletInitializer.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/servlet/support/SpringBootServletInitializer.java @@ -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); + } } } diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/servlet/support/SpringBootServletInitializerTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/servlet/support/SpringBootServletInitializerTests.java index 60dc447cfe..bb8e7deb81 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/servlet/support/SpringBootServletInitializerTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/servlet/support/SpringBootServletInitializerTests.java @@ -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().elements()); + given(servletContext.getAttributeNames()).willReturn(new Vector().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 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