Skip to content

Commit c9a7259

Browse files
committed
8278067: Make HttpURLConnection default keep alive timeout configurable
Reviewed-by: stuefe Backport-of: d8f44aa39e921594505864e6270f42b745265293
1 parent ab4a3e6 commit c9a7259

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
@@ -174,6 +174,16 @@ <H2>Misc HTTP URL stream protocol handler properties</H2>
174174
If HTTP keepalive is enabled (see above) this value determines the
175175
maximum number of idle connections that will be simultaneously kept
176176
alive, per destination.</P>
177+
<LI><P><B>{@systemProperty http.keepAlive.time.server}</B> and
178+
<B>{@systemProperty http.keepAlive.time.proxy}</B> </P>
179+
<P>These properties modify the behavior of the HTTP keepalive cache in the case
180+
where the server (or proxy) has not specified a keepalive time. If the
181+
property is set in this case, then idle connections will be closed after the
182+
specified number of seconds. If the property is set, and the server does
183+
specify a keepalive time in a "Keep-Alive" response header, then the time specified
184+
by the server is used. If the property is not set and also the server
185+
does not specify a keepalive time, then connections are kept alive for an
186+
implementation defined time, assuming {@code http.keepAlive} is {@code true}.</P>
177187
<LI><P><B>http.maxRedirects</B> (default: 20)<BR>
178188
This integer value determines the maximum number, for a given request,
179189
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
@@ -118,13 +118,13 @@ private static int getDefaultPort(String proto) {
118118
recomputing the value of keepingAlive */
119119
int keepAliveConnections = -1; /* number of keep-alives left */
120120

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

@@ -231,7 +231,6 @@ public boolean getHttpKeepAliveSet() {
231231
return keepAliveProp;
232232
}
233233

234-
235234
public String getSpnegoCBT() {
236235
return spnegoCBT;
237236
}
@@ -861,7 +860,7 @@ private boolean parseHTTPHeader(MessageHeader responses, ProgressSource pi, Http
861860
responses.findValue("Keep-Alive"));
862861
/* default should be larger in case of proxy */
863862
keepAliveConnections = p.findInt("max", usingProxy?50:5);
864-
keepAliveTimeout = p.findInt("timeout", usingProxy?60:5);
863+
keepAliveTimeout = p.findInt("timeout", -1);
865864
}
866865
} else if (b[7] != '0') {
867866
/*
@@ -1109,6 +1108,10 @@ public String getProxyHostUsed() {
11091108
}
11101109
}
11111110

1111+
public boolean getUsingProxy() {
1112+
return usingProxy;
1113+
}
1114+
11121115
/**
11131116
* @return the proxy port being used for this client. Meaningless
11141117
* 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
@@ -39,6 +39,8 @@
3939

4040
import jdk.internal.misc.InnocuousThread;
4141
import sun.security.action.GetIntegerAction;
42+
import sun.net.www.protocol.http.HttpURLConnection;
43+
import sun.util.logging.PlatformLogger;
4244

4345
/**
4446
* A class that implements a cache of idle Http connections for keep-alive
@@ -51,6 +53,32 @@ public class KeepAliveCache
5153
implements Runnable {
5254
private static final long serialVersionUID = -2937172892064557949L;
5355

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

118146
if (v == null) {
119147
int keepAliveTimeout = http.getKeepAliveTimeout();
120-
v = new ClientVector(keepAliveTimeout > 0 ?
121-
keepAliveTimeout * 1000 : LIFETIME);
122-
v.put(http);
123-
super.put(key, v);
148+
if (keepAliveTimeout == 0) {
149+
keepAliveTimeout = getUserKeepAlive(http.getUsingProxy());
150+
if (keepAliveTimeout == -1) {
151+
// same default for server and proxy
152+
keepAliveTimeout = 5;
153+
}
154+
} else if (keepAliveTimeout == -1) {
155+
keepAliveTimeout = getUserKeepAlive(http.getUsingProxy());
156+
if (keepAliveTimeout == -1) {
157+
// different default for server and proxy
158+
keepAliveTimeout = http.getUsingProxy() ? 60 : 5;
159+
}
160+
}
161+
// at this point keepAliveTimeout is the number of seconds to keep
162+
// alive, which could be 0, if the user specified 0 for the property
163+
assert keepAliveTimeout >= 0;
164+
if (keepAliveTimeout == 0) {
165+
http.closeServer();
166+
} else {
167+
v = new ClientVector(keepAliveTimeout * 1000);
168+
v.put(http);
169+
super.put(key, v);
170+
}
124171
} else {
125172
v.put(http);
126173
}
127174
}
128175

176+
// returns the keep alive set by user in system property or -1 if not set
177+
private static int getUserKeepAlive(boolean isProxy) {
178+
return isProxy ? userKeepAliveProxy : userKeepAliveServer;
179+
}
180+
129181
/* remove an obsolete HttpClient from its VectorCache */
130182
public synchronized void remove(HttpClient h, Object obj) {
131183
KeepAliveKey key = new KeepAliveKey(h.url, obj);
@@ -241,6 +293,11 @@ synchronized HttpClient get() {
241293
e.hc.closeServer();
242294
} else {
243295
hc = e.hc;
296+
if (KeepAliveCache.logger.isLoggable(PlatformLogger.Level.FINEST)) {
297+
String msg = "cached HttpClient was idle for "
298+
+ Long.toString(currentTime - e.idleStartTime);
299+
KeepAliveCache.logger.finest(msg);
300+
}
244301
}
245302
} while ((hc == null) && (!isEmpty()));
246303
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)