In the Vert.x core CI job for io_uring, there are intermittent failures like this:
026-04-27T15:06:35.6064561Z [globalEventExecutor-1-30] WARN io.netty.util.concurrent.DefaultPromise - An exception was thrown by io.vertx.core.impl.VertxImpl$2.operationComplete()
2026-04-27T15:06:35.6067535Z io.netty.channel.ChannelException: eventfd_write(...) failed: Bad file descriptor
2026-04-27T15:06:35.6069139Z at io.netty.channel.uring.Native.eventFdWrite(Native Method)
2026-04-27T15:06:35.6070941Z at io.netty.channel.uring.IoUringIoHandler.wakeup(IoUringIoHandler.java:634)
2026-04-27T15:06:35.6072882Z at io.netty.channel.SingleThreadIoEventLoop.wakeup(SingleThreadIoEventLoop.java:265)
2026-04-27T15:06:35.6074531Z at io.netty.util.concurrent.SingleThreadEventExecutor.shutdown0(SingleThreadEventExecutor.java:821)
2026-04-27T15:06:35.6077200Z at io.netty.util.concurrent.SingleThreadEventExecutor.shutdownGracefully(SingleThreadEventExecutor.java:835)
2026-04-27T15:06:35.6079283Z at io.netty.util.concurrent.MultithreadEventExecutorGroup.shutdownGracefully(MultithreadEventExecutorGroup.java:191)
2026-04-27T15:06:35.6081023Z at io.vertx.core.impl.VertxImpl$2.operationComplete(VertxImpl.java:995)
2026-04-27T15:06:35.6082401Z at io.netty.util.concurrent.DefaultPromise.notifyListener0(DefaultPromise.java:604)
2026-04-27T15:06:35.6083799Z at io.netty.util.concurrent.DefaultPromise.notifyListenersNow(DefaultPromise.java:571)
2026-04-27T15:06:35.6085138Z at io.netty.util.concurrent.DefaultPromise.notifyListeners(DefaultPromise.java:506)
2026-04-27T15:06:35.6086387Z at io.netty.util.concurrent.DefaultPromise.setValue0(DefaultPromise.java:650)
2026-04-27T15:06:35.6087947Z at io.netty.util.concurrent.DefaultPromise.setSuccess0(DefaultPromise.java:639)
2026-04-27T15:06:35.6089258Z at io.netty.util.concurrent.DefaultPromise.setSuccess(DefaultPromise.java:111)
2026-04-27T15:06:35.6090723Z at io.netty.util.concurrent.MultithreadEventExecutorGroup.lambda$new$0(MultithreadEventExecutorGroup.java:119)
2026-04-27T15:06:35.6092273Z at io.netty.util.concurrent.DefaultPromise.notifyListener0(DefaultPromise.java:604)
2026-04-27T15:06:35.6093595Z at io.netty.util.concurrent.DefaultPromise.notifyListenersNow(DefaultPromise.java:571)
2026-04-27T15:06:35.6094877Z at io.netty.util.concurrent.DefaultPromise.access$200(DefaultPromise.java:37)
2026-04-27T15:06:35.6096035Z at io.netty.util.concurrent.DefaultPromise$1.run(DefaultPromise.java:517)
2026-04-27T15:06:35.6097582Z at io.netty.util.concurrent.AbstractEventExecutor.runTask(AbstractEventExecutor.java:148)
2026-04-27T15:06:35.6098990Z at io.netty.util.concurrent.GlobalEventExecutor$TaskRunner.run(GlobalEventExecutor.java:286)
2026-04-27T15:06:35.6100273Z at io.netty.util.internal.ThreadExecutorMap$2.run(ThreadExecutorMap.java:74)
2026-04-27T15:06:35.6101558Z at io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30)
2026-04-27T15:06:35.6102744Z at java.base/java.lang.Thread.run(Thread.java:829)
I think the following could be the suite of events.
During shutdown, the global event executor tries to wake up the IoHandler. In IoUringIoHandler, an attempt to write to event file descriptor is made, because the value of eventfdAsyncNotify was false when read.
@Override
public void wakeup() {
if (!executor.isExecutorThread(Thread.currentThread()) &&
!eventfdAsyncNotify.getAndSet(true)) { // <-- was false when read at this point
// write to the eventfd which will then trigger an eventfd read completion.
Native.eventFdWrite(eventfd.intValue(), 1L);
}
}
In theory, submitted events are gated by eventfdAsyncNotify, as the comment suggests above io.netty.channel.uring.IoUringIoHandler#drainEventFd.
But if the globlal event executor thread is preempted before the Native.eventFdWrite(eventfd.intValue(), 1L); call, the event loop thread can:
- enter
IoUringIoHandler#prepareToDestroy
- invoke
Native.eventFdWrite(eventfd.intValue(), 1L);
- enter
IoUringIoHandler#handleEventFdRead
- reset
eventfdAsyncNotify to false
- enter
IoUringIoHandler#drainEventFd
- see no event pending (wrong)
- enter
IoUringIoHandler#completeRingClose
- close the file descriptor
Then when the global event executor thread is rescheduled, it tries to write to a deleted file descriptor.
In the Vert.x core CI job for io_uring, there are intermittent failures like this:
I think the following could be the suite of events.
During shutdown, the global event executor tries to wake up the
IoHandler. InIoUringIoHandler, an attempt to write to event file descriptor is made, because the value ofeventfdAsyncNotifywasfalsewhen read.In theory, submitted events are gated by
eventfdAsyncNotify, as the comment suggests aboveio.netty.channel.uring.IoUringIoHandler#drainEventFd.But if the globlal event executor thread is preempted before the
Native.eventFdWrite(eventfd.intValue(), 1L);call, the event loop thread can:IoUringIoHandler#prepareToDestroyNative.eventFdWrite(eventfd.intValue(), 1L);IoUringIoHandler#handleEventFdReadeventfdAsyncNotifyto falseIoUringIoHandler#drainEventFdIoUringIoHandler#completeRingCloseThen when the global event executor thread is rescheduled, it tries to write to a deleted file descriptor.