2019-06-24 09:17:15 +02:00
|
|
|
/*
|
|
|
|
* 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:
|
|
|
|
*
|
2020-10-23 14:44:18 +02:00
|
|
|
* https://www.apache.org/licenses/LICENSE-2.0
|
2019-06-24 09:17:15 +02:00
|
|
|
*
|
|
|
|
* 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.ChannelHandler;
|
|
|
|
import io.netty.channel.ChannelHandlerContext;
|
|
|
|
import io.netty.channel.WriteBufferWaterMark;
|
|
|
|
import io.netty.channel.embedded.EmbeddedChannel;
|
2021-03-12 11:34:08 +01:00
|
|
|
import io.netty.handler.codec.http.HttpHeaderNames;
|
2019-06-24 09:17:15 +02:00
|
|
|
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.util.AsciiString;
|
|
|
|
import io.netty.util.AttributeKey;
|
2019-09-04 12:00:06 +02:00
|
|
|
import io.netty.util.concurrent.Future;
|
2021-08-30 13:15:14 +02:00
|
|
|
import io.netty.util.concurrent.ImmediateEventExecutor;
|
Clean up Future/Promises API (#11575)
Motivation:
The generics for the existing futures, promises, and listeners are too complicated.
This complication comes from the existence of `ChannelPromise` and `ChannelFuture`, which forces listeners to care about the particular _type_ of future being listened on.
Modification:
* Add a `FutureContextListener` which can take a context object as an additional argument. This allows our listeners to have the channel piped through to them, so they don't need to rely on the `ChannelFuture.channel()` method.
* Make the `FutureListener`, along with the `FutureContextListener` sibling, the default listener API, retiring the `GenericFutureListener` since we no longer need to abstract over the type of the future.
* Change all uses of `ChannelPromise` to `Promise<Void>`.
* Change all uses of `ChannelFuture` to `Future<Void>`.
* Change all uses of `GenericFutureListener` to either `FutureListener` or `FutureContextListener` as needed.
* Remove `ChannelFutureListener` and `GenericFutureListener`.
* Introduce a `ChannelFutureListeners` enum to house the constants that previously lived in `ChannelFutureListener`. These constants now implement `FutureContextListener` and take the `Channel` as a context.
* Remove `ChannelPromise` and `ChannelFuture` — all usages now rely on the plain `Future` and `Promise` APIs.
* Add static factory methods to `DefaultPromise` that allow us to create promises that are initialised as successful or failed.
* Remove `CompleteFuture`, `SucceededFuture`, `FailedFuture`, `CompleteChannelFuture`, `SucceededChannelFuture`, and `FailedChannelFuture`.
* Remove `ChannelPromiseNotifier`.
Result:
Cleaner generics and more straight forward code.
2021-08-20 09:55:16 +02:00
|
|
|
import io.netty.util.concurrent.Promise;
|
2021-03-12 11:34:08 +01:00
|
|
|
import org.hamcrest.CoreMatchers;
|
2021-06-30 10:32:01 +02:00
|
|
|
import org.junit.jupiter.api.AfterEach;
|
|
|
|
import org.junit.jupiter.api.BeforeEach;
|
|
|
|
import org.junit.jupiter.api.Test;
|
|
|
|
import org.junit.jupiter.api.function.Executable;
|
2019-06-24 09:17:15 +02:00
|
|
|
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;
|
2020-08-31 16:42:58 +02:00
|
|
|
import java.util.function.Consumer;
|
2019-06-24 09:17:15 +02:00
|
|
|
|
|
|
|
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;
|
2021-06-30 10:32:01 +02:00
|
|
|
import static org.hamcrest.MatcherAssert.assertThat;
|
|
|
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
|
|
|
import static org.junit.jupiter.api.Assertions.assertFalse;
|
|
|
|
import static org.junit.jupiter.api.Assertions.assertNotEquals;
|
|
|
|
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
|
|
|
import static org.junit.jupiter.api.Assertions.assertNull;
|
|
|
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
|
|
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
2019-06-24 09:17:15 +02:00
|
|
|
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);
|
|
|
|
|
2021-06-30 10:32:01 +02:00
|
|
|
@BeforeEach
|
2019-06-24 09:17:15 +02:00
|
|
|
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);
|
|
|
|
|
2021-08-30 13:15:14 +02:00
|
|
|
verify(frameWriter).writeSettingsAck(any(ChannelHandlerContext.class));
|
2019-06-24 09:17:15 +02:00
|
|
|
|
|
|
|
frameInboundWriter.writeInboundSettingsAck();
|
|
|
|
|
|
|
|
Http2SettingsFrame settingsFrame = parentChannel.readInbound();
|
|
|
|
assertNotNull(settingsFrame);
|
|
|
|
Http2SettingsAckFrame settingsAckFrame = parentChannel.readInbound();
|
|
|
|
assertNotNull(settingsAckFrame);
|
|
|
|
|
|
|
|
// Handshake
|
2019-12-16 21:00:32 +01:00
|
|
|
verify(frameWriter).writeSettings(any(ChannelHandlerContext.class),
|
2021-08-30 13:15:14 +02:00
|
|
|
anyHttp2Settings());
|
2019-06-24 09:17:15 +02:00
|
|
|
}
|
|
|
|
|
2021-06-30 10:32:01 +02:00
|
|
|
@AfterEach
|
2019-06-24 09:17:15 +02:00
|
|
|
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() {
|
2020-01-21 02:47:17 +01:00
|
|
|
Http2StreamChannel childChannel = newOutboundStream(new ChannelHandler() {
|
2019-06-24 09:17:15 +02:00
|
|
|
@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());
|
|
|
|
|
2019-12-16 21:00:32 +01:00
|
|
|
verify(frameWriter).writeFrame(any(ChannelHandlerContext.class), eq((byte) 99), eqStreamId(childChannel),
|
2021-08-30 13:15:14 +02:00
|
|
|
any(Http2Flags.class), any(ByteBuf.class));
|
2019-06-24 09:17:15 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
2020-01-21 02:47:17 +01:00
|
|
|
Channel childChannel = newOutboundStream(new ChannelHandler() { });
|
2019-06-24 09:17:15 +02:00
|
|
|
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());
|
|
|
|
|
2020-08-31 16:42:58 +02:00
|
|
|
assertEqualsAndRelease(dataFrame1, inboundHandler.readInbound());
|
|
|
|
assertEqualsAndRelease(dataFrame2, inboundHandler.readInbound());
|
2019-06-24 09:17:15 +02:00
|
|
|
|
|
|
|
assertNull(inboundHandler.readInbound());
|
|
|
|
}
|
|
|
|
|
2021-03-12 11:34:08 +01:00
|
|
|
@Test
|
|
|
|
public void headerMultipleContentLengthValidationShouldPropagate() {
|
2021-03-30 09:40:47 +02:00
|
|
|
headerMultipleContentLengthValidationShouldPropagate(false);
|
|
|
|
}
|
|
|
|
|
|
|
|
@Test
|
|
|
|
public void headerMultipleContentLengthValidationShouldPropagateWithEndStream() {
|
|
|
|
headerMultipleContentLengthValidationShouldPropagate(true);
|
|
|
|
}
|
|
|
|
|
|
|
|
private void headerMultipleContentLengthValidationShouldPropagate(boolean endStream) {
|
2021-06-30 10:32:01 +02:00
|
|
|
final LastInboundHandler inboundHandler = new LastInboundHandler();
|
2021-03-12 11:34:08 +01:00
|
|
|
request.addLong(HttpHeaderNames.CONTENT_LENGTH, 0);
|
|
|
|
request.addLong(HttpHeaderNames.CONTENT_LENGTH, 1);
|
2021-03-30 09:40:47 +02:00
|
|
|
Http2StreamChannel channel = newInboundStream(3, endStream, inboundHandler);
|
2021-06-30 10:32:01 +02:00
|
|
|
|
|
|
|
assertThrows(StreamException.class, new Executable() {
|
|
|
|
@Override
|
|
|
|
public void execute() throws Throwable {
|
|
|
|
inboundHandler.checkException();
|
|
|
|
}
|
|
|
|
});
|
2021-03-30 09:40:47 +02:00
|
|
|
assertNull(inboundHandler.readInbound());
|
|
|
|
assertFalse(channel.isActive());
|
|
|
|
}
|
|
|
|
|
|
|
|
@Test
|
|
|
|
public void headerPlusSignContentLengthValidationShouldPropagate() {
|
|
|
|
headerSignContentLengthValidationShouldPropagateWithEndStream(false, false);
|
|
|
|
}
|
|
|
|
|
|
|
|
@Test
|
|
|
|
public void headerPlusSignContentLengthValidationShouldPropagateWithEndStream() {
|
|
|
|
headerSignContentLengthValidationShouldPropagateWithEndStream(false, true);
|
|
|
|
}
|
|
|
|
|
|
|
|
@Test
|
|
|
|
public void headerMinusSignContentLengthValidationShouldPropagate() {
|
|
|
|
headerSignContentLengthValidationShouldPropagateWithEndStream(true, false);
|
|
|
|
}
|
|
|
|
|
|
|
|
@Test
|
|
|
|
public void headerMinusSignContentLengthValidationShouldPropagateWithEndStream() {
|
|
|
|
headerSignContentLengthValidationShouldPropagateWithEndStream(true, true);
|
|
|
|
}
|
|
|
|
|
|
|
|
private void headerSignContentLengthValidationShouldPropagateWithEndStream(boolean minus, boolean endStream) {
|
2021-06-30 10:32:01 +02:00
|
|
|
final LastInboundHandler inboundHandler = new LastInboundHandler();
|
2021-03-30 09:40:47 +02:00
|
|
|
request.add(HttpHeaderNames.CONTENT_LENGTH, (minus ? "-" : "+") + 1);
|
|
|
|
Http2StreamChannel channel = newInboundStream(3, endStream, inboundHandler);
|
2021-06-30 10:32:01 +02:00
|
|
|
assertThrows(StreamException.class, new Executable() {
|
|
|
|
@Override
|
|
|
|
public void execute() throws Throwable {
|
|
|
|
inboundHandler.checkException();
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2021-03-12 11:34:08 +01:00
|
|
|
assertNull(inboundHandler.readInbound());
|
|
|
|
assertFalse(channel.isActive());
|
|
|
|
}
|
|
|
|
|
|
|
|
@Test
|
|
|
|
public void headerContentLengthNotMatchValidationShouldPropagate() {
|
2021-03-23 20:27:38 +01:00
|
|
|
headerContentLengthNotMatchValidationShouldPropagate(false, false, false);
|
|
|
|
}
|
|
|
|
|
|
|
|
@Test
|
|
|
|
public void headerContentLengthNotMatchValidationShouldPropagateWithEndStream() {
|
|
|
|
headerContentLengthNotMatchValidationShouldPropagate(false, true, false);
|
|
|
|
}
|
|
|
|
|
|
|
|
@Test
|
|
|
|
public void headerContentLengthNotMatchValidationShouldPropagateCloseLocal() {
|
|
|
|
headerContentLengthNotMatchValidationShouldPropagate(true, false, false);
|
|
|
|
}
|
|
|
|
|
|
|
|
@Test
|
|
|
|
public void headerContentLengthNotMatchValidationShouldPropagateWithEndStreamCloseLocal() {
|
|
|
|
headerContentLengthNotMatchValidationShouldPropagate(true, true, false);
|
|
|
|
}
|
|
|
|
|
|
|
|
@Test
|
|
|
|
public void headerContentLengthNotMatchValidationShouldPropagateTrailers() {
|
|
|
|
headerContentLengthNotMatchValidationShouldPropagate(false, false, true);
|
|
|
|
}
|
|
|
|
|
|
|
|
@Test
|
|
|
|
public void headerContentLengthNotMatchValidationShouldPropagateWithEndStreamTrailers() {
|
|
|
|
headerContentLengthNotMatchValidationShouldPropagate(false, true, true);
|
|
|
|
}
|
|
|
|
|
|
|
|
@Test
|
|
|
|
public void headerContentLengthNotMatchValidationShouldPropagateCloseLocalTrailers() {
|
|
|
|
headerContentLengthNotMatchValidationShouldPropagate(true, false, true);
|
|
|
|
}
|
|
|
|
|
|
|
|
@Test
|
|
|
|
public void headerContentLengthNotMatchValidationShouldPropagateWithEndStreamCloseLocalTrailers() {
|
|
|
|
headerContentLengthNotMatchValidationShouldPropagate(true, true, true);
|
|
|
|
}
|
|
|
|
|
|
|
|
private void headerContentLengthNotMatchValidationShouldPropagate(
|
|
|
|
boolean closeLocal, boolean endStream, boolean trailer) {
|
2021-06-30 10:32:01 +02:00
|
|
|
final LastInboundHandler inboundHandler = new LastInboundHandler();
|
2021-03-12 11:34:08 +01:00
|
|
|
request.addLong(HttpHeaderNames.CONTENT_LENGTH, 1);
|
|
|
|
Http2StreamChannel channel = newInboundStream(3, false, inboundHandler);
|
|
|
|
assertTrue(channel.isActive());
|
|
|
|
|
2021-03-23 20:27:38 +01:00
|
|
|
if (closeLocal) {
|
|
|
|
channel.writeAndFlush(new DefaultHttp2HeadersFrame(new DefaultHttp2Headers(), true))
|
|
|
|
.syncUninterruptibly();
|
|
|
|
assertEquals(Http2Stream.State.HALF_CLOSED_LOCAL, channel.stream().state());
|
|
|
|
} else {
|
|
|
|
assertEquals(Http2Stream.State.OPEN, channel.stream().state());
|
|
|
|
}
|
|
|
|
|
|
|
|
if (trailer) {
|
|
|
|
frameInboundWriter.writeInboundHeaders(channel.stream().id(), new DefaultHttp2Headers(), 0, endStream);
|
|
|
|
} else {
|
|
|
|
frameInboundWriter.writeInboundData(channel.stream().id(), bb("foo"), 0, endStream);
|
|
|
|
}
|
2021-06-30 10:32:01 +02:00
|
|
|
|
|
|
|
assertThrows(StreamException.class, new Executable() {
|
|
|
|
@Override
|
|
|
|
public void execute() throws Throwable {
|
|
|
|
inboundHandler.checkException();
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2021-03-12 11:34:08 +01:00
|
|
|
Http2HeadersFrame headersFrame = new DefaultHttp2HeadersFrame(request).stream(channel.stream());
|
|
|
|
assertEquals(headersFrame, inboundHandler.readInbound());
|
|
|
|
assertNull(inboundHandler.readInbound());
|
|
|
|
assertFalse(channel.isActive());
|
|
|
|
}
|
|
|
|
|
2019-06-24 09:17:15 +02:00
|
|
|
@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);
|
|
|
|
}
|
|
|
|
|
2019-07-17 20:12:19 +02:00
|
|
|
@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);
|
2020-01-21 02:47:17 +01:00
|
|
|
childChannel.pipeline().addFirst(new ChannelHandler() {
|
2019-07-17 20:12:19 +02:00
|
|
|
private int count;
|
|
|
|
@Override
|
2020-01-21 02:47:17 +01:00
|
|
|
public void channelRead(ChannelHandlerContext ctx, Object msg) {
|
2019-07-17 20:12:19 +02:00
|
|
|
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();
|
|
|
|
}
|
|
|
|
|
2019-06-24 09:17:15 +02:00
|
|
|
@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) {
|
2019-09-04 12:00:06 +02:00
|
|
|
Future<Http2StreamChannel> future = new Http2StreamChannelBootstrap(parentChannel).handler(handler)
|
|
|
|
.open();
|
|
|
|
return future.syncUninterruptibly().getNow();
|
2019-06-24 09:17:15 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2019-10-09 15:12:52 +02:00
|
|
|
* A child channel for an HTTP/2 stream in IDLE state (that is no headers sent or received),
|
2019-06-24 09:17:15 +02:00
|
|
|
* 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();
|
2019-12-16 21:00:32 +01:00
|
|
|
verify(frameWriter).writeRstStream(any(ChannelHandlerContext.class),
|
2021-08-30 13:15:14 +02:00
|
|
|
eqStreamId(childChannel), eq(Http2Error.CANCEL.code()));
|
2019-06-24 09:17:15 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
@Test
|
|
|
|
public void outboundStreamShouldNotWriteResetFrameOnClose_IfStreamDidntExist() {
|
2019-12-16 21:00:32 +01:00
|
|
|
when(frameWriter.writeHeaders(any(ChannelHandlerContext.class), anyInt(),
|
2021-08-30 13:15:14 +02:00
|
|
|
any(Http2Headers.class), anyInt(), anyBoolean())).thenAnswer(new Answer<Future<Void>>() {
|
2019-06-24 09:17:15 +02:00
|
|
|
|
|
|
|
private boolean headersWritten;
|
|
|
|
@Override
|
Clean up Future/Promises API (#11575)
Motivation:
The generics for the existing futures, promises, and listeners are too complicated.
This complication comes from the existence of `ChannelPromise` and `ChannelFuture`, which forces listeners to care about the particular _type_ of future being listened on.
Modification:
* Add a `FutureContextListener` which can take a context object as an additional argument. This allows our listeners to have the channel piped through to them, so they don't need to rely on the `ChannelFuture.channel()` method.
* Make the `FutureListener`, along with the `FutureContextListener` sibling, the default listener API, retiring the `GenericFutureListener` since we no longer need to abstract over the type of the future.
* Change all uses of `ChannelPromise` to `Promise<Void>`.
* Change all uses of `ChannelFuture` to `Future<Void>`.
* Change all uses of `GenericFutureListener` to either `FutureListener` or `FutureContextListener` as needed.
* Remove `ChannelFutureListener` and `GenericFutureListener`.
* Introduce a `ChannelFutureListeners` enum to house the constants that previously lived in `ChannelFutureListener`. These constants now implement `FutureContextListener` and take the `Channel` as a context.
* Remove `ChannelPromise` and `ChannelFuture` — all usages now rely on the plain `Future` and `Promise` APIs.
* Add static factory methods to `DefaultPromise` that allow us to create promises that are initialised as successful or failed.
* Remove `CompleteFuture`, `SucceededFuture`, `FailedFuture`, `CompleteChannelFuture`, `SucceededChannelFuture`, and `FailedChannelFuture`.
* Remove `ChannelPromiseNotifier`.
Result:
Cleaner generics and more straight forward code.
2021-08-20 09:55:16 +02:00
|
|
|
public Future<Void> answer(InvocationOnMock invocationOnMock) {
|
2019-06-24 09:17:15 +02:00
|
|
|
// 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;
|
2021-08-30 13:15:14 +02:00
|
|
|
return ImmediateEventExecutor.INSTANCE.newFailedFuture(new Exception("boom"));
|
2019-06-24 09:17:15 +02:00
|
|
|
}
|
2021-08-30 13:15:14 +02:00
|
|
|
return ImmediateEventExecutor.INSTANCE.newSucceededFuture(null);
|
2019-06-24 09:17:15 +02:00
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
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.
|
2019-12-16 21:00:32 +01:00
|
|
|
verify(frameWriter, never()).writeRstStream(any(ChannelHandlerContext.class),
|
2021-08-30 13:15:14 +02:00
|
|
|
eqStreamId(childChannel), anyLong());
|
2019-06-24 09:17:15 +02:00
|
|
|
|
|
|
|
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.
|
2020-08-31 16:42:58 +02:00
|
|
|
verify(frameWriter, never()).writeRstStream(any(ChannelHandlerContext.class), eqStreamId(channel),
|
2021-08-30 13:15:14 +02:00
|
|
|
anyLong());
|
2019-06-24 09:17:15 +02:00
|
|
|
}
|
|
|
|
|
2021-06-30 10:32:01 +02:00
|
|
|
@Test
|
2019-06-24 09:17:15 +02:00
|
|
|
public void streamExceptionTriggersChildChannelExceptionAndClose() throws Exception {
|
2021-06-30 10:32:01 +02:00
|
|
|
final LastInboundHandler inboundHandler = new LastInboundHandler();
|
2019-06-24 09:17:15 +02:00
|
|
|
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());
|
2021-06-30 10:32:01 +02:00
|
|
|
|
|
|
|
assertThrows(StreamException.class, new Executable() {
|
|
|
|
@Override
|
|
|
|
public void execute() throws Throwable {
|
|
|
|
inboundHandler.checkException();
|
|
|
|
}
|
|
|
|
});
|
2019-06-24 09:17:15 +02:00
|
|
|
}
|
|
|
|
|
2021-06-30 10:32:01 +02:00
|
|
|
@Test
|
|
|
|
public void streamClosedErrorTranslatedToClosedChannelExceptionOnWrites() throws Exception {
|
2019-06-24 09:17:15 +02:00
|
|
|
LastInboundHandler inboundHandler = new LastInboundHandler();
|
|
|
|
|
|
|
|
final Http2StreamChannel childChannel = newOutboundStream(inboundHandler);
|
|
|
|
assertTrue(childChannel.isActive());
|
|
|
|
|
|
|
|
Http2Headers headers = new DefaultHttp2Headers();
|
2019-12-16 21:00:32 +01:00
|
|
|
when(frameWriter.writeHeaders(any(ChannelHandlerContext.class), anyInt(),
|
2021-08-30 13:15:14 +02:00
|
|
|
eq(headers), anyInt(), anyBoolean())).thenAnswer(invocationOnMock ->
|
|
|
|
ImmediateEventExecutor.INSTANCE.newFailedFuture(
|
|
|
|
new StreamException(childChannel.stream().id(), Http2Error.STREAM_CLOSED, "Stream Closed")));
|
Clean up Future/Promises API (#11575)
Motivation:
The generics for the existing futures, promises, and listeners are too complicated.
This complication comes from the existence of `ChannelPromise` and `ChannelFuture`, which forces listeners to care about the particular _type_ of future being listened on.
Modification:
* Add a `FutureContextListener` which can take a context object as an additional argument. This allows our listeners to have the channel piped through to them, so they don't need to rely on the `ChannelFuture.channel()` method.
* Make the `FutureListener`, along with the `FutureContextListener` sibling, the default listener API, retiring the `GenericFutureListener` since we no longer need to abstract over the type of the future.
* Change all uses of `ChannelPromise` to `Promise<Void>`.
* Change all uses of `ChannelFuture` to `Future<Void>`.
* Change all uses of `GenericFutureListener` to either `FutureListener` or `FutureContextListener` as needed.
* Remove `ChannelFutureListener` and `GenericFutureListener`.
* Introduce a `ChannelFutureListeners` enum to house the constants that previously lived in `ChannelFutureListener`. These constants now implement `FutureContextListener` and take the `Channel` as a context.
* Remove `ChannelPromise` and `ChannelFuture` — all usages now rely on the plain `Future` and `Promise` APIs.
* Add static factory methods to `DefaultPromise` that allow us to create promises that are initialised as successful or failed.
* Remove `CompleteFuture`, `SucceededFuture`, `FailedFuture`, `CompleteChannelFuture`, `SucceededChannelFuture`, and `FailedChannelFuture`.
* Remove `ChannelPromiseNotifier`.
Result:
Cleaner generics and more straight forward code.
2021-08-20 09:55:16 +02:00
|
|
|
final Future<Void> future = childChannel.writeAndFlush(
|
2021-06-30 10:32:01 +02:00
|
|
|
new DefaultHttp2HeadersFrame(new DefaultHttp2Headers()));
|
2019-06-24 09:17:15 +02:00
|
|
|
|
|
|
|
parentChannel.flush();
|
|
|
|
|
|
|
|
assertFalse(childChannel.isActive());
|
|
|
|
assertFalse(childChannel.isOpen());
|
|
|
|
|
|
|
|
inboundHandler.checkException();
|
|
|
|
|
2021-06-30 10:32:01 +02:00
|
|
|
CompletionException e = assertThrows(CompletionException.class, new Executable() {
|
|
|
|
@Override
|
|
|
|
public void execute() {
|
|
|
|
future.syncUninterruptibly();
|
|
|
|
}
|
|
|
|
});
|
|
|
|
assertThat(e.getCause(), CoreMatchers.instanceOf(ClosedChannelException.class));
|
2019-06-24 09:17:15 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
@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.
|
2019-12-16 21:00:32 +01:00
|
|
|
verify(frameWriter).writeRstStream(any(ChannelHandlerContext.class), eqStreamId(childChannel),
|
2021-08-30 13:15:14 +02:00
|
|
|
anyLong());
|
2019-06-24 09:17:15 +02:00
|
|
|
|
|
|
|
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.
|
|
|
|
//
|
2021-06-30 10:32:01 +02:00
|
|
|
@Test
|
|
|
|
public void failedOutboundStreamCreationThrowsAndClosesChannel() throws Exception {
|
2019-06-24 09:17:15 +02:00
|
|
|
LastInboundHandler handler = new LastInboundHandler();
|
|
|
|
Http2StreamChannel childChannel = newOutboundStream(handler);
|
|
|
|
assertTrue(childChannel.isActive());
|
|
|
|
|
|
|
|
Http2Headers headers = new DefaultHttp2Headers();
|
2019-12-16 21:00:32 +01:00
|
|
|
when(frameWriter.writeHeaders(any(ChannelHandlerContext.class), anyInt(),
|
2021-08-30 13:15:14 +02:00
|
|
|
eq(headers), anyInt(), anyBoolean())).thenAnswer(invocationOnMock ->
|
|
|
|
ImmediateEventExecutor.INSTANCE.newFailedFuture(new Http2NoMoreStreamIdsException()));
|
2019-06-24 09:17:15 +02:00
|
|
|
|
Clean up Future/Promises API (#11575)
Motivation:
The generics for the existing futures, promises, and listeners are too complicated.
This complication comes from the existence of `ChannelPromise` and `ChannelFuture`, which forces listeners to care about the particular _type_ of future being listened on.
Modification:
* Add a `FutureContextListener` which can take a context object as an additional argument. This allows our listeners to have the channel piped through to them, so they don't need to rely on the `ChannelFuture.channel()` method.
* Make the `FutureListener`, along with the `FutureContextListener` sibling, the default listener API, retiring the `GenericFutureListener` since we no longer need to abstract over the type of the future.
* Change all uses of `ChannelPromise` to `Promise<Void>`.
* Change all uses of `ChannelFuture` to `Future<Void>`.
* Change all uses of `GenericFutureListener` to either `FutureListener` or `FutureContextListener` as needed.
* Remove `ChannelFutureListener` and `GenericFutureListener`.
* Introduce a `ChannelFutureListeners` enum to house the constants that previously lived in `ChannelFutureListener`. These constants now implement `FutureContextListener` and take the `Channel` as a context.
* Remove `ChannelPromise` and `ChannelFuture` — all usages now rely on the plain `Future` and `Promise` APIs.
* Add static factory methods to `DefaultPromise` that allow us to create promises that are initialised as successful or failed.
* Remove `CompleteFuture`, `SucceededFuture`, `FailedFuture`, `CompleteChannelFuture`, `SucceededChannelFuture`, and `FailedChannelFuture`.
* Remove `ChannelPromiseNotifier`.
Result:
Cleaner generics and more straight forward code.
2021-08-20 09:55:16 +02:00
|
|
|
final Future<Void> future = childChannel.writeAndFlush(new DefaultHttp2HeadersFrame(headers));
|
2019-06-24 09:17:15 +02:00
|
|
|
parentChannel.flush();
|
|
|
|
|
|
|
|
assertFalse(childChannel.isActive());
|
|
|
|
assertFalse(childChannel.isOpen());
|
|
|
|
|
|
|
|
handler.checkException();
|
|
|
|
|
2021-06-30 10:32:01 +02:00
|
|
|
CompletionException e = assertThrows(CompletionException.class, new Executable() {
|
|
|
|
@Override
|
|
|
|
public void execute() {
|
|
|
|
future.syncUninterruptibly();
|
|
|
|
}
|
|
|
|
});
|
|
|
|
assertThat(e.getCause(), CoreMatchers.instanceOf(Http2NoMoreStreamIdsException.class));
|
2019-06-24 09:17:15 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
@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.
|
Clean up Future/Promises API (#11575)
Motivation:
The generics for the existing futures, promises, and listeners are too complicated.
This complication comes from the existence of `ChannelPromise` and `ChannelFuture`, which forces listeners to care about the particular _type_ of future being listened on.
Modification:
* Add a `FutureContextListener` which can take a context object as an additional argument. This allows our listeners to have the channel piped through to them, so they don't need to rely on the `ChannelFuture.channel()` method.
* Make the `FutureListener`, along with the `FutureContextListener` sibling, the default listener API, retiring the `GenericFutureListener` since we no longer need to abstract over the type of the future.
* Change all uses of `ChannelPromise` to `Promise<Void>`.
* Change all uses of `ChannelFuture` to `Future<Void>`.
* Change all uses of `GenericFutureListener` to either `FutureListener` or `FutureContextListener` as needed.
* Remove `ChannelFutureListener` and `GenericFutureListener`.
* Introduce a `ChannelFutureListeners` enum to house the constants that previously lived in `ChannelFutureListener`. These constants now implement `FutureContextListener` and take the `Channel` as a context.
* Remove `ChannelPromise` and `ChannelFuture` — all usages now rely on the plain `Future` and `Promise` APIs.
* Add static factory methods to `DefaultPromise` that allow us to create promises that are initialised as successful or failed.
* Remove `CompleteFuture`, `SucceededFuture`, `FailedFuture`, `CompleteChannelFuture`, `SucceededChannelFuture`, and `FailedChannelFuture`.
* Remove `ChannelPromiseNotifier`.
Result:
Cleaner generics and more straight forward code.
2021-08-20 09:55:16 +02:00
|
|
|
Promise<Void> p = childChannel.newPromise();
|
2021-09-02 10:46:54 +02:00
|
|
|
p.asFuture().addListener(childChannel, (channel, future) -> {
|
Clean up Future/Promises API (#11575)
Motivation:
The generics for the existing futures, promises, and listeners are too complicated.
This complication comes from the existence of `ChannelPromise` and `ChannelFuture`, which forces listeners to care about the particular _type_ of future being listened on.
Modification:
* Add a `FutureContextListener` which can take a context object as an additional argument. This allows our listeners to have the channel piped through to them, so they don't need to rely on the `ChannelFuture.channel()` method.
* Make the `FutureListener`, along with the `FutureContextListener` sibling, the default listener API, retiring the `GenericFutureListener` since we no longer need to abstract over the type of the future.
* Change all uses of `ChannelPromise` to `Promise<Void>`.
* Change all uses of `ChannelFuture` to `Future<Void>`.
* Change all uses of `GenericFutureListener` to either `FutureListener` or `FutureContextListener` as needed.
* Remove `ChannelFutureListener` and `GenericFutureListener`.
* Introduce a `ChannelFutureListeners` enum to house the constants that previously lived in `ChannelFutureListener`. These constants now implement `FutureContextListener` and take the `Channel` as a context.
* Remove `ChannelPromise` and `ChannelFuture` — all usages now rely on the plain `Future` and `Promise` APIs.
* Add static factory methods to `DefaultPromise` that allow us to create promises that are initialised as successful or failed.
* Remove `CompleteFuture`, `SucceededFuture`, `FailedFuture`, `CompleteChannelFuture`, `SucceededChannelFuture`, and `FailedChannelFuture`.
* Remove `ChannelPromiseNotifier`.
Result:
Cleaner generics and more straight forward code.
2021-08-20 09:55:16 +02:00
|
|
|
channelOpen.set(channel.isOpen());
|
|
|
|
channelActive.set(channel.isActive());
|
2019-06-24 09:17:15 +02:00
|
|
|
});
|
2021-08-29 15:44:34 +02:00
|
|
|
childChannel.close().cascadeTo(p).syncUninterruptibly();
|
2019-06-24 09:17:15 +02:00
|
|
|
|
|
|
|
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);
|
|
|
|
|
Clean up Future/Promises API (#11575)
Motivation:
The generics for the existing futures, promises, and listeners are too complicated.
This complication comes from the existence of `ChannelPromise` and `ChannelFuture`, which forces listeners to care about the particular _type_ of future being listened on.
Modification:
* Add a `FutureContextListener` which can take a context object as an additional argument. This allows our listeners to have the channel piped through to them, so they don't need to rely on the `ChannelFuture.channel()` method.
* Make the `FutureListener`, along with the `FutureContextListener` sibling, the default listener API, retiring the `GenericFutureListener` since we no longer need to abstract over the type of the future.
* Change all uses of `ChannelPromise` to `Promise<Void>`.
* Change all uses of `ChannelFuture` to `Future<Void>`.
* Change all uses of `GenericFutureListener` to either `FutureListener` or `FutureContextListener` as needed.
* Remove `ChannelFutureListener` and `GenericFutureListener`.
* Introduce a `ChannelFutureListeners` enum to house the constants that previously lived in `ChannelFutureListener`. These constants now implement `FutureContextListener` and take the `Channel` as a context.
* Remove `ChannelPromise` and `ChannelFuture` — all usages now rely on the plain `Future` and `Promise` APIs.
* Add static factory methods to `DefaultPromise` that allow us to create promises that are initialised as successful or failed.
* Remove `CompleteFuture`, `SucceededFuture`, `FailedFuture`, `CompleteChannelFuture`, `SucceededChannelFuture`, and `FailedChannelFuture`.
* Remove `ChannelPromiseNotifier`.
Result:
Cleaner generics and more straight forward code.
2021-08-20 09:55:16 +02:00
|
|
|
childChannel.closeFuture().addListener(childChannel, (channel, future) -> {
|
|
|
|
channelOpen.set(channel.isOpen());
|
|
|
|
channelActive.set(channel.isActive());
|
2019-06-24 09:17:15 +02:00
|
|
|
});
|
|
|
|
childChannel.close().syncUninterruptibly();
|
|
|
|
|
|
|
|
assertFalse(channelOpen.get());
|
|
|
|
assertFalse(channelActive.get());
|
|
|
|
assertFalse(childChannel.isActive());
|
|
|
|
}
|
|
|
|
|
|
|
|
@Test
|
|
|
|
public void channelClosedWhenWriteFutureFails() {
|
Clean up Future/Promises API (#11575)
Motivation:
The generics for the existing futures, promises, and listeners are too complicated.
This complication comes from the existence of `ChannelPromise` and `ChannelFuture`, which forces listeners to care about the particular _type_ of future being listened on.
Modification:
* Add a `FutureContextListener` which can take a context object as an additional argument. This allows our listeners to have the channel piped through to them, so they don't need to rely on the `ChannelFuture.channel()` method.
* Make the `FutureListener`, along with the `FutureContextListener` sibling, the default listener API, retiring the `GenericFutureListener` since we no longer need to abstract over the type of the future.
* Change all uses of `ChannelPromise` to `Promise<Void>`.
* Change all uses of `ChannelFuture` to `Future<Void>`.
* Change all uses of `GenericFutureListener` to either `FutureListener` or `FutureContextListener` as needed.
* Remove `ChannelFutureListener` and `GenericFutureListener`.
* Introduce a `ChannelFutureListeners` enum to house the constants that previously lived in `ChannelFutureListener`. These constants now implement `FutureContextListener` and take the `Channel` as a context.
* Remove `ChannelPromise` and `ChannelFuture` — all usages now rely on the plain `Future` and `Promise` APIs.
* Add static factory methods to `DefaultPromise` that allow us to create promises that are initialised as successful or failed.
* Remove `CompleteFuture`, `SucceededFuture`, `FailedFuture`, `CompleteChannelFuture`, `SucceededChannelFuture`, and `FailedChannelFuture`.
* Remove `ChannelPromiseNotifier`.
Result:
Cleaner generics and more straight forward code.
2021-08-20 09:55:16 +02:00
|
|
|
final Queue<Promise<Void>> writePromises = new ArrayDeque<>();
|
2019-06-24 09:17:15 +02:00
|
|
|
|
|
|
|
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();
|
2019-12-16 21:00:32 +01:00
|
|
|
when(frameWriter.writeHeaders(any(ChannelHandlerContext.class), anyInt(),
|
2021-08-30 13:15:14 +02:00
|
|
|
eq(headers), anyInt(), anyBoolean())).thenAnswer(invocationOnMock -> {
|
|
|
|
Promise<Void> promise = ImmediateEventExecutor.INSTANCE.newPromise();
|
2019-06-24 09:17:15 +02:00
|
|
|
writePromises.offer(promise);
|
|
|
|
return promise;
|
|
|
|
});
|
|
|
|
|
Clean up Future/Promises API (#11575)
Motivation:
The generics for the existing futures, promises, and listeners are too complicated.
This complication comes from the existence of `ChannelPromise` and `ChannelFuture`, which forces listeners to care about the particular _type_ of future being listened on.
Modification:
* Add a `FutureContextListener` which can take a context object as an additional argument. This allows our listeners to have the channel piped through to them, so they don't need to rely on the `ChannelFuture.channel()` method.
* Make the `FutureListener`, along with the `FutureContextListener` sibling, the default listener API, retiring the `GenericFutureListener` since we no longer need to abstract over the type of the future.
* Change all uses of `ChannelPromise` to `Promise<Void>`.
* Change all uses of `ChannelFuture` to `Future<Void>`.
* Change all uses of `GenericFutureListener` to either `FutureListener` or `FutureContextListener` as needed.
* Remove `ChannelFutureListener` and `GenericFutureListener`.
* Introduce a `ChannelFutureListeners` enum to house the constants that previously lived in `ChannelFutureListener`. These constants now implement `FutureContextListener` and take the `Channel` as a context.
* Remove `ChannelPromise` and `ChannelFuture` — all usages now rely on the plain `Future` and `Promise` APIs.
* Add static factory methods to `DefaultPromise` that allow us to create promises that are initialised as successful or failed.
* Remove `CompleteFuture`, `SucceededFuture`, `FailedFuture`, `CompleteChannelFuture`, `SucceededChannelFuture`, and `FailedChannelFuture`.
* Remove `ChannelPromiseNotifier`.
Result:
Cleaner generics and more straight forward code.
2021-08-20 09:55:16 +02:00
|
|
|
Future<Void> f = childChannel.writeAndFlush(new DefaultHttp2HeadersFrame(headers));
|
2019-06-24 09:17:15 +02:00
|
|
|
assertFalse(f.isDone());
|
Clean up Future/Promises API (#11575)
Motivation:
The generics for the existing futures, promises, and listeners are too complicated.
This complication comes from the existence of `ChannelPromise` and `ChannelFuture`, which forces listeners to care about the particular _type_ of future being listened on.
Modification:
* Add a `FutureContextListener` which can take a context object as an additional argument. This allows our listeners to have the channel piped through to them, so they don't need to rely on the `ChannelFuture.channel()` method.
* Make the `FutureListener`, along with the `FutureContextListener` sibling, the default listener API, retiring the `GenericFutureListener` since we no longer need to abstract over the type of the future.
* Change all uses of `ChannelPromise` to `Promise<Void>`.
* Change all uses of `ChannelFuture` to `Future<Void>`.
* Change all uses of `GenericFutureListener` to either `FutureListener` or `FutureContextListener` as needed.
* Remove `ChannelFutureListener` and `GenericFutureListener`.
* Introduce a `ChannelFutureListeners` enum to house the constants that previously lived in `ChannelFutureListener`. These constants now implement `FutureContextListener` and take the `Channel` as a context.
* Remove `ChannelPromise` and `ChannelFuture` — all usages now rely on the plain `Future` and `Promise` APIs.
* Add static factory methods to `DefaultPromise` that allow us to create promises that are initialised as successful or failed.
* Remove `CompleteFuture`, `SucceededFuture`, `FailedFuture`, `CompleteChannelFuture`, `SucceededChannelFuture`, and `FailedChannelFuture`.
* Remove `ChannelPromiseNotifier`.
Result:
Cleaner generics and more straight forward code.
2021-08-20 09:55:16 +02:00
|
|
|
f.addListener(childChannel, (channel, future)-> {
|
|
|
|
channelOpen.set(channel.isOpen());
|
|
|
|
channelActive.set(channel.isActive());
|
2019-06-24 09:17:15 +02:00
|
|
|
});
|
|
|
|
|
Clean up Future/Promises API (#11575)
Motivation:
The generics for the existing futures, promises, and listeners are too complicated.
This complication comes from the existence of `ChannelPromise` and `ChannelFuture`, which forces listeners to care about the particular _type_ of future being listened on.
Modification:
* Add a `FutureContextListener` which can take a context object as an additional argument. This allows our listeners to have the channel piped through to them, so they don't need to rely on the `ChannelFuture.channel()` method.
* Make the `FutureListener`, along with the `FutureContextListener` sibling, the default listener API, retiring the `GenericFutureListener` since we no longer need to abstract over the type of the future.
* Change all uses of `ChannelPromise` to `Promise<Void>`.
* Change all uses of `ChannelFuture` to `Future<Void>`.
* Change all uses of `GenericFutureListener` to either `FutureListener` or `FutureContextListener` as needed.
* Remove `ChannelFutureListener` and `GenericFutureListener`.
* Introduce a `ChannelFutureListeners` enum to house the constants that previously lived in `ChannelFutureListener`. These constants now implement `FutureContextListener` and take the `Channel` as a context.
* Remove `ChannelPromise` and `ChannelFuture` — all usages now rely on the plain `Future` and `Promise` APIs.
* Add static factory methods to `DefaultPromise` that allow us to create promises that are initialised as successful or failed.
* Remove `CompleteFuture`, `SucceededFuture`, `FailedFuture`, `CompleteChannelFuture`, `SucceededChannelFuture`, and `FailedChannelFuture`.
* Remove `ChannelPromiseNotifier`.
Result:
Cleaner generics and more straight forward code.
2021-08-20 09:55:16 +02:00
|
|
|
Promise<Void> first = writePromises.poll();
|
2019-06-24 09:17:15 +02:00
|
|
|
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() {
|
2020-01-21 02:47:17 +01:00
|
|
|
Http2StreamChannel childChannel = newOutboundStream(new ChannelHandler() { });
|
2019-06-24 09:17:15 +02:00
|
|
|
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(
|
2021-06-08 14:22:16 +02:00
|
|
|
Unpooled.buffer().writeZero(800), 800, parentChannel.newPromise());
|
2019-06-24 09:17:15 +02:00
|
|
|
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());
|
|
|
|
|
Clean up Future/Promises API (#11575)
Motivation:
The generics for the existing futures, promises, and listeners are too complicated.
This complication comes from the existence of `ChannelPromise` and `ChannelFuture`, which forces listeners to care about the particular _type_ of future being listened on.
Modification:
* Add a `FutureContextListener` which can take a context object as an additional argument. This allows our listeners to have the channel piped through to them, so they don't need to rely on the `ChannelFuture.channel()` method.
* Make the `FutureListener`, along with the `FutureContextListener` sibling, the default listener API, retiring the `GenericFutureListener` since we no longer need to abstract over the type of the future.
* Change all uses of `ChannelPromise` to `Promise<Void>`.
* Change all uses of `ChannelFuture` to `Future<Void>`.
* Change all uses of `GenericFutureListener` to either `FutureListener` or `FutureContextListener` as needed.
* Remove `ChannelFutureListener` and `GenericFutureListener`.
* Introduce a `ChannelFutureListeners` enum to house the constants that previously lived in `ChannelFutureListener`. These constants now implement `FutureContextListener` and take the `Channel` as a context.
* Remove `ChannelPromise` and `ChannelFuture` — all usages now rely on the plain `Future` and `Promise` APIs.
* Add static factory methods to `DefaultPromise` that allow us to create promises that are initialised as successful or failed.
* Remove `CompleteFuture`, `SucceededFuture`, `FailedFuture`, `CompleteChannelFuture`, `SucceededChannelFuture`, and `FailedChannelFuture`.
* Remove `ChannelPromiseNotifier`.
Result:
Cleaner generics and more straight forward code.
2021-08-20 09:55:16 +02:00
|
|
|
Future<Void> future = childChannel.writeAndFlush(new DefaultHttp2DataFrame(
|
2019-06-24 09:17:15 +02:00
|
|
|
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);
|
2021-06-08 14:22:16 +02:00
|
|
|
childChannel.unsafe().close(childChannel.newPromise());
|
2019-06-24 09:17:15 +02:00
|
|
|
|
Clean up Future/Promises API (#11575)
Motivation:
The generics for the existing futures, promises, and listeners are too complicated.
This complication comes from the existence of `ChannelPromise` and `ChannelFuture`, which forces listeners to care about the particular _type_ of future being listened on.
Modification:
* Add a `FutureContextListener` which can take a context object as an additional argument. This allows our listeners to have the channel piped through to them, so they don't need to rely on the `ChannelFuture.channel()` method.
* Make the `FutureListener`, along with the `FutureContextListener` sibling, the default listener API, retiring the `GenericFutureListener` since we no longer need to abstract over the type of the future.
* Change all uses of `ChannelPromise` to `Promise<Void>`.
* Change all uses of `ChannelFuture` to `Future<Void>`.
* Change all uses of `GenericFutureListener` to either `FutureListener` or `FutureContextListener` as needed.
* Remove `ChannelFutureListener` and `GenericFutureListener`.
* Introduce a `ChannelFutureListeners` enum to house the constants that previously lived in `ChannelFutureListener`. These constants now implement `FutureContextListener` and take the `Channel` as a context.
* Remove `ChannelPromise` and `ChannelFuture` — all usages now rely on the plain `Future` and `Promise` APIs.
* Add static factory methods to `DefaultPromise` that allow us to create promises that are initialised as successful or failed.
* Remove `CompleteFuture`, `SucceededFuture`, `FailedFuture`, `CompleteChannelFuture`, `SucceededChannelFuture`, and `FailedChannelFuture`.
* Remove `ChannelPromiseNotifier`.
Result:
Cleaner generics and more straight forward code.
2021-08-20 09:55:16 +02:00
|
|
|
Promise<Void> promise = childChannel.newPromise();
|
2019-06-24 09:17:15 +02:00
|
|
|
childChannel.unsafe().close(promise);
|
2021-09-02 10:46:54 +02:00
|
|
|
promise.asFuture().syncUninterruptibly();
|
2019-06-24 09:17:15 +02:00
|
|
|
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());
|
2019-07-21 20:58:23 +02:00
|
|
|
assertNull(inboundHandler.readInbound());
|
|
|
|
|
|
|
|
// As we limited the number to 1 we also need to call read() again.
|
|
|
|
childChannel.read();
|
|
|
|
|
2019-06-24 09:17:15 +02:00
|
|
|
assertEqualsAndRelease(dataFrame3, inboundHandler.<Http2DataFrame>readInbound());
|
|
|
|
assertEqualsAndRelease(dataFrame4, inboundHandler.<Http2DataFrame>readInbound());
|
|
|
|
|
2020-08-31 16:42:58 +02:00
|
|
|
Http2ResetFrame resetFrame = useUserEventForResetFrame() ? inboundHandler.readUserEvent() :
|
|
|
|
inboundHandler.readInbound();
|
2019-06-27 21:52:52 +02:00
|
|
|
|
2019-06-24 09:17:15 +02:00
|
|
|
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();
|
|
|
|
}
|
|
|
|
|
2019-06-27 21:52:52 +02:00
|
|
|
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();
|
|
|
|
}
|
|
|
|
|
2019-06-24 09:17:15 +02:00
|
|
|
@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);
|
|
|
|
|
2020-08-31 16:42:58 +02:00
|
|
|
assertEqualsAndRelease(dataFrame1, inboundHandler.readInbound());
|
2019-06-24 09:17:15 +02:00
|
|
|
|
|
|
|
// 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();
|
|
|
|
|
2020-08-31 16:42:58 +02:00
|
|
|
assertEqualsAndRelease(dataFrame2, inboundHandler.readInbound());
|
2019-06-24 09:17:15 +02:00
|
|
|
|
|
|
|
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);
|
|
|
|
|
2020-08-31 16:42:58 +02:00
|
|
|
assertEqualsAndRelease(dataFrame3, inboundHandler.readInbound());
|
2019-06-24 09:17:15 +02:00
|
|
|
|
|
|
|
frameInboundWriter.writeInboundData(childChannel.stream().id(), bb("4"), 0, false);
|
|
|
|
assertNull(inboundHandler.readInbound());
|
|
|
|
|
|
|
|
childChannel.read();
|
|
|
|
|
2020-08-31 16:42:58 +02:00
|
|
|
assertEqualsAndRelease(dataFrame4, inboundHandler.readInbound());
|
2019-06-24 09:17:15 +02:00
|
|
|
|
|
|
|
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());
|
|
|
|
}
|
|
|
|
|
2019-07-21 20:58:23 +02:00
|
|
|
@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.
|
2020-01-21 02:47:17 +01:00
|
|
|
childChannel.pipeline().addFirst(new ChannelHandler() {
|
2019-07-21 20:58:23 +02:00
|
|
|
|
|
|
|
@Override
|
2020-01-21 02:47:17 +01:00
|
|
|
public void channelReadComplete(ChannelHandlerContext ctx) {
|
|
|
|
ctx.fireChannelReadComplete();
|
2019-07-21 20:58:23 +02:00
|
|
|
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);
|
|
|
|
}
|
|
|
|
|
2020-03-04 10:50:41 +01:00
|
|
|
private static final class FlushSniffer implements ChannelHandler {
|
|
|
|
|
|
|
|
private boolean didFlush;
|
|
|
|
|
|
|
|
public boolean checkFlush() {
|
|
|
|
boolean r = didFlush;
|
|
|
|
didFlush = false;
|
|
|
|
return r;
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
2021-07-08 10:16:00 +02:00
|
|
|
public void flush(ChannelHandlerContext ctx) {
|
2020-03-04 10:50:41 +01:00
|
|
|
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())
|
2021-08-30 13:15:14 +02:00
|
|
|
.writeWindowUpdate(any(ChannelHandlerContext.class), anyInt(), anyInt());
|
2020-03-04 10:50:41 +01:00
|
|
|
// 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())
|
2021-08-30 13:15:14 +02:00
|
|
|
.writeWindowUpdate(any(ChannelHandlerContext.class), anyInt(), anyInt());
|
2020-03-04 10:50:41 +01:00
|
|
|
|
|
|
|
// Call read one more time which should trigger the writing of the flow control update.
|
|
|
|
childChannel.read();
|
|
|
|
verify(frameWriter)
|
2021-08-30 13:15:14 +02:00
|
|
|
.writeWindowUpdate(any(ChannelHandlerContext.class), eq(0), eq(32 * 1024));
|
2020-03-04 10:50:41 +01:00
|
|
|
verify(frameWriter)
|
|
|
|
.writeWindowUpdate(any(ChannelHandlerContext.class), eq(childChannel.stream().id()),
|
2021-08-30 13:15:14 +02:00
|
|
|
eq(32 * 1024));
|
2020-03-04 10:50:41 +01:00
|
|
|
assertTrue(flushSniffer.checkFlush());
|
|
|
|
}
|
|
|
|
|
2019-06-24 09:17:15 +02:00
|
|
|
private static void verifyFramesMultiplexedToCorrectChannel(Http2StreamChannel streamChannel,
|
|
|
|
LastInboundHandler inboundHandler,
|
|
|
|
int numFrames) {
|
|
|
|
for (int i = 0; i < numFrames; i++) {
|
|
|
|
Http2StreamFrame frame = inboundHandler.readInbound();
|
2021-06-30 10:32:01 +02:00
|
|
|
assertNotNull(frame, i + " out of " + numFrames + " received");
|
2019-06-24 09:17:15 +02:00
|
|
|
assertEquals(streamChannel.stream(), frame.stream());
|
|
|
|
release(frame);
|
|
|
|
}
|
|
|
|
assertNull(inboundHandler.readInbound());
|
|
|
|
}
|
|
|
|
|
|
|
|
private static int eqStreamId(Http2StreamChannel channel) {
|
|
|
|
return eq(channel.stream().id());
|
|
|
|
}
|
|
|
|
}
|