Handle the case where JDK notifies aync I/O handler immediately

After some debugging, I found that JDK AIO implementation often performs
I/O immediately from the caller thread if the caller thread is the I/O
thread, and notifies the completion handler also immediately.  This
commit handles such a case correctly during reads and writes.

Additionally, this commit also changes SingleThreadEventExecutor to let
it handle unexpected exceptions such as AssertionError in a robus
manner.
This commit is contained in:
Trustin Lee 2012-12-02 20:03:35 +09:00
parent 00c4b944e4
commit 95e8ec1db9
2 changed files with 108 additions and 65 deletions

View File

@ -98,6 +98,9 @@ public abstract class SingleThreadEventExecutor extends AbstractExecutorService
try {
SingleThreadEventExecutor.this.run();
success = true;
} catch (Throwable t) {
logger.warn("Unexpected exception from an event executor: ", t);
shutdown();
} finally {
// Check if confirmShutdown() was called at the end of the loop.
if (success && lastAccessTimeNanos == 0) {
@ -122,7 +125,11 @@ public abstract class SingleThreadEventExecutor extends AbstractExecutorService
cleanup();
} finally {
threadLock.release();
assert taskQueue.isEmpty();
if (!taskQueue.isEmpty()) {
logger.warn(
"An event executor terminated with " +
"non-empty task queue (" + taskQueue.size() + ')');
}
}
}
}

View File

@ -25,6 +25,7 @@ import io.netty.channel.ChannelMetadata;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.EventLoop;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.SocketChannelConfig;
import java.io.IOException;
import java.net.InetSocketAddress;
@ -61,9 +62,12 @@ public class AioSocketChannel extends AbstractAioChannel implements SocketChanne
private volatile boolean inputShutdown;
private volatile boolean outputShutdown;
private boolean flushing;
private boolean asyncWriteInProgress;
private boolean inDoFlushByteBuffer;
private boolean asyncReadInProgress;
private boolean inBeginRead;
private final AtomicBoolean readSuspended = new AtomicBoolean();
private final AtomicBoolean readInProgress = new AtomicBoolean();
private final Runnable readTask = new Runnable() {
@Override
@ -231,58 +235,95 @@ public class AioSocketChannel extends AbstractAioChannel implements SocketChanne
@Override
protected void doFlushByteBuffer(ByteBuf buf) throws Exception {
if (flushing) {
if (inDoFlushByteBuffer || asyncWriteInProgress) {
return;
}
flushing = true;
inDoFlushByteBuffer = true;
// Ensure the readerIndex of the buffer is 0 before beginning an async write.
// Otherwise, JDK can write into a wrong region of the buffer when a handler calls
// discardReadBytes() later, modifying the readerIndex and the writerIndex unexpectedly.
buf.discardReadBytes();
try {
if (buf.readable()) {
for (;;) {
// Ensure the readerIndex of the buffer is 0 before beginning an async write.
// Otherwise, JDK can write into a wrong region of the buffer when a handler calls
// discardReadBytes() later, modifying the readerIndex and the writerIndex unexpectedly.
buf.discardReadBytes();
if (buf.readable()) {
if (buf.hasNioBuffers()) {
ByteBuffer[] buffers = buf.nioBuffers(buf.readerIndex(), buf.readableBytes());
javaChannel().write(buffers, 0, buffers.length, config.getReadTimeout(),
TimeUnit.MILLISECONDS, this, GATHERING_WRITE_HANDLER);
asyncWriteInProgress = true;
if (buf.hasNioBuffers()) {
ByteBuffer[] buffers = buf.nioBuffers(buf.readerIndex(), buf.readableBytes());
if (buffers.length == 1) {
javaChannel().write(
buffers[0], config.getWriteTimeout(), TimeUnit.MILLISECONDS, this, WRITE_HANDLER);
} else {
javaChannel().write(
buffers, 0, buffers.length, config.getWriteTimeout(), TimeUnit.MILLISECONDS,
this, GATHERING_WRITE_HANDLER);
}
} else {
javaChannel().write(
buf.nioBuffer(), config.getWriteTimeout(), TimeUnit.MILLISECONDS, this, WRITE_HANDLER);
}
if (asyncWriteInProgress) {
// JDK decided to write data (or notify handler) later.
break;
}
// JDK performed the write operation immediately and notified the handler.
// We know this because we set asyncWriteInProgress to false in the handler.
if (!buf.readable()) {
// There's nothing left in the buffer. No need to retry writing.
break;
}
// There's more to write. Continue the loop.
}
} else {
javaChannel().write(buf.nioBuffer(), config.getReadTimeout(), TimeUnit.MILLISECONDS,
this, WRITE_HANDLER);
flushFutureNotifier.notifyFlushFutures();
}
} else {
flushFutureNotifier.notifyFlushFutures();
flushing = false;
} finally {
inDoFlushByteBuffer = false;
}
}
private void beginRead() {
if (readSuspended.get() || inputShutdown) {
if (inBeginRead || asyncReadInProgress || inputShutdown || readSuspended.get()) {
return;
}
// prevent ReadPendingException
if (!readInProgress.compareAndSet(false, true)) {
return;
}
inBeginRead = true;
ByteBuf byteBuf = pipeline().inboundByteBuffer();
if (!byteBuf.readable()) {
byteBuf.discardReadBytes();
}
try {
for (;;) {
ByteBuf byteBuf = pipeline().inboundByteBuffer();
if (!byteBuf.readable()) {
byteBuf.discardReadBytes();
}
expandReadBuffer(byteBuf);
expandReadBuffer(byteBuf);
if (byteBuf.hasNioBuffers()) {
ByteBuffer[] buffers = byteBuf.nioBuffers(byteBuf.writerIndex(), byteBuf.writableBytes());
javaChannel().read(buffers, 0, buffers.length, config.getWriteTimeout(),
TimeUnit.MILLISECONDS, this, SCATTERING_READ_HANDLER);
} else {
// Get a ByteBuffer view on the ByteBuf
ByteBuffer buffer = byteBuf.nioBuffer(byteBuf.writerIndex(), byteBuf.writableBytes());
javaChannel().read(buffer, config.getWriteTimeout(), TimeUnit.MILLISECONDS,
this, READ_HANDLER);
asyncReadInProgress = true;
if (byteBuf.hasNioBuffers()) {
ByteBuffer[] buffers = byteBuf.nioBuffers(byteBuf.writerIndex(), byteBuf.writableBytes());
javaChannel().read(buffers, 0, buffers.length, config.getWriteTimeout(),
TimeUnit.MILLISECONDS, this, SCATTERING_READ_HANDLER);
} else {
// Get a ByteBuffer view on the ByteBuf
ByteBuffer buffer = byteBuf.nioBuffer(byteBuf.writerIndex(), byteBuf.writableBytes());
javaChannel().read(buffer, config.getWriteTimeout(), TimeUnit.MILLISECONDS,
this, READ_HANDLER);
}
if (asyncReadInProgress) {
// JDK decided to read data (or notify handler) later.
break;
}
// The read operation has been finished immediately - schedule another read operation.
}
} finally {
inBeginRead = false;
}
}
@ -290,6 +331,7 @@ public class AioSocketChannel extends AbstractAioChannel implements SocketChanne
@Override
protected void completed0(T result, AioSocketChannel channel) {
channel.asyncWriteInProgress = false;
ByteBuf buf = channel.unsafe().directOutboundContext().outboundByteBuffer();
int writtenBytes = result.intValue();
if (writtenBytes > 0) {
@ -297,38 +339,33 @@ public class AioSocketChannel extends AbstractAioChannel implements SocketChanne
buf.readerIndex(buf.readerIndex() + writtenBytes);
}
boolean empty = !buf.readable();
if (empty) {
// Reset reader/writerIndex to 0 if the buffer is empty.
buf.discardReadBytes();
if (channel.inDoFlushByteBuffer) {
// JDK performed the write operation immediately and notified this handler immediately.
// doFlushByteBuffer() will do subsequent write operations if necessary for us.
return;
}
// Notify flush futures only when the handler is called outside of unsafe().flushNow()
// because flushNow() will do that for us.
ChannelFlushFutureNotifier notifier = channel.flushFutureNotifier;
notifier.increaseWriteCounter(writtenBytes);
notifier.notifyFlushFutures();
// Allow to have the next write pending
channel.flushing = false;
// Stop flushing if disconnected.
if (!channel.isActive()) {
return;
}
if (buf.readable()) {
try {
// Try to flush it again.
channel.doFlushByteBuffer(buf);
} catch (Exception e) {
// Should never happen, anyway call failed just in case
failed0(e, channel);
}
channel.unsafe().flushNow();
} else {
buf.discardReadBytes();
}
}
@Override
protected void failed0(Throwable cause, AioSocketChannel channel) {
channel.asyncWriteInProgress = false;
channel.flushFutureNotifier.notifyFlushFutures(cause);
channel.pipeline().fireExceptionCaught(cause);
@ -341,13 +378,12 @@ public class AioSocketChannel extends AbstractAioChannel implements SocketChanne
return;
}
ByteBuf buf = channel.unsafe().directOutboundContext().outboundByteBuffer();
if (!buf.readable()) {
buf.discardReadBytes();
if (!channel.inDoFlushByteBuffer) {
ByteBuf buf = channel.unsafe().directOutboundContext().outboundByteBuffer();
if (!buf.readable()) {
buf.discardReadBytes();
}
}
// Allow to have the next write pending
channel.flushing = false;
}
}
@ -355,6 +391,8 @@ public class AioSocketChannel extends AbstractAioChannel implements SocketChanne
@Override
protected void completed0(T result, AioSocketChannel channel) {
channel.asyncReadInProgress = false;
if (channel.inputShutdown) {
// Channel has been closed during read. Because the inbound buffer has been deallocated already,
// there's no way to let a user handler access it unfortunately.
@ -394,9 +432,6 @@ public class AioSocketChannel extends AbstractAioChannel implements SocketChanne
}
}
} finally {
// see beginRead
channel.readInProgress.set(false);
if (read) {
pipeline.fireInboundBufferUpdated();
}
@ -412,7 +447,7 @@ public class AioSocketChannel extends AbstractAioChannel implements SocketChanne
}
}
} else {
// start the next read
// Schedule another read operation.
channel.beginRead();
}
}
@ -420,6 +455,7 @@ public class AioSocketChannel extends AbstractAioChannel implements SocketChanne
@Override
protected void failed0(Throwable t, AioSocketChannel channel) {
channel.asyncReadInProgress = false;
if (t instanceof ClosedChannelException) {
return;
}
@ -433,7 +469,7 @@ public class AioSocketChannel extends AbstractAioChannel implements SocketChanne
if (t instanceof IOException || t instanceof InterruptedByTimeoutException) {
channel.unsafe().close(channel.unsafe().voidFuture());
} else {
// start the next read
// Schedule another read operation.
channel.beginRead();
}
}
@ -455,7 +491,7 @@ public class AioSocketChannel extends AbstractAioChannel implements SocketChanne
}
@Override
public AioSocketChannelConfig config() {
public SocketChannelConfig config() {
return config;
}