Skip to content

Commit f5caafb

Browse files
committed
TimeOut property for Netty Connector
MaxConnections for Netty Connector MaxConnectionsTotal for NettyConnector Fixes #4548 Signed-off-by: jansupol <jan.supol@oracle.com>
1 parent a73db94 commit f5caafb

File tree

6 files changed

+221
-19
lines changed

6 files changed

+221
-19
lines changed

connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/JerseyClientHandler.java

+20-1
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import java.io.InputStream;
2121
import java.util.Map;
2222
import java.util.concurrent.CompletableFuture;
23+
import java.util.concurrent.TimeoutException;
2324

2425
import javax.ws.rs.core.Response;
2526

@@ -36,6 +37,7 @@
3637
import io.netty.handler.codec.http.HttpResponse;
3738
import io.netty.handler.codec.http.HttpUtil;
3839
import io.netty.handler.codec.http.LastHttpContent;
40+
import io.netty.handler.timeout.IdleStateEvent;
3941

4042
/**
4143
* Jersey implementation of Netty channel handler.
@@ -51,6 +53,8 @@ class JerseyClientHandler extends SimpleChannelInboundHandler<HttpObject> {
5153
private NettyInputStream nis;
5254
private ClientResponse jerseyResponse;
5355

56+
private boolean readTimedOut;
57+
5458
JerseyClientHandler(ClientRequest request,
5559
CompletableFuture<ClientResponse> responseAvailable,
5660
CompletableFuture<?> responseDone) {
@@ -67,7 +71,12 @@ public void channelReadComplete(ChannelHandlerContext ctx) {
6771
@Override
6872
public void channelInactive(ChannelHandlerContext ctx) {
6973
// assert: no-op, if channel is closed after LastHttpContent has been consumed
70-
responseDone.completeExceptionally(new IOException("Stream closed"));
74+
75+
if (readTimedOut) {
76+
responseDone.completeExceptionally(new TimeoutException("Stream closed: read timeout"));
77+
} else {
78+
responseDone.completeExceptionally(new IOException("Stream closed"));
79+
}
7180
}
7281

7382
protected void notifyResponse() {
@@ -145,4 +154,14 @@ public int read() throws IOException {
145154
public void exceptionCaught(ChannelHandlerContext ctx, final Throwable cause) {
146155
responseDone.completeExceptionally(cause);
147156
}
157+
158+
@Override
159+
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
160+
if (evt instanceof IdleStateEvent) {
161+
readTimedOut = true;
162+
ctx.close();
163+
} else {
164+
super.userEventTriggered(ctx, evt);
165+
}
166+
}
148167
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
/*
2+
* Copyright (c) 2020 Oracle and/or its affiliates. All rights reserved.
3+
*
4+
* This program and the accompanying materials are made available under the
5+
* terms of the Eclipse Public License v. 2.0, which is available at
6+
* http://www.eclipse.org/legal/epl-2.0.
7+
*
8+
* This Source Code may also be made available under the following Secondary
9+
* Licenses when the conditions for such availability set forth in the
10+
* Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
11+
* version 2 with the GNU Classpath Exception, which is available at
12+
* https://www.gnu.org/software/classpath/license.html.
13+
*
14+
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
15+
*/
16+
17+
package org.glassfish.jersey.netty.connector;
18+
19+
import org.glassfish.jersey.internal.util.PropertiesClass;
20+
21+
/**
22+
* Configuration options specific to the Client API that utilizes {@link NettyConnectorProvider}.
23+
*
24+
* @since 2.32
25+
*/
26+
@PropertiesClass
27+
public class NettyClientProperties {
28+
29+
/**
30+
* <p>
31+
* This property determines the maximum number of idle connections that will be simultaneously kept alive
32+
* in total, rather than per destination. The default is 60.
33+
* </p>
34+
*/
35+
public static final String MAX_CONNECTIONS_TOTAL = "jersey.config.client.maxTotalConnections";
36+
37+
/**
38+
* <p>
39+
* This property determines the maximum number of idle connections that will be simultaneously kept alive, per destination.
40+
* The default is 5.
41+
* </p>
42+
* <p>
43+
* This property is a Jersey alternative to System property {@code}http.maxConnections{@code}. The Jersey property takes
44+
* precedence over the system property.
45+
* </p>
46+
*/
47+
public static final String MAX_CONNECTIONS = "jersey.config.client.maxConnections";
48+
}

connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/NettyConnector.java

+110-15
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
import java.util.List;
2626
import java.util.Map;
2727
import java.util.concurrent.CompletableFuture;
28-
import java.util.concurrent.ExecutionException;
28+
import java.util.concurrent.CompletionException;
2929
import java.util.concurrent.ExecutorService;
3030
import java.util.concurrent.Executors;
3131
import java.util.concurrent.Future;
@@ -37,6 +37,8 @@
3737

3838
import io.netty.bootstrap.Bootstrap;
3939
import io.netty.channel.Channel;
40+
import io.netty.channel.ChannelDuplexHandler;
41+
import io.netty.channel.ChannelHandlerContext;
4042
import io.netty.channel.ChannelInitializer;
4143
import io.netty.channel.ChannelOption;
4244
import io.netty.channel.ChannelPipeline;
@@ -58,6 +60,9 @@
5860
import io.netty.handler.ssl.ClientAuth;
5961
import io.netty.handler.ssl.JdkSslContext;
6062
import io.netty.handler.stream.ChunkedWriteHandler;
63+
import io.netty.handler.timeout.IdleState;
64+
import io.netty.handler.timeout.IdleStateEvent;
65+
import io.netty.handler.timeout.IdleStateHandler;
6166
import io.netty.util.concurrent.GenericFutureListener;
6267
import org.glassfish.jersey.client.ClientProperties;
6368
import org.glassfish.jersey.client.ClientRequest;
@@ -79,9 +84,30 @@ class NettyConnector implements Connector {
7984
final Client client;
8085
final HashMap<String, ArrayList<Channel>> connections = new HashMap<>();
8186

87+
// If HTTP keepalive is enabled the value of "http.maxConnections" determines the maximum number
88+
// of idle connections that will be simultaneously kept alive, per destination.
89+
private static final String HTTP_KEEPALIVE_STRING = System.getProperty("http.keepAlive");
90+
// http.keepalive (default: true)
91+
private static final Boolean HTTP_KEEPALIVE =
92+
HTTP_KEEPALIVE_STRING == null ? Boolean.TRUE : Boolean.parseBoolean(HTTP_KEEPALIVE_STRING);
93+
94+
// http.maxConnections (default: 5)
95+
private static final int DEFAULT_MAX_POOL_SIZE = 5;
96+
private static final int MAX_POOL_SIZE = Integer.getInteger("http.maxConnections", DEFAULT_MAX_POOL_SIZE);
97+
private static final int MAX_POOL_IDLE = 60;
98+
99+
private final Integer maxPoolSize; // either from system property, or from Jersey config, or default
100+
private final Integer maxPoolIdle; // either from Jersey config, or default
101+
102+
private static final String INACTIVE_POOLED_CONNECTION_HANDLER = "inactive_pooled_connection_handler";
103+
private static final String PRUNE_INACTIVE_POOL = "prune_inactive_pool";
104+
private static final String READ_TIMEOUT_HANDLER = "read_timeout_handler";
105+
private static final String REQUEST_HANDLER = "request_handler";
106+
82107
NettyConnector(Client client) {
83108

84-
final Object threadPoolSize = client.getConfiguration().getProperties().get(ClientProperties.ASYNC_THREADPOOL_SIZE);
109+
final Map<String, Object> properties = client.getConfiguration().getProperties();
110+
final Object threadPoolSize = properties.get(ClientProperties.ASYNC_THREADPOOL_SIZE);
85111

86112
if (threadPoolSize != null && threadPoolSize instanceof Integer && (Integer) threadPoolSize > 0) {
87113
executorService = Executors.newFixedThreadPool((Integer) threadPoolSize);
@@ -92,20 +118,31 @@ class NettyConnector implements Connector {
92118
}
93119

94120
this.client = client;
121+
122+
final Object maxPoolIdleProperty = properties.get(NettyClientProperties.MAX_CONNECTIONS_TOTAL);
123+
final Object maxPoolSizeProperty = properties.get(NettyClientProperties.MAX_CONNECTIONS);
124+
125+
maxPoolIdle = maxPoolIdleProperty != null ? (Integer) maxPoolIdleProperty : MAX_POOL_IDLE;
126+
maxPoolSize = maxPoolSizeProperty != null
127+
? (Integer) maxPoolSizeProperty
128+
: (HTTP_KEEPALIVE ? MAX_POOL_SIZE : DEFAULT_MAX_POOL_SIZE);
129+
130+
if (maxPoolIdle == null || maxPoolIdle < 0) {
131+
throw new ProcessingException(LocalizationMessages.WRONG_MAX_POOL_IDLE(maxPoolIdle));
132+
}
133+
134+
if (maxPoolSize == null || maxPoolSize < 0) {
135+
throw new ProcessingException(LocalizationMessages.WRONG_MAX_POOL_SIZE(maxPoolIdle));
136+
}
95137
}
96138

97139
@Override
98140
public ClientResponse apply(ClientRequest jerseyRequest) {
99141
try {
100-
CompletableFuture<ClientResponse> resultFuture = execute(jerseyRequest);
101-
102-
Integer timeout = jerseyRequest.resolveProperty(ClientProperties.READ_TIMEOUT, 0);
103-
104-
return (timeout != null && timeout > 0) ? resultFuture.get(timeout, TimeUnit.MILLISECONDS)
105-
: resultFuture.get();
106-
} catch (ExecutionException ex) {
107-
Throwable e = ex.getCause() == null ? ex : ex.getCause();
108-
throw new ProcessingException(e.getMessage(), e);
142+
return execute(jerseyRequest).join();
143+
} catch (CompletionException cex) {
144+
final Throwable t = cex.getCause() == null ? cex : cex.getCause();
145+
throw new ProcessingException(t.getMessage(), t);
109146
} catch (Exception ex) {
110147
throw new ProcessingException(ex.getMessage(), ex);
111148
}
@@ -120,6 +157,11 @@ public Future<?> apply(final ClientRequest jerseyRequest, final AsyncConnectorCa
120157
}
121158

122159
protected CompletableFuture<ClientResponse> execute(final ClientRequest jerseyRequest) {
160+
Integer timeout = jerseyRequest.resolveProperty(ClientProperties.READ_TIMEOUT, 0);
161+
if (timeout == null || timeout < 0) {
162+
throw new ProcessingException(LocalizationMessages.WRONG_READ_TIMEOUT(timeout));
163+
}
164+
123165
final CompletableFuture<ClientResponse> responseAvailable = new CompletableFuture<>();
124166
final CompletableFuture<?> responseDone = new CompletableFuture<>();
125167

@@ -128,6 +170,7 @@ protected CompletableFuture<ClientResponse> execute(final ClientRequest jerseyRe
128170
int port = requestUri.getPort() != -1 ? requestUri.getPort() : "https".equals(requestUri.getScheme()) ? 443 : 80;
129171

130172
try {
173+
131174
String key = requestUri.getScheme() + "://" + host + ":" + port;
132175
ArrayList<Channel> conns;
133176
synchronized (connections) {
@@ -138,9 +181,16 @@ protected CompletableFuture<ClientResponse> execute(final ClientRequest jerseyRe
138181
}
139182
}
140183

141-
Channel chan;
184+
Channel chan = null;
142185
synchronized (conns) {
143-
chan = conns.size() == 0 ? null : conns.remove(conns.size() - 1);
186+
while (chan == null && !conns.isEmpty()) {
187+
chan = conns.remove(conns.size() - 1);
188+
chan.pipeline().remove(INACTIVE_POOLED_CONNECTION_HANDLER);
189+
chan.pipeline().remove(PRUNE_INACTIVE_POOL);
190+
if (!chan.isOpen()) {
191+
chan = null;
192+
}
193+
}
144194
}
145195

146196
if (chan == null) {
@@ -199,16 +249,30 @@ protected void initChannel(SocketChannel ch) throws Exception {
199249
// will leak
200250
final Channel ch = chan;
201251
JerseyClientHandler clientHandler = new JerseyClientHandler(jerseyRequest, responseAvailable, responseDone);
202-
ch.pipeline().addLast(clientHandler);
252+
// read timeout makes sense really as an inactivity timeout
253+
ch.pipeline().addLast(READ_TIMEOUT_HANDLER,
254+
new IdleStateHandler(0, 0, timeout, TimeUnit.MILLISECONDS));
255+
ch.pipeline().addLast(REQUEST_HANDLER, clientHandler);
203256

204257
responseDone.whenComplete((_r, th) -> {
258+
ch.pipeline().remove(READ_TIMEOUT_HANDLER);
205259
ch.pipeline().remove(clientHandler);
206260

207261
if (th == null) {
262+
ch.pipeline().addLast(INACTIVE_POOLED_CONNECTION_HANDLER, new IdleStateHandler(0, 0, maxPoolIdle));
263+
ch.pipeline().addLast(PRUNE_INACTIVE_POOL, new PruneIdlePool(connections, key));
208264
synchronized (connections) {
209265
ArrayList<Channel> conns1 = connections.get(key);
210-
synchronized (conns1) {
266+
if (conns1 == null) {
267+
conns1 = new ArrayList<>(1);
211268
conns1.add(ch);
269+
connections.put(key, conns1);
270+
} else {
271+
synchronized (conns1) {
272+
if (conns1.size() < maxPoolSize) {
273+
conns1.add(ch);
274+
} // else do not add the Channel to the idle pool
275+
}
212276
}
213277
}
214278
} else {
@@ -331,4 +395,35 @@ private static URI getProxyUri(final Object proxy) {
331395
throw new ProcessingException(LocalizationMessages.WRONG_PROXY_URI_TYPE(ClientProperties.PROXY_URI));
332396
}
333397
}
398+
399+
protected static class PruneIdlePool extends ChannelDuplexHandler {
400+
HashMap<String, ArrayList<Channel>> connections;
401+
String key;
402+
403+
public PruneIdlePool(HashMap<String, ArrayList<Channel>> connections, String key) {
404+
this.connections = connections;
405+
this.key = key;
406+
}
407+
408+
@Override
409+
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
410+
if (evt instanceof IdleStateEvent) {
411+
IdleStateEvent e = (IdleStateEvent) evt;
412+
if (e.state() == IdleState.ALL_IDLE) {
413+
ctx.close();
414+
synchronized (connections) {
415+
ArrayList<Channel> chans = connections.get(key);
416+
synchronized (chans) {
417+
chans.remove(ctx.channel());
418+
if (chans.isEmpty()) {
419+
connections.remove(key);
420+
}
421+
}
422+
}
423+
}
424+
} else {
425+
super.userEventTriggered(ctx, evt);
426+
}
427+
}
428+
}
334429
}

connectors/netty-connector/src/main/resources/org/glassfish/jersey/netty/connector/localization.properties

+5-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
#
2-
# Copyright (c) 2016, 2018 Oracle and/or its affiliates. All rights reserved.
2+
# Copyright (c) 2016, 2020 Oracle and/or its affiliates. All rights reserved.
33
#
44
# This program and the accompanying materials are made available under the
55
# terms of the Eclipse Public License v. 2.0, which is available at
@@ -15,3 +15,7 @@
1515
#
1616

1717
wrong.proxy.uri.type=The proxy URI ("{0}") property MUST be an instance of String or URI.
18+
wrong.read.timeout=Unexpected ("{0}") READ_TIMEOUT.
19+
wrong.max.pool.size=Unexpected ("{0}") maximum number of connections per destination.
20+
wrong.max.pool.idle=Unexpected ("{0}") maximum number of connections total.
21+

connectors/netty-connector/src/test/java/org/glassfish/jersey/netty/connector/TimeoutTest.java

+36-1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
package org.glassfish.jersey.netty.connector;
1818

19+
import java.util.concurrent.CompletionException;
1920
import java.util.concurrent.TimeoutException;
2021

2122
import javax.ws.rs.GET;
@@ -80,6 +81,7 @@ public void testSlow() {
8081
target("test/timeout").property(ClientProperties.READ_TIMEOUT, 1_000).request().get();
8182
fail("Timeout expected.");
8283
} catch (ProcessingException e) {
84+
assertEquals(e.getMessage(), "Stream closed: read timeout");
8385
assertThat("Unexpected processing exception cause",
8486
e.getCause(), instanceOf(TimeoutException.class));
8587
}
@@ -91,8 +93,41 @@ public void testTimeoutInRequest() {
9193
target("test/timeout").request().property(ClientProperties.READ_TIMEOUT, 1_000).get();
9294
fail("Timeout expected.");
9395
} catch (ProcessingException e) {
96+
assertEquals(e.getMessage(), "Stream closed: read timeout");
9497
assertThat("Unexpected processing exception cause",
95-
e.getCause(), instanceOf(TimeoutException.class));
98+
e.getCause(), instanceOf(TimeoutException.class));
99+
}
100+
}
101+
102+
@Test
103+
public void testRxSlow() {
104+
try {
105+
target("test/timeout").property(ClientProperties.READ_TIMEOUT, 1_000).request()
106+
.rx().get().toCompletableFuture().join();
107+
fail("Timeout expected.");
108+
} catch (CompletionException cex) {
109+
assertThat("Unexpected async cause",
110+
cex.getCause(), instanceOf(ProcessingException.class));
111+
ProcessingException e = (ProcessingException) cex.getCause();
112+
assertThat("Unexpected processing exception cause",
113+
e.getCause(), instanceOf(TimeoutException.class));
114+
assertEquals(e.getCause().getMessage(), "Stream closed: read timeout");
115+
}
116+
}
117+
118+
@Test
119+
public void testRxTimeoutInRequest() {
120+
try {
121+
target("test/timeout").request().property(ClientProperties.READ_TIMEOUT, 1_000)
122+
.rx().get().toCompletableFuture().join();
123+
fail("Timeout expected.");
124+
} catch (CompletionException cex) {
125+
assertThat("Unexpected async cause",
126+
cex.getCause(), instanceOf(ProcessingException.class));
127+
ProcessingException e = (ProcessingException) cex.getCause();
128+
assertThat("Unexpected processing exception cause",
129+
e.getCause(), instanceOf(TimeoutException.class));
130+
assertEquals(e.getCause().getMessage(), "Stream closed: read timeout");
96131
}
97132
}
98133
}

0 commit comments

Comments
 (0)