8221395: HttpClient leaving connections in CLOSE_WAIT state until Java process ends

When a non WebSocket connection is not returned to the pool, it needs to be closed even if HttpConnection::isOpen yields false.

Reviewed-by: chegar, michaelm
This commit is contained in:
Daniel Fuchs 2019-03-28 12:16:36 +00:00
parent 76cdc8016f
commit 47b9d898ab
3 changed files with 232 additions and 18 deletions

View file

@ -317,14 +317,13 @@ abstract class HttpConnection implements Closeable {
void closeOrReturnToCache(HttpHeaders hdrs) { void closeOrReturnToCache(HttpHeaders hdrs) {
if (hdrs == null) { if (hdrs == null) {
// the connection was closed by server, eof // the connection was closed by server, eof
Log.logTrace("Cannot return connection to pool: closing {0}", this);
close(); close();
return; return;
} }
if (!isOpen()) {
return;
}
HttpClientImpl client = client(); HttpClientImpl client = client();
if (client == null) { if (client == null) {
Log.logTrace("Client released: closing {0}", this);
close(); close();
return; return;
} }
@ -333,10 +332,12 @@ abstract class HttpConnection implements Closeable {
.map((s) -> !s.equalsIgnoreCase("close")) .map((s) -> !s.equalsIgnoreCase("close"))
.orElse(true); .orElse(true);
if (keepAlive) { if (keepAlive && isOpen()) {
Log.logTrace("Returning connection to the pool: {0}", this); Log.logTrace("Returning connection to the pool: {0}", this);
pool.returnToPool(this); pool.returnToPool(this);
} else { } else {
Log.logTrace("Closing connection (keepAlive={0}, isOpen={1}): {2}",
keepAlive, isOpen(), this);
close(); close();
} }
} }

View file

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2017, 2018, Oracle and/or its affiliates. All rights reserved. * Copyright (c) 2017, 2019, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
* *
* This code is free software; you can redistribute it and/or modify it * This code is free software; you can redistribute it and/or modify it
@ -23,7 +23,7 @@
/* /*
* @test * @test
* @bug 8187044 8187111 * @bug 8187044 8187111 8221395
* @summary Verifies that the ConnectionPool correctly handle * @summary Verifies that the ConnectionPool correctly handle
* connection deadlines and purges the right connections * connection deadlines and purges the right connections
* from the cache. * from the cache.
@ -35,4 +35,8 @@
* @run main/othervm * @run main/othervm
* --add-reads java.net.http=java.management * --add-reads java.net.http=java.management
* java.net.http/jdk.internal.net.http.ConnectionPoolTest testPoolSize * java.net.http/jdk.internal.net.http.ConnectionPoolTest testPoolSize
* @run main/othervm
* --add-reads java.net.http=java.management
* -Djdk.httpclient.HttpClient.log=errors,requests,headers,content,ssl,trace,channel
* java.net.http/jdk.internal.net.http.ConnectionPoolTest testCloseOrReturnToPool
*/ */

View file

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2017, 2018, Oracle and/or its affiliates. All rights reserved. * Copyright (c) 2017, 2019, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
* *
* This code is free software; you can redistribute it and/or modify it * This code is free software; you can redistribute it and/or modify it
@ -25,17 +25,25 @@ package jdk.internal.net.http;
import java.io.IOException; import java.io.IOException;
import java.lang.management.ManagementFactory; import java.lang.management.ManagementFactory;
import java.lang.ref.Reference;
import java.net.Authenticator; import java.net.Authenticator;
import java.net.CookieHandler; import java.net.CookieHandler;
import java.net.InetSocketAddress; import java.net.InetSocketAddress;
import java.net.ProxySelector; import java.net.ProxySelector;
import java.net.Socket;
import java.net.SocketAddress;
import java.net.SocketOption;
import java.net.http.HttpHeaders;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel; import java.nio.channels.SocketChannel;
import java.nio.channels.spi.SelectorProvider;
import java.time.Duration; import java.time.Duration;
import java.util.Arrays; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Optional; import java.util.Optional;
import java.util.Random; import java.util.Random;
import java.util.Set;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor; import java.util.concurrent.Executor;
import java.util.concurrent.Flow; import java.util.concurrent.Flow;
@ -53,7 +61,7 @@ import jdk.internal.net.http.common.FlowTube;
* @summary Verifies that the ConnectionPool correctly handle * @summary Verifies that the ConnectionPool correctly handle
* connection deadlines and purges the right connections * connection deadlines and purges the right connections
* from the cache. * from the cache.
* @bug 8187044 8187111 * @bug 8187044 8187111 8221395
* @author danielfuchs * @author danielfuchs
*/ */
public class ConnectionPoolTest { public class ConnectionPoolTest {
@ -78,7 +86,10 @@ public class ConnectionPoolTest {
} else if ("testPoolSize".equals(arg)) { } else if ("testPoolSize".equals(arg)) {
assert args.length == 1 : "testPoolSize should be run in its own VM"; assert args.length == 1 : "testPoolSize should be run in its own VM";
testPoolSize(); testPoolSize();
} } else if ("testCloseOrReturnToPool".equals(arg)) {
assert args.length == 1 : "testCloseOrReturnToPool should be run in its own VM";
testCloseOrReturnToPool();
} else throw new RuntimeException("unknown test case: " + arg);
} }
} }
@ -226,6 +237,113 @@ public class ConnectionPoolTest {
} }
} }
public static void testCloseOrReturnToPool() throws Exception {
HttpClientFacade facade = (HttpClientFacade)HttpClient.newHttpClient();
HttpClientImpl client = facade.impl;
ConnectionPool pool = client.connectionPool();
InetSocketAddress proxy = InetSocketAddress.createUnresolved("bar", 80);
InetSocketAddress addr = InetSocketAddress.createUnresolved("foo1", 80);
HttpConnectionStub conn1 = new HttpConnectionStub(facade, client, addr, proxy, true);
HttpHeaders hdrs = HttpHeaders.of(new HashMap<>(), (s1,s2) -> true);
HttpConnection conn;
conn1.reopen();
if (!conn1.isOpen()) {
throw new RuntimeException("conn1 finished");
}
conn1.closeOrReturnToCache(hdrs);
// Check we can find conn1 in the pool
if (conn1 != (conn = pool.getConnection(true, addr, proxy))) {
throw new RuntimeException("conn1 not returned, got: " + conn);
}
System.out.println("Found connection in the pool: " + conn );
// Try to return it with no headers: the connection should
// be closed and not returned to the pool (EOF).
conn.closeOrReturnToCache(null);
if ((conn = pool.getConnection(true, addr, proxy)) != null) {
throw new RuntimeException(conn + " found in the pool!");
}
if (!conn1.closed) {
throw new RuntimeException("conn1 not closed!");
}
System.out.println("EOF connection successfully closed when returned to pool");
// reopen the connection
conn1.reopen();
if (!conn1.isOpen()) {
throw new RuntimeException("conn1 finished");
}
// Try to return it with empty headers: the connection should
// be returned to the pool.
conn1.closeOrReturnToCache(hdrs);
if (conn1 != (conn = pool.getConnection(true, addr, proxy))) {
throw new RuntimeException("conn1 not returned to pool, got: " + conn);
}
if (conn1.closed) {
throw new RuntimeException("conn1 closed");
}
if (!conn1.isOpen()) {
throw new RuntimeException("conn1 finished");
}
System.out.println("Keep alive connection successfully returned to pool");
// Try to return it with connection: close headers: the connection should
// not be returned to the pool, and should be closed.
HttpHeaders hdrs2 = HttpHeaders.of(Map.of("connection", List.of("close")), (s1, s2) -> true);
conn1.closeOrReturnToCache(hdrs2);
if ((conn = pool.getConnection(true, addr, proxy)) != null) {
throw new RuntimeException(conn + " found in the pool!");
}
if (!conn1.closed) {
throw new RuntimeException("conn1 not closed!");
}
System.out.println("Close connection successfully closed when returned to pool");
// reopen and finish the connection.
conn1.reopen();
conn1.finish(true);
if (conn1.closed) {
throw new RuntimeException("conn1 closed");
}
if (conn1.isOpen()) {
throw new RuntimeException("conn1 is opened!");
}
conn1.closeOrReturnToCache(hdrs2);
if ((conn = pool.getConnection(true, addr, proxy)) != null) {
throw new RuntimeException(conn + " found in the pool!");
}
if (!conn1.closed) {
throw new RuntimeException("conn1 not closed!");
}
System.out.println("Finished 'close' connection successfully closed when returned to pool");
// reopen and finish the connection.
conn1.reopen();
conn1.finish(true);
if (conn1.closed) {
throw new RuntimeException("conn1 closed");
}
if (conn1.isOpen()) {
throw new RuntimeException("conn1 is opened!");
}
conn1.closeOrReturnToCache(hdrs);
if ((conn = pool.getConnection(true, addr, proxy)) != null) {
throw new RuntimeException(conn + " found in the pool!");
}
if (!conn1.closed) {
throw new RuntimeException("conn1 not closed!");
}
System.out.println("Finished keep-alive connection successfully closed when returned to pool");
Reference.reachabilityFence(facade);
}
static <T> T error() { static <T> T error() {
throw new InternalError("Should not reach here: wrong test assumptions!"); throw new InternalError("Should not reach here: wrong test assumptions!");
} }
@ -241,22 +359,108 @@ public class ConnectionPoolTest {
@Override @Override
public void subscribe(Flow.Subscriber<? super List<ByteBuffer>> subscriber) { public void subscribe(Flow.Subscriber<? super List<ByteBuffer>> subscriber) {
} }
@Override public boolean isFinished() { return conn.closed; } @Override public boolean isFinished() { return conn.finished; }
}
static class SocketChannelStub extends SocketChannel {
SocketChannelStub() { super(SelectorProvider.provider()); }
@Override
public SocketChannel bind(SocketAddress local) throws IOException {
return error();
}
@Override
public <T> SocketChannel setOption(SocketOption<T> name, T value) throws IOException {
return error();
}
@Override
public SocketChannel shutdownInput() throws IOException {
return error();
}
@Override
public SocketChannel shutdownOutput() throws IOException {
return error();
}
@Override
public Socket socket() { return error(); }
@Override
public boolean isConnected() { return true; }
@Override
public boolean isConnectionPending() { return false; }
@Override
public boolean connect(SocketAddress remote) throws IOException {
return error();
}
@Override
public boolean finishConnect() throws IOException {
return error();
}
@Override
public SocketAddress getRemoteAddress() throws IOException {
return error();
}
@Override
public int read(ByteBuffer dst) throws IOException {
return error();
}
@Override
public long read(ByteBuffer[] dsts, int offset, int length) throws IOException {
return error();
}
@Override
public int write(ByteBuffer src) throws IOException {
return error();
}
@Override
public long write(ByteBuffer[] srcs, int offset, int length) throws IOException {
return 0;
}
@Override
public SocketAddress getLocalAddress() throws IOException {
return error();
}
@Override
public <T> T getOption(SocketOption<T> name) throws IOException {
return error();
}
@Override
public Set<SocketOption<?>> supportedOptions() {
return error();
}
@Override
protected void implCloseSelectableChannel() throws IOException {
error();
}
@Override
protected void implConfigureBlocking(boolean block) throws IOException {
error();
}
} }
// Emulates an HttpConnection that has a strong reference to its HttpClient. // Emulates an HttpConnection that has a strong reference to its HttpClient.
static class HttpConnectionStub extends HttpConnection { static class HttpConnectionStub extends HttpConnection {
public HttpConnectionStub(HttpClient client, public HttpConnectionStub(
HttpClient client,
InetSocketAddress address, InetSocketAddress address,
InetSocketAddress proxy, InetSocketAddress proxy,
boolean secured) { boolean secured) {
super(address, null); this(client, null, address, proxy, secured);
}
public HttpConnectionStub(
HttpClient client,
HttpClientImpl impl,
InetSocketAddress address,
InetSocketAddress proxy,
boolean secured) {
super(address, impl);
this.key = ConnectionPool.cacheKey(address, proxy); this.key = ConnectionPool.cacheKey(address, proxy);
this.address = address; this.address = address;
this.proxy = proxy; this.proxy = proxy;
this.secured = secured; this.secured = secured;
this.client = client; this.client = client;
this.channel = new SocketChannelStub();
this.flow = new FlowTubeStub(this); this.flow = new FlowTubeStub(this);
} }
@ -266,16 +470,23 @@ public class ConnectionPoolTest {
final ConnectionPool.CacheKey key; final ConnectionPool.CacheKey key;
final HttpClient client; final HttpClient client;
final FlowTubeStub flow; final FlowTubeStub flow;
volatile boolean closed; final SocketChannel channel;
volatile boolean closed, finished;
// Used for testing closeOrReturnToPool.
void finish(boolean finished) { this.finished = finished; }
void reopen() { closed = finished = false;}
// All these return something // All these return something
@Override boolean connected() {return !closed;} @Override boolean connected() {return !closed;}
@Override boolean isSecure() {return secured;} @Override boolean isSecure() {return secured;}
@Override boolean isProxied() {return proxy!=null;} @Override boolean isProxied() {return proxy!=null;}
@Override ConnectionPool.CacheKey cacheKey() {return key;} @Override ConnectionPool.CacheKey cacheKey() {return key;}
@Override FlowTube getConnectionFlow() {return flow;}
@Override SocketChannel channel() {return channel;}
@Override @Override
public void close() { public void close() {
closed=true; closed=finished=true;
System.out.println("closed: " + this); System.out.println("closed: " + this);
} }
@Override @Override
@ -283,13 +494,11 @@ public class ConnectionPoolTest {
return "HttpConnectionStub: " + address + " proxy: " + proxy; return "HttpConnectionStub: " + address + " proxy: " + proxy;
} }
// All these throw errors // All these throw errors
@Override public HttpPublisher publisher() {return error();} @Override public HttpPublisher publisher() {return error();}
@Override public CompletableFuture<Void> connectAsync(Exchange<?> e) {return error();} @Override public CompletableFuture<Void> connectAsync(Exchange<?> e) {return error();}
@Override public CompletableFuture<Void> finishConnect() {return error();} @Override public CompletableFuture<Void> finishConnect() {return error();}
@Override SocketChannel channel() {return error();}
@Override
FlowTube getConnectionFlow() {return flow;}
} }
// Emulates an HttpClient that has a strong reference to its connection pool. // Emulates an HttpClient that has a strong reference to its connection pool.
static class HttpClientStub extends HttpClient { static class HttpClientStub extends HttpClient {