netty5/codec-http2/src/test/java/io/netty/handler/codec/http2/Http2MultiplexCodecTest.java
Scott Mitchell b3dba317d7
HTTP/2 to support asynchronous SETTINGS ACK (#9069)
Motivation:
The HTTP/2 codec will synchronously respond to a SETTINGS frame with a SETTINGS
ACK before the application sees the SETTINGS frame. The application may need to
adjust its state depending upon what is in the SETTINGS frame before applying
the remote settings and responding with an ACK (e.g. to adjust for max
concurrent streams). In order to accomplish this the HTTP/2 codec should allow
for the application to opt-in to sending the SETTINGS ACK.

Modifications:
- DefaultHttp2ConnectionDecoder should support a mode where SETTINGS frames can
  be queued instead of immediately applying and ACKing.
- DefaultHttp2ConnectionEncoder should attempt to poll from the queue (if it
  exists) to apply the earliest received but not yet ACKed SETTINGS frame.
- AbstractHttp2ConnectionHandlerBuilder (and sub classes) should support a new
  option to enable the application to opt-in to managing SETTINGS ACK.

Result:
HTTP/2 allows for asynchronous SETTINGS ACK managed by the application.
2019-04-25 15:52:05 -07:00

1009 lines
44 KiB
Java

/*
* Copyright 2016 The Netty Project
*
* The Netty Project licenses this file to you under the Apache License, version 2.0 (the
* "License"); you may not use this file except in compliance with the License. You may obtain a
* copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package io.netty.handler.codec.http2;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.channel.ChannelPromise;
import io.netty.channel.embedded.EmbeddedChannel;
import io.netty.handler.codec.http.HttpMethod;
import io.netty.handler.codec.http.HttpScheme;
import io.netty.handler.codec.http2.Http2Exception.StreamException;
import io.netty.handler.codec.http2.LastInboundHandler.Consumer;
import io.netty.util.AsciiString;
import io.netty.util.AttributeKey;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.mockito.ArgumentMatcher;
import org.mockito.Mockito;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
import java.net.InetSocketAddress;
import java.nio.channels.ClosedChannelException;
import java.util.ArrayDeque;
import java.util.Queue;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import static io.netty.handler.codec.http2.Http2TestUtil.anyChannelPromise;
import static io.netty.handler.codec.http2.Http2TestUtil.anyHttp2Settings;
import static io.netty.handler.codec.http2.Http2TestUtil.assertEqualsAndRelease;
import static io.netty.handler.codec.http2.Http2TestUtil.bb;
import static io.netty.util.ReferenceCountUtil.release;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.anyShort;
import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
/**
* Unit tests for {@link Http2MultiplexCodec}.
*/
public class Http2MultiplexCodecTest {
private final Http2Headers request = new DefaultHttp2Headers()
.method(HttpMethod.GET.asciiName()).scheme(HttpScheme.HTTPS.name())
.authority(new AsciiString("example.org")).path(new AsciiString("/foo"));
private EmbeddedChannel parentChannel;
private Http2FrameWriter frameWriter;
private Http2FrameInboundWriter frameInboundWriter;
private TestChannelInitializer childChannelInitializer;
private Http2MultiplexCodec codec;
private static final int initialRemoteStreamWindow = 1024;
@Before
public void setUp() {
childChannelInitializer = new TestChannelInitializer();
parentChannel = new EmbeddedChannel();
frameInboundWriter = new Http2FrameInboundWriter(parentChannel);
parentChannel.connect(new InetSocketAddress(0));
frameWriter = Http2TestUtil.mockedFrameWriter();
codec = new Http2MultiplexCodecBuilder(true, childChannelInitializer).frameWriter(frameWriter).build();
parentChannel.pipeline().addLast(codec);
parentChannel.runPendingTasks();
parentChannel.pipeline().fireChannelActive();
parentChannel.writeInbound(Http2CodecUtil.connectionPrefaceBuf());
Http2Settings settings = new Http2Settings().initialWindowSize(initialRemoteStreamWindow);
frameInboundWriter.writeInboundSettings(settings);
verify(frameWriter).writeSettingsAck(eqMultiplexCodecCtx(), anyChannelPromise());
frameInboundWriter.writeInboundSettingsAck();
Http2SettingsFrame settingsFrame = parentChannel.readInbound();
assertNotNull(settingsFrame);
Http2SettingsAckFrame settingsAckFrame = parentChannel.readInbound();
assertNotNull(settingsAckFrame);
// Handshake
verify(frameWriter).writeSettings(eqMultiplexCodecCtx(),
anyHttp2Settings(), anyChannelPromise());
}
private ChannelHandlerContext eqMultiplexCodecCtx() {
return eq(codec.ctx);
}
@After
public void tearDown() throws Exception {
if (childChannelInitializer.handler instanceof LastInboundHandler) {
((LastInboundHandler) childChannelInitializer.handler).finishAndReleaseAll();
}
parentChannel.finishAndReleaseAll();
codec = null;
}
// TODO(buchgr): Flush from child channel
// TODO(buchgr): ChildChannel.childReadComplete()
// TODO(buchgr): GOAWAY Logic
// TODO(buchgr): Test ChannelConfig.setMaxMessagesPerRead
@Test
public void writeUnknownFrame() {
Http2StreamChannel childChannel = newOutboundStream(new ChannelInboundHandlerAdapter() {
@Override
public void channelActive(ChannelHandlerContext ctx) {
ctx.writeAndFlush(new DefaultHttp2HeadersFrame(new DefaultHttp2Headers()));
ctx.writeAndFlush(new DefaultHttp2UnknownFrame((byte) 99, new Http2Flags()));
ctx.fireChannelActive();
}
});
assertTrue(childChannel.isActive());
parentChannel.runPendingTasks();
verify(frameWriter).writeFrame(eq(codec.ctx), eq((byte) 99), eqStreamId(childChannel), any(Http2Flags.class),
any(ByteBuf.class), any(ChannelPromise.class));
}
private Http2StreamChannel newInboundStream(int streamId, boolean endStream, final ChannelHandler childHandler) {
return newInboundStream(streamId, endStream, null, childHandler);
}
private Http2StreamChannel newInboundStream(int streamId, boolean endStream,
AtomicInteger maxReads, final ChannelHandler childHandler) {
final AtomicReference<Http2StreamChannel> streamChannelRef = new AtomicReference<Http2StreamChannel>();
childChannelInitializer.maxReads = maxReads;
childChannelInitializer.handler = new ChannelInboundHandlerAdapter() {
@Override
public void channelRegistered(ChannelHandlerContext ctx) {
assertNull(streamChannelRef.get());
streamChannelRef.set((Http2StreamChannel) ctx.channel());
ctx.pipeline().addLast(childHandler);
ctx.fireChannelRegistered();
}
};
frameInboundWriter.writeInboundHeaders(streamId, request, 0, endStream);
parentChannel.runPendingTasks();
Http2StreamChannel channel = streamChannelRef.get();
assertEquals(streamId, channel.stream().id());
return channel;
}
@Test
public void readUnkownFrame() {
LastInboundHandler handler = new LastInboundHandler();
Http2StreamChannel channel = newInboundStream(3, true, handler);
frameInboundWriter.writeInboundFrame((byte) 99, channel.stream().id(), new Http2Flags(), Unpooled.EMPTY_BUFFER);
// header frame and unknown frame
verifyFramesMultiplexedToCorrectChannel(channel, handler, 2);
Channel childChannel = newOutboundStream(new ChannelInboundHandlerAdapter());
assertTrue(childChannel.isActive());
}
@Test
public void headerAndDataFramesShouldBeDelivered() {
LastInboundHandler inboundHandler = new LastInboundHandler();
Http2StreamChannel channel = newInboundStream(3, false, inboundHandler);
Http2HeadersFrame headersFrame = new DefaultHttp2HeadersFrame(request).stream(channel.stream());
Http2DataFrame dataFrame1 = new DefaultHttp2DataFrame(bb("hello")).stream(channel.stream());
Http2DataFrame dataFrame2 = new DefaultHttp2DataFrame(bb("world")).stream(channel.stream());
assertTrue(inboundHandler.isChannelActive());
frameInboundWriter.writeInboundData(channel.stream().id(), bb("hello"), 0, false);
frameInboundWriter.writeInboundData(channel.stream().id(), bb("world"), 0, false);
assertEquals(headersFrame, inboundHandler.readInbound());
assertEqualsAndRelease(dataFrame1, inboundHandler.<Http2Frame>readInbound());
assertEqualsAndRelease(dataFrame2, inboundHandler.<Http2Frame>readInbound());
assertNull(inboundHandler.readInbound());
}
@Test
public void framesShouldBeMultiplexed() {
LastInboundHandler handler1 = new LastInboundHandler();
Http2StreamChannel channel1 = newInboundStream(3, false, handler1);
LastInboundHandler handler2 = new LastInboundHandler();
Http2StreamChannel channel2 = newInboundStream(5, false, handler2);
LastInboundHandler handler3 = new LastInboundHandler();
Http2StreamChannel channel3 = newInboundStream(11, false, handler3);
verifyFramesMultiplexedToCorrectChannel(channel1, handler1, 1);
verifyFramesMultiplexedToCorrectChannel(channel2, handler2, 1);
verifyFramesMultiplexedToCorrectChannel(channel3, handler3, 1);
frameInboundWriter.writeInboundData(channel2.stream().id(), bb("hello"), 0, false);
frameInboundWriter.writeInboundData(channel1.stream().id(), bb("foo"), 0, true);
frameInboundWriter.writeInboundData(channel2.stream().id(), bb("world"), 0, true);
frameInboundWriter.writeInboundData(channel3.stream().id(), bb("bar"), 0, true);
verifyFramesMultiplexedToCorrectChannel(channel1, handler1, 1);
verifyFramesMultiplexedToCorrectChannel(channel2, handler2, 2);
verifyFramesMultiplexedToCorrectChannel(channel3, handler3, 1);
}
@Test
public void inboundDataFrameShouldUpdateLocalFlowController() throws Http2Exception {
Http2LocalFlowController flowController = Mockito.mock(Http2LocalFlowController.class);
codec.connection().local().flowController(flowController);
LastInboundHandler handler = new LastInboundHandler();
final Http2StreamChannel channel = newInboundStream(3, false, handler);
ByteBuf tenBytes = bb("0123456789");
frameInboundWriter.writeInboundData(channel.stream().id(), tenBytes, 0, true);
// Verify we marked the bytes as consumed
verify(flowController).consumeBytes(argThat(new ArgumentMatcher<Http2Stream>() {
@Override
public boolean matches(Http2Stream http2Stream) {
return http2Stream.id() == channel.stream().id();
}
}), eq(10));
// headers and data frame
verifyFramesMultiplexedToCorrectChannel(channel, handler, 2);
}
@Test
public void unhandledHttp2FramesShouldBePropagated() {
Http2PingFrame pingFrame = new DefaultHttp2PingFrame(0);
frameInboundWriter.writeInboundPing(false, 0);
assertEquals(parentChannel.readInbound(), pingFrame);
DefaultHttp2GoAwayFrame goAwayFrame = new DefaultHttp2GoAwayFrame(1,
parentChannel.alloc().buffer().writeLong(8));
frameInboundWriter.writeInboundGoAway(0, goAwayFrame.errorCode(), goAwayFrame.content().retainedDuplicate());
Http2GoAwayFrame frame = parentChannel.readInbound();
assertEqualsAndRelease(frame, goAwayFrame);
}
@Test
public void channelReadShouldRespectAutoRead() {
LastInboundHandler inboundHandler = new LastInboundHandler();
Http2StreamChannel childChannel = newInboundStream(3, false, inboundHandler);
assertTrue(childChannel.config().isAutoRead());
Http2HeadersFrame headersFrame = inboundHandler.readInbound();
assertNotNull(headersFrame);
childChannel.config().setAutoRead(false);
frameInboundWriter.writeInboundData(childChannel.stream().id(), bb("hello world"), 0, false);
Http2DataFrame dataFrame0 = inboundHandler.readInbound();
assertNotNull(dataFrame0);
release(dataFrame0);
frameInboundWriter.writeInboundData(childChannel.stream().id(), bb("foo"), 0, false);
frameInboundWriter.writeInboundData(childChannel.stream().id(), bb("bar"), 0, false);
assertNull(inboundHandler.readInbound());
childChannel.config().setAutoRead(true);
verifyFramesMultiplexedToCorrectChannel(childChannel, inboundHandler, 2);
}
@Test
public void readInChannelReadWithoutAutoRead() {
useReadWithoutAutoRead(false);
}
@Test
public void readInChannelReadCompleteWithoutAutoRead() {
useReadWithoutAutoRead(true);
}
private void useReadWithoutAutoRead(final boolean readComplete) {
LastInboundHandler inboundHandler = new LastInboundHandler();
Http2StreamChannel childChannel = newInboundStream(3, false, inboundHandler);
assertTrue(childChannel.config().isAutoRead());
childChannel.config().setAutoRead(false);
assertFalse(childChannel.config().isAutoRead());
Http2HeadersFrame headersFrame = inboundHandler.readInbound();
assertNotNull(headersFrame);
// Add a handler which will request reads.
childChannel.pipeline().addFirst(new ChannelInboundHandlerAdapter() {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
ctx.fireChannelRead(msg);
if (!readComplete) {
ctx.read();
}
}
@Override
public void channelReadComplete(ChannelHandlerContext ctx) {
ctx.fireChannelReadComplete();
if (readComplete) {
ctx.read();
}
}
});
frameInboundWriter.writeInboundData(childChannel.stream().id(), bb("hello world"), 0, false);
frameInboundWriter.writeInboundData(childChannel.stream().id(), bb("foo"), 0, false);
frameInboundWriter.writeInboundData(childChannel.stream().id(), bb("bar"), 0, false);
frameInboundWriter.writeInboundData(childChannel.stream().id(), bb("hello world"), 0, false);
frameInboundWriter.writeInboundData(childChannel.stream().id(), bb("foo"), 0, false);
frameInboundWriter.writeInboundData(childChannel.stream().id(), bb("bar"), 0, true);
verifyFramesMultiplexedToCorrectChannel(childChannel, inboundHandler, 6);
}
private Http2StreamChannel newOutboundStream(ChannelHandler handler) {
return new Http2StreamChannelBootstrap(parentChannel).handler(handler)
.open().syncUninterruptibly().getNow();
}
/**
* A child channel for a HTTP/2 stream in IDLE state (that is no headers sent or received),
* should not emit a RST_STREAM frame on close, as this is a connection error of type protocol error.
*/
@Test
public void idleOutboundStreamShouldNotWriteResetFrameOnClose() {
LastInboundHandler handler = new LastInboundHandler();
Channel childChannel = newOutboundStream(handler);
assertTrue(childChannel.isActive());
childChannel.close();
parentChannel.runPendingTasks();
assertFalse(childChannel.isOpen());
assertFalse(childChannel.isActive());
assertNull(parentChannel.readOutbound());
}
@Test
public void outboundStreamShouldWriteResetFrameOnClose_headersSent() {
ChannelHandler handler = new ChannelInboundHandlerAdapter() {
@Override
public void channelActive(ChannelHandlerContext ctx) {
ctx.writeAndFlush(new DefaultHttp2HeadersFrame(new DefaultHttp2Headers()));
ctx.fireChannelActive();
}
};
Http2StreamChannel childChannel = newOutboundStream(handler);
assertTrue(childChannel.isActive());
childChannel.close();
verify(frameWriter).writeRstStream(eqMultiplexCodecCtx(),
eqStreamId(childChannel), eq(Http2Error.CANCEL.code()), anyChannelPromise());
}
@Test
public void outboundStreamShouldNotWriteResetFrameOnClose_IfStreamDidntExist() {
when(frameWriter.writeHeaders(eqMultiplexCodecCtx(), anyInt(),
any(Http2Headers.class), anyInt(), anyShort(), anyBoolean(), anyInt(), anyBoolean(),
any(ChannelPromise.class))).thenAnswer(new Answer<ChannelFuture>() {
private boolean headersWritten;
@Override
public ChannelFuture answer(InvocationOnMock invocationOnMock) {
// We want to fail to write the first headers frame. This is what happens if the connection
// refuses to allocate a new stream due to having received a GOAWAY.
if (!headersWritten) {
headersWritten = true;
return ((ChannelPromise) invocationOnMock.getArgument(8)).setFailure(new Exception("boom"));
}
return ((ChannelPromise) invocationOnMock.getArgument(8)).setSuccess();
}
});
Http2StreamChannel childChannel = newOutboundStream(new ChannelInboundHandlerAdapter() {
@Override
public void channelActive(ChannelHandlerContext ctx) {
ctx.writeAndFlush(new DefaultHttp2HeadersFrame(new DefaultHttp2Headers()));
ctx.fireChannelActive();
}
});
assertFalse(childChannel.isActive());
childChannel.close();
parentChannel.runPendingTasks();
// The channel was never active so we should not generate a RST frame.
verify(frameWriter, never()).writeRstStream(eqMultiplexCodecCtx(), eqStreamId(childChannel), anyLong(),
anyChannelPromise());
assertTrue(parentChannel.outboundMessages().isEmpty());
}
@Test
public void inboundRstStreamFireChannelInactive() {
LastInboundHandler inboundHandler = new LastInboundHandler();
Http2StreamChannel channel = newInboundStream(3, false, inboundHandler);
assertTrue(inboundHandler.isChannelActive());
frameInboundWriter.writeInboundRstStream(channel.stream().id(), Http2Error.INTERNAL_ERROR.code());
assertFalse(inboundHandler.isChannelActive());
// A RST_STREAM frame should NOT be emitted, as we received a RST_STREAM.
verify(frameWriter, Mockito.never()).writeRstStream(eqMultiplexCodecCtx(), eqStreamId(channel),
anyLong(), anyChannelPromise());
}
@Test(expected = StreamException.class)
public void streamExceptionTriggersChildChannelExceptionAndClose() throws Exception {
LastInboundHandler inboundHandler = new LastInboundHandler();
Http2StreamChannel channel = newInboundStream(3, false, inboundHandler);
assertTrue(channel.isActive());
StreamException cause = new StreamException(channel.stream().id(), Http2Error.PROTOCOL_ERROR, "baaam!");
parentChannel.pipeline().fireExceptionCaught(cause);
assertFalse(channel.isActive());
inboundHandler.checkException();
}
@Test(expected = ClosedChannelException.class)
public void streamClosedErrorTranslatedToClosedChannelExceptionOnWrites() throws Exception {
LastInboundHandler inboundHandler = new LastInboundHandler();
final Http2StreamChannel childChannel = newOutboundStream(inboundHandler);
assertTrue(childChannel.isActive());
Http2Headers headers = new DefaultHttp2Headers();
when(frameWriter.writeHeaders(eqMultiplexCodecCtx(), anyInt(),
eq(headers), anyInt(), anyShort(), anyBoolean(), anyInt(), anyBoolean(),
any(ChannelPromise.class))).thenAnswer(new Answer<ChannelFuture>() {
@Override
public ChannelFuture answer(InvocationOnMock invocationOnMock) {
return ((ChannelPromise) invocationOnMock.getArgument(8)).setFailure(
new StreamException(childChannel.stream().id(), Http2Error.STREAM_CLOSED, "Stream Closed"));
}
});
ChannelFuture future = childChannel.writeAndFlush(new DefaultHttp2HeadersFrame(new DefaultHttp2Headers()));
parentChannel.flush();
assertFalse(childChannel.isActive());
assertFalse(childChannel.isOpen());
inboundHandler.checkException();
future.syncUninterruptibly();
}
@Test
public void creatingWritingReadingAndClosingOutboundStreamShouldWork() {
LastInboundHandler inboundHandler = new LastInboundHandler();
Http2StreamChannel childChannel = newOutboundStream(inboundHandler);
assertTrue(childChannel.isActive());
assertTrue(inboundHandler.isChannelActive());
// Write to the child channel
Http2Headers headers = new DefaultHttp2Headers().scheme("https").method("GET").path("/foo.txt");
childChannel.writeAndFlush(new DefaultHttp2HeadersFrame(headers));
// Read from the child channel
frameInboundWriter.writeInboundHeaders(childChannel.stream().id(), headers, 0, false);
Http2HeadersFrame headersFrame = inboundHandler.readInbound();
assertNotNull(headersFrame);
assertEquals(headers, headersFrame.headers());
// Close the child channel.
childChannel.close();
parentChannel.runPendingTasks();
// An active outbound stream should emit a RST_STREAM frame.
verify(frameWriter).writeRstStream(eqMultiplexCodecCtx(), eqStreamId(childChannel),
anyLong(), anyChannelPromise());
assertFalse(childChannel.isOpen());
assertFalse(childChannel.isActive());
assertFalse(inboundHandler.isChannelActive());
}
// Test failing the promise of the first headers frame of an outbound stream. In practice this error case would most
// likely happen due to the max concurrent streams limit being hit or the channel running out of stream identifiers.
//
@Test(expected = Http2NoMoreStreamIdsException.class)
public void failedOutboundStreamCreationThrowsAndClosesChannel() throws Exception {
LastInboundHandler handler = new LastInboundHandler();
Http2StreamChannel childChannel = newOutboundStream(handler);
assertTrue(childChannel.isActive());
Http2Headers headers = new DefaultHttp2Headers();
when(frameWriter.writeHeaders(eqMultiplexCodecCtx(), anyInt(),
eq(headers), anyInt(), anyShort(), anyBoolean(), anyInt(), anyBoolean(),
any(ChannelPromise.class))).thenAnswer(new Answer<ChannelFuture>() {
@Override
public ChannelFuture answer(InvocationOnMock invocationOnMock) {
return ((ChannelPromise) invocationOnMock.getArgument(8)).setFailure(
new Http2NoMoreStreamIdsException());
}
});
ChannelFuture future = childChannel.writeAndFlush(new DefaultHttp2HeadersFrame(headers));
parentChannel.flush();
assertFalse(childChannel.isActive());
assertFalse(childChannel.isOpen());
handler.checkException();
future.syncUninterruptibly();
}
@Test
public void channelClosedWhenCloseListenerCompletes() {
LastInboundHandler inboundHandler = new LastInboundHandler();
Http2StreamChannel childChannel = newInboundStream(3, false, inboundHandler);
assertTrue(childChannel.isOpen());
assertTrue(childChannel.isActive());
final AtomicBoolean channelOpen = new AtomicBoolean(true);
final AtomicBoolean channelActive = new AtomicBoolean(true);
// Create a promise before actually doing the close, because otherwise we would be adding a listener to a future
// that is already completed because we are using EmbeddedChannel which executes code in the JUnit thread.
ChannelPromise p = childChannel.newPromise();
p.addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture future) {
channelOpen.set(future.channel().isOpen());
channelActive.set(future.channel().isActive());
}
});
childChannel.close(p).syncUninterruptibly();
assertFalse(channelOpen.get());
assertFalse(channelActive.get());
assertFalse(childChannel.isActive());
}
@Test
public void channelClosedWhenChannelClosePromiseCompletes() {
LastInboundHandler inboundHandler = new LastInboundHandler();
Http2StreamChannel childChannel = newInboundStream(3, false, inboundHandler);
assertTrue(childChannel.isOpen());
assertTrue(childChannel.isActive());
final AtomicBoolean channelOpen = new AtomicBoolean(true);
final AtomicBoolean channelActive = new AtomicBoolean(true);
childChannel.closeFuture().addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture future) {
channelOpen.set(future.channel().isOpen());
channelActive.set(future.channel().isActive());
}
});
childChannel.close().syncUninterruptibly();
assertFalse(channelOpen.get());
assertFalse(channelActive.get());
assertFalse(childChannel.isActive());
}
@Test
public void channelClosedWhenWriteFutureFails() {
final Queue<ChannelPromise> writePromises = new ArrayDeque<ChannelPromise>();
LastInboundHandler inboundHandler = new LastInboundHandler();
Http2StreamChannel childChannel = newInboundStream(3, false, inboundHandler);
assertTrue(childChannel.isOpen());
assertTrue(childChannel.isActive());
final AtomicBoolean channelOpen = new AtomicBoolean(true);
final AtomicBoolean channelActive = new AtomicBoolean(true);
Http2Headers headers = new DefaultHttp2Headers();
when(frameWriter.writeHeaders(eqMultiplexCodecCtx(), anyInt(),
eq(headers), anyInt(), anyShort(), anyBoolean(), anyInt(), anyBoolean(),
any(ChannelPromise.class))).thenAnswer(new Answer<ChannelFuture>() {
@Override
public ChannelFuture answer(InvocationOnMock invocationOnMock) {
ChannelPromise promise = invocationOnMock.getArgument(8);
writePromises.offer(promise);
return promise;
}
});
ChannelFuture f = childChannel.writeAndFlush(new DefaultHttp2HeadersFrame(headers));
assertFalse(f.isDone());
f.addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture future) throws Exception {
channelOpen.set(future.channel().isOpen());
channelActive.set(future.channel().isActive());
}
});
ChannelPromise first = writePromises.poll();
first.setFailure(new ClosedChannelException());
f.awaitUninterruptibly();
assertFalse(channelOpen.get());
assertFalse(channelActive.get());
assertFalse(childChannel.isActive());
}
@Test
public void channelClosedTwiceMarksPromiseAsSuccessful() {
LastInboundHandler inboundHandler = new LastInboundHandler();
Http2StreamChannel childChannel = newInboundStream(3, false, inboundHandler);
assertTrue(childChannel.isOpen());
assertTrue(childChannel.isActive());
childChannel.close().syncUninterruptibly();
childChannel.close().syncUninterruptibly();
assertFalse(childChannel.isOpen());
assertFalse(childChannel.isActive());
}
@Test
public void settingChannelOptsAndAttrs() {
AttributeKey<String> key = AttributeKey.newInstance("foo");
Channel childChannel = newOutboundStream(new ChannelInboundHandlerAdapter());
childChannel.config().setAutoRead(false).setWriteSpinCount(1000);
childChannel.attr(key).set("bar");
assertFalse(childChannel.config().isAutoRead());
assertEquals(1000, childChannel.config().getWriteSpinCount());
assertEquals("bar", childChannel.attr(key).get());
}
@Test
public void outboundFlowControlWritability() {
Http2StreamChannel childChannel = newOutboundStream(new ChannelInboundHandlerAdapter());
assertTrue(childChannel.isActive());
assertTrue(childChannel.isWritable());
childChannel.writeAndFlush(new DefaultHttp2HeadersFrame(new DefaultHttp2Headers()));
parentChannel.flush();
// Test for initial window size
assertEquals(initialRemoteStreamWindow, childChannel.config().getWriteBufferHighWaterMark());
assertTrue(childChannel.isWritable());
childChannel.write(new DefaultHttp2DataFrame(Unpooled.buffer().writeZero(16 * 1024 * 1024)));
assertFalse(childChannel.isWritable());
}
@Test
public void writabilityAndFlowControl() {
LastInboundHandler inboundHandler = new LastInboundHandler();
Http2StreamChannel childChannel = newInboundStream(3, false, inboundHandler);
assertEquals("", inboundHandler.writabilityStates());
assertTrue(childChannel.isWritable());
// HEADERS frames are not flow controlled, so they should not affect the flow control window.
childChannel.writeAndFlush(new DefaultHttp2HeadersFrame(new DefaultHttp2Headers()));
codec.onHttp2StreamWritabilityChanged(codec.ctx, childChannel.stream(), true);
assertTrue(childChannel.isWritable());
assertEquals("", inboundHandler.writabilityStates());
codec.onHttp2StreamWritabilityChanged(codec.ctx, childChannel.stream(), true);
assertTrue(childChannel.isWritable());
assertEquals("", inboundHandler.writabilityStates());
codec.onHttp2StreamWritabilityChanged(codec.ctx, childChannel.stream(), false);
assertFalse(childChannel.isWritable());
assertEquals("false", inboundHandler.writabilityStates());
codec.onHttp2StreamWritabilityChanged(codec.ctx, childChannel.stream(), false);
assertFalse(childChannel.isWritable());
assertEquals("false", inboundHandler.writabilityStates());
}
@Test
public void channelClosedWhenInactiveFired() {
LastInboundHandler inboundHandler = new LastInboundHandler();
Http2StreamChannel childChannel = newInboundStream(3, false, inboundHandler);
final AtomicBoolean channelOpen = new AtomicBoolean(false);
final AtomicBoolean channelActive = new AtomicBoolean(false);
assertTrue(childChannel.isOpen());
assertTrue(childChannel.isActive());
childChannel.pipeline().addLast(new ChannelInboundHandlerAdapter() {
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
channelOpen.set(ctx.channel().isOpen());
channelActive.set(ctx.channel().isActive());
super.channelInactive(ctx);
}
});
childChannel.close().syncUninterruptibly();
assertFalse(channelOpen.get());
assertFalse(channelActive.get());
}
@Test
public void channelInactiveHappensAfterExceptionCaughtEvents() throws Exception {
final AtomicInteger count = new AtomicInteger(0);
final AtomicInteger exceptionCaught = new AtomicInteger(-1);
final AtomicInteger channelInactive = new AtomicInteger(-1);
final AtomicInteger channelUnregistered = new AtomicInteger(-1);
Http2StreamChannel childChannel = newOutboundStream(new ChannelInboundHandlerAdapter() {
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
ctx.close();
throw new Exception("exception");
}
});
childChannel.pipeline().addLast(new ChannelInboundHandlerAdapter() {
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
channelInactive.set(count.getAndIncrement());
super.channelInactive(ctx);
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
exceptionCaught.set(count.getAndIncrement());
super.exceptionCaught(ctx, cause);
}
@Override
public void channelUnregistered(ChannelHandlerContext ctx) throws Exception {
channelUnregistered.set(count.getAndIncrement());
super.channelUnregistered(ctx);
}
});
childChannel.pipeline().fireUserEventTriggered(new Object());
parentChannel.runPendingTasks();
// The events should have happened in this order because the inactive and deregistration events
// get deferred as they do in the AbstractChannel.
assertEquals(0, exceptionCaught.get());
assertEquals(1, channelInactive.get());
assertEquals(2, channelUnregistered.get());
}
@Test
public void callUnsafeCloseMultipleTimes() {
LastInboundHandler inboundHandler = new LastInboundHandler();
Http2StreamChannel childChannel = newInboundStream(3, false, inboundHandler);
childChannel.unsafe().close(childChannel.voidPromise());
ChannelPromise promise = childChannel.newPromise();
childChannel.unsafe().close(promise);
promise.syncUninterruptibly();
childChannel.closeFuture().syncUninterruptibly();
}
@Test
public void endOfStreamDoesNotDiscardData() {
AtomicInteger numReads = new AtomicInteger(1);
final AtomicBoolean shouldDisableAutoRead = new AtomicBoolean();
Consumer<ChannelHandlerContext> ctxConsumer = new Consumer<ChannelHandlerContext>() {
@Override
public void accept(ChannelHandlerContext obj) {
if (shouldDisableAutoRead.get()) {
obj.channel().config().setAutoRead(false);
}
}
};
LastInboundHandler inboundHandler = new LastInboundHandler(ctxConsumer);
Http2StreamChannel childChannel = newInboundStream(3, false, numReads, inboundHandler);
childChannel.config().setAutoRead(false);
Http2DataFrame dataFrame1 = new DefaultHttp2DataFrame(bb("1")).stream(childChannel.stream());
Http2DataFrame dataFrame2 = new DefaultHttp2DataFrame(bb("2")).stream(childChannel.stream());
Http2DataFrame dataFrame3 = new DefaultHttp2DataFrame(bb("3")).stream(childChannel.stream());
Http2DataFrame dataFrame4 = new DefaultHttp2DataFrame(bb("4")).stream(childChannel.stream());
assertEquals(new DefaultHttp2HeadersFrame(request).stream(childChannel.stream()), inboundHandler.readInbound());
ChannelHandler readCompleteSupressHandler = new ChannelInboundHandlerAdapter() {
@Override
public void channelReadComplete(ChannelHandlerContext ctx) {
// We want to simulate the parent channel calling channelRead and delay calling channelReadComplete.
}
};
parentChannel.pipeline().addFirst(readCompleteSupressHandler);
frameInboundWriter.writeInboundData(childChannel.stream().id(), bb("1"), 0, false);
assertEqualsAndRelease(dataFrame1, inboundHandler.<Http2DataFrame>readInbound());
// Deliver frames, and then a stream closed while read is inactive.
frameInboundWriter.writeInboundData(childChannel.stream().id(), bb("2"), 0, false);
frameInboundWriter.writeInboundData(childChannel.stream().id(), bb("3"), 0, false);
frameInboundWriter.writeInboundData(childChannel.stream().id(), bb("4"), 0, false);
shouldDisableAutoRead.set(true);
childChannel.config().setAutoRead(true);
numReads.set(1);
frameInboundWriter.writeInboundRstStream(childChannel.stream().id(), Http2Error.NO_ERROR.code());
// Detecting EOS should flush all pending data regardless of read calls.
assertEqualsAndRelease(dataFrame2, inboundHandler.<Http2DataFrame>readInbound());
assertEqualsAndRelease(dataFrame3, inboundHandler.<Http2DataFrame>readInbound());
assertEqualsAndRelease(dataFrame4, inboundHandler.<Http2DataFrame>readInbound());
Http2ResetFrame resetFrame = inboundHandler.readInbound();
assertEquals(childChannel.stream(), resetFrame.stream());
assertEquals(Http2Error.NO_ERROR.code(), resetFrame.errorCode());
assertNull(inboundHandler.readInbound());
// Now we want to call channelReadComplete and simulate the end of the read loop.
parentChannel.pipeline().remove(readCompleteSupressHandler);
parentChannel.flushInbound();
childChannel.closeFuture().syncUninterruptibly();
}
@Test
public void childQueueIsDrainedAndNewDataIsDispatchedInParentReadLoopAutoRead() {
AtomicInteger numReads = new AtomicInteger(1);
final AtomicInteger channelReadCompleteCount = new AtomicInteger(0);
final AtomicBoolean shouldDisableAutoRead = new AtomicBoolean();
Consumer<ChannelHandlerContext> ctxConsumer = new Consumer<ChannelHandlerContext>() {
@Override
public void accept(ChannelHandlerContext obj) {
channelReadCompleteCount.incrementAndGet();
if (shouldDisableAutoRead.get()) {
obj.channel().config().setAutoRead(false);
}
}
};
LastInboundHandler inboundHandler = new LastInboundHandler(ctxConsumer);
Http2StreamChannel childChannel = newInboundStream(3, false, numReads, inboundHandler);
childChannel.config().setAutoRead(false);
Http2DataFrame dataFrame1 = new DefaultHttp2DataFrame(bb("1")).stream(childChannel.stream());
Http2DataFrame dataFrame2 = new DefaultHttp2DataFrame(bb("2")).stream(childChannel.stream());
Http2DataFrame dataFrame3 = new DefaultHttp2DataFrame(bb("3")).stream(childChannel.stream());
Http2DataFrame dataFrame4 = new DefaultHttp2DataFrame(bb("4")).stream(childChannel.stream());
assertEquals(new DefaultHttp2HeadersFrame(request).stream(childChannel.stream()), inboundHandler.readInbound());
ChannelHandler readCompleteSupressHandler = new ChannelInboundHandlerAdapter() {
@Override
public void channelReadComplete(ChannelHandlerContext ctx) {
// We want to simulate the parent channel calling channelRead and delay calling channelReadComplete.
}
};
parentChannel.pipeline().addFirst(readCompleteSupressHandler);
frameInboundWriter.writeInboundData(childChannel.stream().id(), bb("1"), 0, false);
assertEqualsAndRelease(dataFrame1, inboundHandler.<Http2DataFrame>readInbound());
// We want one item to be in the queue, and allow the numReads to be larger than 1. This will ensure that
// when beginRead() is called the child channel is added to the readPending queue of the parent channel.
frameInboundWriter.writeInboundData(childChannel.stream().id(), bb("2"), 0, false);
numReads.set(10);
shouldDisableAutoRead.set(true);
childChannel.config().setAutoRead(true);
frameInboundWriter.writeInboundData(childChannel.stream().id(), bb("3"), 0, false);
frameInboundWriter.writeInboundData(childChannel.stream().id(), bb("4"), 0, false);
// Detecting EOS should flush all pending data regardless of read calls.
assertEqualsAndRelease(dataFrame2, inboundHandler.<Http2DataFrame>readInbound());
assertEqualsAndRelease(dataFrame3, inboundHandler.<Http2DataFrame>readInbound());
assertEqualsAndRelease(dataFrame4, inboundHandler.<Http2DataFrame>readInbound());
assertNull(inboundHandler.readInbound());
// Now we want to call channelReadComplete and simulate the end of the read loop.
parentChannel.pipeline().remove(readCompleteSupressHandler);
parentChannel.flushInbound();
// 3 = 1 for initialization + 1 for read when auto read was off + 1 for when auto read was back on
assertEquals(3, channelReadCompleteCount.get());
}
@Test
public void childQueueIsDrainedAndNewDataIsDispatchedInParentReadLoopNoAutoRead() {
final AtomicInteger numReads = new AtomicInteger(1);
final AtomicInteger channelReadCompleteCount = new AtomicInteger(0);
final AtomicBoolean shouldDisableAutoRead = new AtomicBoolean();
Consumer<ChannelHandlerContext> ctxConsumer = new Consumer<ChannelHandlerContext>() {
@Override
public void accept(ChannelHandlerContext obj) {
channelReadCompleteCount.incrementAndGet();
if (shouldDisableAutoRead.get()) {
obj.channel().config().setAutoRead(false);
}
}
};
final LastInboundHandler inboundHandler = new LastInboundHandler(ctxConsumer);
Http2StreamChannel childChannel = newInboundStream(3, false, numReads, inboundHandler);
childChannel.config().setAutoRead(false);
Http2DataFrame dataFrame1 = new DefaultHttp2DataFrame(bb("1")).stream(childChannel.stream());
Http2DataFrame dataFrame2 = new DefaultHttp2DataFrame(bb("2")).stream(childChannel.stream());
Http2DataFrame dataFrame3 = new DefaultHttp2DataFrame(bb("3")).stream(childChannel.stream());
Http2DataFrame dataFrame4 = new DefaultHttp2DataFrame(bb("4")).stream(childChannel.stream());
assertEquals(new DefaultHttp2HeadersFrame(request).stream(childChannel.stream()), inboundHandler.readInbound());
ChannelHandler readCompleteSupressHandler = new ChannelInboundHandlerAdapter() {
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
// We want to simulate the parent channel calling channelRead and delay calling channelReadComplete.
}
};
parentChannel.pipeline().addFirst(readCompleteSupressHandler);
frameInboundWriter.writeInboundData(childChannel.stream().id(), bb("1"), 0, false);
assertEqualsAndRelease(dataFrame1, inboundHandler.<Http2Frame>readInbound());
// We want one item to be in the queue, and allow the numReads to be larger than 1. This will ensure that
// when beginRead() is called the child channel is added to the readPending queue of the parent channel.
frameInboundWriter.writeInboundData(childChannel.stream().id(), bb("2"), 0, false);
numReads.set(2);
childChannel.read();
assertEqualsAndRelease(dataFrame2, inboundHandler.<Http2Frame>readInbound());
assertNull(inboundHandler.readInbound());
// This is the second item that was read, this should be the last until we call read() again. This should also
// notify of readComplete().
frameInboundWriter.writeInboundData(childChannel.stream().id(), bb("3"), 0, false);
assertEqualsAndRelease(dataFrame3, inboundHandler.<Http2Frame>readInbound());
frameInboundWriter.writeInboundData(childChannel.stream().id(), bb("4"), 0, false);
assertNull(inboundHandler.readInbound());
childChannel.read();
assertEqualsAndRelease(dataFrame4, inboundHandler.<Http2Frame>readInbound());
assertNull(inboundHandler.readInbound());
// Now we want to call channelReadComplete and simulate the end of the read loop.
parentChannel.pipeline().remove(readCompleteSupressHandler);
parentChannel.flushInbound();
// 3 = 1 for initialization + 1 for first read of 2 items + 1 for second read of 2 items +
// 1 for parent channel readComplete
assertEquals(4, channelReadCompleteCount.get());
}
private static void verifyFramesMultiplexedToCorrectChannel(Http2StreamChannel streamChannel,
LastInboundHandler inboundHandler,
int numFrames) {
for (int i = 0; i < numFrames; i++) {
Http2StreamFrame frame = inboundHandler.readInbound();
assertNotNull(frame);
assertEquals(streamChannel.stream(), frame.stream());
release(frame);
}
assertNull(inboundHandler.readInbound());
}
private static int eqStreamId(Http2StreamChannel channel) {
return eq(channel.stream().id());
}
}