Preserve order when using alternate event loops (#10069)
Motivation: When the HttpContentCompressor is put on an alternate EventExecutor, the order of events should be preserved so that the compressed content is correctly created. Modifications: - checking that the executor in the ChannelHandlerContext is the same executor as the current executor when evaluating if the handler should be skipped. - Add unit test Result: Fixes https://github.com/netty/netty/issues/10067 Co-authored-by: Norman Maurer <norman_maurer@apple.com>
This commit is contained in:
parent
0453222015
commit
7fce06a0d1
@ -15,14 +15,32 @@
|
|||||||
*/
|
*/
|
||||||
package io.netty.handler.codec.http;
|
package io.netty.handler.codec.http;
|
||||||
|
|
||||||
|
import io.netty.bootstrap.Bootstrap;
|
||||||
|
import io.netty.bootstrap.ServerBootstrap;
|
||||||
import io.netty.buffer.ByteBufUtil;
|
import io.netty.buffer.ByteBufUtil;
|
||||||
import io.netty.buffer.Unpooled;
|
import io.netty.buffer.Unpooled;
|
||||||
|
import io.netty.channel.Channel;
|
||||||
|
import io.netty.channel.ChannelFuture;
|
||||||
|
import io.netty.channel.ChannelHandlerContext;
|
||||||
|
import io.netty.channel.ChannelInboundHandlerAdapter;
|
||||||
|
import io.netty.channel.ChannelInitializer;
|
||||||
|
import io.netty.channel.ChannelOutboundHandlerAdapter;
|
||||||
|
import io.netty.channel.ChannelPromise;
|
||||||
|
import io.netty.channel.DefaultEventLoopGroup;
|
||||||
|
import io.netty.channel.EventLoopGroup;
|
||||||
import io.netty.channel.embedded.EmbeddedChannel;
|
import io.netty.channel.embedded.EmbeddedChannel;
|
||||||
|
import io.netty.channel.local.LocalAddress;
|
||||||
|
import io.netty.channel.local.LocalChannel;
|
||||||
|
import io.netty.channel.local.LocalServerChannel;
|
||||||
import io.netty.handler.codec.DecoderResult;
|
import io.netty.handler.codec.DecoderResult;
|
||||||
import io.netty.handler.codec.EncoderException;
|
import io.netty.handler.codec.EncoderException;
|
||||||
import io.netty.handler.codec.compression.ZlibWrapper;
|
import io.netty.handler.codec.compression.ZlibWrapper;
|
||||||
import io.netty.util.CharsetUtil;
|
import io.netty.util.CharsetUtil;
|
||||||
import io.netty.util.ReferenceCountUtil;
|
import io.netty.util.ReferenceCountUtil;
|
||||||
|
import java.util.UUID;
|
||||||
|
import java.util.concurrent.BlockingQueue;
|
||||||
|
import java.util.concurrent.LinkedBlockingQueue;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
|
||||||
import static io.netty.handler.codec.http.HttpHeadersTestUtils.of;
|
import static io.netty.handler.codec.http.HttpHeadersTestUtils.of;
|
||||||
@ -259,6 +277,104 @@ public class HttpContentCompressorTest {
|
|||||||
assertThat(ch.readOutbound(), is(nullValue()));
|
assertThat(ch.readOutbound(), is(nullValue()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testExecutorPreserveOrdering() throws Exception {
|
||||||
|
final EventLoopGroup compressorGroup = new DefaultEventLoopGroup(1);
|
||||||
|
EventLoopGroup localGroup = new DefaultEventLoopGroup(1);
|
||||||
|
Channel server = null;
|
||||||
|
Channel client = null;
|
||||||
|
try {
|
||||||
|
ServerBootstrap bootstrap = new ServerBootstrap()
|
||||||
|
.channel(LocalServerChannel.class)
|
||||||
|
.group(localGroup)
|
||||||
|
.childHandler(new ChannelInitializer<LocalChannel>() {
|
||||||
|
@Override
|
||||||
|
protected void initChannel(LocalChannel ch) throws Exception {
|
||||||
|
ch.pipeline()
|
||||||
|
.addLast(new HttpServerCodec())
|
||||||
|
.addLast(new HttpObjectAggregator(1024))
|
||||||
|
.addLast(compressorGroup, new HttpContentCompressor())
|
||||||
|
.addLast(new ChannelOutboundHandlerAdapter() {
|
||||||
|
@Override
|
||||||
|
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise)
|
||||||
|
throws Exception {
|
||||||
|
super.write(ctx, msg, promise);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.addLast(new ChannelInboundHandlerAdapter() {
|
||||||
|
@Override
|
||||||
|
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
|
||||||
|
if (msg instanceof FullHttpRequest) {
|
||||||
|
FullHttpResponse res =
|
||||||
|
new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK,
|
||||||
|
Unpooled.copiedBuffer("Hello, World", CharsetUtil.US_ASCII));
|
||||||
|
ctx.writeAndFlush(res);
|
||||||
|
ReferenceCountUtil.release(msg);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
super.channelRead(ctx, msg);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
LocalAddress address = new LocalAddress(UUID.randomUUID().toString());
|
||||||
|
server = bootstrap.bind(address).sync().channel();
|
||||||
|
|
||||||
|
final BlockingQueue<HttpObject> responses = new LinkedBlockingQueue<HttpObject>();
|
||||||
|
|
||||||
|
client = new Bootstrap()
|
||||||
|
.channel(LocalChannel.class)
|
||||||
|
.remoteAddress(address)
|
||||||
|
.group(localGroup)
|
||||||
|
.handler(new ChannelInitializer<LocalChannel>() {
|
||||||
|
@Override
|
||||||
|
protected void initChannel(LocalChannel ch) throws Exception {
|
||||||
|
ch.pipeline().addLast(new HttpClientCodec()).addLast(new ChannelInboundHandlerAdapter() {
|
||||||
|
@Override
|
||||||
|
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
|
||||||
|
if (msg instanceof HttpObject) {
|
||||||
|
responses.put((HttpObject) msg);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
super.channelRead(ctx, msg);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}).connect().sync().channel();
|
||||||
|
|
||||||
|
client.writeAndFlush(newRequest()).sync();
|
||||||
|
|
||||||
|
assertEncodedResponse((HttpResponse) responses.poll(1, TimeUnit.SECONDS));
|
||||||
|
HttpContent c = (HttpContent) responses.poll(1, TimeUnit.SECONDS);
|
||||||
|
assertNotNull(c);
|
||||||
|
assertThat(ByteBufUtil.hexDump(c.content()),
|
||||||
|
is("1f8b0800000000000000f248cdc9c9d75108cf2fca4901000000ffff"));
|
||||||
|
c.release();
|
||||||
|
|
||||||
|
c = (HttpContent) responses.poll(1, TimeUnit.SECONDS);
|
||||||
|
assertNotNull(c);
|
||||||
|
assertThat(ByteBufUtil.hexDump(c.content()), is("0300c6865b260c000000"));
|
||||||
|
c.release();
|
||||||
|
|
||||||
|
LastHttpContent last = (LastHttpContent) responses.poll(1, TimeUnit.SECONDS);
|
||||||
|
assertNotNull(last);
|
||||||
|
assertThat(last.content().readableBytes(), is(0));
|
||||||
|
last.release();
|
||||||
|
|
||||||
|
assertNull(responses.poll(1, TimeUnit.SECONDS));
|
||||||
|
} finally {
|
||||||
|
if (client != null) {
|
||||||
|
client.close().sync();
|
||||||
|
}
|
||||||
|
if (server != null) {
|
||||||
|
server.close().sync();
|
||||||
|
}
|
||||||
|
compressorGroup.shutdownGracefully();
|
||||||
|
localGroup.shutdownGracefully();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* If the length of the content is unknown, {@link HttpContentEncoder} should not skip encoding the content
|
* If the length of the content is unknown, {@link HttpContentEncoder} should not skip encoding the content
|
||||||
* even if the actual length is turned out to be 0.
|
* even if the actual length is turned out to be 0.
|
||||||
@ -543,7 +659,10 @@ public class HttpContentCompressorTest {
|
|||||||
Object o = ch.readOutbound();
|
Object o = ch.readOutbound();
|
||||||
assertThat(o, is(instanceOf(HttpResponse.class)));
|
assertThat(o, is(instanceOf(HttpResponse.class)));
|
||||||
|
|
||||||
HttpResponse res = (HttpResponse) o;
|
assertEncodedResponse((HttpResponse) o);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void assertEncodedResponse(HttpResponse res) {
|
||||||
assertThat(res, is(not(instanceOf(HttpContent.class))));
|
assertThat(res, is(not(instanceOf(HttpContent.class))));
|
||||||
assertThat(res.headers().get(HttpHeaderNames.TRANSFER_ENCODING), is("chunked"));
|
assertThat(res.headers().get(HttpHeaderNames.TRANSFER_ENCODING), is("chunked"));
|
||||||
assertThat(res.headers().get(HttpHeaderNames.CONTENT_LENGTH), is(nullValue()));
|
assertThat(res.headers().get(HttpHeaderNames.CONTENT_LENGTH), is(nullValue()));
|
||||||
|
@ -51,6 +51,8 @@ import static io.netty.channel.ChannelHandlerMask.MASK_DEREGISTER;
|
|||||||
import static io.netty.channel.ChannelHandlerMask.MASK_DISCONNECT;
|
import static io.netty.channel.ChannelHandlerMask.MASK_DISCONNECT;
|
||||||
import static io.netty.channel.ChannelHandlerMask.MASK_EXCEPTION_CAUGHT;
|
import static io.netty.channel.ChannelHandlerMask.MASK_EXCEPTION_CAUGHT;
|
||||||
import static io.netty.channel.ChannelHandlerMask.MASK_FLUSH;
|
import static io.netty.channel.ChannelHandlerMask.MASK_FLUSH;
|
||||||
|
import static io.netty.channel.ChannelHandlerMask.MASK_ONLY_INBOUND;
|
||||||
|
import static io.netty.channel.ChannelHandlerMask.MASK_ONLY_OUTBOUND;
|
||||||
import static io.netty.channel.ChannelHandlerMask.MASK_READ;
|
import static io.netty.channel.ChannelHandlerMask.MASK_READ;
|
||||||
import static io.netty.channel.ChannelHandlerMask.MASK_USER_EVENT_TRIGGERED;
|
import static io.netty.channel.ChannelHandlerMask.MASK_USER_EVENT_TRIGGERED;
|
||||||
import static io.netty.channel.ChannelHandlerMask.MASK_WRITE;
|
import static io.netty.channel.ChannelHandlerMask.MASK_WRITE;
|
||||||
@ -906,20 +908,33 @@ abstract class AbstractChannelHandlerContext implements ChannelHandlerContext, R
|
|||||||
|
|
||||||
private AbstractChannelHandlerContext findContextInbound(int mask) {
|
private AbstractChannelHandlerContext findContextInbound(int mask) {
|
||||||
AbstractChannelHandlerContext ctx = this;
|
AbstractChannelHandlerContext ctx = this;
|
||||||
|
EventExecutor currentExecutor = executor();
|
||||||
do {
|
do {
|
||||||
ctx = ctx.next;
|
ctx = ctx.next;
|
||||||
} while ((ctx.executionMask & mask) == 0);
|
} while (skipContext(ctx, currentExecutor, mask, MASK_ONLY_INBOUND));
|
||||||
return ctx;
|
return ctx;
|
||||||
}
|
}
|
||||||
|
|
||||||
private AbstractChannelHandlerContext findContextOutbound(int mask) {
|
private AbstractChannelHandlerContext findContextOutbound(int mask) {
|
||||||
AbstractChannelHandlerContext ctx = this;
|
AbstractChannelHandlerContext ctx = this;
|
||||||
|
EventExecutor currentExecutor = executor();
|
||||||
do {
|
do {
|
||||||
ctx = ctx.prev;
|
ctx = ctx.prev;
|
||||||
} while ((ctx.executionMask & mask) == 0);
|
} while (skipContext(ctx, currentExecutor, mask, MASK_ONLY_OUTBOUND));
|
||||||
return ctx;
|
return ctx;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static boolean skipContext(
|
||||||
|
AbstractChannelHandlerContext ctx, EventExecutor currentExecutor, int mask, int onlyMask) {
|
||||||
|
// Ensure we correctly handle MASK_EXCEPTION_CAUGHT which is not included in the MASK_EXCEPTION_CAUGHT
|
||||||
|
return (ctx.executionMask & (onlyMask | mask)) == 0 ||
|
||||||
|
// We can only skip if the EventExecutor is the same as otherwise we need to ensure we offload
|
||||||
|
// everything to preserve ordering.
|
||||||
|
//
|
||||||
|
// See https://github.com/netty/netty/issues/10067
|
||||||
|
(ctx.executor() == currentExecutor && (ctx.executionMask & mask) == 0);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public ChannelPromise voidPromise() {
|
public ChannelPromise voidPromise() {
|
||||||
return channel().voidPromise();
|
return channel().voidPromise();
|
||||||
|
@ -54,11 +54,13 @@ final class ChannelHandlerMask {
|
|||||||
static final int MASK_WRITE = 1 << 15;
|
static final int MASK_WRITE = 1 << 15;
|
||||||
static final int MASK_FLUSH = 1 << 16;
|
static final int MASK_FLUSH = 1 << 16;
|
||||||
|
|
||||||
private static final int MASK_ALL_INBOUND = MASK_EXCEPTION_CAUGHT | MASK_CHANNEL_REGISTERED |
|
static final int MASK_ONLY_INBOUND = MASK_CHANNEL_REGISTERED |
|
||||||
MASK_CHANNEL_UNREGISTERED | MASK_CHANNEL_ACTIVE | MASK_CHANNEL_INACTIVE | MASK_CHANNEL_READ |
|
MASK_CHANNEL_UNREGISTERED | MASK_CHANNEL_ACTIVE | MASK_CHANNEL_INACTIVE | MASK_CHANNEL_READ |
|
||||||
MASK_CHANNEL_READ_COMPLETE | MASK_USER_EVENT_TRIGGERED | MASK_CHANNEL_WRITABILITY_CHANGED;
|
MASK_CHANNEL_READ_COMPLETE | MASK_USER_EVENT_TRIGGERED | MASK_CHANNEL_WRITABILITY_CHANGED;
|
||||||
private static final int MASK_ALL_OUTBOUND = MASK_EXCEPTION_CAUGHT | MASK_BIND | MASK_CONNECT | MASK_DISCONNECT |
|
private static final int MASK_ALL_INBOUND = MASK_EXCEPTION_CAUGHT | MASK_ONLY_INBOUND;
|
||||||
|
static final int MASK_ONLY_OUTBOUND = MASK_BIND | MASK_CONNECT | MASK_DISCONNECT |
|
||||||
MASK_CLOSE | MASK_DEREGISTER | MASK_READ | MASK_WRITE | MASK_FLUSH;
|
MASK_CLOSE | MASK_DEREGISTER | MASK_READ | MASK_WRITE | MASK_FLUSH;
|
||||||
|
private static final int MASK_ALL_OUTBOUND = MASK_EXCEPTION_CAUGHT | MASK_ONLY_OUTBOUND;
|
||||||
|
|
||||||
private static final FastThreadLocal<Map<Class<? extends ChannelHandler>, Integer>> MASKS =
|
private static final FastThreadLocal<Map<Class<? extends ChannelHandler>, Integer>> MASKS =
|
||||||
new FastThreadLocal<Map<Class<? extends ChannelHandler>, Integer>>() {
|
new FastThreadLocal<Map<Class<? extends ChannelHandler>, Integer>>() {
|
||||||
|
Loading…
Reference in New Issue
Block a user