Skip to content

Commit 93a184e

Browse files
committed
http3: refactor HTTP/3 connection pool management in a separate class
1 parent 3a287f3 commit 93a184e

File tree

3 files changed

+470
-36
lines changed

3 files changed

+470
-36
lines changed

src/java.net.http/share/classes/jdk/internal/net/http/Http3ClientImpl.java

Lines changed: 9 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -102,8 +102,7 @@ public final class Http3ClientImpl implements AutoCloseable {
102102
private final Logger debug = Utils.getDebugLogger(this::dbgString);
103103

104104
final HttpClientImpl client;
105-
/* Map key is "scheme:host:port" */
106-
private final Map<String,Http3Connection> connections = new ConcurrentHashMap<>();
105+
private final Http3ConnectionPool connections = new Http3ConnectionPool(debug);
107106
private final Map<String,ConnectionRecovery> reconnections = new ConcurrentHashMap<>();
108107
private final Set<Http3Connection> pendingClose = ConcurrentHashMap.newKeySet();
109108
private final Set<String> noH3 = ConcurrentHashMap.newKeySet();
@@ -244,7 +243,7 @@ Http3Connection findPooledConnectionFor(HttpRequestImpl request,
244243
throws IOException {
245244
if (request.secure() && request.proxy() == null) {
246245
var config = request.http3Discovery();
247-
final var pooled = connections.get(connectionKey(request));
246+
final var pooled = connections.lookupFor(request);
248247
if (pooled == null) {
249248
return null;
250249
}
@@ -266,41 +265,15 @@ Http3Connection findPooledConnectionFor(HttpRequestImpl request,
266265
return null;
267266
}
268267
}
269-
boolean suitable = switch (config) {
270-
case HTTP_3_URI_ONLY -> {
271-
if (altService == null) {
272-
// the pooled connection was created as a result of a direct connection
273-
// against the origin, so is a valid one to use for HTTP_3_URI_ONLY request
274-
yield true;
275-
}
276-
// At this point, we have found a pooled connection which matches the request's
277-
// authority and that pooled connection was created because some origin
278-
// advertised the authority as an alternate service. We can use this pooled
279-
// connection with HTTP_3_URI_ONLY, only if the authority of the
280-
// alternate service is the same as the authority of the origin that
281-
// advertised it
282-
yield altService.originHasSameAuthority();
283-
}
284-
// can't use the connection with ALT_SVC unless the endpoint
285-
// was advertised through altService
286-
case ALT_SVC -> altService != null && altService.wasAdvertised();
287-
default -> true;
288-
};
289-
if (suitable) {
290-
// found a valid connection in pool, return it
291-
if (debug.on()) {
292-
debug.log("Found Http3Connection in connection pool");
293-
}
294-
return pooled;
295-
}
296268
if (debug.on()) {
297-
if (altService != null) {
298-
debug.log("pooled connection for alt-service %s cannot be used for %s",
299-
altService, config);
300-
}
269+
debug.log("Found Http3Connection in connection pool");
301270
}
302-
return null;
271+
// found a valid connection in pool, return it
272+
return pooled;
303273
} else {
274+
if (debug.on()) {
275+
debug.log("Pooled connection expired. Removing it.");
276+
}
304277
removeFromPool(pooled);
305278
}
306279
}
@@ -706,7 +679,7 @@ public void abort(Throwable t) {
706679
lock.lock();
707680
try {
708681
closed = true;
709-
connectionList = new ArrayList<>(connections.values());
682+
connectionList = new ArrayList<>(connections.values().toList());
710683
connectionList.addAll(pendingClose);
711684
pendingClose.clear();
712685
connections.clear();
Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
/*
2+
* Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved.
3+
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4+
*
5+
* This code is free software; you can redistribute it and/or modify it
6+
* under the terms of the GNU General Public License version 2 only, as
7+
* published by the Free Software Foundation. Oracle designates this
8+
* particular file as subject to the "Classpath" exception as provided
9+
* by Oracle in the LICENSE file that accompanied this code.
10+
*
11+
* This code is distributed in the hope that it will be useful, but WITHOUT
12+
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
13+
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
14+
* version 2 for more details (a copy is included in the LICENSE file that
15+
* accompanied this code).
16+
*
17+
* You should have received a copy of the GNU General Public License version
18+
* 2 along with this work; if not, write to the Free Software Foundation,
19+
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
20+
*
21+
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
22+
* or visit www.oracle.com if you need additional information or have any
23+
* questions.
24+
*/
25+
package jdk.internal.net.http;
26+
27+
import java.net.http.HttpOption.Http3DiscoveryMode;
28+
import java.util.Locale;
29+
import java.util.Map;
30+
import java.util.Objects;
31+
import java.util.Optional;
32+
import java.util.concurrent.ConcurrentHashMap;
33+
34+
import jdk.internal.net.http.common.Logger;
35+
36+
import static java.net.http.HttpOption.Http3DiscoveryMode.ALT_SVC;
37+
import static java.net.http.HttpOption.Http3DiscoveryMode.ANY;
38+
import static java.net.http.HttpOption.Http3DiscoveryMode.HTTP_3_URI_ONLY;
39+
40+
/**
41+
* This class encapsulate the HTTP/3 connection pool managed
42+
* by an instance of {@link Http3ClientImpl}.
43+
*/
44+
class Http3ConnectionPool {
45+
/* Map key is "scheme:host:port" */
46+
private final Map<String,Http3Connection> advertised = new ConcurrentHashMap<>();
47+
/* Map key is "scheme:host:port" */
48+
private final Map<String,Http3Connection> unadvertised = new ConcurrentHashMap<>();
49+
50+
private final Logger debug;
51+
Http3ConnectionPool(Logger logger) {
52+
this.debug = Objects.requireNonNull(logger);
53+
}
54+
55+
// https:<host>:<port>
56+
String connectionKey(HttpRequestImpl request) {
57+
var uri = request.uri();
58+
var scheme = uri.getScheme().toLowerCase(Locale.ROOT);
59+
var host = uri.getHost();
60+
var port = uri.getPort();
61+
assert scheme.equals("https");
62+
if (port < 0) port = 443; // https
63+
return String.format("%s:%s:%d", scheme, host, port);
64+
}
65+
66+
private Http3Connection lookupUnadvertised(String key, Http3DiscoveryMode discoveryMode) {
67+
var unadvertisedConn = unadvertised.get(key);
68+
if (unadvertisedConn == null) return null;
69+
if (discoveryMode == ANY) return unadvertisedConn;
70+
if (discoveryMode == ALT_SVC) return null;
71+
72+
assert discoveryMode == HTTP_3_URI_ONLY : String.valueOf(discoveryMode);
73+
74+
// Double check that if there is an alt service, it has same origin.
75+
final var altService = Optional.ofNullable(unadvertisedConn)
76+
.map(Http3Connection::connection)
77+
.flatMap(HttpQuicConnection::getSourceAltService)
78+
.orElse(null);
79+
80+
if (altService == null || altService.originHasSameAuthority()) {
81+
return unadvertisedConn;
82+
}
83+
84+
// We should never come here.
85+
assert false : "unadvertised connection with different origin: %s -> %s"
86+
.formatted(key, altService);
87+
return null;
88+
}
89+
90+
Http3Connection lookupFor(HttpRequestImpl request) {
91+
var discoveryMode = request.http3Discovery();
92+
var key = connectionKey(request);
93+
94+
// If not ALT_SVC, we can use unadvertised connections
95+
if (discoveryMode != ALT_SVC) {
96+
var unadvertisedConn = lookupUnadvertised(key, discoveryMode);
97+
if (unadvertisedConn != null) return unadvertisedConn;
98+
}
99+
100+
// Then see if we have a connection which was advertised.
101+
var advertisedConn = advertised.get(key);
102+
// We can use it for HTTP3_URI_ONLY too if it has same origin
103+
if (advertisedConn != null) {
104+
final var altService = advertisedConn.connection()
105+
.getSourceAltService().orElse(null);
106+
assert altService != null && altService.wasAdvertised();
107+
switch (discoveryMode) {
108+
case ANY -> {
109+
return advertisedConn;
110+
}
111+
case ALT_SVC -> {
112+
return advertisedConn;
113+
}
114+
case HTTP_3_URI_ONLY -> {
115+
if (altService != null && altService.originHasSameAuthority()) {
116+
return advertisedConn;
117+
}
118+
}
119+
}
120+
}
121+
122+
if (debug.on()) {
123+
debug.log("No suitable connection found in the pool for discovery mode %s",
124+
discoveryMode);
125+
}
126+
return null;
127+
}
128+
129+
Http3Connection putIfAbsent(String key, Http3Connection c) {
130+
Objects.requireNonNull(key);
131+
Objects.requireNonNull(c);
132+
assert key.equals(c.key());
133+
var altService = c.connection().getSourceAltService().orElse(null);
134+
if (altService != null && altService.wasAdvertised()) {
135+
return advertised.putIfAbsent(key, c);
136+
}
137+
assert altService == null || altService.originHasSameAuthority();
138+
return unadvertised.putIfAbsent(key, c);
139+
}
140+
141+
Http3Connection put(String key, Http3Connection c) {
142+
Objects.requireNonNull(key);
143+
Objects.requireNonNull(c);
144+
assert key.equals(c.key()) : "key mismatch %s -> %s"
145+
.formatted(key, c.key());
146+
var altService = c.connection().getSourceAltService().orElse(null);
147+
if (altService != null && altService.wasAdvertised()) {
148+
return advertised.put(key, c);
149+
}
150+
assert altService == null || altService.originHasSameAuthority();
151+
return unadvertised.put(key, c);
152+
}
153+
154+
boolean remove(String key, Http3Connection c) {
155+
Objects.requireNonNull(key);
156+
Objects.requireNonNull(c);
157+
assert key.equals(c.key()) : "key mismatch %s -> %s"
158+
.formatted(key, c.key());
159+
160+
var altService = c.connection().getSourceAltService().orElse(null);
161+
if (altService != null && altService.wasAdvertised()) {
162+
boolean remUndavertised = unadvertised.remove(key, c);
163+
assert !remUndavertised
164+
: "advertised connection found in unadvertised pool for " + key;
165+
return advertised.remove(key, c);
166+
}
167+
168+
assert altService == null || altService.originHasSameAuthority();
169+
return unadvertised.remove(key, c);
170+
}
171+
172+
void clear() {
173+
advertised.clear();
174+
unadvertised.clear();
175+
}
176+
177+
java.util.stream.Stream<Http3Connection> values() {
178+
return java.util.stream.Stream.concat(
179+
advertised.values().stream(),
180+
unadvertised.values().stream());
181+
}
182+
183+
}
184+

0 commit comments

Comments
 (0)