From d404fdafa00612dc74acf5a08ecc9284ec47dcd1 Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Wed, 8 Oct 2025 15:28:24 +0200 Subject: [PATCH] Prevent ReactorResourceFactory from participating in pause scenarios Prior to this commit, ReactorResourceFactory was not restarted properly when the ApplicationContext was resumed by the TestContext Framework after a context pause. The reason is that the managed LoopResources were disposed when ConfigurableApplicationContext.pause() was invoked and reacquired when ConfigurableApplicationContext.restart() was invoked. To address that, this commit overrides isPauseable() in ReactorResourceFactory to return false, thereby avoiding participation in pause scenarios. Closes gh-35585 --- .../http/client/ReactorResourceFactory.java | 18 ++++++-- .../client/ReactorResourceFactoryTests.java | 42 +++++++++++++++++-- 2 files changed, 53 insertions(+), 7 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/http/client/ReactorResourceFactory.java b/spring-web/src/main/java/org/springframework/http/client/ReactorResourceFactory.java index a27df1b525e..53beed1da2a 100644 --- a/spring-web/src/main/java/org/springframework/http/client/ReactorResourceFactory.java +++ b/spring-web/src/main/java/org/springframework/http/client/ReactorResourceFactory.java @@ -34,20 +34,22 @@ import org.springframework.util.Assert; /** * Factory to manage Reactor Netty resources, i.e. {@link LoopResources} for - * event loop threads, and {@link ConnectionProvider} for the connection pool, + * event loop threads and {@link ConnectionProvider} for the connection pool, * within the lifecycle of a Spring {@code ApplicationContext}. * *
This factory implements {@link SmartLifecycle} and is expected typically * to be declared as a Spring-managed bean. * - *
Notice that after a {@link SmartLifecycle} stop/restart, new instances of + *
Note that after a {@link SmartLifecycle} stop/restart, new instances of * the configured {@link LoopResources} and {@link ConnectionProvider} are - * created, so any references to those should be updated. + * created, so any references to those should be updated. However, this factory + * does not participate in {@linkplain #isPauseable() pause} scenarios. * * @author Rossen Stoyanchev * @author Brian Clozel * @author Sebastien Deleuze * @author Juergen Hoeller + * @author Sam Brannen * @since 6.1 */ public class ReactorResourceFactory @@ -328,6 +330,16 @@ public class ReactorResourceFactory return this.running; } + /** + * Returns {@code false} to indicate that a {@code ReactorResourceFactory} + * should be skipped in a pause scenario. + * @since 7.0 + */ + @Override + public boolean isPauseable() { + return false; + } + @Override public int getPhase() { // Same as plain Lifecycle diff --git a/spring-web/src/test/java/org/springframework/http/client/ReactorResourceFactoryTests.java b/spring-web/src/test/java/org/springframework/http/client/ReactorResourceFactoryTests.java index d34dd9ef14b..0121ec8636f 100644 --- a/spring-web/src/test/java/org/springframework/http/client/ReactorResourceFactoryTests.java +++ b/spring-web/src/test/java/org/springframework/http/client/ReactorResourceFactoryTests.java @@ -38,6 +38,7 @@ import static org.mockito.Mockito.verifyNoMoreInteractions; * @author Rossen Stoyanchev * @author Sebastien Deleuze * @author Juergen Hoeller + * @author Sam Brannen */ class ReactorResourceFactoryTests { @@ -157,7 +158,7 @@ class ReactorResourceFactoryTests { } @Test - void restartWithGlobalResources() { + void stopAndStartWithGlobalResources() { this.resourceFactory.setUseGlobalResources(true); this.resourceFactory.start(); this.resourceFactory.stop(); @@ -174,7 +175,7 @@ class ReactorResourceFactoryTests { } @Test - void restartWithLocalResources() { + void stopAndStartWithLocalResources() { this.resourceFactory.setUseGlobalResources(false); this.resourceFactory.start(); this.resourceFactory.stop(); @@ -197,7 +198,7 @@ class ReactorResourceFactoryTests { } @Test - void restartWithExternalResources() { + void stopAndStartWithExternalResources() { this.resourceFactory.setUseGlobalResources(false); this.resourceFactory.setConnectionProvider(this.connectionProvider); this.resourceFactory.setLoopResources(this.loopResources); @@ -220,7 +221,7 @@ class ReactorResourceFactoryTests { } @Test - void restartWithinApplicationContext() { + void stopAndStartWithinApplicationContext() { GenericApplicationContext context = new GenericApplicationContext(); context.registerBean(ReactorResourceFactory.class); context.refresh(); @@ -241,9 +242,42 @@ class ReactorResourceFactoryTests { assertThat(resourceFactory.getConnectionProvider()).isSameAs(globalResources); assertThat(resourceFactory.getLoopResources()).isSameAs(globalResources); assertThat(globalResources.isDisposed()).isFalse(); + + context.close(); + assertThat(resourceFactory.isRunning()).isFalse(); + assertThat(globalResources.isDisposed()).isTrue(); + } + + @Test // gh-35585 + void pauseAndRestartWithinApplicationContext() { + GenericApplicationContext context = new GenericApplicationContext(); + context.registerBean(ReactorResourceFactory.class); + context.refresh(); + + ReactorResourceFactory resourceFactory = context.getBean(ReactorResourceFactory.class); + assertThat(resourceFactory.isRunning()).isTrue(); + + HttpResources globalResources = HttpResources.get(); + assertThat(resourceFactory.getConnectionProvider()).isSameAs(globalResources); + assertThat(resourceFactory.getLoopResources()).isSameAs(globalResources); + assertThat(globalResources.isDisposed()).isFalse(); + + context.pause(); + globalResources = HttpResources.get(); + assertThat(resourceFactory.isRunning()).isTrue(); + assertThat(resourceFactory.getConnectionProvider()).isSameAs(globalResources); + assertThat(resourceFactory.getLoopResources()).isSameAs(globalResources); + assertThat(globalResources.isDisposed()).isFalse(); + + context.restart(); + globalResources = HttpResources.get(); + assertThat(resourceFactory.isRunning()).isTrue(); + assertThat(resourceFactory.getConnectionProvider()).isSameAs(globalResources); + assertThat(resourceFactory.getLoopResources()).isSameAs(globalResources); assertThat(globalResources.isDisposed()).isFalse(); context.close(); + assertThat(resourceFactory.isRunning()).isFalse(); assertThat(globalResources.isDisposed()).isTrue(); }