View Javadoc
1   /*
2    * Copyright 2024 The Netty Project
3    *
4    * The Netty Project licenses this file to you under the Apache License,
5    * version 2.0 (the "License"); you may not use this file except in compliance
6    * with the License. You may obtain a copy of the License at:
7    *
8    *   https://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12   * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13   * License for the specific language governing permissions and limitations
14   * under the License.
15   */
16  package io.netty.channel.uring;
17  
18  import io.netty.buffer.ByteBuf;
19  import io.netty.channel.Channel;
20  import io.netty.channel.ChannelException;
21  import io.netty.channel.ChannelFuture;
22  import io.netty.channel.ChannelFutureListener;
23  import io.netty.channel.ChannelMetadata;
24  import io.netty.channel.ChannelOutboundBuffer;
25  import io.netty.channel.ChannelPipeline;
26  import io.netty.channel.ChannelPromise;
27  import io.netty.channel.DefaultFileRegion;
28  import io.netty.channel.EventLoop;
29  import io.netty.channel.FileRegion;
30  import io.netty.channel.IoRegistration;
31  import io.netty.channel.socket.DuplexChannel;
32  import io.netty.channel.unix.IovArray;
33  import io.netty.util.ReferenceCounted;
34  import io.netty.util.internal.SystemPropertyUtil;
35  import io.netty.util.internal.logging.InternalLogger;
36  import io.netty.util.internal.logging.InternalLoggerFactory;
37  
38  import java.io.IOException;
39  import java.net.SocketAddress;
40  import java.nio.ByteBuffer;
41  import java.nio.channels.WritableByteChannel;
42  
43  import static io.netty.channel.unix.Errors.ioResult;
44  
45  abstract class AbstractIoUringStreamChannel extends AbstractIoUringChannel implements DuplexChannel {
46      private static final InternalLogger logger = InternalLoggerFactory.getInstance(AbstractIoUringStreamChannel.class);
47      private static final ChannelMetadata METADATA = new ChannelMetadata(false, 16);
48  
49      /**
50       * Maximum bytes per chunk when converting a generic {@link FileRegion} to a {@link ByteBuf}
51       * for the io_uring async send path. Overridable via the {@code io.netty.iouring.fileRegionChunkSize}
52       * system property; capped at 16 MiB to guard against pathological configurations that would
53       * risk direct-memory OOM.
54       */
55      private static final int FILE_REGION_MAX_CHUNK_SIZE = Math.min(16 * 1024 * 1024,
56              Math.max(1, SystemPropertyUtil.getInt("io.netty.iouring.fileRegionChunkSize", 64 * 1024)));
57  
58      // Store the opCode so we know if we used WRITE or WRITEV.
59      byte writeOpCode;
60      // Keep track of the ids used for write and read so we can cancel these when needed.
61      long writeId;
62      byte readOpCode;
63      long readId;
64  
65      // The configured buffer ring if any
66      private IoUringBufferRing bufferRing;
67  
68      AbstractIoUringStreamChannel(Channel parent, LinuxSocket socket, boolean active) {
69          super(parent, socket, active);
70      }
71  
72      AbstractIoUringStreamChannel(Channel parent, LinuxSocket socket, SocketAddress remote) {
73          super(parent, socket, remote);
74      }
75  
76      @Override
77      protected final boolean isStreamSocket() {
78          return true;
79      }
80  
81      @Override
82      public ChannelMetadata metadata() {
83          return METADATA;
84      }
85  
86      @Override
87      protected AbstractUringUnsafe newUnsafe() {
88          return new IoUringStreamUnsafe();
89      }
90  
91      @Override
92      public final ChannelFuture shutdown() {
93          return shutdown(newPromise());
94      }
95  
96      @Override
97      public final ChannelFuture shutdown(final ChannelPromise promise) {
98          ChannelFuture shutdownOutputFuture = shutdownOutput();
99          if (shutdownOutputFuture.isDone()) {
100             shutdownOutputDone(shutdownOutputFuture, promise);
101         } else {
102             shutdownOutputFuture.addListener(new ChannelFutureListener() {
103                 @Override
104                 public void operationComplete(final ChannelFuture shutdownOutputFuture) throws Exception {
105                     shutdownOutputDone(shutdownOutputFuture, promise);
106                 }
107             });
108         }
109         return promise;
110     }
111 
112     @Override
113     protected final void doShutdownOutput0() throws Exception {
114         socket.shutdown(false, true);
115     }
116 
117     private void shutdownInput0(final ChannelPromise promise) {
118         try {
119             socket.shutdown(true, false);
120             promise.setSuccess();
121         } catch (Throwable cause) {
122             promise.setFailure(cause);
123         }
124     }
125 
126     @Override
127     public final boolean isOutputShutdown() {
128         return socket.isOutputShutdown();
129     }
130 
131     @Override
132     public final boolean isInputShutdown() {
133         return socket.isInputShutdown();
134     }
135 
136     @Override
137     public final boolean isShutdown() {
138         return socket.isShutdown();
139     }
140 
141     @Override
142     public final ChannelFuture shutdownOutput() {
143         return shutdownOutput(newPromise());
144     }
145 
146     @Override
147     public final ChannelFuture shutdownOutput(final ChannelPromise promise) {
148         EventLoop loop = eventLoop();
149         if (loop.inEventLoop()) {
150             ((AbstractUnsafe) unsafe()).shutdownOutput(promise);
151         } else {
152             loop.execute(new Runnable() {
153                 @Override
154                 public void run() {
155                     ((AbstractUnsafe) unsafe()).shutdownOutput(promise);
156                 }
157             });
158         }
159 
160         return promise;
161     }
162 
163     @Override
164     public final ChannelFuture shutdownInput() {
165         return shutdownInput(newPromise());
166     }
167 
168     @Override
169     public final ChannelFuture shutdownInput(final ChannelPromise promise) {
170         EventLoop loop = eventLoop();
171         if (loop.inEventLoop()) {
172             shutdownInput0(promise);
173         } else {
174             loop.execute(new Runnable() {
175                 @Override
176                 public void run() {
177                     shutdownInput0(promise);
178                 }
179             });
180         }
181         return promise;
182     }
183 
184     private void shutdownOutputDone(final ChannelFuture shutdownOutputFuture, final ChannelPromise promise) {
185         ChannelFuture shutdownInputFuture = shutdownInput();
186         if (shutdownInputFuture.isDone()) {
187             shutdownDone(shutdownOutputFuture, shutdownInputFuture, promise);
188         } else {
189             shutdownInputFuture.addListener(new ChannelFutureListener() {
190                 @Override
191                 public void operationComplete(ChannelFuture shutdownInputFuture) throws Exception {
192                     shutdownDone(shutdownOutputFuture, shutdownInputFuture, promise);
193                 }
194             });
195         }
196     }
197 
198     private static void shutdownDone(ChannelFuture shutdownOutputFuture,
199                                      ChannelFuture shutdownInputFuture,
200                                      ChannelPromise promise) {
201         Throwable shutdownOutputCause = shutdownOutputFuture.cause();
202         Throwable shutdownInputCause = shutdownInputFuture.cause();
203         if (shutdownOutputCause != null) {
204             if (shutdownInputCause != null) {
205                 logger.info("Exception suppressed because a previous exception occurred.",
206                              shutdownInputCause);
207             }
208             promise.setFailure(shutdownOutputCause);
209         } else if (shutdownInputCause != null) {
210             promise.setFailure(shutdownInputCause);
211         } else {
212             promise.setSuccess();
213         }
214     }
215 
216     @Override
217     protected final void doRegister(ChannelPromise promise) {
218         ChannelPromise registerPromise = this.newPromise();
219         // Ensure that the buffer group is properly set before channel::read
220         registerPromise.addListener(f -> {
221             if (f.isSuccess()) {
222                try {
223                    short bgid = ((IoUringStreamChannelConfig) config()).getBufferGroupId();
224                    if (bgid >= 0) {
225                        final IoUringIoHandler ioUringIoHandler = registration().attachment();
226                        bufferRing = ioUringIoHandler.findBufferRing(bgid);
227                    }
228                    if (active) {
229                        // Register for POLLRDHUP if this channel is already considered active.
230                        schedulePollRdHup();
231                    }
232                } finally {
233                    promise.setSuccess();
234                }
235             } else {
236                 promise.setFailure(f.cause());
237             }
238         });
239 
240         super.doRegister(registerPromise);
241     }
242 
243     @Override
244     protected Object filterOutboundMessage(Object msg) {
245         if (IoUring.isSpliceSupported() && msg instanceof DefaultFileRegion) {
246             return new IoUringFileRegion((DefaultFileRegion) msg);
247         }
248 
249         if (msg instanceof FileRegion) {
250             // Generic FileRegion -- pass through to the write path for chunked conversion.
251             return msg;
252         }
253 
254         return super.filterOutboundMessage(msg);
255     }
256 
257     protected class IoUringStreamUnsafe extends AbstractUringUnsafe {
258 
259         private ByteBuf readBuffer;
260 
261         // Chunk buffer for generic FileRegion writes. Non-null while a send is in flight.
262         private ByteBuf fileRegionChunkBuf;
263 
264         @Override
265         protected int scheduleWriteMultiple(ChannelOutboundBuffer in) {
266             assert writeId == 0;
267 
268             int fd = fd().intValue();
269             IoRegistration registration = registration();
270             IoUringIoHandler handler = registration.attachment();
271             IovArray iovArray = handler.iovArray();
272             int offset = iovArray.count();
273 
274             IovArrayReferenceCollector collector = handler.iovArrayReferenceCollector();
275             try {
276                 try {
277                     in.forEachFlushedMessage(filterWriteMultiple(collector));
278                 } catch (Exception e) {
279                     // This should never happen, anyway fallback to single write.
280                     return scheduleWriteSingle(in.current());
281                 }
282                 long iovArrayAddress = iovArray.memoryAddress(offset);
283                 int iovArrayLength = iovArray.count() - offset;
284                 // Should not use sendmsg_zc, just use normal writev.
285                 IoUringIoOps ops = IoUringIoOps.newWritev(
286                         fd, (byte) 0, 0, iovArrayAddress, iovArrayLength, nextOpsId());
287 
288                 byte opCode = ops.opcode();
289                 // record(...) copies the collector's references into the slot, so the collector stays reusable.
290                 writeTracker.recordStream(opCode, collector.referencesArray(), collector.referencesCount());
291                 writeId = registration.submit(ops);
292                 writeOpCode = opCode;
293                 if (writeId == 0) {
294                     writeTracker.abandonStream();
295                     return 0;
296                 }
297                 return 1;
298             } finally {
299                 // The slot copied the references it needs, and an exception must not leave the event loop's
300                 // shared collector holding this write's buffers.
301                 collector.reset();
302             }
303         }
304 
305         protected ChannelOutboundBuffer.MessageProcessor filterWriteMultiple(IovArrayReferenceCollector collector) {
306            return collector;
307         }
308 
309         @Override
310         protected int scheduleWriteSingle(Object msg) {
311             assert writeId == 0;
312 
313             int fd = fd().intValue();
314             IoRegistration registration = registration();
315             final IoUringIoOps ops;
316             if (msg instanceof IoUringFileRegion) {
317                 IoUringFileRegion fileRegion = (IoUringFileRegion) msg;
318                 try {
319                     fileRegion.open();
320                 } catch (IOException e) {
321                     this.handleWriteError(e);
322                     return 0;
323                 }
324                 ops = fileRegion.splice(fd);
325             } else if (msg instanceof FileRegion) {
326                 return scheduleWriteFileRegion(fd, registration, (FileRegion) msg);
327             } else {
328                 ByteBuf buf = (ByteBuf) msg;
329                 long address = IoUring.memoryAddress(buf) + buf.readerIndex();
330                 int length = buf.readableBytes();
331                 ops = IoUringIoOps.newSend(fd, (byte) 0, 0, address, length, nextOpsId());
332             }
333             byte opCode = ops.opcode();
334             // A splice picks its own data to tell its two stages apart, so it never enters the channel-level
335             // slot array used for zero-copy writes. It still has to occupy this single slot though: the file and
336             // pipe descriptors it splices between have to outlive the SQE, and writeTracker.retainAll()
337             // only retains what was recorded here.
338             writeTracker.recordStream(opCode, (ReferenceCounted) msg);
339             writeId = registration.submit(ops);
340             writeOpCode = opCode;
341             if (writeId == 0) {
342                 writeTracker.abandonStream();
343                 return 0;
344             }
345             return 1;
346         }
347 
348         // Read a chunk from a generic FileRegion into a direct ByteBuf and submit it as an
349         // io_uring async send. If fileRegionChunkBuf is non-null, re-submits the remaining
350         // bytes from a previous partial/failed send.
351         private int scheduleWriteFileRegion(int fd, IoRegistration registration, FileRegion region) {
352             ByteBuf buf = fileRegionChunkBuf;
353             if (buf == null) {
354                 long remaining = region.count() - region.transferred();
355                 if (remaining > 0) {
356                     int chunkSize = (int) Math.min(remaining, FILE_REGION_MAX_CHUNK_SIZE);
357                     buf = alloc().directBuffer(chunkSize);
358                     try {
359                         ByteBufWritableByteChannel ch = new ByteBufWritableByteChannel(buf);
360                         // Mirror epoll's writeFileRegion(): stop calling transferTo() once
361                         // the region reports it has been fully transferred. The FileRegion
362                         // contract permits implementations to assume no further invocations
363                         // past transferred() == count().
364                         while (buf.writableBytes() > 0 && region.transferred() < region.count()) {
365                             long t = region.transferTo(ch, region.transferred());
366                             if (t <= 0) {
367                                 break;
368                             }
369                         }
370                         if (buf.readableBytes() == 0) {
371                             buf.release();
372                             handleWriteError(new ChannelException(
373                                     "FileRegion.transferTo(...) produced 0 bytes (count="
374                                             + region.count() + ", transferred=" + region.transferred() + ')'));
375                             return 0;
376                         }
377                     } catch (Exception e) {
378                         buf.release();
379                         handleWriteError(e);
380                         return 0;
381                     }
382                 } else {
383                     // Empty or fully-transferred region. Submit a 0-byte send so the completion
384                     // path removes it from the outbound buffer via the normal async flow.
385                     buf = alloc().directBuffer(0);
386                 }
387                 fileRegionChunkBuf = buf;
388             }
389             long address = IoUring.memoryAddress(buf) + buf.readerIndex();
390             int length = buf.readableBytes();
391             IoUringIoOps ops = IoUringIoOps.newSend(fd, (byte) 0, 0, address, length, nextOpsId());
392             byte opCode = ops.opcode();
393             writeTracker.recordStream(opCode, buf);
394             writeId = registration.submit(ops);
395             writeOpCode = opCode;
396             if (writeId == 0) {
397                 writeTracker.abandonStream();
398                 // Submission only fails when the registration is no longer valid (channel is
399                 // being deregistered). Ending the slot above is the only cleanup needed here:
400                 // unregistered() will release fileRegionChunkBuf and the outbound buffer will
401                 // release the FileRegion -- mirroring the plain ByteBuf path above.
402                 return 0;
403             }
404             return 1;
405         }
406 
407         private int calculateRecvFlags(boolean first) {
408             // Depending on if this is the first read or not we will use Native.MSG_DONTWAIT.
409             // The idea is that if the socket is blocking we can do the first read in a blocking fashion
410             // and so not need to also register POLLIN. As we can not 100 % sure if reads after the first will
411             // be possible directly we schedule these with Native.MSG_DONTWAIT. This allows us to still be
412             // able to signal the fireChannelReadComplete() in a timely manner and be consistent with other
413             // transports.
414             if (first) {
415                 return 0;
416             }
417             return Native.MSG_DONTWAIT;
418         }
419 
420         private short calculateRecvIoPrio(boolean first, boolean socketIsEmpty) {
421             // Depending on if socketIsEmpty is true we will arm the poll upfront and skip the initial transfer
422             // attempt.
423             // See https://github.com/axboe/liburing/wiki/io_uring-and-networking-in-2023#socket-state
424             if (first) {
425                 // IORING_RECVSEND_POLL_FIRST and IORING_CQE_F_SOCK_NONEMPTY were added in the same release (5.19).
426                 // We need to check if it's supported as otherwise providing these would result in an -EINVAL.
427                 return socketIsEmpty && IoUring.isCqeFSockNonEmptySupported() ?
428                         Native.IORING_RECVSEND_POLL_FIRST : 0;
429             }
430             return 0;
431         }
432 
433         @Override
434         protected int scheduleRead0(boolean first, boolean socketIsEmpty) {
435             assert readBuffer == null;
436             assert readId == 0 : readId;
437             final IoUringRecvByteAllocatorHandle allocHandle = recvBufAllocHandle();
438 
439             if (bufferRing != null && bufferRing.isUsable()) {
440                 return scheduleReadProviderBuffer(bufferRing, first, socketIsEmpty);
441             }
442 
443             // We either have no buffer ring configured or we force a recv without using a buffer ring.
444             ByteBuf byteBuf = allocHandle.allocate(alloc());
445             try {
446                 int fd = fd().intValue();
447                 IoRegistration registration = registration();
448                 short ioPrio = calculateRecvIoPrio(first, socketIsEmpty);
449                 int recvFlags = calculateRecvFlags(first);
450                 short opsId = nextOpsId();
451 
452                 IoUringIoOps ops = IoUringIoOps.newRecv(fd, (byte) 0, ioPrio, recvFlags,
453                         IoUring.memoryAddress(byteBuf) + byteBuf.writerIndex(), byteBuf.writableBytes(), opsId);
454                 readId = registration.submit(ops);
455                 readOpCode = Native.IORING_OP_RECV;
456                 if (readId == 0) {
457                     return 0;
458                 }
459                 readBuffer = byteBuf;
460                 byteBuf = null;
461                 return 1;
462             } finally {
463                 if (byteBuf != null) {
464                     byteBuf.release();
465                 }
466             }
467         }
468 
469         private int scheduleReadProviderBuffer(IoUringBufferRing bufferRing, boolean first, boolean socketIsEmpty) {
470             short bgId = bufferRing.bufferGroupId();
471             try {
472                 boolean multishot = IoUring.isRecvMultishotEnabled();
473                 byte flags = (byte) Native.IOSQE_BUFFER_SELECT;
474                 short ioPrio;
475                 final int recvFlags;
476                 if (multishot) {
477                     ioPrio = Native.IORING_RECV_MULTISHOT;
478                     recvFlags = 0;
479                 } else {
480                     // We should only use the calculate*() methods if this is not a multishot recv, as otherwise
481                     // the would be applied until the multishot will be re-armed.
482                     ioPrio = calculateRecvIoPrio(first, socketIsEmpty);
483                     recvFlags = calculateRecvFlags(first);
484                 }
485                 if (IoUring.isRecvsendBundleEnabled()) {
486                     // See https://github.com/axboe/liburing/wiki/
487                     // What's-new-with-io_uring-in-6.10#add-support-for-sendrecv-bundles
488                     ioPrio |= Native.IORING_RECVSEND_BUNDLE;
489                 }
490                 IoRegistration registration = registration();
491                 int fd = fd().intValue();
492                 short opsId = nextOpsId();
493                 IoUringIoOps ops = IoUringIoOps.newRecv(
494                         fd, flags, ioPrio, recvFlags, 0,
495                         0, opsId, bgId
496                 );
497                 readId = registration.submit(ops);
498                 readOpCode = Native.IORING_OP_RECV;
499                 if (readId == 0) {
500                     return 0;
501                 }
502                 if (multishot) {
503                     // Return -1 to signal we used multishot and so expect multiple recvComplete(...) calls.
504                     return -1;
505                 }
506                 return 1;
507             } catch (IllegalArgumentException illegalArgumentException) {
508                 this.handleReadException(pipeline(), null, illegalArgumentException, false, recvBufAllocHandle());
509                 return 0;
510             }
511         }
512 
513         @Override
514         protected void readComplete0(byte op, int res, int flags, short data, int outstanding) {
515             ByteBuf byteBuf = readBuffer;
516             readBuffer = null;
517             if (res == Native.ERRNO_ECANCELED_NEGATIVE) {
518                 readId = 0;
519                 // In case of cancellation we should reset the last used buffer ring to null as we will select a new one
520                 // when calling scheduleRead(..)
521                 if (byteBuf != null) {
522                     //recv without buffer ring
523                     byteBuf.release();
524                 }
525                 return;
526             }
527             boolean rearm = (flags & Native.IORING_CQE_F_MORE) == 0;
528             boolean useBufferRing = (flags & Native.IORING_CQE_F_BUFFER) != 0;
529             short bid = (short) (flags >> Native.IORING_CQE_BUFFER_SHIFT);
530             boolean more = (flags & Native.IORING_CQE_F_BUF_MORE) != 0;
531 
532             boolean completeRead = shouldCompleteReadLoop(flags, isReadMultishot());
533             if (rearm) {
534                 // Only reset if we don't use multi-shot or we need to re-arm because the multi-shot was cancelled.
535                 readId = 0;
536             }
537 
538             boolean allDataRead = false;
539 
540             final IoUringRecvByteAllocatorHandle allocHandle = recvBufAllocHandle();
541             final ChannelPipeline pipeline = pipeline();
542 
543             try {
544                 if (res < 0) {
545                     if (res == Native.ERRNO_NOBUFS_NEGATIVE) {
546                         // try to expand the buffer ring by adding more buffers to it if there is any space left.
547                         if (!bufferRing.expand()) {
548                             // We couldn't expand the ring anymore so notify the user that we did run out of buffers
549                             // without the ability to expand it.
550                             // If this happens to often the user should most likely increase the buffer ring size.
551                             pipeline.fireUserEventTriggered(bufferRing.getExhaustedEvent());
552                         }
553 
554                         // Let's trigger a read again without consulting the RecvByteBufAllocator.Handle as
555                         // we can't count this as a "real" read operation.
556                         // Because of how our BufferRing works we should have it filled again.
557                         scheduleRead(allocHandle.isFirstRead());
558                         return;
559                     }
560 
561                     // If res is negative we should pass it to ioResult(...) which will either throw
562                     // or convert it to 0 if we could not read because the socket was not readable.
563                     allocHandle.lastBytesRead(ioResult("io_uring read", res));
564                 } else if (res > 0) {
565                     if (useBufferRing) {
566                         // If RECVSEND_BUNDLE is used we need to do a bit more work here.
567                         // In this case we might need to obtain multiple buffers out of the buffer ring as
568                         // multiple of them might have been filled for one recv operation.
569                         // See https://github.com/axboe/liburing/wiki/
570                         // What's-new-with-io_uring-in-6.10#add-support-for-sendrecv-bundles
571                         int read = res;
572                         for (;;) {
573                             int attemptedBytesRead = bufferRing.attemptedBytesRead(bid);
574                             // Snapshot the number of allocated buffers before calling useBuffer(...) as that call
575                             // might grow the ring (and so change allocatedBuffers) once the last posted buffer is
576                             // consumed. The bundle completion we are walking here was produced by the kernel using
577                             // the ring size as it was *before* any such growth, so the wrap-around for the next bid
578                             // in this bundle must be computed with that same (old) size to stay in sync with the
579                             // kernel.
580                             int allocatedBuffers = bufferRing.allocatedBuffers();
581                             byteBuf = bufferRing.useBuffer(bid, read, more);
582                             read -= byteBuf.readableBytes();
583                             allocHandle.attemptedBytesRead(attemptedBytesRead);
584                             allocHandle.lastBytesRead(byteBuf.readableBytes());
585 
586                             assert read >= 0;
587                             if (read == 0) {
588                                 // Just break here, we will handle the byteBuf below and also fill the bufferRing
589                                 // if needed later.
590                                 break;
591                             }
592                             allocHandle.incMessagesRead(1);
593                             pipeline.fireChannelRead(byteBuf);
594                             byteBuf = null;
595                             bid = bufferRing.nextBid(bid, allocatedBuffers);
596                             if (!allocHandle.continueReading()) {
597                                 // We should call fireChannelReadComplete() to mimic a normal read loop.
598                                 allocHandle.readComplete();
599                                 pipeline.fireChannelReadComplete();
600                                 allocHandle.reset(config());
601                             }
602                         }
603                     } else {
604                         int attemptedBytesRead = byteBuf.writableBytes();
605                         byteBuf.writerIndex(byteBuf.writerIndex() + res);
606                         allocHandle.attemptedBytesRead(attemptedBytesRead);
607                         allocHandle.lastBytesRead(res);
608                     }
609                 } else {
610                     // EOF which we signal with -1.
611                     allocHandle.lastBytesRead(-1);
612                 }
613                 if (allocHandle.lastBytesRead() <= 0) {
614                     // byteBuf might be null if we used a buffer ring.
615                     if (byteBuf != null) {
616                         // nothing was read, release the buffer.
617                         byteBuf.release();
618                         byteBuf = null;
619                     }
620                     allDataRead = allocHandle.lastBytesRead() < 0;
621                     if (allDataRead) {
622                         // There is nothing left to read as we received an EOF.
623                         shutdownInput(true);
624                     }
625                     allocHandle.readComplete();
626                     pipeline.fireChannelReadComplete();
627                     return;
628                 }
629 
630                 allocHandle.incMessagesRead(1);
631                 pipeline.fireChannelRead(byteBuf);
632                 byteBuf = null;
633                 scheduleNextRead(pipeline, allocHandle, rearm, completeRead);
634             } catch (Throwable t) {
635                 handleReadException(pipeline, byteBuf, t, allDataRead, allocHandle);
636             }
637         }
638 
639         private void scheduleNextRead(ChannelPipeline pipeline, IoUringRecvByteAllocatorHandle allocHandle,
640                                       boolean rearm, boolean completeRead) {
641             if (allocHandle.continueReading() && !completeRead) {
642                 if (rearm) {
643                     // We only should schedule another read if we need to rearm.
644                     // See https://github.com/axboe/liburing/wiki/io_uring-and-networking-in-2023#multi-shot
645                     scheduleRead(false);
646                 }
647             } else {
648                 // We did not fill the whole ByteBuf so we should break the "read loop" and try again later.
649                 allocHandle.readComplete();
650                 pipeline.fireChannelReadComplete();
651             }
652         }
653 
654         protected final void handleReadException(ChannelPipeline pipeline, ByteBuf byteBuf,
655                                          Throwable cause, boolean allDataRead,
656                                          IoUringRecvByteAllocatorHandle allocHandle) {
657             if (byteBuf != null) {
658                 if (byteBuf.isReadable()) {
659                     pipeline.fireChannelRead(byteBuf);
660                 } else {
661                     byteBuf.release();
662                 }
663             }
664             allocHandle.readComplete();
665             pipeline.fireChannelReadComplete();
666             pipeline.fireExceptionCaught(cause);
667             if (allDataRead || cause instanceof IOException) {
668                 shutdownInput(true);
669             }
670         }
671 
672         private boolean handleWriteCompleteFileRegion(ChannelOutboundBuffer channelOutboundBuffer,
673                                                       IoUringFileRegion fileRegion, int res, short data) {
674             try {
675                 if (res == Native.ERRNO_ECANCELED_NEGATIVE) {
676                     return true;
677                 }
678                 int result = res >= 0 ? res : ioResult("io_uring splice", res);
679                 if (result == 0 && fileRegion.count() > 0) {
680                     validateFileRegion(fileRegion.fileRegion, fileRegion.transfered());
681                     return false;
682                 }
683                 int progress = fileRegion.handleResult(result, data);
684                 if (progress == -1) {
685                     // Done with writing
686                     channelOutboundBuffer.remove();
687                 } else if (progress > 0) {
688                     channelOutboundBuffer.progress(progress);
689                 }
690             } catch (Throwable cause) {
691                 handleWriteError(cause);
692             }
693             return true;
694         }
695 
696         @Override
697         boolean writeComplete0(byte op, int res, int flags, long data, int outstanding) {
698             if ((flags & Native.IORING_CQE_F_NOTIF) == 0) {
699                 // We only want to reset these if IORING_CQE_F_NOTIF is not set.
700                 // If it's set we know this is only an extra notification for a write but we already handled
701                 // the write completions before.
702                 // See https://man7.org/linux/man-pages/man2/io_uring_enter.2.html section: IORING_OP_SEND_ZC
703                 writeId = 0;
704                 writeOpCode = 0;
705                 // A completion that never went through the slot finds it inactive, which makes this a no-op.
706                 writeTracker.completeStream(flags);
707             }
708             ChannelOutboundBuffer channelOutboundBuffer = unsafe().outboundBuffer();
709             if (channelOutboundBuffer == null) {
710                 // The completion may arrive after close() or shutdownOutput() already dropped the buffer.
711                 releaseFileRegionChunkBuf();
712                 return true;
713             }
714             Object current = channelOutboundBuffer.current();
715             if (current instanceof IoUringFileRegion) {
716                 IoUringFileRegion fileRegion = (IoUringFileRegion) current;
717                 // A splice picks its own data to tell its two stages apart, so narrowing here can not drop bits.
718                 return handleWriteCompleteFileRegion(channelOutboundBuffer, fileRegion, res, (short) data);
719             }
720 
721             if (current instanceof FileRegion) {
722                 return handleWriteCompleteGenericFileRegion(
723                         channelOutboundBuffer, (FileRegion) current, res);
724             }
725 
726             if (res >= 0) {
727                 channelOutboundBuffer.removeBytes(res);
728             } else if (res == Native.ERRNO_ECANCELED_NEGATIVE) {
729                 return true;
730             } else {
731                 try {
732                     if (ioResult("io_uring write", res) == 0) {
733                         return false;
734                     }
735                 } catch (Throwable cause) {
736                     handleWriteError(cause);
737                 }
738             }
739             return true;
740         }
741 
742         // Returns true when the completion can be treated as "written all" for this SQE
743         // (the framework may still schedule further writes from the outbound buffer); returns
744         // false to signal the framework that POLLOUT should be armed so the chunk buffer is
745         // resubmitted once the socket becomes writable again.
746         private boolean handleWriteCompleteGenericFileRegion(
747                 ChannelOutboundBuffer channelOutboundBuffer, FileRegion region, int res) {
748             try {
749                 if (res == Native.ERRNO_ECANCELED_NEGATIVE) {
750                     releaseFileRegionChunkBuf();
751                     return true;
752                 }
753                 if (res >= 0) {
754                     ByteBuf buf = fileRegionChunkBuf;
755                     assert buf != null;
756                     buf.skipBytes(res);
757                     channelOutboundBuffer.progress(res);
758                     if (!buf.isReadable()) {
759                         // Chunk fully sent.
760                         releaseFileRegionChunkBuf();
761                         if (region.transferred() >= region.count()) {
762                             channelOutboundBuffer.remove();
763                         }
764                     } else {
765                         // Partial send -- schedule POLLOUT to re-send the remainder.
766                         return false;
767                     }
768                 } else {
769                     // Keep the chunk buffer -- on retryable errors (EAGAIN) ioResult returns 0
770                     // and scheduleWriteFileRegion() will re-submit the same fileRegionChunkBuf
771                     // once POLLOUT fires. On a non-retryable error ioResult throws, and the
772                     // outer catch releases the buffer.
773                     if (ioResult("io_uring write", res) == 0) {
774                         return false;
775                     }
776                 }
777             } catch (Throwable cause) {
778                 releaseFileRegionChunkBuf();
779                 handleWriteError(cause);
780             }
781             return true;
782         }
783 
784         private void releaseFileRegionChunkBuf() {
785             if (fileRegionChunkBuf != null) {
786                 fileRegionChunkBuf.release();
787                 fileRegionChunkBuf = null;
788             }
789         }
790 
791         @Override
792         public void unregistered() {
793             // Abandons the single slot through writeTracker.releaseAll() before the chunk buffer is
794             // dropped below, so a reference a shutdown retained on that buffer is released first.
795             super.unregistered();
796             assert readBuffer == null;
797             releaseFileRegionChunkBuf();
798         }
799     }
800 
801     @Override
802     protected final void cancelOutstandingReads(IoRegistration registration, int numOutstandingReads) {
803         if (readId != 0) {
804             // Let's try to cancel outstanding reads as these might be submitted and waiting for data (via fastpoll).
805             assert numOutstandingReads == 1 || numOutstandingReads == -1;
806             IoUringIoOps ops = IoUringIoOps.newAsyncCancel((byte) 0, readId, readOpCode);
807             long id = registration.submit(ops);
808             assert id != 0;
809             readId = 0;
810         }
811     }
812 
813     @Override
814     protected final void cancelOutstandingWrites(IoRegistration registration, int numOutstandingWrites) {
815         if (writeId != 0) {
816             // Let's try to cancel outstanding writes as these might be submitted and waiting to finish writing
817             // (via fastpoll).
818             assert numOutstandingWrites == 1;
819             assert writeOpCode != 0;
820             long id = registration.submit(IoUringIoOps.newAsyncCancel((byte) 0, writeId, writeOpCode));
821             assert id != 0;
822             writeId = 0;
823         }
824     }
825 
826     @Override
827     protected boolean socketIsEmpty(int flags) {
828         return IoUring.isCqeFSockNonEmptySupported() && (flags & Native.IORING_CQE_F_SOCK_NONEMPTY) == 0;
829     }
830 
831     protected boolean shouldCompleteReadLoop(int flags, boolean multishot) {
832         return socketIsEmpty(flags);
833     }
834 
835     @Override
836     boolean isPollInFirst() {
837         return bufferRing == null || !bufferRing.isUsable();
838     }
839 
840     /**
841      * A {@link WritableByteChannel} backed by a {@link ByteBuf}.
842      * Writes are capped to {@link ByteBuf#writableBytes()} to prevent overflow
843      * when {@link FileRegion#transferTo} writes more than the chunk size.
844      */
845     private static final class ByteBufWritableByteChannel implements WritableByteChannel {
846         private final ByteBuf buf;
847 
848         ByteBufWritableByteChannel(ByteBuf buf) {
849             this.buf = buf;
850         }
851 
852         @Override
853         public int write(ByteBuffer src) {
854             int toWrite = Math.min(src.remaining(), buf.writableBytes());
855             if (toWrite == 0) {
856                 return 0;
857             }
858             if (toWrite < src.remaining()) {
859                 int oldLimit = src.limit();
860                 src.limit(src.position() + toWrite);
861                 buf.writeBytes(src);
862                 src.limit(oldLimit);
863                 return toWrite;
864             }
865             buf.writeBytes(src);
866             return toWrite;
867         }
868 
869         @Override
870         public boolean isOpen() {
871             return true;
872         }
873 
874         @Override
875         public void close() {
876             // NOOP
877         }
878     }
879 }