-
-
Notifications
You must be signed in to change notification settings - Fork 15.9k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Http2ConnectionHandler to allow decoupling close(..) from GOAWAY graceful close #9094
Changes from all commits
971b458
01281c6
2ffd428
ec2ce58
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -76,15 +76,22 @@ public class Http2ConnectionHandler extends ByteToMessageDecoder implements Http | |
private final Http2ConnectionDecoder decoder; | ||
private final Http2ConnectionEncoder encoder; | ||
private final Http2Settings initialSettings; | ||
private final boolean decoupleCloseAndGoAway; | ||
private ChannelFutureListener closeListener; | ||
private BaseDecoder byteDecoder; | ||
private long gracefulShutdownTimeoutMillis; | ||
|
||
protected Http2ConnectionHandler(Http2ConnectionDecoder decoder, Http2ConnectionEncoder encoder, | ||
Http2Settings initialSettings) { | ||
this(decoder, encoder, initialSettings, false); | ||
} | ||
|
||
protected Http2ConnectionHandler(Http2ConnectionDecoder decoder, Http2ConnectionEncoder encoder, | ||
Http2Settings initialSettings, boolean decoupleCloseAndGoAway) { | ||
this.initialSettings = checkNotNull(initialSettings, "initialSettings"); | ||
this.decoder = checkNotNull(decoder, "decoder"); | ||
this.encoder = checkNotNull(encoder, "encoder"); | ||
this.decoupleCloseAndGoAway = decoupleCloseAndGoAway; | ||
if (encoder.connection() != decoder.connection()) { | ||
throw new IllegalArgumentException("Encoder and Decoder do not share the same connection object"); | ||
} | ||
|
@@ -449,6 +456,10 @@ public void disconnect(ChannelHandlerContext ctx, ChannelPromise promise) throws | |
|
||
@Override | ||
public void close(ChannelHandlerContext ctx, ChannelPromise promise) throws Exception { | ||
if (decoupleCloseAndGoAway) { | ||
ctx.close(promise); | ||
return; | ||
} | ||
promise = promise.unvoid(); | ||
// Avoid NotYetConnectedException | ||
if (!ctx.channel().isActive()) { | ||
|
@@ -461,22 +472,36 @@ public void close(ChannelHandlerContext ctx, ChannelPromise promise) throws Exce | |
// a GO_AWAY has been sent we send a empty buffer just so we can wait to close until all other data has been | ||
// flushed to the OS. | ||
// https://github.com/netty/netty/issues/5307 | ||
final ChannelFuture future = connection().goAwaySent() ? ctx.write(EMPTY_BUFFER) : goAway(ctx, null); | ||
ChannelFuture f = connection().goAwaySent() ? ctx.write(EMPTY_BUFFER) : goAway(ctx, null, ctx.newPromise()); | ||
ctx.flush(); | ||
doGracefulShutdown(ctx, future, promise); | ||
doGracefulShutdown(ctx, f, promise); | ||
} | ||
|
||
private void doGracefulShutdown(ChannelHandlerContext ctx, ChannelFuture future, ChannelPromise promise) { | ||
private void doGracefulShutdown(ChannelHandlerContext ctx, ChannelFuture future, final ChannelPromise promise) { | ||
if (isGracefulShutdownComplete()) { | ||
// If there are no active streams, close immediately after the GO_AWAY write completes. | ||
future.addListener(new ClosingChannelFutureListener(ctx, promise)); | ||
} else { | ||
// If there are active streams we should wait until they are all closed before closing the connection. | ||
if (gracefulShutdownTimeoutMillis < 0) { | ||
closeListener = new ClosingChannelFutureListener(ctx, promise); | ||
} else { | ||
closeListener = new ClosingChannelFutureListener(ctx, promise, | ||
gracefulShutdownTimeoutMillis, MILLISECONDS); | ||
final ClosingChannelFutureListener tmp = gracefulShutdownTimeoutMillis < 0 ? | ||
new ClosingChannelFutureListener(ctx, promise) : | ||
new ClosingChannelFutureListener(ctx, promise, gracefulShutdownTimeoutMillis, MILLISECONDS); | ||
// The ClosingChannelFutureListener will cascade promise completion. We need to always notify the | ||
// new ClosingChannelFutureListener when the graceful close completes if the promise is not null. | ||
if (closeListener == null) { | ||
closeListener = tmp; | ||
} else if (promise != null) { | ||
final ChannelFutureListener oldCloseListener = closeListener; | ||
closeListener = new ChannelFutureListener() { | ||
@Override | ||
public void operationComplete(ChannelFuture future) throws Exception { | ||
try { | ||
oldCloseListener.operationComplete(future); | ||
} finally { | ||
tmp.operationComplete(future); | ||
} | ||
} | ||
}; | ||
} | ||
} | ||
} | ||
|
@@ -636,14 +661,11 @@ protected void onConnectionError(ChannelHandlerContext ctx, boolean outbound, | |
} | ||
|
||
ChannelPromise promise = ctx.newPromise(); | ||
ChannelFuture future = goAway(ctx, http2Ex); | ||
switch (http2Ex.shutdownHint()) { | ||
case GRACEFUL_SHUTDOWN: | ||
ChannelFuture future = goAway(ctx, http2Ex, ctx.newPromise()); | ||
if (http2Ex.shutdownHint() == Http2Exception.ShutdownHint.GRACEFUL_SHUTDOWN) { | ||
doGracefulShutdown(ctx, future, promise); | ||
break; | ||
default: | ||
} else { | ||
future.addListener(new ClosingChannelFutureListener(ctx, promise)); | ||
break; | ||
} | ||
} | ||
|
||
|
@@ -814,6 +836,12 @@ public void operationComplete(ChannelFuture future) throws Exception { | |
} | ||
}); | ||
} | ||
// if closeListener != null this means we have already initiated graceful closure. doGracefulShutdown will apply | ||
// the gracefulShutdownTimeoutMillis on each invocation, however we only care to apply the timeout on the | ||
// start of graceful shutdown. | ||
if (errorCode == NO_ERROR.code() && closeListener == null) { | ||
doGracefulShutdown(ctx, future, null); | ||
} | ||
|
||
return future; | ||
} | ||
|
@@ -842,10 +870,10 @@ private void checkCloseConnection(ChannelFuture future) { | |
* Close the remote endpoint with with a {@code GO_AWAY} frame. Does <strong>not</strong> flush | ||
* immediately, this is the responsibility of the caller. | ||
*/ | ||
private ChannelFuture goAway(ChannelHandlerContext ctx, Http2Exception cause) { | ||
private ChannelFuture goAway(ChannelHandlerContext ctx, Http2Exception cause, ChannelPromise promise) { | ||
long errorCode = cause != null ? cause.error().code() : NO_ERROR.code(); | ||
int lastKnownStream = connection().remote().lastStreamCreated(); | ||
return goAway(ctx, lastKnownStream, errorCode, Http2CodecUtil.toByteBuf(ctx, cause), ctx.newPromise()); | ||
return goAway(ctx, lastKnownStream, errorCode, Http2CodecUtil.toByteBuf(ctx, cause), promise); | ||
} | ||
|
||
private void processRstStreamWriteResult(ChannelHandlerContext ctx, Http2Stream stream, ChannelFuture future) { | ||
|
@@ -917,17 +945,25 @@ private static final class ClosingChannelFutureListener implements ChannelFuture | |
timeoutTask = ctx.executor().schedule(new Runnable() { | ||
@Override | ||
public void run() { | ||
ctx.close(promise); | ||
doClose(); | ||
} | ||
}, timeout, unit); | ||
} | ||
|
||
@Override | ||
public void operationComplete(ChannelFuture sentGoAwayFuture) throws Exception { | ||
public void operationComplete(ChannelFuture sentGoAwayFuture) { | ||
if (timeoutTask != null) { | ||
timeoutTask.cancel(false); | ||
} | ||
ctx.close(promise); | ||
doClose(); | ||
} | ||
|
||
private void doClose() { | ||
if (promise == null) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why can promise now be null? I don't see why this is done. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oh, I see it now, on line 844. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yip this class (e.g. |
||
ctx.close(); | ||
} else { | ||
ctx.close(promise); | ||
} | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To confirm I understand: this is a bug fix that was just noticed when messing with this PR. It isn't necessary for the new feature (any more than previously).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A
null
promise was possible but wasn't likely (e.g. would have to come from the pipeline, or from the ChanntHandlerContext promise factory). However now we are explicitly passingnull
when we don't care about being notified when the operation completes (e.g. after we write aGOAWAY
we don't really care about knowing when graceful close completes).