netty5/transport/src/test/java/io/netty/channel/ReentrantChannelTest.java

281 lines
9.3 KiB
Java
Raw Normal View History

2013-08-21 16:12:58 +02:00
/*
* Copyright 2012 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.channel;
import io.netty.bootstrap.Bootstrap;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.LoggingHandler.Event;
import io.netty.channel.local.LocalAddress;
2013-08-21 16:12:58 +02:00
import org.junit.Test;
import java.nio.channels.ClosedChannelException;
import static org.junit.Assert.*;
public class ReentrantChannelTest extends BaseChannelTest {
2013-08-21 16:12:58 +02:00
@Test
public void testWritabilityChanged() throws Exception {
2013-08-21 16:12:58 +02:00
LocalAddress addr = new LocalAddress("testWritabilityChanged");
2013-08-21 16:12:58 +02:00
ServerBootstrap sb = getLocalServerBootstrap();
sb.bind(addr).sync().channel();
2013-08-21 16:12:58 +02:00
Bootstrap cb = getLocalClientBootstrap();
2013-08-21 16:12:58 +02:00
setInterest(Event.WRITE, Event.FLUSH, Event.WRITABILITY);
2013-08-21 16:12:58 +02:00
Channel clientChannel = cb.connect(addr).sync().channel();
clientChannel.config().setWriteBufferLowWaterMark(512);
clientChannel.config().setWriteBufferHighWaterMark(1024);
// What is supposed to happen from this point:
//
// 1. Because this write attempt has been made from a non-I/O thread,
// ChannelOutboundBuffer.pendingWriteBytes will be increased before
// write() event is really evaluated.
// -> channelWritabilityChanged() will be triggered,
// because the Channel became unwritable.
//
// 2. The write() event is handled by the pipeline in an I/O thread.
// -> write() will be triggered.
//
// 3. Once the write() event is handled, ChannelOutboundBuffer.pendingWriteBytes
// will be decreased.
// -> channelWritabilityChanged() will be triggered,
// because the Channel became writable again.
//
// 4. The message is added to the ChannelOutboundBuffer and thus
// pendingWriteBytes will be increased again.
// -> channelWritabilityChanged() will be triggered.
//
// 5. The flush() event causes the write request in theChannelOutboundBuffer
// to be removed.
// -> flush() and channelWritabilityChanged() will be triggered.
//
// Note that the channelWritabilityChanged() in the step 4 can occur between
2017-04-19 22:37:03 +02:00
// the flush() and the channelWritabilityChanged() in the step 5, because
// the flush() is invoked from a non-I/O thread while the other are from
// an I/O thread.
ChannelFuture future = clientChannel.write(createTestBuf(2000));
2013-08-21 16:12:58 +02:00
clientChannel.flush();
future.sync();
2013-08-21 16:12:58 +02:00
clientChannel.close().sync();
2013-08-21 16:12:58 +02:00
assertLog(
// Case 1:
"WRITABILITY: writable=false\n" +
"WRITE\n" +
"WRITABILITY: writable=false\n" +
"WRITABILITY: writable=false\n" +
"FLUSH\n" +
"WRITABILITY: writable=true\n",
// Case 2:
"WRITABILITY: writable=false\n" +
"WRITE\n" +
"WRITABILITY: writable=false\n" +
"FLUSH\n" +
"WRITABILITY: writable=true\n" +
"WRITABILITY: writable=true\n");
[#5028] Fix re-entrance issue with channelWritabilityChanged(...) and write(...) Motivation: When always triggered fireChannelWritabilityChanged() directly when the update the pending bytes in the ChannelOutboundBuffer was made from within the EventLoop. This is problematic as this can cause some re-entrance issue if the user has a custom ChannelOutboundHandler that does multiple writes from within the write(...) method and also has a handler that will intercept the channelWritabilityChanged event and trigger another write when the Channel is writable. This can also easily happen if the user just use a MessageToMessageEncoder subclass and triggers a write from channelWritabilityChanged(). Beside this we also triggered fireChannelWritabilityChanged() too often when a user did a write from outside the EventLoop. In this case we increased the pending bytes of the outboundbuffer before scheduled the actual write and decreased again before the write then takes place. Both of this may trigger a fireChannelWritabilityChanged() event which then may be re-triggered once the actual write ends again in the ChannelOutboundBuffer. The third gotcha was that a user may get multiple events even if the writability of the channel not changed. Modification: - Always invoke the fireChannelWritabilityChanged() later on the EventLoop. - Only trigger the fireChannelWritabilityChanged() if the channel is still active and if the writability of the channel changed. No need to cause events that were already triggered without a real writability change. - when write(...) is called from outside the EventLoop we only increase the pending bytes in the outbound buffer (so that Channel.isWritable() is updated directly) but not cause a fireChannelWritabilityChanged(). The fireChannelWritabilityChanged() is then triggered once the task is picked up by the EventLoop as usual. Result: No more re-entrance possible because of writes from within channelWritabilityChanged(...) method and no events without a real writability change.
2016-04-01 11:45:43 +02:00
}
2013-08-21 16:12:58 +02:00
/**
* Similar to {@link #testWritabilityChanged()} with slight variation.
*/
[#5028] Fix re-entrance issue with channelWritabilityChanged(...) and write(...) Motivation: When always triggered fireChannelWritabilityChanged() directly when the update the pending bytes in the ChannelOutboundBuffer was made from within the EventLoop. This is problematic as this can cause some re-entrance issue if the user has a custom ChannelOutboundHandler that does multiple writes from within the write(...) method and also has a handler that will intercept the channelWritabilityChanged event and trigger another write when the Channel is writable. This can also easily happen if the user just use a MessageToMessageEncoder subclass and triggers a write from channelWritabilityChanged(). Beside this we also triggered fireChannelWritabilityChanged() too often when a user did a write from outside the EventLoop. In this case we increased the pending bytes of the outboundbuffer before scheduled the actual write and decreased again before the write then takes place. Both of this may trigger a fireChannelWritabilityChanged() event which then may be re-triggered once the actual write ends again in the ChannelOutboundBuffer. The third gotcha was that a user may get multiple events even if the writability of the channel not changed. Modification: - Always invoke the fireChannelWritabilityChanged() later on the EventLoop. - Only trigger the fireChannelWritabilityChanged() if the channel is still active and if the writability of the channel changed. No need to cause events that were already triggered without a real writability change. - when write(...) is called from outside the EventLoop we only increase the pending bytes in the outbound buffer (so that Channel.isWritable() is updated directly) but not cause a fireChannelWritabilityChanged(). The fireChannelWritabilityChanged() is then triggered once the task is picked up by the EventLoop as usual. Result: No more re-entrance possible because of writes from within channelWritabilityChanged(...) method and no events without a real writability change.
2016-04-01 11:45:43 +02:00
@Test
public void testFlushInWritabilityChanged() throws Exception {
2013-08-21 16:12:58 +02:00
[#5028] Fix re-entrance issue with channelWritabilityChanged(...) and write(...) Motivation: When always triggered fireChannelWritabilityChanged() directly when the update the pending bytes in the ChannelOutboundBuffer was made from within the EventLoop. This is problematic as this can cause some re-entrance issue if the user has a custom ChannelOutboundHandler that does multiple writes from within the write(...) method and also has a handler that will intercept the channelWritabilityChanged event and trigger another write when the Channel is writable. This can also easily happen if the user just use a MessageToMessageEncoder subclass and triggers a write from channelWritabilityChanged(). Beside this we also triggered fireChannelWritabilityChanged() too often when a user did a write from outside the EventLoop. In this case we increased the pending bytes of the outboundbuffer before scheduled the actual write and decreased again before the write then takes place. Both of this may trigger a fireChannelWritabilityChanged() event which then may be re-triggered once the actual write ends again in the ChannelOutboundBuffer. The third gotcha was that a user may get multiple events even if the writability of the channel not changed. Modification: - Always invoke the fireChannelWritabilityChanged() later on the EventLoop. - Only trigger the fireChannelWritabilityChanged() if the channel is still active and if the writability of the channel changed. No need to cause events that were already triggered without a real writability change. - when write(...) is called from outside the EventLoop we only increase the pending bytes in the outbound buffer (so that Channel.isWritable() is updated directly) but not cause a fireChannelWritabilityChanged(). The fireChannelWritabilityChanged() is then triggered once the task is picked up by the EventLoop as usual. Result: No more re-entrance possible because of writes from within channelWritabilityChanged(...) method and no events without a real writability change.
2016-04-01 11:45:43 +02:00
LocalAddress addr = new LocalAddress("testFlushInWritabilityChanged");
2013-08-21 16:12:58 +02:00
ServerBootstrap sb = getLocalServerBootstrap();
sb.bind(addr).sync().channel();
2013-08-21 16:12:58 +02:00
Bootstrap cb = getLocalClientBootstrap();
setInterest(Event.WRITE, Event.FLUSH, Event.WRITABILITY);
Channel clientChannel = cb.connect(addr).sync().channel();
clientChannel.config().setWriteBufferLowWaterMark(512);
clientChannel.config().setWriteBufferHighWaterMark(1024);
clientChannel.pipeline().addLast(new ChannelHandler() {
@Override
public void channelWritabilityChanged(ChannelHandlerContext ctx) throws Exception {
if (!ctx.channel().isWritable()) {
ctx.channel().flush();
}
ctx.fireChannelWritabilityChanged();
}
});
2013-08-21 16:12:58 +02:00
assertTrue(clientChannel.isWritable());
clientChannel.write(createTestBuf(2000)).sync();
2013-08-23 15:38:30 +02:00
clientChannel.close().sync();
2013-08-21 16:12:58 +02:00
assertLog(
// Case 1:
"WRITABILITY: writable=false\n" +
"FLUSH\n" +
[#5028] Fix re-entrance issue with channelWritabilityChanged(...) and write(...) Motivation: When always triggered fireChannelWritabilityChanged() directly when the update the pending bytes in the ChannelOutboundBuffer was made from within the EventLoop. This is problematic as this can cause some re-entrance issue if the user has a custom ChannelOutboundHandler that does multiple writes from within the write(...) method and also has a handler that will intercept the channelWritabilityChanged event and trigger another write when the Channel is writable. This can also easily happen if the user just use a MessageToMessageEncoder subclass and triggers a write from channelWritabilityChanged(). Beside this we also triggered fireChannelWritabilityChanged() too often when a user did a write from outside the EventLoop. In this case we increased the pending bytes of the outboundbuffer before scheduled the actual write and decreased again before the write then takes place. Both of this may trigger a fireChannelWritabilityChanged() event which then may be re-triggered once the actual write ends again in the ChannelOutboundBuffer. The third gotcha was that a user may get multiple events even if the writability of the channel not changed. Modification: - Always invoke the fireChannelWritabilityChanged() later on the EventLoop. - Only trigger the fireChannelWritabilityChanged() if the channel is still active and if the writability of the channel changed. No need to cause events that were already triggered without a real writability change. - when write(...) is called from outside the EventLoop we only increase the pending bytes in the outbound buffer (so that Channel.isWritable() is updated directly) but not cause a fireChannelWritabilityChanged(). The fireChannelWritabilityChanged() is then triggered once the task is picked up by the EventLoop as usual. Result: No more re-entrance possible because of writes from within channelWritabilityChanged(...) method and no events without a real writability change.
2016-04-01 11:45:43 +02:00
"WRITE\n" +
"WRITABILITY: writable=false\n" +
"WRITABILITY: writable=false\n" +
"FLUSH\n" +
"WRITABILITY: writable=true\n",
// Case 2:
"WRITABILITY: writable=false\n" +
"FLUSH\n" +
"WRITE\n" +
"WRITABILITY: writable=false\n" +
"FLUSH\n" +
"WRITABILITY: writable=true\n" +
"WRITABILITY: writable=true\n");
2013-08-21 16:12:58 +02:00
}
@Test
public void testWriteFlushPingPong() throws Exception {
LocalAddress addr = new LocalAddress("testWriteFlushPingPong");
ServerBootstrap sb = getLocalServerBootstrap();
sb.bind(addr).sync().channel();
Bootstrap cb = getLocalClientBootstrap();
setInterest(Event.WRITE, Event.FLUSH, Event.CLOSE, Event.EXCEPTION);
Channel clientChannel = cb.connect(addr).sync().channel();
clientChannel.pipeline().addLast(new ChannelHandler() {
int writeCount;
int flushCount;
@Override
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
if (writeCount < 5) {
writeCount++;
ctx.channel().flush();
}
ctx.write(msg, promise);
}
@Override
public void flush(ChannelHandlerContext ctx) throws Exception {
if (flushCount < 5) {
flushCount++;
ctx.channel().write(createTestBuf(2000));
}
ctx.flush();
}
});
clientChannel.writeAndFlush(createTestBuf(2000));
clientChannel.close().sync();
assertLog(
"WRITE\n" +
"FLUSH\n" +
"WRITE\n" +
"FLUSH\n" +
"WRITE\n" +
"FLUSH\n" +
"WRITE\n" +
"FLUSH\n" +
"WRITE\n" +
"FLUSH\n" +
"WRITE\n" +
"FLUSH\n" +
"CLOSE\n");
}
@Test
public void testCloseInFlush() throws Exception {
LocalAddress addr = new LocalAddress("testCloseInFlush");
ServerBootstrap sb = getLocalServerBootstrap();
sb.bind(addr).sync().channel();
Bootstrap cb = getLocalClientBootstrap();
setInterest(Event.WRITE, Event.FLUSH, Event.CLOSE, Event.EXCEPTION);
Channel clientChannel = cb.connect(addr).sync().channel();
clientChannel.pipeline().addLast(new ChannelHandler() {
@Override
public void write(final ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
promise.addListener(future -> ctx.channel().close());
ctx.write(msg, promise);
ctx.channel().flush();
}
});
clientChannel.write(createTestBuf(2000)).sync();
clientChannel.closeFuture().sync();
assertLog("WRITE\nFLUSH\nCLOSE\n");
}
@Test
public void testFlushFailure() throws Exception {
LocalAddress addr = new LocalAddress("testFlushFailure");
ServerBootstrap sb = getLocalServerBootstrap();
sb.bind(addr).sync().channel();
Bootstrap cb = getLocalClientBootstrap();
setInterest(Event.WRITE, Event.FLUSH, Event.CLOSE, Event.EXCEPTION);
Channel clientChannel = cb.connect(addr).sync().channel();
clientChannel.pipeline().addLast(new ChannelHandler() {
@Override
public void flush(ChannelHandlerContext ctx) throws Exception {
throw new Exception("intentional failure");
}
}, new ChannelHandler() {
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
ctx.close();
}
});
try {
clientChannel.writeAndFlush(createTestBuf(2000)).sync();
fail();
} catch (Throwable cce) {
// FIXME: shouldn't this contain the "intentional failure" exception?
assertEquals(ClosedChannelException.class, cce.getClass());
}
clientChannel.closeFuture().sync();
assertLog("WRITE\nCLOSE\n");
}
2013-08-21 16:12:58 +02:00
}