Summary
AbstractIoUringStreamChannel only handles DefaultFileRegion in its write path. Writing any other FileRegion implementation to an io_uring channel causes an UnsupportedOperationException from filterOutboundMessage, or a ClassCastException in scheduleWriteSingle. This is inconsistent with both EPOLL and NIO, which accept any FileRegion.
Expected behavior
Any FileRegion implementation should be writable to an io_uring channel, consistent with the FileRegion interface contract and the behavior of EPOLL and NIO transports.
Actual behavior
Writing a custom FileRegion (that is not DefaultFileRegion) to an IoUringSocketChannel fails with:
java.lang.UnsupportedOperationException: unsupported message type: MyFileRegion
at io.netty.channel.uring.AbstractIoUringChannel.filterOutboundMessage(AbstractIoUringChannel.java:1248)
at io.netty.channel.AbstractChannel$AbstractUnsafe.write(AbstractChannel.java:877)
Additionally, scheduleWriteSingle has no FileRegion branch -- it only handles IoUringFileRegion and ByteBuf with a hard cast:
} else {
ByteBuf buf = (ByteBuf) msg; // would ClassCastException if a FileRegion reached here
Root cause
In AbstractIoUringStreamChannel.filterOutboundMessage():
protected Object filterOutboundMessage(Object msg) {
if (IoUring.isSpliceSupported() && msg instanceof DefaultFileRegion) {
return new IoUringFileRegion((DefaultFileRegion) msg);
}
// Any non-DefaultFileRegion FileRegion falls through to super,
// which only accepts ByteBuf and throws UnsupportedOperationException.
return super.filterOutboundMessage(msg);
}
Compare with EPOLL's AbstractEpollStreamChannel.filterOutboundMessage(), which accepts any FileRegion:
protected Object filterOutboundMessage(Object msg) {
if (msg instanceof ByteBuf) { ... }
if (msg instanceof FileRegion || msg instanceof SpliceOutTask) {
return msg; // accepts ANY FileRegion
}
throw new UnsupportedOperationException(...);
}
And EPOLL's doWriteSingle() has a two-tier fallback:
if (msg instanceof DefaultFileRegion) {
return writeDefaultFileRegion(in, (DefaultFileRegion) msg); // sendfile
} else if (msg instanceof FileRegion) {
return writeFileRegion(in, (FileRegion) msg); // transferTo fallback
}
io_uring has no such fallback.
Affected use case
Apache Spark wraps DefaultFileRegion inside a composite FileRegion implementation (MessageWithHeader) that prepends a frame header before the file body. This is a common pattern for protocols that need framing around file transfers. The composite FileRegion delegates transferTo() to the inner DefaultFileRegion, so it works correctly with NIO and EPOLL but crashes with io_uring.
I am currently evaluating Apache Spark's support for io_uring transport. This issue blocks io_uring adoption for any framework that wraps DefaultFileRegion in a custom FileRegion.
Proposed fix
Add a generic FileRegion fallback in filterOutboundMessage. Since io_uring cannot use synchronous transferTo in its async write path, convert the FileRegion to a direct ByteBuf eagerly:
protected Object filterOutboundMessage(Object msg) {
if (IoUring.isSpliceSupported() && msg instanceof DefaultFileRegion) {
return new IoUringFileRegion((DefaultFileRegion) msg);
}
if (msg instanceof FileRegion) {
FileRegion region = (FileRegion) msg;
long remaining = region.count() - region.transferred();
ByteBuf buf = alloc().directBuffer((int) remaining);
try {
ByteBufWritableByteChannel channel = new ByteBufWritableByteChannel(buf);
while (region.transferred() < region.count()) {
long transferred = region.transferTo(channel, region.transferred());
if (transferred <= 0) {
break;
}
}
} catch (IOException e) {
buf.release();
throw new RuntimeException("Failed to convert FileRegion to ByteBuf", e);
} finally {
region.release();
}
return buf;
}
return super.filterOutboundMessage(msg);
}
This is not zero-copy but is functionally correct and consistent with EPOLL's writeFileRegion fallback. DefaultFileRegion still uses the optimized splice path via IoUringFileRegion.
Comparison across transports
| Transport |
DefaultFileRegion |
Generic FileRegion |
| NIO |
FileChannel.transferTo(SocketChannel) -- JDK sendfile |
region.transferTo(SocketChannel) -- works |
| EPOLL |
socket.sendFile() -- native sendfile |
region.transferTo(byteChannel) -- fallback, works |
| io_uring (current) |
splice() via IoUringFileRegion -- works |
crashes |
| io_uring (proposed) |
splice() via IoUringFileRegion -- unchanged |
convert to ByteBuf, send via async path -- works |
Environment
- Netty version: 4.2.x
- Discovered while evaluating io_uring for Apache Spark's shuffle file transfer
Summary
AbstractIoUringStreamChannelonly handlesDefaultFileRegionin its write path. Writing any otherFileRegionimplementation to an io_uring channel causes anUnsupportedOperationExceptionfromfilterOutboundMessage, or aClassCastExceptioninscheduleWriteSingle. This is inconsistent with both EPOLL and NIO, which accept anyFileRegion.Expected behavior
Any
FileRegionimplementation should be writable to an io_uring channel, consistent with theFileRegioninterface contract and the behavior of EPOLL and NIO transports.Actual behavior
Writing a custom
FileRegion(that is notDefaultFileRegion) to anIoUringSocketChannelfails with:Additionally,
scheduleWriteSinglehas noFileRegionbranch -- it only handlesIoUringFileRegionandByteBufwith a hard cast:Root cause
In
AbstractIoUringStreamChannel.filterOutboundMessage():Compare with EPOLL's
AbstractEpollStreamChannel.filterOutboundMessage(), which accepts anyFileRegion:And EPOLL's
doWriteSingle()has a two-tier fallback:io_uring has no such fallback.
Affected use case
Apache Spark wraps
DefaultFileRegioninside a compositeFileRegionimplementation (MessageWithHeader) that prepends a frame header before the file body. This is a common pattern for protocols that need framing around file transfers. The compositeFileRegiondelegatestransferTo()to the innerDefaultFileRegion, so it works correctly with NIO and EPOLL but crashes with io_uring.I am currently evaluating Apache Spark's support for io_uring transport. This issue blocks io_uring adoption for any framework that wraps
DefaultFileRegionin a customFileRegion.Proposed fix
Add a generic
FileRegionfallback infilterOutboundMessage. Since io_uring cannot use synchronoustransferToin its async write path, convert theFileRegionto a directByteBufeagerly:This is not zero-copy but is functionally correct and consistent with EPOLL's
writeFileRegionfallback.DefaultFileRegionstill uses the optimized splice path viaIoUringFileRegion.Comparison across transports
DefaultFileRegionFileRegionFileChannel.transferTo(SocketChannel)-- JDK sendfileregion.transferTo(SocketChannel)-- workssocket.sendFile()-- native sendfileregion.transferTo(byteChannel)-- fallback, workssplice()viaIoUringFileRegion-- workssplice()viaIoUringFileRegion-- unchangedEnvironment