1198 lines
53 KiB
Java
1198 lines
53 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.ChannelPromise;
|
|
import io.netty.channel.WriteBufferWaterMark;
|
|
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 io.netty.util.concurrent.Future;
|
|
import org.junit.After;
|
|
import org.junit.Before;
|
|
import org.junit.Test;
|
|
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.UUID;
|
|
import java.util.concurrent.CompletionException;
|
|
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.assertNotEquals;
|
|
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.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;
|
|
|
|
public abstract class Http2MultiplexTest<C extends Http2FrameCodec> {
|
|
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 C codec;
|
|
|
|
private static final int initialRemoteStreamWindow = 1024;
|
|
|
|
protected abstract C newCodec(TestChannelInitializer childChannelInitializer, Http2FrameWriter frameWriter);
|
|
protected abstract ChannelHandler newMultiplexer(TestChannelInitializer childChannelInitializer);
|
|
|
|
@Before
|
|
public void setUp() {
|
|
childChannelInitializer = new TestChannelInitializer();
|
|
parentChannel = new EmbeddedChannel();
|
|
frameInboundWriter = new Http2FrameInboundWriter(parentChannel);
|
|
parentChannel.connect(new InetSocketAddress(0));
|
|
frameWriter = Http2TestUtil.mockedFrameWriter();
|
|
codec = newCodec(childChannelInitializer, frameWriter);
|
|
parentChannel.pipeline().addLast(codec);
|
|
ChannelHandler multiplexer = newMultiplexer(childChannelInitializer);
|
|
if (multiplexer != null) {
|
|
parentChannel.pipeline().addLast(multiplexer);
|
|
}
|
|
|
|
parentChannel.pipeline().fireChannelActive();
|
|
|
|
parentChannel.writeInbound(Http2CodecUtil.connectionPrefaceBuf());
|
|
|
|
Http2Settings settings = new Http2Settings().initialWindowSize(initialRemoteStreamWindow);
|
|
frameInboundWriter.writeInboundSettings(settings);
|
|
|
|
verify(frameWriter).writeSettingsAck(any(ChannelHandlerContext.class), anyChannelPromise());
|
|
|
|
frameInboundWriter.writeInboundSettingsAck();
|
|
|
|
Http2SettingsFrame settingsFrame = parentChannel.readInbound();
|
|
assertNotNull(settingsFrame);
|
|
Http2SettingsAckFrame settingsAckFrame = parentChannel.readInbound();
|
|
assertNotNull(settingsAckFrame);
|
|
|
|
// Handshake
|
|
verify(frameWriter).writeSettings(any(ChannelHandlerContext.class),
|
|
anyHttp2Settings(), anyChannelPromise());
|
|
}
|
|
|
|
@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 ChannelHandler() {
|
|
@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());
|
|
|
|
verify(frameWriter).writeFrame(any(ChannelHandlerContext.class), 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 ChannelHandler() {
|
|
@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);
|
|
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 ChannelHandler() { });
|
|
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(http2Stream -> 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 channelReadShouldRespectAutoReadAndNotProduceNPE() throws Exception {
|
|
LastInboundHandler inboundHandler = new LastInboundHandler();
|
|
Http2StreamChannel childChannel = newInboundStream(3, false, inboundHandler);
|
|
assertTrue(childChannel.config().isAutoRead());
|
|
Http2HeadersFrame headersFrame = inboundHandler.readInbound();
|
|
assertNotNull(headersFrame);
|
|
|
|
childChannel.config().setAutoRead(false);
|
|
childChannel.pipeline().addFirst(new ChannelHandler() {
|
|
private int count;
|
|
@Override
|
|
public void channelRead(ChannelHandlerContext ctx, Object msg) {
|
|
ctx.fireChannelRead(msg);
|
|
// Close channel after 2 reads so there is still something in the inboundBuffer when the close happens.
|
|
if (++count == 2) {
|
|
ctx.close();
|
|
}
|
|
}
|
|
});
|
|
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);
|
|
frameInboundWriter.writeInboundData(childChannel.stream().id(), bb("bar"), 0, false);
|
|
|
|
assertNull(inboundHandler.readInbound());
|
|
|
|
childChannel.config().setAutoRead(true);
|
|
verifyFramesMultiplexedToCorrectChannel(childChannel, inboundHandler, 3);
|
|
inboundHandler.checkException();
|
|
}
|
|
|
|
@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 ChannelHandler() {
|
|
@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) {
|
|
Future<Http2StreamChannel> future = new Http2StreamChannelBootstrap(parentChannel).handler(handler)
|
|
.open();
|
|
return future.syncUninterruptibly().getNow();
|
|
}
|
|
|
|
/**
|
|
* A child channel for an 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();
|
|
|
|
assertFalse(childChannel.isOpen());
|
|
assertFalse(childChannel.isActive());
|
|
assertNull(parentChannel.readOutbound());
|
|
}
|
|
|
|
@Test
|
|
public void outboundStreamShouldWriteResetFrameOnClose_headersSent() {
|
|
ChannelHandler handler = new ChannelHandler() {
|
|
@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(any(ChannelHandlerContext.class),
|
|
eqStreamId(childChannel), eq(Http2Error.CANCEL.code()), anyChannelPromise());
|
|
}
|
|
|
|
@Test
|
|
public void outboundStreamShouldNotWriteResetFrameOnClose_IfStreamDidntExist() {
|
|
when(frameWriter.writeHeaders(any(ChannelHandlerContext.class), anyInt(),
|
|
any(Http2Headers.class), 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(5)).setFailure(new Exception("boom"));
|
|
}
|
|
return ((ChannelPromise) invocationOnMock.getArgument(5)).setSuccess();
|
|
}
|
|
});
|
|
|
|
Http2StreamChannel childChannel = newOutboundStream(new ChannelHandler() {
|
|
@Override
|
|
public void channelActive(ChannelHandlerContext ctx) {
|
|
ctx.writeAndFlush(new DefaultHttp2HeadersFrame(new DefaultHttp2Headers()));
|
|
ctx.fireChannelActive();
|
|
}
|
|
});
|
|
|
|
assertFalse(childChannel.isActive());
|
|
|
|
childChannel.close();
|
|
// The channel was never active so we should not generate a RST frame.
|
|
verify(frameWriter, never()).writeRstStream(any(ChannelHandlerContext.class),
|
|
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(any(ChannelHandlerContext.class), 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 Throwable {
|
|
LastInboundHandler inboundHandler = new LastInboundHandler();
|
|
|
|
final Http2StreamChannel childChannel = newOutboundStream(inboundHandler);
|
|
assertTrue(childChannel.isActive());
|
|
|
|
Http2Headers headers = new DefaultHttp2Headers();
|
|
when(frameWriter.writeHeaders(any(ChannelHandlerContext.class), anyInt(),
|
|
eq(headers), anyInt(), anyBoolean(),
|
|
any(ChannelPromise.class))).thenAnswer(invocationOnMock -> {
|
|
return ((ChannelPromise) invocationOnMock.getArgument(5)).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();
|
|
|
|
try {
|
|
future.syncUninterruptibly();
|
|
} catch (CompletionException e) {
|
|
throw e.getCause();
|
|
}
|
|
}
|
|
|
|
@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();
|
|
|
|
// An active outbound stream should emit a RST_STREAM frame.
|
|
verify(frameWriter).writeRstStream(any(ChannelHandlerContext.class), 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 Throwable {
|
|
LastInboundHandler handler = new LastInboundHandler();
|
|
Http2StreamChannel childChannel = newOutboundStream(handler);
|
|
assertTrue(childChannel.isActive());
|
|
|
|
Http2Headers headers = new DefaultHttp2Headers();
|
|
when(frameWriter.writeHeaders(any(ChannelHandlerContext.class), anyInt(),
|
|
eq(headers), anyInt(), anyBoolean(), any(ChannelPromise.class))).thenAnswer(invocationOnMock -> {
|
|
return ((ChannelPromise) invocationOnMock.getArgument(5)).setFailure(
|
|
new Http2NoMoreStreamIdsException());
|
|
});
|
|
|
|
ChannelFuture future = childChannel.writeAndFlush(new DefaultHttp2HeadersFrame(headers));
|
|
parentChannel.flush();
|
|
|
|
assertFalse(childChannel.isActive());
|
|
assertFalse(childChannel.isOpen());
|
|
|
|
handler.checkException();
|
|
|
|
try {
|
|
future.syncUninterruptibly();
|
|
} catch (CompletionException e) {
|
|
throw e.getCause();
|
|
}
|
|
}
|
|
|
|
@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((ChannelFutureListener) 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((ChannelFutureListener) 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(any(ChannelHandlerContext.class), anyInt(),
|
|
eq(headers), anyInt(), anyBoolean(),
|
|
any(ChannelPromise.class))).thenAnswer(invocationOnMock -> {
|
|
ChannelPromise promise = invocationOnMock.getArgument(5);
|
|
writePromises.offer(promise);
|
|
return promise;
|
|
});
|
|
|
|
ChannelFuture f = childChannel.writeAndFlush(new DefaultHttp2HeadersFrame(headers));
|
|
assertFalse(f.isDone());
|
|
f.addListener((ChannelFutureListener) future-> {
|
|
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(UUID.randomUUID().toString());
|
|
|
|
Channel childChannel = newOutboundStream(new ChannelHandler() { });
|
|
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 ChannelHandler() { });
|
|
assertTrue(childChannel.isActive());
|
|
|
|
assertTrue(childChannel.isWritable());
|
|
childChannel.writeAndFlush(new DefaultHttp2HeadersFrame(new DefaultHttp2Headers()));
|
|
parentChannel.flush();
|
|
|
|
// Test for initial window size
|
|
assertTrue(initialRemoteStreamWindow < childChannel.config().getWriteBufferHighWaterMark());
|
|
|
|
assertTrue(childChannel.isWritable());
|
|
childChannel.write(new DefaultHttp2DataFrame(Unpooled.buffer().writeZero(16 * 1024 * 1024)));
|
|
assertEquals(0, childChannel.bytesBeforeUnwritable());
|
|
assertFalse(childChannel.isWritable());
|
|
}
|
|
|
|
@Test
|
|
public void writabilityOfParentIsRespected() {
|
|
Http2StreamChannel childChannel = newOutboundStream(new ChannelHandler() { });
|
|
childChannel.config().setWriteBufferWaterMark(new WriteBufferWaterMark(2048, 4096));
|
|
parentChannel.config().setWriteBufferWaterMark(new WriteBufferWaterMark(256, 512));
|
|
assertTrue(childChannel.isWritable());
|
|
assertTrue(parentChannel.isActive());
|
|
|
|
childChannel.writeAndFlush(new DefaultHttp2HeadersFrame(new DefaultHttp2Headers()));
|
|
parentChannel.flush();
|
|
|
|
assertTrue(childChannel.isWritable());
|
|
childChannel.write(new DefaultHttp2DataFrame(Unpooled.buffer().writeZero(256)));
|
|
assertTrue(childChannel.isWritable());
|
|
childChannel.writeAndFlush(new DefaultHttp2DataFrame(Unpooled.buffer().writeZero(512)));
|
|
|
|
long bytesBeforeUnwritable = childChannel.bytesBeforeUnwritable();
|
|
assertNotEquals(0, bytesBeforeUnwritable);
|
|
// Add something to the ChannelOutboundBuffer of the parent to simulate queuing in the parents channel buffer
|
|
// and verify that this only affect the writability of the parent channel while the child stays writable
|
|
// until it used all of its credits.
|
|
parentChannel.unsafe().outboundBuffer().addMessage(
|
|
Unpooled.buffer().writeZero(800), 800, parentChannel.voidPromise());
|
|
assertFalse(parentChannel.isWritable());
|
|
|
|
assertTrue(childChannel.isWritable());
|
|
assertEquals(4096, childChannel.bytesBeforeUnwritable());
|
|
|
|
// Flush everything which simulate writing everything to the socket.
|
|
parentChannel.flush();
|
|
assertTrue(parentChannel.isWritable());
|
|
assertTrue(childChannel.isWritable());
|
|
assertEquals(bytesBeforeUnwritable, childChannel.bytesBeforeUnwritable());
|
|
|
|
ChannelFuture future = childChannel.writeAndFlush(new DefaultHttp2DataFrame(
|
|
Unpooled.buffer().writeZero((int) bytesBeforeUnwritable)));
|
|
assertFalse(childChannel.isWritable());
|
|
assertTrue(parentChannel.isWritable());
|
|
|
|
parentChannel.flush();
|
|
assertFalse(future.isDone());
|
|
assertTrue(parentChannel.isWritable());
|
|
assertFalse(childChannel.isWritable());
|
|
|
|
// Now write an window update frame for the stream which then should ensure we will flush the bytes that were
|
|
// queued in the RemoteFlowController before for the stream.
|
|
frameInboundWriter.writeInboundWindowUpdate(childChannel.stream().id(), (int) bytesBeforeUnwritable);
|
|
assertTrue(childChannel.isWritable());
|
|
assertTrue(future.isDone());
|
|
}
|
|
|
|
@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 ChannelHandler() {
|
|
@Override
|
|
public void channelInactive(ChannelHandlerContext ctx) {
|
|
channelOpen.set(ctx.channel().isOpen());
|
|
channelActive.set(ctx.channel().isActive());
|
|
|
|
ctx.fireChannelInactive();
|
|
}
|
|
});
|
|
|
|
childChannel.close().syncUninterruptibly();
|
|
assertFalse(channelOpen.get());
|
|
assertFalse(channelActive.get());
|
|
}
|
|
|
|
@Test
|
|
public void channelInactiveHappensAfterExceptionCaughtEvents() {
|
|
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 ChannelHandler() {
|
|
|
|
@Override
|
|
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
|
|
ctx.close();
|
|
throw new Exception("exception");
|
|
}
|
|
});
|
|
|
|
childChannel.pipeline().addLast(new ChannelHandler() {
|
|
|
|
@Override
|
|
public void channelInactive(ChannelHandlerContext ctx) {
|
|
channelInactive.set(count.getAndIncrement());
|
|
ctx.fireChannelInactive();
|
|
}
|
|
|
|
@Override
|
|
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
|
|
exceptionCaught.set(count.getAndIncrement());
|
|
ctx.fireExceptionCaught(cause);
|
|
}
|
|
|
|
@Override
|
|
public void channelUnregistered(ChannelHandlerContext ctx) {
|
|
channelUnregistered.set(count.getAndIncrement());
|
|
ctx.fireChannelUnregistered();
|
|
}
|
|
});
|
|
|
|
childChannel.pipeline().fireUserEventTriggered(new Object());
|
|
|
|
// 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 = 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 ChannelHandler() {
|
|
@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());
|
|
assertNull(inboundHandler.readInbound());
|
|
|
|
// As we limited the number to 1 we also need to call read() again.
|
|
childChannel.read();
|
|
|
|
assertEqualsAndRelease(dataFrame3, inboundHandler.<Http2DataFrame>readInbound());
|
|
assertEqualsAndRelease(dataFrame4, inboundHandler.<Http2DataFrame>readInbound());
|
|
|
|
Http2ResetFrame resetFrame = useUserEventForResetFrame() ? inboundHandler.<Http2ResetFrame>readUserEvent() :
|
|
inboundHandler.<Http2ResetFrame>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();
|
|
}
|
|
|
|
protected abstract boolean useUserEventForResetFrame();
|
|
|
|
protected abstract boolean ignoreWindowUpdateFrames();
|
|
|
|
@Test
|
|
public void windowUpdateFrames() {
|
|
AtomicInteger numReads = new AtomicInteger(1);
|
|
LastInboundHandler inboundHandler = new LastInboundHandler();
|
|
Http2StreamChannel childChannel = newInboundStream(3, false, numReads, inboundHandler);
|
|
|
|
assertEquals(new DefaultHttp2HeadersFrame(request).stream(childChannel.stream()), inboundHandler.readInbound());
|
|
|
|
frameInboundWriter.writeInboundWindowUpdate(childChannel.stream().id(), 4);
|
|
|
|
Http2WindowUpdateFrame updateFrame = inboundHandler.readInbound();
|
|
if (ignoreWindowUpdateFrames()) {
|
|
assertNull(updateFrame);
|
|
} else {
|
|
assertEquals(new DefaultHttp2WindowUpdateFrame(4).stream(childChannel.stream()), updateFrame);
|
|
}
|
|
|
|
frameInboundWriter.writeInboundWindowUpdate(Http2CodecUtil.CONNECTION_STREAM_ID, 6);
|
|
|
|
assertNull(parentChannel.readInbound());
|
|
childChannel.close().syncUninterruptibly();
|
|
}
|
|
|
|
@Test
|
|
public void childQueueIsDrainedAndNewDataIsDispatchedInParentReadLoopAutoRead() {
|
|
AtomicInteger numReads = new AtomicInteger(1);
|
|
final AtomicInteger channelReadCompleteCount = new AtomicInteger(0);
|
|
final AtomicBoolean shouldDisableAutoRead = new AtomicBoolean();
|
|
Consumer<ChannelHandlerContext> ctxConsumer = 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 ChannelHandler() {
|
|
@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 = 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 ChannelHandler() {
|
|
@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.<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());
|
|
}
|
|
|
|
@Test
|
|
public void useReadWithoutAutoReadInRead() {
|
|
useReadWithoutAutoReadBuffered(false);
|
|
}
|
|
|
|
@Test
|
|
public void useReadWithoutAutoReadInReadComplete() {
|
|
useReadWithoutAutoReadBuffered(true);
|
|
}
|
|
|
|
private void useReadWithoutAutoReadBuffered(final boolean triggerOnReadComplete) {
|
|
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);
|
|
|
|
// Write some bytes to get the channel into the idle state with buffered data and also verify we
|
|
// do not dispatch it until we receive a read() call.
|
|
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);
|
|
|
|
// Add a handler which will request reads.
|
|
childChannel.pipeline().addFirst(new ChannelHandler() {
|
|
|
|
@Override
|
|
public void channelReadComplete(ChannelHandlerContext ctx) {
|
|
ctx.fireChannelReadComplete();
|
|
if (triggerOnReadComplete) {
|
|
ctx.read();
|
|
ctx.read();
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void channelRead(ChannelHandlerContext ctx, Object msg) {
|
|
ctx.fireChannelRead(msg);
|
|
if (!triggerOnReadComplete) {
|
|
ctx.read();
|
|
ctx.read();
|
|
}
|
|
}
|
|
});
|
|
|
|
inboundHandler.channel().read();
|
|
|
|
verifyFramesMultiplexedToCorrectChannel(childChannel, inboundHandler, 3);
|
|
|
|
frameInboundWriter.writeInboundData(childChannel.stream().id(), bb("hello world2"), 0, false);
|
|
frameInboundWriter.writeInboundData(childChannel.stream().id(), bb("foo2"), 0, false);
|
|
frameInboundWriter.writeInboundData(childChannel.stream().id(), bb("bar2"), 0, true);
|
|
|
|
verifyFramesMultiplexedToCorrectChannel(childChannel, inboundHandler, 3);
|
|
}
|
|
|
|
private static final class FlushSniffer implements ChannelHandler {
|
|
|
|
private boolean didFlush;
|
|
|
|
public boolean checkFlush() {
|
|
boolean r = didFlush;
|
|
didFlush = false;
|
|
return r;
|
|
}
|
|
|
|
@Override
|
|
public void flush(ChannelHandlerContext ctx) throws Exception {
|
|
didFlush = true;
|
|
ctx.flush();
|
|
}
|
|
}
|
|
|
|
@Test
|
|
public void windowUpdatesAreFlushed() {
|
|
LastInboundHandler inboundHandler = new LastInboundHandler();
|
|
FlushSniffer flushSniffer = new FlushSniffer();
|
|
parentChannel.pipeline().addFirst(flushSniffer);
|
|
|
|
Http2StreamChannel childChannel = newInboundStream(3, false, inboundHandler);
|
|
assertTrue(childChannel.config().isAutoRead());
|
|
childChannel.config().setAutoRead(false);
|
|
assertFalse(childChannel.config().isAutoRead());
|
|
|
|
Http2HeadersFrame headersFrame = inboundHandler.readInbound();
|
|
assertNotNull(headersFrame);
|
|
|
|
assertTrue(flushSniffer.checkFlush());
|
|
|
|
// Write some bytes to get the channel into the idle state with buffered data and also verify we
|
|
// do not dispatch it until we receive a read() call.
|
|
frameInboundWriter.writeInboundData(childChannel.stream().id(), bb(16 * 1024), 0, false);
|
|
frameInboundWriter.writeInboundData(childChannel.stream().id(), bb(16 * 1024), 0, false);
|
|
assertTrue(flushSniffer.checkFlush());
|
|
|
|
verify(frameWriter, never())
|
|
.writeWindowUpdate(any(ChannelHandlerContext.class), anyInt(), anyInt(), anyChannelPromise());
|
|
// only the first one was read because it was legacy auto-read behavior.
|
|
verifyFramesMultiplexedToCorrectChannel(childChannel, inboundHandler, 1);
|
|
assertFalse(flushSniffer.checkFlush());
|
|
|
|
// Trigger a read of the second frame.
|
|
childChannel.read();
|
|
verifyFramesMultiplexedToCorrectChannel(childChannel, inboundHandler, 1);
|
|
// We expect a flush here because the StreamChannel will flush the smaller increment but the
|
|
// connection will collect the bytes and decide not to send a wire level frame until more are consumed.
|
|
assertTrue(flushSniffer.checkFlush());
|
|
verify(frameWriter, never())
|
|
.writeWindowUpdate(any(ChannelHandlerContext.class), anyInt(), anyInt(), anyChannelPromise());
|
|
|
|
// Call read one more time which should trigger the writing of the flow control update.
|
|
childChannel.read();
|
|
verify(frameWriter)
|
|
.writeWindowUpdate(any(ChannelHandlerContext.class), eq(0), eq(32 * 1024), anyChannelPromise());
|
|
verify(frameWriter)
|
|
.writeWindowUpdate(any(ChannelHandlerContext.class), eq(childChannel.stream().id()),
|
|
eq(32 * 1024), anyChannelPromise());
|
|
assertTrue(flushSniffer.checkFlush());
|
|
}
|
|
|
|
private static void verifyFramesMultiplexedToCorrectChannel(Http2StreamChannel streamChannel,
|
|
LastInboundHandler inboundHandler,
|
|
int numFrames) {
|
|
for (int i = 0; i < numFrames; i++) {
|
|
Http2StreamFrame frame = inboundHandler.readInbound();
|
|
assertNotNull(i + " out of " + numFrames + " received", frame);
|
|
assertEquals(streamChannel.stream(), frame.stream());
|
|
release(frame);
|
|
}
|
|
assertNull(inboundHandler.readInbound());
|
|
}
|
|
|
|
private static int eqStreamId(Http2StreamChannel channel) {
|
|
return eq(channel.stream().id());
|
|
}
|
|
}
|