Repository: netty/netty
Issue: HttpContentCompressor doesn't work when put on an alternate EventExecutor

### Expected behavior

The following isn't expected to mangle or lose the response content:

    pipeline.addLast(eventExecutor, new HttpContentCompressor());

### Actual behavior

The content may be mangled or completely lost.

### Steps to reproduce

`                        .addLast(compressorGroup, new HttpContentCompressor())`

### Minimal yet complete reproducer code (or URL to code)

https://github.com/atcurtis/netty/blob/e8dd352c8d17943df310ba1371329c0990e5bbb9/codec-http/src/test/java/io/netty/handler/codec/http/HttpContentCompressorTest.java#L281

### Netty version

I checked 4.1.42.Final and 4.1.46.Final

### JVM version (e.g. `java -version`)

Java HotSpot(TM) 64-Bit Server VM (build 25.121-b13, mixed mode)


### OS version (e.g. `uname -a`)
 3.10.0-957.21.3.el7.x86_64

Issue comments:

Pull request review comments:
this is not correct (and also not the one above) as you will cause a `ClassCastException` later on if the `ChannelHandlerContext` does not wrap an `ChannelOutboundHandler`.

Patch that resolved the issue:
diff --git a/transport/src/main/java/io/netty/channel/AbstractChannelHandlerContext.java b/transport/src/main/java/io/netty/channel/AbstractChannelHandlerContext.java
index ec4c9b45be0..92c912a5000 100644
--- a/transport/src/main/java/io/netty/channel/AbstractChannelHandlerContext.java
+++ b/transport/src/main/java/io/netty/channel/AbstractChannelHandlerContext.java
@@ -51,6 +51,8 @@
 import static io.netty.channel.ChannelHandlerMask.MASK_DISCONNECT;
 import static io.netty.channel.ChannelHandlerMask.MASK_EXCEPTION_CAUGHT;
 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_USER_EVENT_TRIGGERED;
 import static io.netty.channel.ChannelHandlerMask.MASK_WRITE;
@@ -906,20 +908,33 @@ private boolean isNotValidPromise(ChannelPromise promise, boolean allowVoidPromi
 
     private AbstractChannelHandlerContext findContextInbound(int mask) {
         AbstractChannelHandlerContext ctx = this;
+        EventExecutor currentExecutor = executor();
         do {
             ctx = ctx.next;
-        } while ((ctx.executionMask & mask) == 0);
+        } while (skipContext(ctx, currentExecutor, mask, MASK_ONLY_INBOUND));
         return ctx;
     }
 
     private AbstractChannelHandlerContext findContextOutbound(int mask) {
         AbstractChannelHandlerContext ctx = this;
+        EventExecutor currentExecutor = executor();
         do {
             ctx = ctx.prev;
-        } while ((ctx.executionMask & mask) == 0);
+        } while (skipContext(ctx, currentExecutor, mask, MASK_ONLY_OUTBOUND));
         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
     public ChannelPromise voidPromise() {
         return channel().voidPromise();
diff --git a/transport/src/main/java/io/netty/channel/ChannelHandlerMask.java b/transport/src/main/java/io/netty/channel/ChannelHandlerMask.java
index 526006a84e3..dad793cf38c 100644
--- a/transport/src/main/java/io/netty/channel/ChannelHandlerMask.java
+++ b/transport/src/main/java/io/netty/channel/ChannelHandlerMask.java
@@ -54,11 +54,13 @@ final class ChannelHandlerMask {
     static final int MASK_WRITE = 1 << 15;
     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_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;
+    private static final int MASK_ALL_OUTBOUND = MASK_EXCEPTION_CAUGHT | MASK_ONLY_OUTBOUND;
 
     private static final FastThreadLocal<Map<Class<? extends ChannelHandler>, Integer>> MASKS =
             new FastThreadLocal<Map<Class<? extends ChannelHandler>, Integer>>() {


Test patch:
diff --git a/codec-http/src/test/java/io/netty/handler/codec/http/HttpContentCompressorTest.java b/codec-http/src/test/java/io/netty/handler/codec/http/HttpContentCompressorTest.java
index d0676fd907a..bb43b3c9380 100644
--- a/codec-http/src/test/java/io/netty/handler/codec/http/HttpContentCompressorTest.java
+++ b/codec-http/src/test/java/io/netty/handler/codec/http/HttpContentCompressorTest.java
@@ -15,14 +15,32 @@
  */
 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.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.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.EncoderException;
 import io.netty.handler.codec.compression.ZlibWrapper;
 import io.netty.util.CharsetUtil;
 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 static io.netty.handler.codec.http.HttpHeadersTestUtils.of;
@@ -259,6 +277,104 @@ public void testFullContent() throws Exception {
         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
      * even if the actual length is turned out to be 0.
@@ -543,7 +659,10 @@ private static void assertEncodedResponse(EmbeddedChannel ch) {
         Object o = ch.readOutbound();
         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.headers().get(HttpHeaderNames.TRANSFER_ENCODING), is("chunked"));
         assertThat(res.headers().get(HttpHeaderNames.CONTENT_LENGTH), is(nullValue()));