Make handling of GoAwayClosedStreamException more consistent.

This commit is contained in:
nmittler 2016-01-20 15:11:50 -08:00
parent 484c46c62b
commit 70ef5b1172
5 changed files with 70 additions and 38 deletions

View File

@ -35,6 +35,7 @@ import static io.netty.handler.codec.http2.Http2Error.PROTOCOL_ERROR;
import static io.netty.handler.codec.http2.Http2Exception.connectionError;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufUtil;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelPromise;
@ -217,7 +218,8 @@ class BufferingHttp2ConnectionEncoder extends DecoratingHttp2ConnectionEncoder {
private void cancelGoAwayStreams(int lastStreamId, long errorCode, ByteBuf debugData) {
Iterator<PendingStream> iter = pendingStreams.values().iterator();
Exception e = new GoAwayClosedStreamException(lastStreamId, errorCode, debugData);
Exception e = new GoAwayClosedStreamException(lastStreamId, errorCode,
ByteBufUtil.getBytes(debugData));
while (iter.hasNext()) {
PendingStream stream = iter.next();
if (stream.streamId > lastStreamId) {

View File

@ -31,15 +31,13 @@
package io.grpc.netty;
import io.netty.buffer.ByteBuf;
class GoAwayClosedStreamException extends Exception {
private static final long serialVersionUID = 1326785622777291198L;
private final int lastStreamId;
private final long errorCode;
private final ByteBuf debugData;
private final byte[] debugData;
GoAwayClosedStreamException(int lastStreamId, long errorCode, ByteBuf debugData) {
GoAwayClosedStreamException(int lastStreamId, long errorCode, byte[] debugData) {
this.lastStreamId = lastStreamId;
this.errorCode = errorCode;
this.debugData = debugData;
@ -53,7 +51,7 @@ class GoAwayClosedStreamException extends Exception {
return errorCode;
}
ByteBuf debugData() {
byte[] debugData() {
return debugData;
}
}

View File

@ -47,6 +47,7 @@ import io.grpc.internal.ClientTransport.PingCallback;
import io.grpc.internal.GrpcUtil;
import io.grpc.internal.Http2Ping;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufUtil;
import io.netty.buffer.Unpooled;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
@ -111,6 +112,7 @@ class NettyClientHandler extends AbstractNettyHandler {
private WriteQueue clientWriteQueue;
private Http2Ping ping;
private Status goAwayStatus;
private Throwable goAwayStatusThrowable;
private int nextStreamId;
static NettyClientHandler newHandler(ClientTransport.Listener listener,
@ -186,7 +188,7 @@ class NettyClientHandler extends AbstractNettyHandler {
connection.addListener(new Http2ConnectionAdapter() {
@Override
public void onGoAwayReceived(int lastStreamId, long errorCode, ByteBuf debugData) {
goAwayStatus(statusFromGoAway(errorCode, debugData));
goAwayStatus(statusFromGoAway(errorCode, ByteBufUtil.getBytes(debugData)));
goingAway();
}
});
@ -350,7 +352,17 @@ class NettyClientHandler extends AbstractNettyHandler {
final NettyClientStream stream = command.stream();
final Http2Headers headers = command.headers();
stream.id(streamId);
encoder().writeHeaders(ctx(), streamId, headers, 0, false, promise)
if (goAwayStatus != null) {
// The connection is going away, just terminate the stream now.
promise.setFailure(goAwayStatusThrowable);
return;
}
// Create an intermediate promise so that we can intercept the failure reported back to the
// application.
ChannelPromise tempPromise = ctx().newPromise();
encoder().writeHeaders(ctx(), streamId, headers, 0, false, tempPromise)
.addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture future) throws Exception {
@ -360,25 +372,25 @@ class NettyClientHandler extends AbstractNettyHandler {
Http2Stream http2Stream = connection().stream(streamId);
if (http2Stream != null) {
http2Stream.setProperty(streamKey, stream);
} else if (stream.isClosed()) {
// The stream has been cancelled and Netty is sending a RST_STREAM frame which
// causes it to purge pending writes from the flow-controller and delete the
// http2Stream. The stream listener has already been notified of cancellation
// so there is nothing to do.
return;
} else {
throw new IllegalStateException("Stream closed but http2 stream not defined");
// Attach the client stream to the HTTP/2 stream object as user data.
stream.setHttp2Stream(http2Stream);
}
// Attach the client stream to the HTTP/2 stream object as user data.
stream.setHttp2Stream(http2Stream);
// Otherwise, the stream has been cancelled and Netty is sending a
// RST_STREAM frame which causes it to purge pending writes from the
// flow-controller and delete the http2Stream. The stream listener has already
// been notified of cancellation so there is nothing to do.
// Just forward on the success status to the original promise.
promise.setSuccess();
} else {
if (future.cause() instanceof GoAwayClosedStreamException) {
GoAwayClosedStreamException e = (GoAwayClosedStreamException) future.cause();
final Throwable cause = future.cause();
if (cause instanceof GoAwayClosedStreamException) {
GoAwayClosedStreamException e = (GoAwayClosedStreamException) cause;
goAwayStatus(statusFromGoAway(e.errorCode(), e.debugData()));
stream.transportReportStatus(goAwayStatus, false, new Metadata());
promise.setFailure(goAwayStatusThrowable);
} else {
stream.transportReportStatus(Status.fromThrowable(future.cause()), true,
new Metadata());
promise.setFailure(cause);
}
}
}
@ -486,7 +498,11 @@ class NettyClientHandler extends AbstractNettyHandler {
}
private void goAwayStatus(Status status) {
goAwayStatus = goAwayStatus == null ? status : goAwayStatus;
// Don't overwrite if we already have a goAwayStatus.
if (goAwayStatus == null) {
goAwayStatus = status;
goAwayStatusThrowable = status.asException();
}
}
private void cancelPing() {
@ -496,11 +512,11 @@ class NettyClientHandler extends AbstractNettyHandler {
}
}
private Status statusFromGoAway(long errorCode, ByteBuf debugData) {
private Status statusFromGoAway(long errorCode, byte[] debugData) {
Status status = GrpcUtil.Http2Error.statusForCode((int) errorCode);
if (debugData.isReadable()) {
if (debugData != null && debugData.length > 0) {
// If a debug message was provided, use it.
String msg = debugData.toString(UTF_8);
String msg = new String(debugData, UTF_8);
status = status.augmentDescription(msg);
}
return status;

View File

@ -34,6 +34,7 @@ package io.grpc.netty;
import static io.grpc.internal.GrpcUtil.Http2Error.CANCEL;
import static io.netty.handler.codec.http2.Http2CodecUtil.DEFAULT_PRIORITY_WEIGHT;
import static io.netty.handler.codec.http2.Http2Stream.State.HALF_CLOSED_LOCAL;
import static io.netty.util.CharsetUtil.UTF_8;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
@ -83,6 +84,7 @@ import java.util.List;
*/
@RunWith(JUnit4.class)
public class BufferingHttp2ConnectionEncoderTest {
private static final byte[] DEBUG_DATA = "test exception".getBytes(UTF_8);
private BufferingHttp2ConnectionEncoder encoder;
@ -197,7 +199,7 @@ public class BufferingHttp2ConnectionEncoderTest {
public void bufferingNewStreamFailsAfterGoAwayReceived() {
encoder.writeSettingsAck(ctx, newPromise());
connection.local().maxActiveStreams(0);
connection.goAwayReceived(1, 8, null);
connection.goAwayReceived(1, 8, Unpooled.wrappedBuffer(DEBUG_DATA));
ChannelFuture future = encoderWriteHeaders(3);
assertEquals(0, encoder.numBufferedStreams());
@ -221,7 +223,7 @@ public class BufferingHttp2ConnectionEncoderTest {
flush();
assertEquals(4, encoder.numBufferedStreams());
connection.goAwayReceived(11, 8, null);
connection.goAwayReceived(11, 8, Unpooled.wrappedBuffer(DEBUG_DATA));
assertEquals(5, connection.numActiveStreams());
// The 4 buffered streams must have been failed.

View File

@ -61,6 +61,7 @@ import io.grpc.Status;
import io.grpc.StatusException;
import io.grpc.internal.ClientTransport;
import io.grpc.internal.ClientTransport.PingCallback;
import io.grpc.internal.GrpcUtil;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelFuture;
@ -264,8 +265,8 @@ public class NettyClientHandlerTest extends NettyHandlerTestBase<NettyClientHand
channelRead(goAwayFrame(0));
assertTrue(future.isDone());
assertFalse(future.isSuccess());
verify(stream).transportReportStatus(any(Status.class), eq(false),
notNull(Metadata.class));
Status status = Status.fromThrowable(future.cause());
assertEquals(GrpcUtil.Http2Error.NO_ERROR.status(), status);
}
@Test
@ -286,16 +287,29 @@ public class NettyClientHandlerTest extends NettyHandlerTestBase<NettyClientHand
public void receivedGoAwayShouldFailUnknownBufferedStreams() throws Exception {
receiveMaxConcurrentStreams(0);
enqueue(new CreateStreamCommand(grpcHeaders, stream));
ChannelFuture future = enqueue(new CreateStreamCommand(grpcHeaders, stream));
// Read a GOAWAY that indicates our stream was never processed by the server.
channelRead(goAwayFrame(0, 8 /* Cancel */, Unpooled.copiedBuffer("this is a test", UTF_8)));
ArgumentCaptor<Status> captor = ArgumentCaptor.forClass(Status.class);
verify(stream).transportReportStatus(captor.capture(), eq(false),
notNull(Metadata.class));
assertEquals(Status.CANCELLED.getCode(), captor.getValue().getCode());
assertEquals("HTTP/2 error code: CANCEL\nthis is a test",
captor.getValue().getDescription());
assertTrue(future.isDone());
assertFalse(future.isSuccess());
Status status = Status.fromThrowable(future.cause());
assertEquals(Status.CANCELLED.getCode(), status.getCode());
assertEquals("HTTP/2 error code: CANCEL\nthis is a test", status.getDescription());
}
@Test
public void receivedGoAwayShouldFailNewStreams() throws Exception {
// Read a GOAWAY that indicates our stream was never processed by the server.
channelRead(goAwayFrame(0, 8 /* Cancel */, Unpooled.copiedBuffer("this is a test", UTF_8)));
// Now try to create a stream.
ChannelFuture future = enqueue(new CreateStreamCommand(grpcHeaders, stream));
assertTrue(future.isDone());
assertFalse(future.isSuccess());
Status status = Status.fromThrowable(future.cause());
assertEquals(Status.CANCELLED.getCode(), status.getCode());
assertEquals("HTTP/2 error code: CANCEL\nthis is a test", status.getDescription());
}
@Test