Skip to content

Commit 35f57d3

Browse files
authored
Fixed connection pooling in the UrlConnectionHttpClient. (#2660)
1 parent 21879c8 commit 35f57d3

File tree

3 files changed

+73
-8
lines changed

3 files changed

+73
-8
lines changed
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"category": "URL Connection Http Client",
3+
"contributor": "",
4+
"type": "bugfix",
5+
"description": "Fixed connection pooling for HTTPS endpoints. Previously, each request would create a new connection."
6+
}

http-clients/url-connection-client/src/main/java/software/amazon/awssdk/http/urlconnection/UrlConnectionHttpClient.java

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
import javax.net.ssl.KeyManager;
3737
import javax.net.ssl.SSLContext;
3838
import javax.net.ssl.SSLSession;
39+
import javax.net.ssl.SSLSocketFactory;
3940
import javax.net.ssl.TrustManager;
4041
import javax.net.ssl.X509TrustManager;
4142
import software.amazon.awssdk.annotations.SdkPublicApi;
@@ -71,18 +72,18 @@ public final class UrlConnectionHttpClient implements SdkHttpClient {
7172

7273
private final AttributeMap options;
7374
private final UrlConnectionFactory connectionFactory;
74-
private final SSLContext sslContext;
7575

7676
private UrlConnectionHttpClient(AttributeMap options, UrlConnectionFactory connectionFactory) {
7777
this.options = options;
7878
if (connectionFactory != null) {
79-
this.sslContext = null;
8079
this.connectionFactory = connectionFactory;
8180
} else {
82-
this.sslContext = getSslContext(options);
83-
this.connectionFactory = this::createDefaultConnection;
84-
}
81+
// Note: This socket factory MUST be reused between requests because the connection pool in the JVM is keyed by both
82+
// URL and SSLSocketFactory. If the socket factory is not reused, connections will not be reused between requests.
83+
SSLSocketFactory socketFactory = getSslContext(options).getSocketFactory();
8584

85+
this.connectionFactory = url -> createDefaultConnection(url, socketFactory);
86+
}
8687
}
8788

8889
public static Builder builder() {
@@ -142,7 +143,7 @@ private HttpURLConnection createAndConfigureConnection(HttpExecuteRequest reques
142143
return connection;
143144
}
144145

145-
private HttpURLConnection createDefaultConnection(URI uri) {
146+
private HttpURLConnection createDefaultConnection(URI uri, SSLSocketFactory socketFactory) {
146147
HttpURLConnection connection = invokeSafely(() -> (HttpURLConnection) uri.toURL().openConnection());
147148

148149
if (connection instanceof HttpsURLConnection) {
@@ -152,7 +153,7 @@ private HttpURLConnection createDefaultConnection(URI uri) {
152153
httpsConnection.setHostnameVerifier(NoOpHostNameVerifier.INSTANCE);
153154
}
154155

155-
httpsConnection.setSSLSocketFactory(sslContext.getSocketFactory());
156+
httpsConnection.setSSLSocketFactory(socketFactory);
156157
}
157158

158159
connection.setConnectTimeout(saturatedCast(options.get(SdkHttpConfigurationOption.CONNECTION_TIMEOUT).toMillis()));

test/http-client-tests/src/main/java/software/amazon/awssdk/http/SdkHttpClientTestSuite.java

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,14 +29,18 @@
2929
import com.github.tomakehurst.wiremock.client.ResponseDefinitionBuilder;
3030
import com.github.tomakehurst.wiremock.common.FatalStartupException;
3131
import com.github.tomakehurst.wiremock.http.RequestMethod;
32+
import com.github.tomakehurst.wiremock.http.trafficlistener.WiremockNetworkTrafficListener;
3233
import com.github.tomakehurst.wiremock.junit.WireMockRule;
3334
import com.github.tomakehurst.wiremock.matching.RequestPatternBuilder;
3435
import java.io.ByteArrayInputStream;
3536
import java.io.IOException;
3637
import java.net.HttpURLConnection;
38+
import java.net.Socket;
3739
import java.net.URI;
40+
import java.nio.ByteBuffer;
3841
import java.nio.charset.StandardCharsets;
3942
import java.util.Random;
43+
import java.util.concurrent.atomic.AtomicInteger;
4044
import javax.net.ssl.SSLHandshakeException;
4145
import javax.net.ssl.TrustManager;
4246
import javax.net.ssl.TrustManagerFactory;
@@ -57,11 +61,14 @@
5761
public abstract class SdkHttpClientTestSuite {
5862
private static final Logger LOG = Logger.loggerFor(SdkHttpClientTestSuite.class);
5963

64+
private static final ConnectionCountingTrafficListener CONNECTION_COUNTER = new ConnectionCountingTrafficListener();
65+
6066
@Rule
6167
public WireMockRule mockServer = createWireMockRule();
6268

6369
private final Random rng = new Random();
6470

71+
6572
@Test
6673
public void supportsResponseCode200() throws Exception {
6774
testForResponseCode(HttpURLConnection.HTTP_OK);
@@ -113,6 +120,30 @@ public void validatesHttpsCertificateIssuer() throws Exception {
113120
.isInstanceOf(SSLHandshakeException.class);
114121
}
115122

123+
@Test
124+
public void connectionPoolingWorks() throws Exception {
125+
int initialOpenedConnections = CONNECTION_COUNTER.openedConnections();
126+
127+
SdkHttpClientOptions httpClientOptions = new SdkHttpClientOptions();
128+
httpClientOptions.trustAll(true);
129+
SdkHttpClient client = createSdkHttpClient(httpClientOptions);
130+
131+
stubForMockRequest(200);
132+
133+
for (int i = 0; i < 5; i++) {
134+
SdkHttpFullRequest req = mockSdkRequest("http://localhost:" + mockServer.port(), SdkHttpMethod.POST);
135+
HttpExecuteResponse response =
136+
client.prepareRequest(HttpExecuteRequest.builder()
137+
.request(req)
138+
.contentStreamProvider(req.contentStreamProvider().orElse(null))
139+
.build())
140+
.call();
141+
response.responseBody().ifPresent(IoUtils::drainInputStream);
142+
}
143+
144+
assertThat(CONNECTION_COUNTER.openedConnections()).isEqualTo(initialOpenedConnections + 1);
145+
}
146+
116147
@Test
117148
public void testCustomTlsTrustManager() throws Exception {
118149
WireMockServer selfSignedServer = HttpTestUtils.createSelfSignedServer();
@@ -279,7 +310,9 @@ private WireMockRule createWireMockRule() {
279310
int maxAttempts = 5;
280311
for (int i = 0; i < maxAttempts; ++i) {
281312
try {
282-
return new WireMockRule(wireMockConfig().dynamicPort().dynamicHttpsPort());
313+
return new WireMockRule(wireMockConfig().dynamicPort()
314+
.dynamicHttpsPort()
315+
.networkTrafficListener(CONNECTION_COUNTER));
283316
} catch (FatalStartupException e) {
284317
int attemptNum = i + 1;
285318
LOG.debug(() -> "Was not able to start WireMock server. Attempt " + attemptNum, e);
@@ -298,4 +331,29 @@ private WireMockRule createWireMockRule() {
298331

299332
throw new RuntimeException("Unable to setup WireMock rule");
300333
}
334+
335+
private static class ConnectionCountingTrafficListener implements WiremockNetworkTrafficListener {
336+
private final AtomicInteger openedConnections = new AtomicInteger(0);
337+
338+
@Override
339+
public void opened(Socket socket) {
340+
openedConnections.incrementAndGet();
341+
}
342+
343+
@Override
344+
public void incoming(Socket socket, ByteBuffer bytes) {
345+
}
346+
347+
@Override
348+
public void outgoing(Socket socket, ByteBuffer bytes) {
349+
}
350+
351+
@Override
352+
public void closed(Socket socket) {
353+
}
354+
355+
public int openedConnections() {
356+
return openedConnections.get();
357+
}
358+
}
301359
}

0 commit comments

Comments
 (0)