netty5/codec/src/test/java/io/netty/handler/codec/ByteToMessageDecoderTest.java
Norman Maurer 0e4c073bcf
Remove the intermediate List from ByteToMessageDecoder (and sub-class… (#8626)
Motivation:

ByteToMessageDecoder requires using an intermediate List to put results into. This intermediate list adds overhead (memory/CPU) which grows as the number of objects increases. This overhead can be avoided by directly propagating events through the ChannelPipeline via ctx.fireChannelRead(...). This also makes the semantics more clear and allows us to keep track if we need to call ctx.read() in all cases.

Modifications:

- Remove List from the method signature of ByteToMessageDecoder.decode(...) and decodeLast(...)
- Adjust all sub-classes
- Adjust unit tests
- Fix javadocs.

Result:

Adjust ByteToMessageDecoder as noted in https://github.com/netty/netty/issues/8525.
2019-12-16 21:00:32 +01:00

508 lines
18 KiB
Java

/*
* Copyright 2013 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;
import io.netty.buffer.AbstractByteBufAllocator;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufAllocator;
import io.netty.buffer.CompositeByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.buffer.UnpooledByteBufAllocator;
import io.netty.buffer.UnpooledHeapByteBuf;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelOutboundHandlerAdapter;
import io.netty.channel.embedded.EmbeddedChannel;
import org.junit.Ignore;
import org.junit.Test;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.ThreadLocalRandom;
import static io.netty.buffer.Unpooled.wrappedBuffer;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
public class ByteToMessageDecoderTest {
@Test
public void testRemoveItself() {
EmbeddedChannel channel = new EmbeddedChannel(new ByteToMessageDecoder() {
private boolean removed;
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in) {
assertFalse(removed);
in.readByte();
ctx.pipeline().remove(this);
removed = true;
}
});
ByteBuf buf = Unpooled.wrappedBuffer(new byte[] {'a', 'b', 'c'});
channel.writeInbound(buf.copy());
ByteBuf b = channel.readInbound();
assertEquals(b, buf.skipBytes(1));
b.release();
buf.release();
}
@Test
public void testRemoveItselfWriteBuffer() {
final ByteBuf buf = Unpooled.buffer().writeBytes(new byte[] {'a', 'b', 'c'});
EmbeddedChannel channel = new EmbeddedChannel(new ByteToMessageDecoder() {
private boolean removed;
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in) {
assertFalse(removed);
in.readByte();
ctx.pipeline().remove(this);
// This should not let it keep call decode
buf.writeByte('d');
removed = true;
}
});
channel.writeInbound(buf.copy());
ByteBuf expected = Unpooled.wrappedBuffer(new byte[] {'b', 'c'});
ByteBuf b = channel.readInbound();
assertEquals(expected, b);
expected.release();
buf.release();
b.release();
}
/**
* Verifies that internal buffer of the ByteToMessageDecoder is released once decoder is removed from pipeline. In
* this case input is read fully.
*/
@Test
public void testInternalBufferClearReadAll() {
final ByteBuf buf = Unpooled.buffer().writeBytes(new byte[] {'a'});
EmbeddedChannel channel = newInternalBufferTestChannel();
assertFalse(channel.writeInbound(buf));
assertFalse(channel.finish());
}
/**
* Verifies that internal buffer of the ByteToMessageDecoder is released once decoder is removed from pipeline. In
* this case input was not fully read.
*/
@Test
public void testInternalBufferClearReadPartly() {
final ByteBuf buf = Unpooled.buffer().writeBytes(new byte[] {'a', 'b'});
EmbeddedChannel channel = newInternalBufferTestChannel();
assertTrue(channel.writeInbound(buf));
assertTrue(channel.finish());
ByteBuf expected = Unpooled.wrappedBuffer(new byte[] {'b'});
ByteBuf b = channel.readInbound();
assertEquals(expected, b);
assertNull(channel.readInbound());
expected.release();
b.release();
}
private EmbeddedChannel newInternalBufferTestChannel() {
return new EmbeddedChannel(new ByteToMessageDecoder() {
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in) {
ByteBuf byteBuf = internalBuffer();
assertEquals(1, byteBuf.refCnt());
in.readByte();
// Removal from pipeline should clear internal buffer
ctx.pipeline().remove(this);
}
@Override
protected void handlerRemoved0(ChannelHandlerContext ctx) {
assertCumulationReleased(internalBuffer());
}
});
}
@Test
public void handlerRemovedWillNotReleaseBufferIfDecodeInProgress() {
EmbeddedChannel channel = new EmbeddedChannel(new ByteToMessageDecoder() {
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in) throws Exception {
ctx.pipeline().remove(this);
assertTrue(in.refCnt() != 0);
}
@Override
protected void handlerRemoved0(ChannelHandlerContext ctx) {
assertCumulationReleased(internalBuffer());
}
});
byte[] bytes = new byte[1024];
ThreadLocalRandom.current().nextBytes(bytes);
assertTrue(channel.writeInbound(Unpooled.wrappedBuffer(bytes)));
assertTrue(channel.finishAndReleaseAll());
}
private static void assertCumulationReleased(ByteBuf byteBuf) {
assertTrue("unexpected value: " + byteBuf,
byteBuf == null || byteBuf == Unpooled.EMPTY_BUFFER || byteBuf.refCnt() == 0);
}
@Test
public void testFireChannelReadCompleteOnInactive() throws InterruptedException {
final BlockingQueue<Integer> queue = new LinkedBlockingDeque<>();
final ByteBuf buf = Unpooled.buffer().writeBytes(new byte[] {'a', 'b'});
EmbeddedChannel channel = new EmbeddedChannel(new ByteToMessageDecoder() {
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in) {
int readable = in.readableBytes();
assertTrue(readable > 0);
in.skipBytes(readable);
}
@Override
protected void decodeLast(ChannelHandlerContext ctx, ByteBuf in) {
assertFalse(in.isReadable());
ctx.fireChannelRead("data");
}
}, new ChannelHandler() {
@Override
public void channelInactive(ChannelHandlerContext ctx) {
queue.add(3);
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
queue.add(1);
}
@Override
public void channelReadComplete(ChannelHandlerContext ctx) {
if (!ctx.channel().isActive()) {
queue.add(2);
}
}
});
assertFalse(channel.writeInbound(buf));
channel.finish();
assertEquals(1, (int) queue.take());
assertEquals(2, (int) queue.take());
assertEquals(3, (int) queue.take());
assertTrue(queue.isEmpty());
}
// See https://github.com/netty/netty/issues/4635
@Test
public void testRemoveWhileInCallDecode() {
final Object upgradeMessage = new Object();
final ByteToMessageDecoder decoder = new ByteToMessageDecoder() {
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in) {
assertEquals('a', in.readByte());
ctx.fireChannelRead(upgradeMessage);
}
};
EmbeddedChannel channel = new EmbeddedChannel(decoder, new ChannelHandler() {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
if (msg == upgradeMessage) {
ctx.pipeline().remove(decoder);
return;
}
ctx.fireChannelRead(msg);
}
});
ByteBuf buf = Unpooled.wrappedBuffer(new byte[] { 'a', 'b', 'c' });
assertTrue(channel.writeInbound(buf.copy()));
ByteBuf b = channel.readInbound();
assertEquals(b, buf.skipBytes(1));
assertFalse(channel.finish());
buf.release();
b.release();
}
@Test
public void testDecodeLastEmptyBuffer() {
EmbeddedChannel channel = new EmbeddedChannel(new ByteToMessageDecoder() {
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in) {
int readable = in.readableBytes();
assertTrue(readable > 0);
ctx.fireChannelRead(in.readBytes(readable));
}
});
byte[] bytes = new byte[1024];
ThreadLocalRandom.current().nextBytes(bytes);
assertTrue(channel.writeInbound(Unpooled.copiedBuffer(bytes)));
assertBuffer(Unpooled.wrappedBuffer(bytes), channel.readInbound());
assertNull(channel.readInbound());
assertFalse(channel.finish());
assertNull(channel.readInbound());
}
@Test
public void testDecodeLastNonEmptyBuffer() {
EmbeddedChannel channel = new EmbeddedChannel(new ByteToMessageDecoder() {
private boolean decodeLast;
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in) {
int readable = in.readableBytes();
assertTrue(readable > 0);
if (!decodeLast && readable == 1) {
return;
}
ctx.fireChannelRead(in.readBytes(decodeLast ? readable : readable - 1));
}
@Override
protected void decodeLast(ChannelHandlerContext ctx, ByteBuf in) throws Exception {
assertFalse(decodeLast);
decodeLast = true;
super.decodeLast(ctx, in);
}
});
byte[] bytes = new byte[1024];
ThreadLocalRandom.current().nextBytes(bytes);
assertTrue(channel.writeInbound(Unpooled.copiedBuffer(bytes)));
assertBuffer(Unpooled.wrappedBuffer(bytes, 0, bytes.length - 1), channel.readInbound());
assertNull(channel.readInbound());
assertTrue(channel.finish());
assertBuffer(Unpooled.wrappedBuffer(bytes, bytes.length - 1, 1), channel.readInbound());
assertNull(channel.readInbound());
}
private static void assertBuffer(ByteBuf expected, ByteBuf buffer) {
try {
assertEquals(expected, buffer);
} finally {
buffer.release();
expected.release();
}
}
@Test
public void testReadOnlyBuffer() {
EmbeddedChannel channel = new EmbeddedChannel(new ByteToMessageDecoder() {
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in) { }
});
assertFalse(channel.writeInbound(Unpooled.buffer(8).writeByte(1).asReadOnly()));
assertFalse(channel.writeInbound(Unpooled.wrappedBuffer(new byte[] { (byte) 2 })));
assertFalse(channel.finish());
}
static class WriteFailingByteBuf extends UnpooledHeapByteBuf {
private final Error error = new Error();
private int untilFailure;
WriteFailingByteBuf(int untilFailure, int capacity) {
super(UnpooledByteBufAllocator.DEFAULT, capacity, capacity);
this.untilFailure = untilFailure;
}
@Override
public ByteBuf writeBytes(ByteBuf src) {
if (--untilFailure <= 0) {
throw error;
}
return super.writeBytes(src);
}
Error writeError() {
return error;
}
}
@Test
public void releaseWhenMergeCumulateThrows() {
WriteFailingByteBuf oldCumulation = new WriteFailingByteBuf(1, 64);
ByteBuf in = Unpooled.buffer().writeZero(12);
Throwable thrown = null;
try {
ByteToMessageDecoder.MERGE_CUMULATOR.cumulate(UnpooledByteBufAllocator.DEFAULT, oldCumulation, in);
} catch (Throwable t) {
thrown = t;
}
assertSame(oldCumulation.writeError(), thrown);
assertEquals(0, in.refCnt());
assertEquals(1, oldCumulation.refCnt());
oldCumulation.release();
}
@Test
public void releaseWhenMergeCumulateThrowsInExpand() {
releaseWhenMergeCumulateThrowsInExpand(1, true);
releaseWhenMergeCumulateThrowsInExpand(2, true);
releaseWhenMergeCumulateThrowsInExpand(3, false); // sentinel test case
}
private void releaseWhenMergeCumulateThrowsInExpand(int untilFailure, boolean shouldFail) {
ByteBuf oldCumulation = UnpooledByteBufAllocator.DEFAULT.heapBuffer(8, 8);
final WriteFailingByteBuf newCumulation = new WriteFailingByteBuf(untilFailure, 16);
ByteBufAllocator allocator = new AbstractByteBufAllocator(false) {
@Override
public boolean isDirectBufferPooled() {
return false;
}
@Override
protected ByteBuf newHeapBuffer(int initialCapacity, int maxCapacity) {
return newCumulation;
}
@Override
protected ByteBuf newDirectBuffer(int initialCapacity, int maxCapacity) {
throw new UnsupportedOperationException();
}
};
ByteBuf in = Unpooled.buffer().writeZero(12);
Throwable thrown = null;
try {
ByteToMessageDecoder.MERGE_CUMULATOR.cumulate(allocator, oldCumulation, in);
} catch (Throwable t) {
thrown = t;
}
assertEquals(0, in.refCnt());
if (shouldFail) {
assertSame(newCumulation.writeError(), thrown);
assertEquals(1, oldCumulation.refCnt());
oldCumulation.release();
assertEquals(0, newCumulation.refCnt());
} else {
assertNull(thrown);
assertEquals(0, oldCumulation.refCnt());
assertEquals(1, newCumulation.refCnt());
newCumulation.release();
}
}
@Test
public void releaseWhenCompositeCumulateThrows() {
final Error error = new Error();
ByteBuf cumulation = new CompositeByteBuf(UnpooledByteBufAllocator.DEFAULT, false, 64) {
@Override
public CompositeByteBuf addComponent(boolean increaseWriterIndex, ByteBuf buffer) {
throw error;
}
};
ByteBuf in = Unpooled.buffer().writeZero(12);
try {
ByteToMessageDecoder.COMPOSITE_CUMULATOR.cumulate(UnpooledByteBufAllocator.DEFAULT, cumulation, in);
fail();
} catch (Error expected) {
assertSame(error, expected);
assertEquals(0, in.refCnt());
cumulation.release();
}
}
@Test
public void testDoesNotOverRead() {
class ReadInterceptingHandler extends ChannelOutboundHandlerAdapter {
private int readsTriggered;
@Override
public void read(ChannelHandlerContext ctx) throws Exception {
readsTriggered++;
super.read(ctx);
}
}
ReadInterceptingHandler interceptor = new ReadInterceptingHandler();
EmbeddedChannel channel = new EmbeddedChannel();
channel.config().setAutoRead(false);
channel.pipeline().addLast(interceptor, new FixedLengthFrameDecoder(3));
assertEquals(0, interceptor.readsTriggered);
// 0 complete frames, 1 partial frame: SHOULD trigger a read
channel.writeInbound(wrappedBuffer(new byte[] { 0, 1 }));
assertEquals(1, interceptor.readsTriggered);
// 2 complete frames, 0 partial frames: should NOT trigger a read
channel.writeInbound(wrappedBuffer(new byte[] { 2 }), wrappedBuffer(new byte[] { 3, 4, 5 }));
assertEquals(1, interceptor.readsTriggered);
// 1 complete frame, 1 partial frame: should NOT trigger a read
channel.writeInbound(wrappedBuffer(new byte[] { 6, 7, 8 }), wrappedBuffer(new byte[] { 9 }));
assertEquals(1, interceptor.readsTriggered);
// 1 complete frame, 1 partial frame: should NOT trigger a read
channel.writeInbound(wrappedBuffer(new byte[] { 10, 11 }), wrappedBuffer(new byte[] { 12 }));
assertEquals(1, interceptor.readsTriggered);
// 0 complete frames, 1 partial frame: SHOULD trigger a read
channel.writeInbound(wrappedBuffer(new byte[] { 13 }));
assertEquals(2, interceptor.readsTriggered);
// 1 complete frame, 0 partial frames: should NOT trigger a read
channel.writeInbound(wrappedBuffer(new byte[] { 14 }));
assertEquals(2, interceptor.readsTriggered);
for (int i = 0; i < 5; i++) {
ByteBuf read = channel.readInbound();
assertEquals(i * 3 + 0, read.getByte(0));
assertEquals(i * 3 + 1, read.getByte(1));
assertEquals(i * 3 + 2, read.getByte(2));
read.release();
}
assertFalse(channel.finish());
}
@Test
public void testDisorder() {
ByteToMessageDecoder decoder = new ByteToMessageDecoder() {
int count;
//read 4 byte then remove this decoder
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in) {
ctx.fireChannelRead(in.readByte());
if (++count >= 4) {
ctx.pipeline().remove(this);
}
}
};
EmbeddedChannel channel = new EmbeddedChannel(decoder);
assertTrue(channel.writeInbound(Unpooled.wrappedBuffer(new byte[]{1, 2, 3, 4, 5})));
assertEquals((byte) 1, (byte) channel.readInbound());
assertEquals((byte) 2, (byte) channel.readInbound());
assertEquals((byte) 3, (byte) channel.readInbound());
assertEquals((byte) 4, (byte) channel.readInbound());
ByteBuf buffer5 = channel.readInbound();
assertEquals((byte) 5, buffer5.readByte());
assertFalse(buffer5.isReadable());
assertTrue(buffer5.release());
assertFalse(channel.finish());
}
}