mirror of
https://github.com/openjdk/jdk.git
synced 2026-01-28 12:09:14 +00:00
8326498: java.net.http.HttpClient connection leak using http/2
Reviewed-by: vyazici, djelinski, dfuchs
This commit is contained in:
parent
67ef81eb78
commit
c19b12927d
@ -27,11 +27,9 @@ package jdk.internal.net.http;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.ProtocolException;
|
||||
import java.net.http.HttpClient.Version;
|
||||
import java.time.Duration;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
@ -708,12 +706,10 @@ final class Exchange<T> {
|
||||
if (s == null) {
|
||||
// s can be null if an exception occurred
|
||||
// asynchronously while sending the preface.
|
||||
Throwable t = c.getRecordedCause();
|
||||
final Http2TerminationCause tc = c.getTerminationCause();
|
||||
IOException ioe;
|
||||
if (t != null) {
|
||||
if (!cached)
|
||||
c.close();
|
||||
ioe = new IOException("Can't get stream 1: " + t, t);
|
||||
if (tc != null) {
|
||||
ioe = new IOException("Can't get stream 1", tc.getCloseCause());
|
||||
} else {
|
||||
ioe = new IOException("Can't get stream 1");
|
||||
}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 2015, 2024, Oracle and/or its affiliates. All rights reserved.
|
||||
* Copyright (c) 2015, 2025, Oracle and/or its affiliates. All rights reserved.
|
||||
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
|
||||
*
|
||||
* This code is free software; you can redistribute it and/or modify it
|
||||
@ -25,7 +25,6 @@
|
||||
|
||||
package jdk.internal.net.http;
|
||||
|
||||
import java.io.EOFException;
|
||||
import java.io.IOException;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.util.Base64;
|
||||
@ -234,11 +233,8 @@ class Http2ClientImpl {
|
||||
}
|
||||
}
|
||||
|
||||
private EOFException STOPPED;
|
||||
void stop() {
|
||||
if (debug.on()) debug.log("stopping");
|
||||
STOPPED = new EOFException("HTTP/2 client stopped");
|
||||
STOPPED.setStackTrace(new StackTraceElement[0]);
|
||||
connectionPoolLock.lock();
|
||||
try {
|
||||
stopping = true;
|
||||
@ -253,10 +249,7 @@ class Http2ClientImpl {
|
||||
private boolean close(Http2Connection h2c) {
|
||||
// close all streams
|
||||
try { h2c.closeAllStreams(); } catch (Throwable t) {}
|
||||
// send GOAWAY
|
||||
try { h2c.close(); } catch (Throwable t) {}
|
||||
// attempt graceful shutdown
|
||||
try { h2c.shutdown(STOPPED); } catch (Throwable t) {}
|
||||
// double check and close any new streams
|
||||
try { h2c.closeAllStreams(); } catch (Throwable t) {}
|
||||
// Allows for use of removeIf in stop()
|
||||
|
||||
@ -25,21 +25,20 @@
|
||||
|
||||
package jdk.internal.net.http;
|
||||
|
||||
import java.io.Closeable;
|
||||
import java.io.EOFException;
|
||||
import java.io.IOException;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.lang.invoke.MethodHandles;
|
||||
import java.lang.invoke.VarHandle;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.ProtocolException;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpClient.Version;
|
||||
import java.net.http.HttpHeaders;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.channels.ClosedChannelException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.net.http.HttpConnectTimeoutException;
|
||||
import java.time.Duration;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
@ -49,6 +48,7 @@ import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ConcurrentLinkedQueue;
|
||||
import java.util.concurrent.Flow;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
@ -56,6 +56,7 @@ import java.util.concurrent.locks.Lock;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import javax.net.ssl.SSLEngine;
|
||||
import javax.net.ssl.SSLException;
|
||||
|
||||
@ -130,7 +131,7 @@ import static jdk.internal.net.http.frame.SettingsFrame.MAX_HEADER_LIST_SIZE;
|
||||
* and incoming stream creation (Server push). Incoming frames destined for a
|
||||
* stream are provided by calling Stream.incoming().
|
||||
*/
|
||||
class Http2Connection {
|
||||
class Http2Connection implements Closeable {
|
||||
|
||||
final Logger debug = Utils.getDebugLogger(this::dbgString);
|
||||
static final Logger DEBUG_LOGGER =
|
||||
@ -143,6 +144,8 @@ class Http2Connection {
|
||||
private static final int MAX_SERVER_STREAM_ID = Integer.MAX_VALUE - 1; // 2147483646
|
||||
// may be null; must be accessed/updated with the stateLock held
|
||||
private IdleConnectionTimeoutEvent idleConnectionTimeoutEvent;
|
||||
private final AtomicBoolean goAwaySent = new AtomicBoolean();
|
||||
private final AtomicBoolean goAwayRecvd = new AtomicBoolean();
|
||||
|
||||
/**
|
||||
* Flag set when no more streams to be opened on this connection.
|
||||
@ -215,7 +218,7 @@ class Http2Connection {
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link #shutdown(Throwable) Shuts down} the connection, unless this event is
|
||||
* {@link #close(Http2TerminationCause) Closes} the connection, unless this event is
|
||||
* {@link #cancelled}
|
||||
*/
|
||||
@Override
|
||||
@ -228,26 +231,20 @@ class Http2Connection {
|
||||
try {
|
||||
if (cancelled) {
|
||||
if (debug.on()) {
|
||||
debug.log("Not initiating idle connection shutdown");
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (!markIdleShutdownInitiated()) {
|
||||
if (debug.on()) {
|
||||
debug.log("Unexpected state %s, skipping idle connection shutdown",
|
||||
describeClosedState(closedState));
|
||||
debug.log("Idle timeout event already cancelled, not initiating idle connection close");
|
||||
}
|
||||
return;
|
||||
}
|
||||
// the connection has been idle long enough, we now
|
||||
// mark a state indicating that the connection is chosen
|
||||
// for idle termination and should not be handed out (from the pool)
|
||||
// for newer requests.
|
||||
connTerminator.markForIdleTermination();
|
||||
} finally {
|
||||
stateLock.unlock();
|
||||
}
|
||||
if (debug.on()) {
|
||||
debug.log("Initiating shutdown of HTTP connection which is idle for too long");
|
||||
}
|
||||
HttpConnectTimeoutException hte = new HttpConnectTimeoutException(
|
||||
"HTTP connection idle, no active streams. Shutting down.");
|
||||
shutdown(hte);
|
||||
// terminate the connection due to being idle long enough
|
||||
connTerminator.idleTimedOut();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -256,7 +253,7 @@ class Http2Connection {
|
||||
void cancel() {
|
||||
assert stateLock.isHeldByCurrentThread() : "Current thread doesn't hold " + stateLock;
|
||||
// mark as cancelled to prevent potentially already triggered event from actually
|
||||
// doing the shutdown
|
||||
// doing the close
|
||||
this.cancelled = true;
|
||||
// cancel the timer to prevent the event from being triggered (if it hasn't already)
|
||||
client().cancelTimer(this);
|
||||
@ -376,16 +373,7 @@ class Http2Connection {
|
||||
}
|
||||
|
||||
|
||||
private static final int HALF_CLOSED_LOCAL = 1;
|
||||
private static final int HALF_CLOSED_REMOTE = 2;
|
||||
private static final int SHUTDOWN_REQUESTED = 4;
|
||||
// state when idle connection management initiates a shutdown of the connection, after
|
||||
// which the connection will go into SHUTDOWN_REQUESTED state
|
||||
private static final int IDLE_SHUTDOWN_INITIATED = 8;
|
||||
private final ReentrantLock stateLock = new ReentrantLock();
|
||||
private volatile int closedState;
|
||||
|
||||
//-------------------------------------
|
||||
final HttpConnection connection;
|
||||
private final Http2ClientImpl client2;
|
||||
private final ConcurrentHashMap<Integer,Stream<?>> streams = new ConcurrentHashMap<>();
|
||||
@ -403,7 +391,9 @@ class Http2Connection {
|
||||
private final Decoder hpackIn;
|
||||
final SettingsFrame clientSettings;
|
||||
private volatile SettingsFrame serverSettings;
|
||||
|
||||
private record PushContinuationState(PushPromiseDecoder pushContDecoder, PushPromiseFrame pushContFrame) {}
|
||||
|
||||
private volatile PushContinuationState pushContinuationState;
|
||||
private final String key; // for HttpClientImpl.connections map
|
||||
private final FramesDecoder framesDecoder;
|
||||
@ -418,7 +408,7 @@ class Http2Connection {
|
||||
private final FramesController framesController = new FramesController();
|
||||
private final Http2TubeSubscriber subscriber;
|
||||
final ConnectionWindowUpdateSender windowUpdater;
|
||||
private final AtomicReference<Throwable> cause = new AtomicReference<>();
|
||||
private final Terminator connTerminator = new Terminator();
|
||||
private volatile Supplier<ByteBuffer> initial;
|
||||
private volatile Stream<?> initialStream;
|
||||
|
||||
@ -640,35 +630,31 @@ class Http2Connection {
|
||||
}
|
||||
|
||||
void abandonStream() {
|
||||
boolean shouldClose = false;
|
||||
stateLock.lock();
|
||||
try {
|
||||
long reserved = --numReservedClientStreams;
|
||||
assert reserved >= 0;
|
||||
if (finalStream && reserved == 0 && streams.isEmpty()) {
|
||||
shouldClose = true;
|
||||
}
|
||||
} catch (Throwable t) {
|
||||
shutdown(t); // in case the assert fires...
|
||||
close(Http2TerminationCause.forException(t)); // in case the assert fires...
|
||||
} finally {
|
||||
stateLock.unlock();
|
||||
}
|
||||
|
||||
// We should close the connection here if
|
||||
// it's not pooled. If it's not pooled it will
|
||||
// be marked final stream, reserved will be 0
|
||||
// after decrementing it by one, and there should
|
||||
// be no active request-response streams.
|
||||
if (shouldClose) {
|
||||
shutdown(new IOException("HTTP/2 connection abandoned"));
|
||||
// if the connection is eligible to be closed, we close it here
|
||||
if (shouldClose()) {
|
||||
close(Http2TerminationCause.noErrorTermination());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
boolean shouldClose() {
|
||||
/*
|
||||
* return true if the connection is marked as "final stream" and there
|
||||
* are no active streams on that connection and the connection isn't
|
||||
* reserved for a new stream.
|
||||
*/
|
||||
final boolean shouldClose() {
|
||||
stateLock.lock();
|
||||
try {
|
||||
return finalStream() && streams.isEmpty();
|
||||
return finalStream() && streams.isEmpty() && numReservedClientStreams == 0;
|
||||
} finally {
|
||||
stateLock.unlock();
|
||||
}
|
||||
@ -840,22 +826,8 @@ class Http2Connection {
|
||||
return clientSettings.getParameter(MAX_CONCURRENT_STREAMS);
|
||||
}
|
||||
|
||||
void close() {
|
||||
if (markHalfClosedLocal()) {
|
||||
// we send a GOAWAY frame only if the remote side hasn't already indicated
|
||||
// the intention to close the connection by previously sending a GOAWAY of its own
|
||||
if (connection.channel().isOpen() && !isMarked(closedState, HALF_CLOSED_REMOTE)) {
|
||||
Log.logTrace("Closing HTTP/2 connection: to {0}", connection.address());
|
||||
GoAwayFrame f = new GoAwayFrame(0,
|
||||
ErrorFrame.NO_ERROR,
|
||||
"Requested by user".getBytes(UTF_8));
|
||||
// TODO: set last stream. For now zero ok.
|
||||
sendFrame(f);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
long count;
|
||||
|
||||
final void asyncReceive(ByteBuffer buffer) {
|
||||
// We don't need to read anything and
|
||||
// we don't want to send anything back to the server
|
||||
@ -904,44 +876,26 @@ class Http2Connection {
|
||||
} catch (Throwable e) {
|
||||
String msg = Utils.stackTrace(e);
|
||||
Log.logTrace(msg);
|
||||
shutdown(e);
|
||||
close(Http2TerminationCause.forException(e));
|
||||
}
|
||||
}
|
||||
|
||||
Throwable getRecordedCause() {
|
||||
return cause.get();
|
||||
/**
|
||||
* Closes the connection normally (with a NO_ERROR termination cause), if not already closed.
|
||||
*/
|
||||
@Override
|
||||
public final void close() {
|
||||
close(Http2TerminationCause.noErrorTermination());
|
||||
}
|
||||
|
||||
void shutdown(Throwable t) {
|
||||
int state = closedState;
|
||||
if (debug.on()) debug.log(() -> "Shutting down h2c (state="+describeClosedState(state)+"): " + t);
|
||||
stateLock.lock();
|
||||
try {
|
||||
if (!markShutdownRequested()) return;
|
||||
cause.compareAndSet(null, t);
|
||||
} finally {
|
||||
stateLock.unlock();
|
||||
}
|
||||
|
||||
if (Log.errors()) {
|
||||
if (t!= null && (!(t instanceof EOFException) || isActive())) {
|
||||
Log.logError(t);
|
||||
} else if (t != null) {
|
||||
Log.logError("Shutting down connection: {0}", t.getMessage());
|
||||
} else {
|
||||
Log.logError("Shutting down connection");
|
||||
}
|
||||
}
|
||||
client2.removeFromPool(this);
|
||||
subscriber.stop(cause.get());
|
||||
for (Stream<?> s : streams.values()) {
|
||||
try {
|
||||
s.connectionClosing(t);
|
||||
} catch (Throwable e) {
|
||||
Log.logError("Failed to close stream {0}: {1}", s.streamid, e);
|
||||
}
|
||||
}
|
||||
connection.close(cause.get());
|
||||
/**
|
||||
* Closes the connection with the given termination cause, if not already closed.
|
||||
*
|
||||
* @param tc the termination cause. cannot be null.
|
||||
*/
|
||||
final void close(final Http2TerminationCause tc) {
|
||||
Objects.requireNonNull(tc, "termination cause cannot be null");
|
||||
this.connTerminator.terminate(tc);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -981,15 +935,14 @@ class Http2Connection {
|
||||
} else {
|
||||
if (frame instanceof SettingsFrame) {
|
||||
// The stream identifier for a SETTINGS frame MUST be zero
|
||||
framesDecoder.close(
|
||||
protocolError(ErrorFrame.PROTOCOL_ERROR,
|
||||
"The stream identifier for a SETTINGS frame MUST be zero");
|
||||
protocolError(GoAwayFrame.PROTOCOL_ERROR);
|
||||
return;
|
||||
}
|
||||
|
||||
if (frame instanceof PushPromiseFrame && !serverPushEnabled()) {
|
||||
String protocolError = "received a PUSH_PROMISE when SETTINGS_ENABLE_PUSH is 0";
|
||||
protocolError(ResetFrame.PROTOCOL_ERROR, protocolError);
|
||||
protocolError(ErrorFrame.PROTOCOL_ERROR, protocolError);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -1144,7 +1097,9 @@ class Http2Connection {
|
||||
// Otherwise, if the frame is dropped after having been added to the
|
||||
// inputQ, releaseUnconsumed above should be called.
|
||||
final void dropDataFrame(DataFrame df) {
|
||||
if (isMarked(closedState, SHUTDOWN_REQUESTED)) return;
|
||||
if (!isOpen()) {
|
||||
return;
|
||||
}
|
||||
if (debug.on()) {
|
||||
debug.log("Dropping data frame for stream %d (%d payload bytes)",
|
||||
df.streamid(), df.payloadLength());
|
||||
@ -1154,7 +1109,9 @@ class Http2Connection {
|
||||
|
||||
final void ensureWindowUpdated(DataFrame df) {
|
||||
try {
|
||||
if (isMarked(closedState, SHUTDOWN_REQUESTED)) return;
|
||||
if (!isOpen()) {
|
||||
return;
|
||||
}
|
||||
int length = df.payloadLength();
|
||||
if (length > 0) {
|
||||
windowUpdater.update(length);
|
||||
@ -1251,12 +1208,16 @@ class Http2Connection {
|
||||
case AltSvcFrame.TYPE -> processAltSvcFrame(0, (AltSvcFrame) frame,
|
||||
connection, connection.client());
|
||||
|
||||
default -> protocolError(ErrorFrame.PROTOCOL_ERROR);
|
||||
default -> protocolError(ErrorFrame.PROTOCOL_ERROR, "unknown frame: " + frame);
|
||||
}
|
||||
}
|
||||
|
||||
boolean isOpen() {
|
||||
return !isMarkedForShutdown() && connection.channel().isOpen();
|
||||
/**
|
||||
* Returns true if this connection hasn't been terminated and the underlying
|
||||
* {@linkplain NetworkChannel#isOpen() channel is open}. false otherwise.
|
||||
*/
|
||||
final boolean isOpen() {
|
||||
return this.connTerminator.terminationCause.get() == null && connection.channel().isOpen();
|
||||
}
|
||||
|
||||
void resetStream(int streamid, int code) {
|
||||
@ -1295,6 +1256,7 @@ class Http2Connection {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private void decrementStreamsCount0(int streamid) {
|
||||
Stream<?> s = streams.get(streamid);
|
||||
if (s == null || !s.deRegister())
|
||||
@ -1346,8 +1308,7 @@ class Http2Connection {
|
||||
// corresponding entry in the window controller.
|
||||
windowController.removeStream(streamid);
|
||||
}
|
||||
if (finalStream() && streams.isEmpty()) {
|
||||
// should be only 1 stream, but there might be more if server push
|
||||
if (shouldClose()) {
|
||||
close();
|
||||
} else {
|
||||
// Start timer if property present and not already created
|
||||
@ -1372,9 +1333,7 @@ class Http2Connection {
|
||||
/**
|
||||
* Increments this connection's send Window by the amount in the given frame.
|
||||
*/
|
||||
private void handleWindowUpdate(WindowUpdateFrame f)
|
||||
throws IOException
|
||||
{
|
||||
private void handleWindowUpdate(WindowUpdateFrame f) {
|
||||
int amount = f.getUpdate();
|
||||
if (amount <= 0) {
|
||||
// ## temporarily disable to workaround a bug in Jetty where it
|
||||
@ -1383,37 +1342,19 @@ class Http2Connection {
|
||||
} else {
|
||||
boolean success = windowController.increaseConnectionWindow(amount);
|
||||
if (!success) {
|
||||
protocolError(ErrorFrame.FLOW_CONTROL_ERROR); // overflow
|
||||
protocolError(ErrorFrame.FLOW_CONTROL_ERROR, null); // overflow
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void protocolError(int errorCode)
|
||||
throws IOException
|
||||
{
|
||||
protocolError(errorCode, null);
|
||||
private void protocolError(final int errorCode, final String msg) {
|
||||
final Http2TerminationCause terminationCause =
|
||||
Http2TerminationCause.forH2Error(errorCode, msg);
|
||||
framesDecoder.close(terminationCause.getLogMsg());
|
||||
close(terminationCause);
|
||||
}
|
||||
|
||||
private void protocolError(int errorCode, String msg)
|
||||
throws IOException
|
||||
{
|
||||
String protocolError = "protocol error" + (msg == null?"":(": " + msg));
|
||||
ProtocolException protocolException =
|
||||
new ProtocolException(protocolError);
|
||||
this.cause.compareAndSet(null, protocolException);
|
||||
if (markHalfClosedLocal()) {
|
||||
framesDecoder.close(protocolError);
|
||||
subscriber.stop(protocolException);
|
||||
if (debug.on()) debug.log("Sending GOAWAY due to " + protocolException);
|
||||
GoAwayFrame frame = new GoAwayFrame(0, errorCode);
|
||||
sendFrame(frame);
|
||||
}
|
||||
shutdown(protocolException);
|
||||
}
|
||||
|
||||
private void handleSettings(SettingsFrame frame)
|
||||
throws IOException
|
||||
{
|
||||
private void handleSettings(SettingsFrame frame) {
|
||||
assert frame.streamid() == 0;
|
||||
if (!frame.getFlag(SettingsFrame.ACK)) {
|
||||
int newWindowSize = frame.getParameter(INITIAL_WINDOW_SIZE);
|
||||
@ -1430,9 +1371,7 @@ class Http2Connection {
|
||||
}
|
||||
}
|
||||
|
||||
private void handlePing(PingFrame frame)
|
||||
throws IOException
|
||||
{
|
||||
private void handlePing(PingFrame frame) {
|
||||
frame.setFlag(PingFrame.ACK);
|
||||
sendUnorderedFrame(frame);
|
||||
}
|
||||
@ -1442,7 +1381,7 @@ class Http2Connection {
|
||||
assert lastProcessedStream >= 0 : "unexpected last stream id: "
|
||||
+ lastProcessedStream + " in GOAWAY frame";
|
||||
|
||||
markHalfClosedRemote();
|
||||
goAwayRecvd.set(true);
|
||||
setFinalStream(); // don't allow any new streams on this connection
|
||||
if (debug.on()) {
|
||||
debug.log("processing incoming GOAWAY with last processed stream id:%s in frame %s",
|
||||
@ -1599,20 +1538,20 @@ class Http2Connection {
|
||||
// must be done with "stateLock" held to co-ordinate idle connection management
|
||||
stateLock.lock();
|
||||
try {
|
||||
cancelIdleShutdownEvent();
|
||||
// consider the reservation successful only if the connection's state hasn't moved
|
||||
// to "being closed"
|
||||
return isOpen();
|
||||
cancelIdleCloseEvent();
|
||||
// consider the reservation successful only if the connection is open and
|
||||
// hasn't been chosen for idle termination
|
||||
return !this.connTerminator.isMarkedForIdleTermination() && isOpen();
|
||||
} finally {
|
||||
stateLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancels any event that might have been scheduled to shutdown this connection. Must be called
|
||||
* Cancels any event that might have been scheduled to close this connection. Must be called
|
||||
* with the stateLock held.
|
||||
*/
|
||||
private void cancelIdleShutdownEvent() {
|
||||
private void cancelIdleCloseEvent() {
|
||||
assert stateLock.isHeldByCurrentThread() : "Current thread doesn't hold " + stateLock;
|
||||
if (idleConnectionTimeoutEvent == null) {
|
||||
return;
|
||||
@ -1627,20 +1566,23 @@ class Http2Connection {
|
||||
// the stream is closed.
|
||||
stateLock.lock();
|
||||
try {
|
||||
if (!isMarkedForShutdown()) {
|
||||
if (isOpen() && !this.connTerminator.isMarkedForIdleTermination()) {
|
||||
if (debug.on()) {
|
||||
debug.log("Opened stream %d", streamid);
|
||||
}
|
||||
client().streamReference();
|
||||
streams.put(streamid, stream);
|
||||
cancelIdleShutdownEvent();
|
||||
// don't consider the connection idle anymore
|
||||
cancelIdleCloseEvent();
|
||||
return;
|
||||
}
|
||||
} finally {
|
||||
stateLock.unlock();
|
||||
}
|
||||
if (debug.on()) debug.log("connection closed: closing stream %d", stream);
|
||||
stream.cancel(new IOException("Stream " + streamid + " cancelled", cause.get()));
|
||||
final Http2TerminationCause terminationCause = getTerminationCause();
|
||||
assert terminationCause != null : "termination cause is null";
|
||||
stream.cancel(new IOException("Stream " + streamid + " cancelled", terminationCause.getCloseCause()));
|
||||
}
|
||||
|
||||
/**
|
||||
@ -1743,11 +1685,10 @@ class Http2Connection {
|
||||
int streamid = nextstreamid;
|
||||
Throwable cause = null;
|
||||
synchronized (this) {
|
||||
if (isMarked(closedState, SHUTDOWN_REQUESTED)) {
|
||||
cause = this.cause.get();
|
||||
if (cause == null) {
|
||||
cause = new IOException("Connection closed");
|
||||
}
|
||||
if (!isOpen()) {
|
||||
final Http2TerminationCause terminationCause = getTerminationCause();
|
||||
assert terminationCause != null : "termination cause is null";
|
||||
cause = terminationCause.getCloseCause();
|
||||
}
|
||||
}
|
||||
if (cause != null) {
|
||||
@ -1762,7 +1703,7 @@ class Http2Connection {
|
||||
return stream;
|
||||
} else {
|
||||
stream.cancelImpl(new IOException("Request cancelled"));
|
||||
if (finalStream() && streams.isEmpty()) {
|
||||
if (shouldClose()) {
|
||||
close();
|
||||
}
|
||||
return null;
|
||||
@ -1795,14 +1736,12 @@ class Http2Connection {
|
||||
}
|
||||
publisher.signalEnqueued();
|
||||
} catch (IOException e) {
|
||||
if (!isMarked(closedState, SHUTDOWN_REQUESTED)) {
|
||||
if (!client2.stopping()) {
|
||||
Log.logError(e);
|
||||
shutdown(e);
|
||||
} else if (debug.on()) {
|
||||
debug.log("Failed to send %s while stopping: %s", frame, e);
|
||||
}
|
||||
if (!client2.stopping()) {
|
||||
Log.logError(e);
|
||||
} else if (debug.on()) {
|
||||
debug.log("Failed to send %s while stopping: %s", frame, e);
|
||||
}
|
||||
close(Http2TerminationCause.forException(e));
|
||||
}
|
||||
}
|
||||
|
||||
@ -1817,14 +1756,12 @@ class Http2Connection {
|
||||
publisher.enqueue(encodeFrame(frame));
|
||||
publisher.signalEnqueued();
|
||||
} catch (IOException e) {
|
||||
if (!isMarked(closedState, SHUTDOWN_REQUESTED)) {
|
||||
if (!client2.stopping()) {
|
||||
Log.logError(e);
|
||||
shutdown(e);
|
||||
} else if (debug.on()) {
|
||||
debug.log("Failed to send %s while stopping: %s", frame, e);
|
||||
}
|
||||
if (!client2.stopping()) {
|
||||
Log.logError(e);
|
||||
} else if (debug.on()) {
|
||||
debug.log("Failed to send %s while stopping: %s", frame, e);
|
||||
}
|
||||
close(Http2TerminationCause.forException(e));
|
||||
}
|
||||
}
|
||||
|
||||
@ -1839,10 +1776,12 @@ class Http2Connection {
|
||||
publisher.enqueueUnordered(encodeFrame(frame));
|
||||
publisher.signalEnqueued();
|
||||
} catch (IOException e) {
|
||||
if (!isMarked(closedState, SHUTDOWN_REQUESTED)) {
|
||||
if (!client2.stopping()) {
|
||||
Log.logError(e);
|
||||
shutdown(e);
|
||||
} else if (debug.on()) {
|
||||
debug.log("Failed to send %s while stopping: %s", frame, e);
|
||||
}
|
||||
close(Http2TerminationCause.forException(e));
|
||||
}
|
||||
}
|
||||
|
||||
@ -1868,6 +1807,7 @@ class Http2Connection {
|
||||
try {
|
||||
while (!queue.isEmpty() && !scheduler.isStopped()) {
|
||||
ByteBuffer buffer = queue.poll();
|
||||
assert buffer != null : "null buffer obtained from non-empty queue";
|
||||
if (debug.on())
|
||||
debug.log("sending %d to Http2Connection.asyncReceive",
|
||||
buffer.remaining());
|
||||
@ -1877,7 +1817,12 @@ class Http2Connection {
|
||||
errorRef.compareAndSet(null, t);
|
||||
} finally {
|
||||
Throwable x = errorRef.get();
|
||||
if (x != null) {
|
||||
// if there was any error or if the TubeSubscriber completed normally,
|
||||
// then close the connection
|
||||
if (x != null || completed) {
|
||||
// although the connection terminator stops the scheduler too,
|
||||
// we don't want to wait that "long" and instead we should immediately
|
||||
// stop the scheduler so that we don't enter "processQueue" anymore.
|
||||
scheduler.stop();
|
||||
if (client2.stopping()) {
|
||||
if (debug.on()) {
|
||||
@ -1888,7 +1833,11 @@ class Http2Connection {
|
||||
debug.log("Stopping scheduler", x);
|
||||
}
|
||||
}
|
||||
Http2Connection.this.shutdown(x);
|
||||
// terminate the connection
|
||||
final Http2TerminationCause tc = (x != null)
|
||||
? Http2TerminationCause.forException(x)
|
||||
: Http2TerminationCause.noErrorTermination();
|
||||
Http2Connection.this.close(tc);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1938,11 +1887,17 @@ class Http2Connection {
|
||||
@Override
|
||||
public void onComplete() {
|
||||
if (completed) return;
|
||||
String msg = isActive()
|
||||
? "EOF reached while reading"
|
||||
: "Idle connection closed by HTTP/2 peer";
|
||||
if (debug.on()) debug.log(msg);
|
||||
errorRef.compareAndSet(null, new EOFException(msg));
|
||||
if (isActive()) {
|
||||
final String msg = "EOF reached while reading";
|
||||
errorRef.compareAndSet(null, new EOFException(msg));
|
||||
if (debug.on()) {
|
||||
debug.log(msg);
|
||||
}
|
||||
} else {
|
||||
if (debug.on()) {
|
||||
debug.log("HTTP/2 connection (with no active streams) closed by peer");
|
||||
}
|
||||
}
|
||||
completed = true;
|
||||
runOrSchedule();
|
||||
}
|
||||
@ -1955,16 +1910,13 @@ class Http2Connection {
|
||||
dropped = true;
|
||||
}
|
||||
|
||||
void stop(Throwable error) {
|
||||
if (errorRef.compareAndSet(null, error)) {
|
||||
completed = true;
|
||||
scheduler.stop();
|
||||
queue.clear();
|
||||
if (subscription != null) {
|
||||
subscription.cancel();
|
||||
}
|
||||
queue.clear();
|
||||
private void close() {
|
||||
scheduler.stop();
|
||||
queue.clear();
|
||||
if (subscription != null) {
|
||||
subscription.cancel();
|
||||
}
|
||||
queue.clear();
|
||||
}
|
||||
}
|
||||
|
||||
@ -1990,6 +1942,7 @@ class Http2Connection {
|
||||
static final class ConnectionWindowUpdateSender extends WindowUpdateSender {
|
||||
|
||||
final int initialWindowSize;
|
||||
|
||||
public ConnectionWindowUpdateSender(Http2Connection connection,
|
||||
int initialWindowSize) {
|
||||
super(connection, initialWindowSize);
|
||||
@ -2004,13 +1957,9 @@ class Http2Connection {
|
||||
@Override
|
||||
protected boolean windowSizeExceeded(long received) {
|
||||
if (connection.isOpen()) {
|
||||
try {
|
||||
connection.protocolError(ErrorFrame.FLOW_CONTROL_ERROR,
|
||||
"connection window exceeded (%s > %s)"
|
||||
.formatted(received, windowSize));
|
||||
} catch (IOException io) {
|
||||
connection.shutdown(io);
|
||||
}
|
||||
connection.protocolError(ErrorFrame.FLOW_CONTROL_ERROR,
|
||||
"connection window exceeded (%s > %s)"
|
||||
.formatted(received, windowSize));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@ -2033,72 +1982,144 @@ class Http2Connection {
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isMarked(int state, int mask) {
|
||||
return (state & mask) == mask;
|
||||
}
|
||||
|
||||
private boolean isMarkedForShutdown() {
|
||||
final int closedSt = closedState;
|
||||
return isMarked(closedSt, IDLE_SHUTDOWN_INITIATED)
|
||||
|| isMarked(closedSt, SHUTDOWN_REQUESTED);
|
||||
}
|
||||
|
||||
private boolean markShutdownRequested() {
|
||||
return markClosedState(SHUTDOWN_REQUESTED);
|
||||
}
|
||||
|
||||
private boolean markHalfClosedLocal() {
|
||||
return markClosedState(HALF_CLOSED_LOCAL);
|
||||
}
|
||||
|
||||
private boolean markHalfClosedRemote() {
|
||||
return markClosedState(HALF_CLOSED_REMOTE);
|
||||
}
|
||||
|
||||
private boolean markIdleShutdownInitiated() {
|
||||
return markClosedState(IDLE_SHUTDOWN_INITIATED);
|
||||
}
|
||||
|
||||
private boolean markClosedState(int flag) {
|
||||
int state, desired;
|
||||
do {
|
||||
state = desired = closedState;
|
||||
if ((state & flag) == flag) return false;
|
||||
desired = state | flag;
|
||||
} while (!CLOSED_STATE.compareAndSet(this, state, desired));
|
||||
return true;
|
||||
}
|
||||
|
||||
String describeClosedState(int state) {
|
||||
if (state == 0) return "active";
|
||||
String desc = null;
|
||||
if (isMarked(state, IDLE_SHUTDOWN_INITIATED)) {
|
||||
desc = "idle-shutdown-initiated";
|
||||
private void sendGoAway(final GoAwayFrame goAway) {
|
||||
// currently we send a GOAWAY just once irrespective of what value the
|
||||
// last stream id was in the GOAWAY frame
|
||||
if (!goAwaySent.compareAndSet(false, true)) {
|
||||
// already sent
|
||||
return;
|
||||
}
|
||||
if (isMarked(state, SHUTDOWN_REQUESTED)) {
|
||||
desc = desc == null ? "shutdown" : desc + "+shutdown";
|
||||
if (Log.trace()) {
|
||||
Log.logTrace("{0} sending GOAWAY {1}", connection, goAway);
|
||||
} else if (debug.on()) {
|
||||
debug.log("sending GOAWAY " + goAway);
|
||||
}
|
||||
if (isMarked(state, HALF_CLOSED_LOCAL | HALF_CLOSED_REMOTE)) {
|
||||
if (desc == null) return "closed";
|
||||
else return desc + "+closed";
|
||||
}
|
||||
if (isMarked(state, HALF_CLOSED_LOCAL)) {
|
||||
if (desc == null) return "half-closed-local";
|
||||
else return desc + "+half-closed-local";
|
||||
}
|
||||
if (isMarked(state, HALF_CLOSED_REMOTE)) {
|
||||
if (desc == null) return "half-closed-remote";
|
||||
else return desc + "+half-closed-remote";
|
||||
}
|
||||
return "0x" + Integer.toString(state, 16);
|
||||
// this merely enqueues the frame
|
||||
sendFrame(goAway);
|
||||
}
|
||||
|
||||
private static final VarHandle CLOSED_STATE;
|
||||
static {
|
||||
try {
|
||||
CLOSED_STATE = MethodHandles.lookup().findVarHandle(Http2Connection.class, "closedState", int.class);
|
||||
} catch (Exception x) {
|
||||
throw new ExceptionInInitializerError(x);
|
||||
/**
|
||||
* Returns the termination cause if the connection is closed, else returns null.
|
||||
*/
|
||||
final Http2TerminationCause getTerminationCause() {
|
||||
return this.connTerminator.determineTerminationCause();
|
||||
}
|
||||
|
||||
// Responsible for doing all the necessary work for closing a Http2Connection
|
||||
private final class Terminator {
|
||||
// the cause for closing the connection. Must only be set in the
|
||||
// Terminator.terminate(Http2TerminationCause) method.
|
||||
private final AtomicReference<Http2TerminationCause> terminationCause = new AtomicReference<>();
|
||||
// true if it has been decided to terminate the connection due to being idle,
|
||||
// false otherwise. should be accessed only when holding the stateLock
|
||||
private boolean chosenForIdleTermination;
|
||||
|
||||
private void terminate(final Http2TerminationCause terminationCause) {
|
||||
Objects.requireNonNull(terminationCause, "termination cause cannot be null");
|
||||
// allow to be terminated only once
|
||||
stateLock.lock();
|
||||
try {
|
||||
final boolean success = this.terminationCause.compareAndSet(null, terminationCause);
|
||||
if (!success) {
|
||||
// already terminated or is being terminated by some other thread
|
||||
return;
|
||||
}
|
||||
// disable the idle timeout event, since we are now going to terminate the
|
||||
// connection
|
||||
Http2Connection.this.cancelIdleCloseEvent();
|
||||
} finally {
|
||||
stateLock.unlock();
|
||||
}
|
||||
// do the actual termination
|
||||
doTerminate();
|
||||
}
|
||||
|
||||
private void doTerminate() {
|
||||
final Http2TerminationCause tc = terminationCause.get();
|
||||
assert tc != null : "missing termination cause";
|
||||
// we send a GOAWAY frame only if the remote side hasn't already indicated
|
||||
// the intention to close the connection by previously sending a GOAWAY of its own
|
||||
if (!Http2Connection.this.goAwayRecvd.get()) {
|
||||
final int lastStream = 0; // TODO: set last stream. For now zero is ok.
|
||||
final String peerVisibleReason = tc.getPeerVisibleReason();
|
||||
final GoAwayFrame goAway;
|
||||
if (peerVisibleReason == null) {
|
||||
goAway = new GoAwayFrame(lastStream, tc.getCloseCode());
|
||||
} else {
|
||||
goAway = new GoAwayFrame(lastStream, tc.getCloseCode(),
|
||||
peerVisibleReason.getBytes(UTF_8));
|
||||
}
|
||||
sendGoAway(goAway);
|
||||
}
|
||||
// now close the connection
|
||||
|
||||
if (Log.errors() || debug.on()) {
|
||||
final String stateStr = "Abnormal close=" + tc.isAbnormalClose() +
|
||||
", has active streams=" + isActive() +
|
||||
", GOAWAY received=" + goAwayRecvd.get() +
|
||||
", GOAWAY sent=" + goAwaySent.get();
|
||||
if (Log.errors()) {
|
||||
Log.logError("Closing connection {0} ({1}) due to: {2}",
|
||||
connection, stateStr, tc);
|
||||
} else {
|
||||
debug.log("Closing connection (" + stateStr + ") due to: " + tc);
|
||||
}
|
||||
}
|
||||
// close the TubeSubscriber
|
||||
subscriber.close();
|
||||
client2.removeFromPool(Http2Connection.this);
|
||||
// notify the HTTP/2 streams of the connection closure
|
||||
for (final Stream<?> s : streams.values()) {
|
||||
try {
|
||||
s.connectionClosing(tc.getCloseCause());
|
||||
} catch (Throwable e) {
|
||||
Log.logError("Failed to close stream {0}: {1}", s.streamid, e);
|
||||
}
|
||||
}
|
||||
// close the underlying connection
|
||||
connection.close(tc.getCloseCause());
|
||||
}
|
||||
|
||||
private void markForIdleTermination() {
|
||||
assert stateLock.isHeldByCurrentThread() : Thread.currentThread()
|
||||
+ " not holding stateLock";
|
||||
this.chosenForIdleTermination = true;
|
||||
}
|
||||
|
||||
private boolean isMarkedForIdleTermination() {
|
||||
assert stateLock.isHeldByCurrentThread() : Thread.currentThread()
|
||||
+ " not holding stateLock";
|
||||
return this.chosenForIdleTermination;
|
||||
}
|
||||
|
||||
private void idleTimedOut() {
|
||||
if (debug.on()) {
|
||||
debug.log("closing connection due to being idle");
|
||||
}
|
||||
this.terminate(Http2TerminationCause.idleTimedOut());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the termination cause for the connection. This method guarantees that if the
|
||||
* {@linkplain Http2Connection#isOpen() connection is not open}, when this method is called,
|
||||
* then it returns a non-null termination cause. Returns null if the connection is open.
|
||||
*/
|
||||
private Http2TerminationCause determineTerminationCause() {
|
||||
final Http2TerminationCause tc = this.terminationCause.get();
|
||||
if (tc != null) {
|
||||
// already terminated, return the cause
|
||||
return tc;
|
||||
}
|
||||
if (!connection.channel().isOpen()) {
|
||||
// if the underlying SocketChannel isn't open, then terminate the connection.
|
||||
// that way when Http2Connection.isOpen() returns false in that situation, then this
|
||||
// getTerminationCause() will return a termination cause.
|
||||
terminate(Http2TerminationCause.forException(new ClosedChannelException()));
|
||||
final Http2TerminationCause terminated = this.terminationCause.get();
|
||||
assert terminated != null : "missing termination cause";
|
||||
return terminated;
|
||||
}
|
||||
return null; // connection still open
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,281 @@
|
||||
/*
|
||||
* Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved.
|
||||
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
|
||||
*
|
||||
* This code is free software; you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License version 2 only, as
|
||||
* published by the Free Software Foundation. Oracle designates this
|
||||
* particular file as subject to the "Classpath" exception as provided
|
||||
* by Oracle in the LICENSE file that accompanied this code.
|
||||
*
|
||||
* This code is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
|
||||
* version 2 for more details (a copy is included in the LICENSE file that
|
||||
* accompanied this code).
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License version
|
||||
* 2 along with this work; if not, write to the Free Software Foundation,
|
||||
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
*
|
||||
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
|
||||
* or visit www.oracle.com if you need additional information or have any
|
||||
* questions.
|
||||
*/
|
||||
package jdk.internal.net.http;
|
||||
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.ProtocolException;
|
||||
import java.util.Objects;
|
||||
|
||||
import jdk.internal.net.http.common.Utils;
|
||||
import jdk.internal.net.http.frame.ErrorFrame;
|
||||
|
||||
/**
|
||||
* Termination cause for an {@linkplain Http2Connection HTTP/2 connection}
|
||||
*/
|
||||
public abstract sealed class Http2TerminationCause {
|
||||
private String logMsg;
|
||||
private String peerVisibleReason;
|
||||
private final int closeCode;
|
||||
private final Throwable originalCause;
|
||||
private final IOException reportedCause;
|
||||
|
||||
private Http2TerminationCause(final int closeCode, final Throwable closeCause) {
|
||||
this.closeCode = closeCode;
|
||||
this.originalCause = closeCause;
|
||||
if (closeCause != null) {
|
||||
this.logMsg = closeCause.toString();
|
||||
}
|
||||
this.reportedCause = toReportedCause(this.originalCause, this.logMsg);
|
||||
}
|
||||
|
||||
private Http2TerminationCause(final int closeCode, final String loggedAs) {
|
||||
this.closeCode = closeCode;
|
||||
this.originalCause = null;
|
||||
this.logMsg = loggedAs;
|
||||
this.reportedCause = toReportedCause(null, this.logMsg);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the error code (specified for HTTP/2 ErrorFrame) that caused the
|
||||
* connection termination.
|
||||
*/
|
||||
public final int getCloseCode() {
|
||||
return this.closeCode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@link IOException} that is considered the cause of the connection termination.
|
||||
* Even a {@linkplain #isAbnormalClose() normal} termination will have
|
||||
* an {@code IOException} associated with it, so this method will always return a non-null instance.
|
||||
*/
|
||||
public final IOException getCloseCause() {
|
||||
return this.reportedCause;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns {@code true} if the connection was terminated due to some exception. {@code false}
|
||||
* otherwise.
|
||||
* A normal connection termination (for example, the connection idle timing out locally)
|
||||
* is not considered as an abnormal termination and this method returns {@code false} for
|
||||
* such cases.
|
||||
*/
|
||||
public abstract boolean isAbnormalClose();
|
||||
|
||||
/**
|
||||
* Returns the connection termination cause, represented as a string. Unlike the
|
||||
* {@linkplain #getPeerVisibleReason() peer-visible reason}, this log message will not be
|
||||
* sent across to the peer and it is thus allowed to include additional details that might
|
||||
* help debugging a connection termination.
|
||||
*/
|
||||
public final String getLogMsg() {
|
||||
return logMsg;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the connection termination cause, represented as a string. This represents the
|
||||
* "debugData" that is sent to the peer in a
|
||||
* {@linkplain jdk.internal.net.http.frame.GoAwayFrame GOAWAY frame}.
|
||||
*/
|
||||
public final String getPeerVisibleReason() {
|
||||
return this.peerVisibleReason;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the connection termination cause, represented as a string, which will be sent
|
||||
* to the peer in a {@linkplain jdk.internal.net.http.frame.GoAwayFrame GOAWAY frame}.
|
||||
* Unlike the {@link #getLogMsg() log message},
|
||||
* it is expected that this peer-visible reason will not contain anything that is not meant
|
||||
* to be viewed by the peer.
|
||||
*/
|
||||
protected final void setPeerVisibleReason(final String reasonPhrase) {
|
||||
this.peerVisibleReason = reasonPhrase;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a connection termination cause that represents an
|
||||
* {@linkplain #isAbnormalClose() abnormal} termination due to the given {@code cause}.
|
||||
*
|
||||
* @param cause the termination cause, cannot be null.
|
||||
*/
|
||||
public static Http2TerminationCause forException(final Throwable cause) {
|
||||
Objects.requireNonNull(cause);
|
||||
if (cause instanceof ProtocolException pe) {
|
||||
return new ProtocolError(pe);
|
||||
}
|
||||
return new InternalError(cause);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a connection termination cause that represents a
|
||||
* {@linkplain #isAbnormalClose() normal} termination.
|
||||
*/
|
||||
public static Http2TerminationCause noErrorTermination() {
|
||||
return NoError.INSTANCE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a connection termination cause that represents a
|
||||
* {@linkplain #isAbnormalClose() normal} termination due to the connection
|
||||
* being idle.
|
||||
*/
|
||||
public static Http2TerminationCause idleTimedOut() {
|
||||
return NoError.IDLE_TIMED_OUT;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a connection termination cause that represents an
|
||||
* {@linkplain #isAbnormalClose() abnormal} termination due to the given {@code errorCode}.
|
||||
* Although this method does no checks for the {@code errorCode}, it is expected to be one
|
||||
* of the error codes specified by the HTTP/2 RFC for the ErrorFrame.
|
||||
*
|
||||
* @param errorCode the error code
|
||||
* @param loggedAs optional log message to be associated with this termination cause
|
||||
*/
|
||||
public static Http2TerminationCause forH2Error(final int errorCode, final String loggedAs) {
|
||||
if (errorCode == ErrorFrame.PROTOCOL_ERROR) {
|
||||
return new ProtocolError(loggedAs);
|
||||
} else if (errorCode == ErrorFrame.FLOW_CONTROL_ERROR) {
|
||||
// we treat flow control error as a protocol error currently
|
||||
return new ProtocolError(loggedAs, true);
|
||||
}
|
||||
return new H2StandardError(errorCode, loggedAs);
|
||||
}
|
||||
|
||||
private static IOException toReportedCause(final Throwable original,
|
||||
final String fallbackExceptionMsg) {
|
||||
if (original == null) {
|
||||
return fallbackExceptionMsg == null
|
||||
? new IOException("connection terminated")
|
||||
: new IOException(fallbackExceptionMsg);
|
||||
} else if (original instanceof IOException ioe) {
|
||||
return ioe;
|
||||
} else {
|
||||
return Utils.toIOException(original);
|
||||
}
|
||||
}
|
||||
|
||||
private static final class NoError extends Http2TerminationCause {
|
||||
private static final IOException NO_ERROR_MARKER =
|
||||
new IOException("HTTP/2 connection closed normally - no error");
|
||||
private static final IOException NO_ERROR_IDLE_TIMED_OUT_MARKER =
|
||||
new IOException("HTTP/2 connection idle timed out - no error");
|
||||
|
||||
static {
|
||||
// remove the stacktrace from the marker exception instances
|
||||
NO_ERROR_MARKER.setStackTrace(new StackTraceElement[0]);
|
||||
NO_ERROR_IDLE_TIMED_OUT_MARKER.setStackTrace(new StackTraceElement[0]);
|
||||
}
|
||||
|
||||
private static final NoError INSTANCE = new NoError(false);
|
||||
private static final NoError IDLE_TIMED_OUT = new NoError(true);
|
||||
|
||||
private final boolean idleTimedOut;
|
||||
|
||||
private NoError(final boolean idleTimedOut) {
|
||||
super(ErrorFrame.NO_ERROR,
|
||||
idleTimedOut ? NO_ERROR_IDLE_TIMED_OUT_MARKER : NO_ERROR_MARKER);
|
||||
this.idleTimedOut = idleTimedOut;
|
||||
setPeerVisibleReason(idleTimedOut ? "idle timed out" : "no error");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isAbnormalClose() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return this.idleTimedOut
|
||||
? "No error - idle timed out"
|
||||
: "No error - normal termination";
|
||||
}
|
||||
}
|
||||
|
||||
private static sealed class H2StandardError extends Http2TerminationCause {
|
||||
private H2StandardError(final int errCode, final String msg) {
|
||||
super(errCode, msg);
|
||||
setPeerVisibleReason(ErrorFrame.stringForCode(errCode));
|
||||
}
|
||||
|
||||
private H2StandardError(final int errCode, final Throwable cause) {
|
||||
super(errCode, cause);
|
||||
setPeerVisibleReason(ErrorFrame.stringForCode(errCode));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isAbnormalClose() {
|
||||
return getCloseCode() != ErrorFrame.NO_ERROR;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return ErrorFrame.stringForCode(this.getCloseCode());
|
||||
}
|
||||
}
|
||||
|
||||
private static final class ProtocolError extends H2StandardError {
|
||||
private ProtocolError(final String msg) {
|
||||
this(msg, false);
|
||||
}
|
||||
|
||||
private ProtocolError(final String msg, final boolean flowControlError) {
|
||||
super(flowControlError
|
||||
? ErrorFrame.FLOW_CONTROL_ERROR
|
||||
: ErrorFrame.PROTOCOL_ERROR,
|
||||
new ProtocolException(msg));
|
||||
}
|
||||
|
||||
private ProtocolError(final ProtocolException pe) {
|
||||
super(ErrorFrame.PROTOCOL_ERROR, pe);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isAbnormalClose() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Protocol error - " + this.getLogMsg();
|
||||
}
|
||||
}
|
||||
|
||||
private static final class InternalError extends Http2TerminationCause {
|
||||
private InternalError(final Throwable cause) {
|
||||
super(ErrorFrame.INTERNAL_ERROR, cause);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isAbnormalClose() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Internal error - " + this.getLogMsg();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -540,9 +540,7 @@ abstract class HttpConnection implements Closeable {
|
||||
* Closes this connection due to the given cause.
|
||||
* @param cause the cause for which the connection is closed, may be null
|
||||
*/
|
||||
void close(Throwable cause) {
|
||||
close();
|
||||
}
|
||||
abstract void close(Throwable cause);
|
||||
|
||||
/**
|
||||
* {@return the underlying connection flow, if applicable}
|
||||
|
||||
@ -0,0 +1,67 @@
|
||||
/*
|
||||
* Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved.
|
||||
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
|
||||
*
|
||||
* This code is free software; you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License version 2 only, as
|
||||
* published by the Free Software Foundation.
|
||||
*
|
||||
* This code is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
|
||||
* version 2 for more details (a copy is included in the LICENSE file that
|
||||
* accompanied this code).
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License version
|
||||
* 2 along with this work; if not, write to the Free Software Foundation,
|
||||
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
*
|
||||
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
|
||||
* or visit www.oracle.com if you need additional information or have any
|
||||
* questions.
|
||||
*/
|
||||
package jdk.internal.net.http;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.net.http.HttpClient;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
|
||||
public final class HttpClientImplAccess {
|
||||
|
||||
private static final Field openedConnections; // Set<> jdk.internal.net.http.HttpClientImpl#openedConnections
|
||||
|
||||
static {
|
||||
try {
|
||||
openedConnections = Class.forName("jdk.internal.net.http.HttpClientImpl")
|
||||
.getDeclaredField("openedConnections");
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private HttpClientImplAccess() {
|
||||
throw new AssertionError();
|
||||
}
|
||||
|
||||
private static HttpClientImpl impl(final HttpClient client) {
|
||||
if (client instanceof HttpClientImpl impl) return impl;
|
||||
if (client instanceof HttpClientFacade facade) return facade.impl;
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@code jdk.internal.net.http.HttpClientImpl#openedConnections Set}.
|
||||
* Returns null if the underlying client isn't of type jdk.internal.net.http.HttpClientImpl.
|
||||
*/
|
||||
public static Set<?> getOpenedConnections(final HttpClient client)
|
||||
throws IllegalAccessException {
|
||||
Objects.requireNonNull(client, "client");
|
||||
final HttpClientImpl clientImpl = impl(client);
|
||||
if (clientImpl == null) {
|
||||
return null;
|
||||
}
|
||||
openedConnections.setAccessible(true);
|
||||
return (Set<?>) openedConnections.get(clientImpl);
|
||||
}
|
||||
}
|
||||
208
test/jdk/java/net/httpclient/http2/BurstyRequestsTest.java
Normal file
208
test/jdk/java/net/httpclient/http2/BurstyRequestsTest.java
Normal file
@ -0,0 +1,208 @@
|
||||
/*
|
||||
* Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved.
|
||||
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
|
||||
*
|
||||
* This code is free software; you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License version 2 only, as
|
||||
* published by the Free Software Foundation.
|
||||
*
|
||||
* This code is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
|
||||
* version 2 for more details (a copy is included in the LICENSE file that
|
||||
* accompanied this code).
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License version
|
||||
* 2 along with this work; if not, write to the Free Software Foundation,
|
||||
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
*
|
||||
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
|
||||
* or visit www.oracle.com if you need additional information or have any
|
||||
* questions.
|
||||
*/
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.URI;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpHeaders;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.net.http.HttpResponse.BodyHandlers;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.Future;
|
||||
|
||||
import javax.net.ssl.SSLSession;
|
||||
|
||||
import jdk.httpclient.test.lib.http2.BodyOutputStream;
|
||||
import jdk.httpclient.test.lib.http2.Http2Handler;
|
||||
import jdk.httpclient.test.lib.http2.Http2TestExchange;
|
||||
import jdk.httpclient.test.lib.http2.Http2TestExchangeSupplier;
|
||||
import jdk.httpclient.test.lib.http2.Http2TestServer;
|
||||
import jdk.httpclient.test.lib.http2.Http2TestServerConnection;
|
||||
import jdk.internal.net.http.HttpClientImplAccess;
|
||||
import jdk.internal.net.http.common.HttpHeadersBuilder;
|
||||
import jdk.test.lib.net.URIBuilder;
|
||||
import org.junit.jupiter.api.AfterAll;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import static java.net.http.HttpClient.Builder.NO_PROXY;
|
||||
import static java.net.http.HttpClient.Version.HTTP_2;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
/*
|
||||
* @test
|
||||
* @bug 8326498 8361091
|
||||
* @summary verify that the HttpClient does not leak connections when dealing with
|
||||
* sudden rush of HTTP/2 requests
|
||||
* @library /test/lib /test/jdk/java/net/httpclient/lib ../access
|
||||
* @build jdk.test.lib.net.SimpleSSLContext
|
||||
* jdk.httpclient.test.lib.http2.Http2TestServer
|
||||
* jdk.httpclient.test.lib.http2.Http2Handler
|
||||
* jdk.httpclient.test.lib.http2.Http2TestExchange
|
||||
* jdk.httpclient.test.lib.http2.Http2TestExchangeSupplier
|
||||
* java.net.http/jdk.internal.net.http.HttpClientImplAccess
|
||||
* @run junit ${test.main.class}
|
||||
*/
|
||||
class BurstyRequestsTest {
|
||||
|
||||
private static final String HANDLER_PATH = "/8326498/";
|
||||
|
||||
// we use a h2c server but it doesn't matter if it is h2c or h2
|
||||
private static Http2TestServer http2Server;
|
||||
|
||||
@BeforeAll
|
||||
static void beforeAll() throws Exception {
|
||||
http2Server = new Http2TestServer(false, 0);
|
||||
http2Server.setExchangeSupplier(new ExchangeSupplier());
|
||||
http2Server.addHandler(new Handler(), HANDLER_PATH);
|
||||
http2Server.start();
|
||||
System.err.println("started HTTP/2 server " + http2Server.getAddress());
|
||||
}
|
||||
|
||||
@AfterAll
|
||||
static void afterAll() {
|
||||
if (http2Server != null) {
|
||||
System.err.println("stopping server " + http2Server.getAddress());
|
||||
http2Server.stop();
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Issues a burst of HTTP/2 requests to the same server (host/port) and expects all of
|
||||
* them to complete normally.
|
||||
* Once these requests have completed, the test then peeks into an internal field of the
|
||||
* HttpClientImpl to verify that the client is holding on to at most 1 connection.
|
||||
*/
|
||||
@Test
|
||||
void testOpenConnections() throws Exception {
|
||||
final URI reqURI = URIBuilder.newBuilder()
|
||||
.scheme("http")
|
||||
.host(http2Server.getAddress().getAddress())
|
||||
.port(http2Server.getAddress().getPort())
|
||||
.path(HANDLER_PATH)
|
||||
.build();
|
||||
final HttpRequest req = HttpRequest.newBuilder().uri(reqURI).build();
|
||||
|
||||
final int numRequests = 20;
|
||||
// latch for the tasks to wait on, before issuing the requests
|
||||
final CountDownLatch startLatch = new CountDownLatch(numRequests);
|
||||
final List<Future<Void>> futures = new ArrayList<>();
|
||||
|
||||
try (final ExecutorService executor = Executors.newCachedThreadPool();
|
||||
final HttpClient client = HttpClient.newBuilder()
|
||||
.proxy(NO_PROXY)
|
||||
.version(HTTP_2)
|
||||
.build()) {
|
||||
// our test needs to peek into the internal field of jdk.internal.net.http.HttpClientImpl
|
||||
final Set<?> openedConnections = HttpClientImplAccess.getOpenedConnections(client);
|
||||
assertNotNull(openedConnections, "HttpClientImpl#openedConnections field is null or not available");
|
||||
|
||||
for (int i = 0; i < numRequests; i++) {
|
||||
final Future<Void> f = executor.submit(new RequestIssuer(startLatch, client, req));
|
||||
futures.add(f);
|
||||
}
|
||||
// wait for the requests to complete
|
||||
for (final Future<Void> f : futures) {
|
||||
f.get();
|
||||
}
|
||||
System.err.println("all " + numRequests + " requests completed successfully");
|
||||
// the request completion happens asynchronously to the closing of the HTTP/2 Stream
|
||||
// as well as the HTTP/2 connection. we wait for at most 1 connection to be retained
|
||||
// by HttpClientImpl.
|
||||
System.err.println("waiting for at least " + (numRequests - 1) + " connections to be closed");
|
||||
// now verify that the current open TCP connections is not more than 1.
|
||||
// we let the test timeout if we never reach that count.
|
||||
int size = openedConnections.size();
|
||||
System.err.println("currently " + size + " open connections: " + openedConnections);
|
||||
while (size > 1) {
|
||||
// wait
|
||||
Thread.sleep(100);
|
||||
final int prev = size;
|
||||
size = openedConnections.size();
|
||||
if (prev != size) {
|
||||
System.err.println("currently " + size + " open connections: " + openedConnections);
|
||||
}
|
||||
}
|
||||
// we expect at most 1 connection will stay open
|
||||
assertTrue((size == 0 || size == 1),
|
||||
"unexpected number of current open connections: " + size);
|
||||
}
|
||||
}
|
||||
|
||||
private static final class RequestIssuer implements Callable<Void> {
|
||||
private final CountDownLatch startLatch;
|
||||
private final HttpClient client;
|
||||
private final HttpRequest request;
|
||||
|
||||
private RequestIssuer(final CountDownLatch startLatch, final HttpClient client,
|
||||
final HttpRequest request) {
|
||||
this.startLatch = startLatch;
|
||||
this.client = client;
|
||||
this.request = request;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Void call() throws Exception {
|
||||
this.startLatch.countDown(); // announce our arrival
|
||||
this.startLatch.await(); // wait for other threads to arrive
|
||||
// issue the request
|
||||
final HttpResponse<Void> resp = this.client.send(request, BodyHandlers.discarding());
|
||||
if (resp.statusCode() != 200) {
|
||||
throw new AssertionError("unexpected response status code: " + resp.statusCode());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static final class Handler implements Http2Handler {
|
||||
private static final int NO_RESP_BODY = -1;
|
||||
|
||||
@Override
|
||||
public void handle(final Http2TestExchange exchange) throws IOException {
|
||||
System.err.println("handling request " + exchange.getRequestURI());
|
||||
exchange.sendResponseHeaders(200, NO_RESP_BODY);
|
||||
}
|
||||
}
|
||||
|
||||
private static final class ExchangeSupplier implements Http2TestExchangeSupplier {
|
||||
|
||||
@Override
|
||||
public Http2TestExchange get(int streamid, String method, HttpHeaders reqheaders,
|
||||
HttpHeadersBuilder rspheadersBuilder, URI uri, InputStream is,
|
||||
SSLSession sslSession, BodyOutputStream os,
|
||||
Http2TestServerConnection conn, boolean pushAllowed) {
|
||||
// don't close the connection when/if the client sends a GOAWAY
|
||||
conn.closeConnOnIncomingGoAway = false;
|
||||
return Http2TestExchangeSupplier.ofDefault().get(streamid, method, reqheaders,
|
||||
rspheadersBuilder, uri, is, sslSession, os, conn, pushAllowed);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 2024, Oracle and/or its affiliates. All rights reserved.
|
||||
* Copyright (c) 2024, 2025, Oracle and/or its affiliates. All rights reserved.
|
||||
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
|
||||
*
|
||||
* This code is free software; you can redistribute it and/or modify it
|
||||
@ -250,11 +250,15 @@ public class H2GoAwayTest {
|
||||
} catch (ExecutionException ee) {
|
||||
final Throwable cause = ee.getCause();
|
||||
if (!(cause instanceof IOException ioe)) {
|
||||
System.err.println("unexpected exception: " + cause
|
||||
+ ", for request " + REQ_URI_BASE + reqQueryPart);
|
||||
throw cause;
|
||||
}
|
||||
// verify it failed for the right reason
|
||||
if (ioe.getMessage() == null
|
||||
|| !ioe.getMessage().contains("request not processed by peer")) {
|
||||
System.err.println("unexpected exception message: " + ioe.getMessage()
|
||||
+ ", for request " + REQ_URI_BASE + reqQueryPart);
|
||||
// propagate the original failure
|
||||
throw ioe;
|
||||
}
|
||||
|
||||
@ -117,6 +117,7 @@ public class Http2TestServerConnection {
|
||||
final Properties properties;
|
||||
volatile boolean stopping;
|
||||
volatile int nextPushStreamId = 2;
|
||||
public volatile boolean closeConnOnIncomingGoAway = true;
|
||||
ConcurrentLinkedQueue<PingRequest> pings = new ConcurrentLinkedQueue<>();
|
||||
// the max stream id of a processed H2 request. -1 implies none were processed.
|
||||
private final AtomicInteger maxProcessedRequestStreamId = new AtomicInteger(-1);
|
||||
@ -537,8 +538,12 @@ public class Http2TestServerConnection {
|
||||
outputQ.put(frame);
|
||||
return;
|
||||
} else if (f instanceof GoAwayFrame) {
|
||||
System.err.println(server.name + ": Closing connection: "+ f.toString());
|
||||
close(ErrorFrame.NO_ERROR);
|
||||
if (closeConnOnIncomingGoAway) {
|
||||
System.err.println(server.name + ": Closing connection: "+ f.toString());
|
||||
close(ErrorFrame.NO_ERROR);
|
||||
} else {
|
||||
System.err.println(server.name + ": Will not close connection for incoming GOAWAY: " + f);
|
||||
}
|
||||
} else if (f instanceof PingFrame) {
|
||||
handlePing((PingFrame)f);
|
||||
} else
|
||||
|
||||
@ -501,9 +501,15 @@ public class ConnectionPoolTest {
|
||||
@Override SocketChannel channel() {return channel;}
|
||||
@Override
|
||||
public void close() {
|
||||
closed=finished=true;
|
||||
System.out.println("closed: " + this);
|
||||
this.close(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
void close(final Throwable cause) {
|
||||
closed=finished=true;
|
||||
System.out.println("closed: " + this + " cause: " + cause);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "HttpConnectionStub: " + address + " proxy: " + proxy;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user