Skip to content

Commit 7f40f16

Browse files
committed
8278067: Make HttpURLConnection default keep alive timeout configurable
Backport-of: d8f44aa39e921594505864e6270f42b745265293
1 parent 774213f commit 7f40f16

File tree

4 files changed

+261
-13
lines changed

4 files changed

+261
-13
lines changed

src/java.base/share/classes/java/net/doc-files/net-properties.html

+10
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,16 @@ <H2>Misc HTTP URL stream protocol handler properties</H2>
176176
If HTTP keepalive is enabled (see above) this value determines the
177177
maximum number of idle connections that will be simultaneously kept
178178
alive, per destination.</P>
179+
<LI><P><B>{@systemProperty http.keepAlive.time.server}</B> and
180+
<B>{@systemProperty http.keepAlive.time.proxy}</B> </P>
181+
<P>These properties modify the behavior of the HTTP keepalive cache in the case
182+
where the server (or proxy) has not specified a keepalive time. If the
183+
property is set in this case, then idle connections will be closed after the
184+
specified number of seconds. If the property is set, and the server does
185+
specify a keepalive time in a "Keep-Alive" response header, then the time specified
186+
by the server is used. If the property is not set and also the server
187+
does not specify a keepalive time, then connections are kept alive for an
188+
implementation defined time, assuming {@code http.keepAlive} is {@code true}.</P>
179189
<LI><P><B>{@systemProperty http.maxRedirects}</B> (default: {@code 20})<BR>
180190
This integer value determines the maximum number, for a given request,
181191
of HTTP redirects that will be automatically followed by the

src/java.base/share/classes/sun/net/www/http/HttpClient.java

+12-9
Original file line numberDiff line numberDiff line change
@@ -122,13 +122,13 @@ private static int getDefaultPort(String proto) {
122122
recomputing the value of keepingAlive */
123123
int keepAliveConnections = -1; /* number of keep-alives left */
124124

125-
/**Idle timeout value, in milliseconds. Zero means infinity,
126-
* iff keepingAlive=true.
127-
* Unfortunately, we can't always believe this one. If I'm connected
128-
* through a Netscape proxy to a server that sent me a keep-alive
129-
* time of 15 sec, the proxy unilaterally terminates my connection
130-
* after 5 sec. So we have to hard code our effective timeout to
131-
* 4 sec for the case where we're using a proxy. *SIGH*
125+
/*
126+
* The timeout if specified by the server. Following values possible
127+
* 0: the server specified no keep alive headers
128+
* -1: the server provided "Connection: keep-alive" but did not specify a
129+
* a particular time in a "Keep-Alive:" headers
130+
* Positive values are the number of seconds specified by the server
131+
* in a "Keep-Alive" header
132132
*/
133133
int keepAliveTimeout = 0;
134134

@@ -235,7 +235,6 @@ public boolean getHttpKeepAliveSet() {
235235
return keepAliveProp;
236236
}
237237

238-
239238
public String getSpnegoCBT() {
240239
return spnegoCBT;
241240
}
@@ -898,7 +897,7 @@ private boolean parseHTTPHeader(MessageHeader responses, ProgressSource pi, Http
898897
responses.findValue("Keep-Alive"));
899898
/* default should be larger in case of proxy */
900899
keepAliveConnections = p.findInt("max", usingProxy?50:5);
901-
keepAliveTimeout = p.findInt("timeout", usingProxy?60:5);
900+
keepAliveTimeout = p.findInt("timeout", -1);
902901
}
903902
} else if (b[7] != '0') {
904903
/*
@@ -1151,6 +1150,10 @@ public String getProxyHostUsed() {
11511150
}
11521151
}
11531152

1153+
public boolean getUsingProxy() {
1154+
return usingProxy;
1155+
}
1156+
11541157
/**
11551158
* @return the proxy port being used for this client. Meaningless
11561159
* if getProxyHostUsed() gives null.

src/java.base/share/classes/sun/net/www/http/KeepAliveCache.java

+61-4
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@
4141

4242
import jdk.internal.misc.InnocuousThread;
4343
import sun.security.action.GetIntegerAction;
44+
import sun.net.www.protocol.http.HttpURLConnection;
45+
import sun.util.logging.PlatformLogger;
4446

4547
/**
4648
* A class that implements a cache of idle Http connections for keep-alive
@@ -54,6 +56,32 @@ public class KeepAliveCache
5456
@java.io.Serial
5557
private static final long serialVersionUID = -2937172892064557949L;
5658

59+
// Keep alive time set according to priority specified here:
60+
// 1. If server specifies a time with a Keep-Alive header
61+
// 2. If user specifies a time with system property below
62+
// 3. Default values which depend on proxy vs server and whether
63+
// a Connection: keep-alive header was sent by server
64+
65+
// name suffixed with "server" or "proxy"
66+
private static final String keepAliveProp = "http.keepAlive.time.";
67+
68+
private static final int userKeepAliveServer;
69+
private static final int userKeepAliveProxy;
70+
71+
static final PlatformLogger logger = HttpURLConnection.getHttpLogger();
72+
73+
@SuppressWarnings("removal")
74+
static int getUserKeepAliveSeconds(String type) {
75+
int v = AccessController.doPrivileged(
76+
new GetIntegerAction(keepAliveProp+type, -1)).intValue();
77+
return v < -1 ? -1 : v;
78+
}
79+
80+
static {
81+
userKeepAliveServer = getUserKeepAliveSeconds("server");
82+
userKeepAliveProxy = getUserKeepAliveSeconds("proxy");
83+
}
84+
5785
/* maximum # keep-alive connections to maintain at once
5886
* This should be 2 by the HTTP spec, but because we don't support pipe-lining
5987
* a larger value is more appropriate. So we now set a default of 5, and the value
@@ -126,10 +154,29 @@ public Void run() {
126154

127155
if (v == null) {
128156
int keepAliveTimeout = http.getKeepAliveTimeout();
129-
v = new ClientVector(keepAliveTimeout > 0 ?
130-
keepAliveTimeout * 1000 : LIFETIME);
131-
v.put(http);
132-
super.put(key, v);
157+
if (keepAliveTimeout == 0) {
158+
keepAliveTimeout = getUserKeepAlive(http.getUsingProxy());
159+
if (keepAliveTimeout == -1) {
160+
// same default for server and proxy
161+
keepAliveTimeout = 5;
162+
}
163+
} else if (keepAliveTimeout == -1) {
164+
keepAliveTimeout = getUserKeepAlive(http.getUsingProxy());
165+
if (keepAliveTimeout == -1) {
166+
// different default for server and proxy
167+
keepAliveTimeout = http.getUsingProxy() ? 60 : 5;
168+
}
169+
}
170+
// at this point keepAliveTimeout is the number of seconds to keep
171+
// alive, which could be 0, if the user specified 0 for the property
172+
assert keepAliveTimeout >= 0;
173+
if (keepAliveTimeout == 0) {
174+
http.closeServer();
175+
} else {
176+
v = new ClientVector(keepAliveTimeout * 1000);
177+
v.put(http);
178+
super.put(key, v);
179+
}
133180
} else {
134181
v.put(http);
135182
}
@@ -138,6 +185,11 @@ public Void run() {
138185
}
139186
}
140187

188+
// returns the keep alive set by user in system property or -1 if not set
189+
private static int getUserKeepAlive(boolean isProxy) {
190+
return isProxy ? userKeepAliveProxy : userKeepAliveServer;
191+
}
192+
141193
/* remove an obsolete HttpClient from its VectorCache */
142194
public void remove(HttpClient h, Object obj) {
143195
cacheLock.lock();
@@ -276,6 +328,11 @@ HttpClient get() {
276328
e.hc.closeServer();
277329
} else {
278330
hc = e.hc;
331+
if (KeepAliveCache.logger.isLoggable(PlatformLogger.Level.FINEST)) {
332+
String msg = "cached HttpClient was idle for "
333+
+ Long.toString(currentTime - e.idleStartTime);
334+
KeepAliveCache.logger.finest(msg);
335+
}
279336
}
280337
} while ((hc == null) && (!isEmpty()));
281338
return hc;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
/*
2+
* Copyright (c) 2022, 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.
8+
*
9+
* This code is distributed in the hope that it will be useful, but WITHOUT
10+
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
11+
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
12+
* version 2 for more details (a copy is included in the LICENSE file that
13+
* accompanied this code).
14+
*
15+
* You should have received a copy of the GNU General Public License version
16+
* 2 along with this work; if not, write to the Free Software Foundation,
17+
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
18+
*
19+
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
20+
* or visit www.oracle.com if you need additional information or have any
21+
* questions.
22+
*/
23+
24+
/*
25+
* @test
26+
* @library /test/lib
27+
* @bug 8278067
28+
* @run main/othervm -Dhttp.keepAlive.time.server=30 KeepAliveProperty long
29+
* @run main/othervm -Dhttp.keepAlive.time.server=1 KeepAliveProperty short
30+
* @run main/othervm -ea -Dhttp.keepAlive.time.server=0 KeepAliveProperty short
31+
*/
32+
33+
import java.net.*;
34+
import java.io.*;
35+
import java.nio.charset.*;
36+
import java.util.logging.*;
37+
import jdk.test.lib.net.URIBuilder;
38+
import static java.net.Proxy.NO_PROXY;
39+
40+
public class KeepAliveProperty {
41+
42+
static volatile boolean pass = false;
43+
44+
static class Server extends Thread {
45+
final ServerSocket server;
46+
47+
Server (ServerSocket server) {
48+
super ();
49+
this.server = server;
50+
}
51+
52+
void readAll (Socket s) throws IOException {
53+
byte[] buf = new byte [128];
54+
int c;
55+
String request = "";
56+
InputStream is = s.getInputStream ();
57+
while ((c=is.read(buf)) > 0) {
58+
request += new String(buf, 0, c, StandardCharsets.US_ASCII);
59+
if (request.contains("\r\n\r\n")) {
60+
return;
61+
}
62+
}
63+
if (c == -1)
64+
throw new IOException("Socket closed");
65+
}
66+
67+
Socket s = null;
68+
String BODY;
69+
String CLEN;
70+
PrintStream out;
71+
72+
public void run() {
73+
try {
74+
s = server.accept();
75+
readAll(s);
76+
77+
BODY = "Hello world";
78+
CLEN = "Content-Length: " + BODY.length() + "\r\n";
79+
out = new PrintStream(new BufferedOutputStream(s.getOutputStream() ));
80+
81+
/* send the header */
82+
out.print("HTTP/1.1 200 OK\r\n");
83+
out.print("Content-Type: text/plain; charset=iso-8859-1\r\n");
84+
out.print(CLEN);
85+
out.print("\r\n");
86+
out.print(BODY);
87+
out.flush();
88+
} catch (Exception e) {
89+
pass = false;
90+
try {
91+
if (s != null)
92+
s.close();
93+
server.close();
94+
} catch (IOException unused) {}
95+
return;
96+
}
97+
98+
// second request may legitimately fail
99+
100+
try (Socket s2 = s; ServerSocket server2 = server; PrintStream out2 = out) {
101+
// wait for second request.
102+
readAll(s2);
103+
104+
BODY = "Goodbye world";
105+
CLEN = "Content-Length: " + BODY.length() + "\r\n";
106+
107+
/* send the header */
108+
out2.print("HTTP/1.1 200 OK\r\n");
109+
out2.print("Content-Type: text/plain; charset=iso-8859-1\r\n");
110+
out2.print(CLEN);
111+
out2.print("\r\n");
112+
out2.print(BODY);
113+
out2.flush();
114+
pass = !expectClose;
115+
if (!pass) System.out.println("Failed: expected close");
116+
} catch (Exception e) {
117+
pass = expectClose;
118+
if (!pass) System.out.println("Failed: did not expect close");
119+
}
120+
}
121+
}
122+
123+
static String fetch(URL url) throws Exception {
124+
InputStream in = url.openConnection(NO_PROXY).getInputStream();
125+
String s = "";
126+
byte b[] = new byte[128];
127+
int n;
128+
do {
129+
n = in.read(b);
130+
if (n > 0)
131+
s += new String(b, 0, n, StandardCharsets.US_ASCII);
132+
} while (n > 0);
133+
in.close();
134+
return s;
135+
}
136+
137+
static volatile boolean expectClose;
138+
139+
public static void main(String args[]) throws Exception {
140+
// exercise the logging code
141+
Logger logger = Logger.getLogger("sun.net.www.protocol.http.HttpURLConnection");
142+
logger.setLevel(Level.FINEST);
143+
ConsoleHandler h = new ConsoleHandler();
144+
h.setLevel(Level.FINEST);
145+
logger.addHandler(h);
146+
147+
expectClose = args[0].equals("short");
148+
InetAddress loopback = InetAddress.getLoopbackAddress();
149+
ServerSocket ss = new ServerSocket();
150+
ss.bind(new InetSocketAddress(loopback, 0));
151+
Server s = new Server(ss);
152+
s.start();
153+
154+
URL url = URIBuilder.newBuilder()
155+
.scheme("http")
156+
.loopback()
157+
.port(ss.getLocalPort())
158+
.toURL();
159+
System.out.println("URL: " + url);
160+
161+
if (!fetch(url).equals("Hello world"))
162+
throw new RuntimeException("Failed on first request");
163+
164+
// Wait a while to see if connection is closed
165+
Thread.sleep(3 * 1000);
166+
167+
try {
168+
if (!fetch(url).equals("Goodbye world"))
169+
throw new RuntimeException("Failed on second request");
170+
} catch (Exception e) {
171+
if (!expectClose)
172+
throw e;
173+
}
174+
175+
if (!pass)
176+
throw new RuntimeException("Failed in server");
177+
}
178+
}

0 commit comments

Comments
 (0)