netty5/codec-http2/src/test/java/io/netty/handler/codec/http2/LastInboundHandler.java
Scott Mitchell 53b2dea3f4
HTTP/2 child channel read cycle doesn't respect RecvByteBufAllocator and (#8147)
Motivation:
Http2MultiplexCodec queues data internally if data is delivered from the
parent channel but the child channel did not request data. If the parent
channel notifies of a stream closure it is possible data in the queue
will be discarded before closing the channel.
Http2MultiplexCodec interacts with RecvByteBufAllocator to control the
child channel's demand for read. However it currently only ever reads a
maximum of one time per loop. This can thrash the read loop and bloat
the call stack if auto read is on, because channelReadComplete will
re-enter the read loop synchronously, and also neglect to deliver data
during the parent's read loop (if it is active). This also meant the
readPendingQueue was not utilized as originally intended (to extend the
child channel's read loop during the parent channel's read loop if
demand for data still existed).

Modifications:
- Modify the child channel's read loop to respect the
RecvByteBufAllocator, and append to the parents readPendingQueue if
appropriate.
- Stream closure notification behaves like EPOLL and KQUEUE transports
and reads all queued data, because the data is already queued in memory
and it is known there will be no more data. This will also replenish the
connection flow control window which may otherwise be constrained by a
closed stream.

Result:
More correct read loop and less risk of dropping data.
2018-07-26 19:44:21 -04:00

223 lines
6.4 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.channel.Channel;
import io.netty.channel.ChannelDuplexHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.embedded.EmbeddedChannel;
import io.netty.util.ReferenceCountUtil;
import io.netty.util.internal.PlatformDependent;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.locks.LockSupport;
import static io.netty.util.internal.ObjectUtil.checkNotNull;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
/**
* Channel handler that allows to easily access inbound messages.
*/
public class LastInboundHandler extends ChannelDuplexHandler {
private final List<Object> queue = new ArrayList<Object>();
private final Consumer<ChannelHandlerContext> channelReadCompleteConsumer;
private Throwable lastException;
private ChannelHandlerContext ctx;
private boolean channelActive;
private String writabilityStates = "";
// TODO(scott): use JDK 8's Consumer
public interface Consumer<T> {
void accept(T obj);
}
private static final Consumer<Object> NOOP_CONSUMER = new Consumer<Object>() {
@Override
public void accept(Object obj) {
}
};
@SuppressWarnings("unchecked")
public static <T> Consumer<T> noopConsumer() {
return (Consumer<T>) NOOP_CONSUMER;
}
public LastInboundHandler() {
this(LastInboundHandler.<ChannelHandlerContext>noopConsumer());
}
public LastInboundHandler(Consumer<ChannelHandlerContext> channelReadCompleteConsumer) {
this.channelReadCompleteConsumer = checkNotNull(channelReadCompleteConsumer, "channelReadCompleteConsumer");
}
@Override
public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
super.handlerAdded(ctx);
this.ctx = ctx;
}
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
if (channelActive) {
throw new IllegalStateException("channelActive may only be fired once.");
}
channelActive = true;
super.channelActive(ctx);
}
public boolean isChannelActive() {
return channelActive;
}
public String writabilityStates() {
return writabilityStates;
}
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
if (!channelActive) {
throw new IllegalStateException("channelInactive may only be fired once after channelActive.");
}
channelActive = false;
super.channelInactive(ctx);
}
@Override
public void channelWritabilityChanged(ChannelHandlerContext ctx) throws Exception {
if (writabilityStates == "") {
writabilityStates = String.valueOf(ctx.channel().isWritable());
} else {
writabilityStates += "," + ctx.channel().isWritable();
}
super.channelWritabilityChanged(ctx);
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
queue.add(msg);
}
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
channelReadCompleteConsumer.accept(ctx);
}
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
queue.add(new UserEvent(evt));
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
if (lastException != null) {
cause.printStackTrace();
} else {
lastException = cause;
}
}
public void checkException() throws Exception {
if (lastException == null) {
return;
}
Throwable t = lastException;
lastException = null;
PlatformDependent.throwException(t);
}
@SuppressWarnings("unchecked")
public <T> T readInbound() {
for (int i = 0; i < queue.size(); i++) {
Object o = queue.get(i);
if (!(o instanceof UserEvent)) {
queue.remove(i);
return (T) o;
}
}
return null;
}
public <T> T blockingReadInbound() {
T msg;
while ((msg = readInbound()) == null) {
LockSupport.parkNanos(MILLISECONDS.toNanos(10));
}
return msg;
}
@SuppressWarnings("unchecked")
public <T> T readUserEvent() {
for (int i = 0; i < queue.size(); i++) {
Object o = queue.get(i);
if (o instanceof UserEvent) {
queue.remove(i);
return (T) ((UserEvent) o).evt;
}
}
return null;
}
/**
* Useful to test order of events and messages.
*/
@SuppressWarnings("unchecked")
public <T> T readInboundMessageOrUserEvent() {
if (queue.isEmpty()) {
return null;
}
Object o = queue.remove(0);
if (o instanceof UserEvent) {
return (T) ((UserEvent) o).evt;
}
return (T) o;
}
public void writeOutbound(Object... msgs) throws Exception {
for (Object msg : msgs) {
ctx.write(msg);
}
ctx.flush();
EmbeddedChannel ch = (EmbeddedChannel) ctx.channel();
ch.runPendingTasks();
ch.checkException();
checkException();
}
public void finishAndReleaseAll() throws Exception {
checkException();
Object o;
while ((o = readInboundMessageOrUserEvent()) != null) {
ReferenceCountUtil.release(o);
}
}
public Channel channel() {
return ctx.channel();
}
private static final class UserEvent {
private final Object evt;
UserEvent(Object evt) {
this.evt = evt;
}
}
}