Skip to content

Commit 125b8e7

Browse files
committed
Make ReactorResourceFactory lifecycle-aware
With this commit, ReactorResourceFactory now implements Lifecycle which allows supporting JVM Checkpoint Restore in Spring Boot with Reactor Netty server, and helps to support Reactor Netty client as well. Closes gh-31178
1 parent 2a916a3 commit 125b8e7

File tree

2 files changed

+145
-42
lines changed

2 files changed

+145
-42
lines changed

spring-web/src/main/java/org/springframework/http/client/reactive/ReactorResourceFactory.java

+79-42
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2020 the original author or authors.
2+
* Copyright 2002-2023 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.
@@ -26,6 +26,7 @@
2626

2727
import org.springframework.beans.factory.DisposableBean;
2828
import org.springframework.beans.factory.InitializingBean;
29+
import org.springframework.context.Lifecycle;
2930
import org.springframework.lang.Nullable;
3031
import org.springframework.util.Assert;
3132

@@ -34,14 +35,16 @@
3435
* event loop threads, and {@link ConnectionProvider} for the connection pool,
3536
* within the lifecycle of a Spring {@code ApplicationContext}.
3637
*
37-
* <p>This factory implements {@link InitializingBean} and {@link DisposableBean}
38-
* and is expected typically to be declared as a Spring-managed bean.
38+
* <p>This factory implements {@link InitializingBean}, {@link DisposableBean}
39+
* and {@link Lifecycle} and is expected typically to be declared as a
40+
* Spring-managed bean.
3941
*
4042
* @author Rossen Stoyanchev
4143
* @author Brian Clozel
44+
* @author Sebastien Deleuze
4245
* @since 5.1
4346
*/
44-
public class ReactorResourceFactory implements InitializingBean, DisposableBean {
47+
public class ReactorResourceFactory implements InitializingBean, DisposableBean, Lifecycle {
4548

4649
private boolean useGlobalResources = true;
4750

@@ -66,6 +69,10 @@ public class ReactorResourceFactory implements InitializingBean, DisposableBean
6669

6770
private Duration shutdownTimeout = Duration.ofSeconds(LoopResources.DEFAULT_SHUTDOWN_TIMEOUT);
6871

72+
private volatile boolean running;
73+
74+
private final Object lifecycleMonitor = new Object();
75+
6976

7077
/**
7178
* Whether to use global Reactor Netty resources via {@link HttpResources}.
@@ -196,54 +203,84 @@ public void setShutdownTimeout(Duration shutdownTimeout) {
196203

197204
@Override
198205
public void afterPropertiesSet() {
199-
if (this.useGlobalResources) {
200-
Assert.isTrue(this.loopResources == null && this.connectionProvider == null,
201-
"'useGlobalResources' is mutually exclusive with explicitly configured resources");
202-
HttpResources httpResources = HttpResources.get();
203-
if (this.globalResourcesConsumer != null) {
204-
this.globalResourcesConsumer.accept(httpResources);
205-
}
206-
this.connectionProvider = httpResources;
207-
this.loopResources = httpResources;
208-
}
209-
else {
210-
if (this.loopResources == null) {
211-
this.manageLoopResources = true;
212-
this.loopResources = this.loopResourcesSupplier.get();
213-
}
214-
if (this.connectionProvider == null) {
215-
this.manageConnectionProvider = true;
216-
this.connectionProvider = this.connectionProviderSupplier.get();
217-
}
218-
}
206+
start();
219207
}
220208

221209
@Override
222210
public void destroy() {
223-
if (this.useGlobalResources) {
224-
HttpResources.disposeLoopsAndConnectionsLater(this.shutdownQuietPeriod, this.shutdownTimeout).block();
225-
}
226-
else {
227-
try {
228-
ConnectionProvider provider = this.connectionProvider;
229-
if (provider != null && this.manageConnectionProvider) {
230-
provider.disposeLater().block();
211+
stop();
212+
}
213+
214+
@Override
215+
public void start() {
216+
synchronized (this.lifecycleMonitor) {
217+
if (!isRunning()) {
218+
if (this.useGlobalResources) {
219+
Assert.isTrue(this.loopResources == null && this.connectionProvider == null,
220+
"'useGlobalResources' is mutually exclusive with explicitly configured resources");
221+
HttpResources httpResources = HttpResources.get();
222+
if (this.globalResourcesConsumer != null) {
223+
this.globalResourcesConsumer.accept(httpResources);
224+
}
225+
this.connectionProvider = httpResources;
226+
this.loopResources = httpResources;
231227
}
228+
else {
229+
if (this.loopResources == null) {
230+
this.manageLoopResources = true;
231+
this.loopResources = this.loopResourcesSupplier.get();
232+
}
233+
if (this.connectionProvider == null) {
234+
this.manageConnectionProvider = true;
235+
this.connectionProvider = this.connectionProviderSupplier.get();
236+
}
237+
}
238+
this.running = true;
232239
}
233-
catch (Throwable ex) {
234-
// ignore
235-
}
240+
}
236241

237-
try {
238-
LoopResources resources = this.loopResources;
239-
if (resources != null && this.manageLoopResources) {
240-
resources.disposeLater(this.shutdownQuietPeriod, this.shutdownTimeout).block();
242+
}
243+
244+
@Override
245+
public void stop() {
246+
synchronized (this.lifecycleMonitor) {
247+
if (isRunning()) {
248+
if (this.useGlobalResources) {
249+
HttpResources.disposeLoopsAndConnectionsLater(this.shutdownQuietPeriod, this.shutdownTimeout).block();
250+
this.connectionProvider = null;
251+
this.loopResources = null;
241252
}
242-
}
243-
catch (Throwable ex) {
244-
// ignore
253+
else {
254+
try {
255+
ConnectionProvider provider = this.connectionProvider;
256+
if (provider != null && this.manageConnectionProvider) {
257+
this.connectionProvider = null;
258+
provider.disposeLater().block();
259+
}
260+
}
261+
catch (Throwable ex) {
262+
// ignore
263+
}
264+
265+
try {
266+
LoopResources resources = this.loopResources;
267+
if (resources != null && this.manageLoopResources) {
268+
this.loopResources = null;
269+
resources.disposeLater(this.shutdownQuietPeriod, this.shutdownTimeout).block();
270+
}
271+
}
272+
catch (Throwable ex) {
273+
// ignore
274+
}
275+
}
276+
this.running = false;
245277
}
246278
}
247279
}
248280

281+
@Override
282+
public boolean isRunning() {
283+
return this.running;
284+
}
285+
249286
}

spring-web/src/test/java/org/springframework/http/client/reactive/ReactorResourceFactoryTests.java

+66
Original file line numberDiff line numberDiff line change
@@ -157,4 +157,70 @@ void externalResources() {
157157
verifyNoMoreInteractions(this.connectionProvider, this.loopResources);
158158
}
159159

160+
@Test
161+
void stopThenStartWithGlobalResources() {
162+
163+
this.resourceFactory.setUseGlobalResources(true);
164+
this.resourceFactory.afterPropertiesSet();
165+
this.resourceFactory.stop();
166+
this.resourceFactory.start();
167+
168+
HttpResources globalResources = HttpResources.get();
169+
assertThat(this.resourceFactory.getConnectionProvider()).isSameAs(globalResources);
170+
assertThat(this.resourceFactory.getLoopResources()).isSameAs(globalResources);
171+
assertThat(globalResources.isDisposed()).isFalse();
172+
173+
this.resourceFactory.destroy();
174+
175+
assertThat(globalResources.isDisposed()).isTrue();
176+
}
177+
178+
@Test
179+
void stopThenStartWithLocalResources() {
180+
181+
this.resourceFactory.setUseGlobalResources(false);
182+
this.resourceFactory.afterPropertiesSet();
183+
this.resourceFactory.stop();
184+
this.resourceFactory.start();
185+
186+
ConnectionProvider connectionProvider = this.resourceFactory.getConnectionProvider();
187+
LoopResources loopResources = this.resourceFactory.getLoopResources();
188+
189+
assertThat(connectionProvider).isNotSameAs(HttpResources.get());
190+
assertThat(loopResources).isNotSameAs(HttpResources.get());
191+
192+
// The below does not work since ConnectionPoolProvider simply checks if pool is empty.
193+
// assertFalse(connectionProvider.isDisposed());
194+
assertThat(loopResources.isDisposed()).isFalse();
195+
196+
this.resourceFactory.destroy();
197+
198+
assertThat(connectionProvider.isDisposed()).isTrue();
199+
assertThat(loopResources.isDisposed()).isTrue();
200+
}
201+
202+
@Test
203+
void stopThenStartWithExternalResources() {
204+
205+
this.resourceFactory.setUseGlobalResources(false);
206+
this.resourceFactory.setConnectionProvider(this.connectionProvider);
207+
this.resourceFactory.setLoopResources(this.loopResources);
208+
this.resourceFactory.afterPropertiesSet();
209+
this.resourceFactory.stop();
210+
this.resourceFactory.start();
211+
212+
ConnectionProvider connectionProvider = this.resourceFactory.getConnectionProvider();
213+
LoopResources loopResources = this.resourceFactory.getLoopResources();
214+
215+
assertThat(connectionProvider).isSameAs(this.connectionProvider);
216+
assertThat(loopResources).isSameAs(this.loopResources);
217+
218+
verifyNoMoreInteractions(this.connectionProvider, this.loopResources);
219+
220+
this.resourceFactory.destroy();
221+
222+
// Not managed (destroy has no impact)...
223+
verifyNoMoreInteractions(this.connectionProvider, this.loopResources);
224+
}
225+
160226
}

0 commit comments

Comments
 (0)