Skip to content

Commit 7785f94

Browse files
committed
Revise and align Reactor client lifecycle management
Closes gh-32945
1 parent 4f6f2c0 commit 7785f94

File tree

5 files changed

+133
-121
lines changed

5 files changed

+133
-121
lines changed

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

+43-49
Original file line numberDiff line numberDiff line change
@@ -50,19 +50,21 @@ public class ReactorNettyClientRequestFactory implements ClientHttpRequestFactor
5050
private static final Function<HttpClient, HttpClient> defaultInitializer = client -> client.compress(true);
5151

5252

53-
private HttpClient httpClient;
54-
5553
@Nullable
5654
private final ReactorResourceFactory resourceFactory;
5755

5856
@Nullable
5957
private final Function<HttpClient, HttpClient> mapper;
6058

61-
private Duration exchangeTimeout = Duration.ofSeconds(5);
59+
@Nullable
60+
private Integer connectTimeout;
6261

6362
private Duration readTimeout = Duration.ofSeconds(10);
6463

65-
private volatile boolean running = true;
64+
private Duration exchangeTimeout = Duration.ofSeconds(5);
65+
66+
@Nullable
67+
private volatile HttpClient httpClient;
6668

6769
private final Object lifecycleMonitor = new Object();
6870

@@ -107,25 +109,11 @@ public ReactorNettyClientRequestFactory(HttpClient httpClient) {
107109
* @param mapper a mapper for further initialization of the created client
108110
*/
109111
public ReactorNettyClientRequestFactory(ReactorResourceFactory resourceFactory, Function<HttpClient, HttpClient> mapper) {
110-
this.httpClient = createHttpClient(resourceFactory, mapper);
111112
this.resourceFactory = resourceFactory;
112113
this.mapper = mapper;
113-
}
114-
115-
116-
private static HttpClient createHttpClient(ReactorResourceFactory resourceFactory, Function<HttpClient, HttpClient> mapper) {
117-
ConnectionProvider provider = resourceFactory.getConnectionProvider();
118-
Assert.notNull(provider, "No ConnectionProvider: is ReactorResourceFactory not initialized yet?");
119-
return defaultInitializer.andThen(mapper).andThen(applyLoopResources(resourceFactory))
120-
.apply(HttpClient.create(provider));
121-
}
122-
123-
private static Function<HttpClient, HttpClient> applyLoopResources(ReactorResourceFactory factory) {
124-
return httpClient -> {
125-
LoopResources resources = factory.getLoopResources();
126-
Assert.notNull(resources, "No LoopResources: is ReactorResourceFactory not initialized yet?");
127-
return httpClient.runOn(resources);
128-
};
114+
if (resourceFactory.isRunning()) {
115+
this.httpClient = createHttpClient(resourceFactory, mapper);
116+
}
129117
}
130118

131119

@@ -138,7 +126,11 @@ private static Function<HttpClient, HttpClient> applyLoopResources(ReactorResour
138126
*/
139127
public void setConnectTimeout(int connectTimeout) {
140128
Assert.isTrue(connectTimeout >= 0, "Timeout must be a non-negative value");
141-
this.httpClient.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, connectTimeout);
129+
this.connectTimeout = connectTimeout;
130+
HttpClient httpClient = this.httpClient;
131+
if (httpClient != null) {
132+
this.httpClient = httpClient.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, this.connectTimeout);
133+
}
142134
}
143135

144136
/**
@@ -150,8 +142,7 @@ public void setConnectTimeout(int connectTimeout) {
150142
*/
151143
public void setConnectTimeout(Duration connectTimeout) {
152144
Assert.notNull(connectTimeout, "ConnectTimeout must not be null");
153-
Assert.isTrue(!connectTimeout.isNegative(), "Timeout must be a non-negative value");
154-
this.httpClient.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, (int)connectTimeout.toMillis());
145+
setConnectTimeout((int) connectTimeout.toMillis());
155146
}
156147

157148
/**
@@ -192,52 +183,55 @@ public void setExchangeTimeout(Duration exchangeTimeout) {
192183
this.exchangeTimeout = exchangeTimeout;
193184
}
194185

186+
private HttpClient createHttpClient(ReactorResourceFactory factory, Function<HttpClient, HttpClient> mapper) {
187+
HttpClient httpClient = defaultInitializer.andThen(mapper)
188+
.apply(HttpClient.create(factory.getConnectionProvider()));
189+
httpClient = httpClient.runOn(factory.getLoopResources());
190+
if (this.connectTimeout != null) {
191+
httpClient = httpClient.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, this.connectTimeout);
192+
}
193+
return httpClient;
194+
}
195+
195196

196197
@Override
197198
public ClientHttpRequest createRequest(URI uri, HttpMethod httpMethod) throws IOException {
198-
return new ReactorNettyClientRequest(this.httpClient, uri, httpMethod, this.exchangeTimeout, this.readTimeout);
199+
HttpClient httpClient = this.httpClient;
200+
if (httpClient == null) {
201+
Assert.state(this.resourceFactory != null && this.mapper != null, "Illegal configuration");
202+
httpClient = createHttpClient(this.resourceFactory, this.mapper);
203+
}
204+
return new ReactorNettyClientRequest(httpClient, uri, httpMethod, this.exchangeTimeout, this.readTimeout);
199205
}
200206

207+
201208
@Override
202209
public void start() {
203-
synchronized (this.lifecycleMonitor) {
204-
if (!isRunning()) {
205-
if (this.resourceFactory != null && this.mapper != null) {
210+
if (this.resourceFactory != null && this.mapper != null) {
211+
synchronized (this.lifecycleMonitor) {
212+
if (this.httpClient == null) {
206213
this.httpClient = createHttpClient(this.resourceFactory, this.mapper);
207214
}
208-
else {
209-
logger.warn("Restarting a ReactorNettyClientRequestFactory bean is only supported with externally managed Reactor Netty resources");
210-
}
211-
this.running = true;
212215
}
213216
}
217+
else {
218+
logger.warn("Restarting a ReactorNettyClientRequestFactory bean is only supported " +
219+
"with externally managed Reactor Netty resources");
220+
}
214221
}
215222

216223
@Override
217224
public void stop() {
218-
synchronized (this.lifecycleMonitor) {
219-
if (isRunning()) {
220-
this.running = false;
225+
if (this.resourceFactory != null && this.mapper != null) {
226+
synchronized (this.lifecycleMonitor) {
227+
this.httpClient = null;
221228
}
222229
}
223230
}
224231

225-
@Override
226-
public final void stop(Runnable callback) {
227-
synchronized (this.lifecycleMonitor) {
228-
stop();
229-
callback.run();
230-
}
231-
}
232-
233232
@Override
234233
public boolean isRunning() {
235-
return this.running;
236-
}
237-
238-
@Override
239-
public boolean isAutoStartup() {
240-
return false;
234+
return (this.httpClient != null);
241235
}
242236

243237
@Override

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

+8-2
Original file line numberDiff line numberDiff line change
@@ -237,7 +237,7 @@ public void destroy() {
237237
@Override
238238
public void start() {
239239
synchronized (this.lifecycleMonitor) {
240-
if (!isRunning()) {
240+
if (!this.running) {
241241
if (this.useGlobalResources) {
242242
Assert.isTrue(this.loopResources == null && this.connectionProvider == null,
243243
"'useGlobalResources' is mutually exclusive with explicitly configured resources");
@@ -267,7 +267,7 @@ public void start() {
267267
@Override
268268
public void stop() {
269269
synchronized (this.lifecycleMonitor) {
270-
if (isRunning()) {
270+
if (this.running) {
271271
if (this.useGlobalResources) {
272272
HttpResources.disposeLoopsAndConnectionsLater(this.shutdownQuietPeriod, this.shutdownTimeout).block();
273273
this.connectionProvider = null;
@@ -306,4 +306,10 @@ public boolean isRunning() {
306306
return this.running;
307307
}
308308

309+
@Override
310+
public int getPhase() {
311+
// Same as plain Lifecycle
312+
return 0;
313+
}
314+
309315
}

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

+41-60
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2023 the original author or authors.
2+
* Copyright 2002-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.
@@ -54,31 +54,40 @@ public class ReactorClientHttpConnector implements ClientHttpConnector, SmartLif
5454
private static final Function<HttpClient, HttpClient> defaultInitializer = client -> client.compress(true);
5555

5656

57-
private HttpClient httpClient;
58-
5957
@Nullable
6058
private final ReactorResourceFactory resourceFactory;
6159

6260
@Nullable
6361
private final Function<HttpClient, HttpClient> mapper;
6462

65-
private volatile boolean running = true;
63+
@Nullable
64+
private volatile HttpClient httpClient;
6665

6766
private final Object lifecycleMonitor = new Object();
6867

6968

7069
/**
7170
* Default constructor. Initializes {@link HttpClient} via:
72-
* <pre class="code">
73-
* HttpClient.create().compress()
74-
* </pre>
71+
* <pre class="code">HttpClient.create().compress(true)</pre>
7572
*/
7673
public ReactorClientHttpConnector() {
7774
this.httpClient = defaultInitializer.apply(HttpClient.create());
7875
this.resourceFactory = null;
7976
this.mapper = null;
8077
}
8178

79+
/**
80+
* Constructor with a pre-configured {@code HttpClient} instance.
81+
* @param httpClient the client to use
82+
* @since 5.1
83+
*/
84+
public ReactorClientHttpConnector(HttpClient httpClient) {
85+
Assert.notNull(httpClient, "HttpClient is required");
86+
this.httpClient = httpClient;
87+
this.resourceFactory = null;
88+
this.mapper = null;
89+
}
90+
8291
/**
8392
* Constructor with externally managed Reactor Netty resources, including
8493
* {@link LoopResources} for event loop threads, and {@link ConnectionProvider}
@@ -98,50 +107,34 @@ public ReactorClientHttpConnector() {
98107
* @since 5.1
99108
*/
100109
public ReactorClientHttpConnector(ReactorResourceFactory resourceFactory, Function<HttpClient, HttpClient> mapper) {
101-
this.httpClient = createHttpClient(resourceFactory, mapper);
102110
this.resourceFactory = resourceFactory;
103111
this.mapper = mapper;
112+
if (resourceFactory.isRunning()) {
113+
this.httpClient = createHttpClient(resourceFactory, mapper);
114+
}
104115
}
105116

106-
private static HttpClient createHttpClient(ReactorResourceFactory resourceFactory, Function<HttpClient, HttpClient> mapper) {
107-
ConnectionProvider provider = resourceFactory.getConnectionProvider();
108-
Assert.notNull(provider, "No ConnectionProvider: is ReactorResourceFactory not initialized yet?");
109-
return defaultInitializer.andThen(mapper).andThen(applyLoopResources(resourceFactory))
110-
.apply(HttpClient.create(provider));
111-
}
112-
113-
private static Function<HttpClient, HttpClient> applyLoopResources(ReactorResourceFactory factory) {
114-
return httpClient -> {
115-
LoopResources resources = factory.getLoopResources();
116-
Assert.notNull(resources, "No LoopResources: is ReactorResourceFactory not initialized yet?");
117-
return httpClient.runOn(resources);
118-
};
119-
}
120-
121-
122-
/**
123-
* Constructor with a pre-configured {@code HttpClient} instance.
124-
* @param httpClient the client to use
125-
* @since 5.1
126-
*/
127-
public ReactorClientHttpConnector(HttpClient httpClient) {
128-
Assert.notNull(httpClient, "HttpClient is required");
129-
this.httpClient = httpClient;
130-
this.resourceFactory = null;
131-
this.mapper = null;
117+
private static HttpClient createHttpClient(ReactorResourceFactory factory, Function<HttpClient, HttpClient> mapper) {
118+
return defaultInitializer.andThen(mapper).andThen(httpClient -> httpClient.runOn(factory.getLoopResources()))
119+
.apply(HttpClient.create(factory.getConnectionProvider()));
132120
}
133121

134122

135123
@Override
136124
public Mono<ClientHttpResponse> connect(HttpMethod method, URI uri,
137125
Function<? super ClientHttpRequest, Mono<Void>> requestCallback) {
138126

139-
AtomicReference<ReactorClientHttpResponse> responseRef = new AtomicReference<>();
127+
HttpClient httpClient = this.httpClient;
128+
if (httpClient == null) {
129+
Assert.state(this.resourceFactory != null && this.mapper != null, "Illegal configuration");
130+
httpClient = createHttpClient(this.resourceFactory, this.mapper);
131+
}
140132

141-
HttpClient.RequestSender requestSender = this.httpClient
133+
HttpClient.RequestSender requestSender = httpClient
142134
.request(io.netty.handler.codec.http.HttpMethod.valueOf(method.name()));
143135

144136
requestSender = setUri(requestSender, uri);
137+
AtomicReference<ReactorClientHttpResponse> responseRef = new AtomicReference<>();
145138

146139
return requestSender
147140
.send((request, outbound) -> requestCallback.apply(adaptRequest(method, uri, request, outbound)))
@@ -176,46 +169,34 @@ private ReactorClientHttpRequest adaptRequest(HttpMethod method, URI uri, HttpCl
176169
return new ReactorClientHttpRequest(method, uri, request, nettyOutbound);
177170
}
178171

172+
179173
@Override
180174
public void start() {
181-
synchronized (this.lifecycleMonitor) {
182-
if (!isRunning()) {
183-
if (this.resourceFactory != null && this.mapper != null) {
175+
if (this.resourceFactory != null && this.mapper != null) {
176+
synchronized (this.lifecycleMonitor) {
177+
if (this.httpClient == null) {
184178
this.httpClient = createHttpClient(this.resourceFactory, this.mapper);
185179
}
186-
else {
187-
logger.warn("Restarting a ReactorClientHttpConnector bean is only supported with externally managed Reactor Netty resources");
188-
}
189-
this.running = true;
190180
}
191181
}
182+
else {
183+
logger.warn("Restarting a ReactorClientHttpConnector bean is only supported " +
184+
"with externally managed Reactor Netty resources");
185+
}
192186
}
193187

194188
@Override
195189
public void stop() {
196-
synchronized (this.lifecycleMonitor) {
197-
if (isRunning()) {
198-
this.running = false;
190+
if (this.resourceFactory != null && this.mapper != null) {
191+
synchronized (this.lifecycleMonitor) {
192+
this.httpClient = null;
199193
}
200194
}
201195
}
202196

203-
@Override
204-
public final void stop(Runnable callback) {
205-
synchronized (this.lifecycleMonitor) {
206-
stop();
207-
callback.run();
208-
}
209-
}
210-
211197
@Override
212198
public boolean isRunning() {
213-
return this.running;
214-
}
215-
216-
@Override
217-
public boolean isAutoStartup() {
218-
return false;
199+
return (this.httpClient != null);
219200
}
220201

221202
@Override

0 commit comments

Comments
 (0)