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:
parent
00c4b944e4
commit
95e8ec1db9
@ -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() + ')');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user