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                             byteBuf = bufferRing.useBuffer(bid, read, more);
575                             read -= byteBuf.readableBytes();
576                             allocHandle.attemptedBytesRead(attemptedBytesRead);
577                             allocHandle.lastBytesRead(byteBuf.readableBytes());
578 
579                             assert read >= 0;
580                             if (read == 0) {
581                                 // Just break here, we will handle the byteBuf below and also fill the bufferRing
582                                 // if needed later.
583                                 break;
584                             }
585                             allocHandle.incMessagesRead(1);
586                             pipeline.fireChannelRead(byteBuf);
587                             byteBuf = null;
588                             bid = bufferRing.nextBid(bid);
589                             if (!allocHandle.continueReading()) {
590                                 // We should call fireChannelReadComplete() to mimic a normal read loop.
591                                 allocHandle.readComplete();
592                                 pipeline.fireChannelReadComplete();
593                                 allocHandle.reset(config());
594                             }
595                         }
596                     } else {
597                         int attemptedBytesRead = byteBuf.writableBytes();
598                         byteBuf.writerIndex(byteBuf.writerIndex() + res);
599                         allocHandle.attemptedBytesRead(attemptedBytesRead);
600                         allocHandle.lastBytesRead(res);
601                     }
602                 } else {
603                     // EOF which we signal with -1.
604                     allocHandle.lastBytesRead(-1);
605                 }
606                 if (allocHandle.lastBytesRead() <= 0) {
607                     // byteBuf might be null if we used a buffer ring.
608                     if (byteBuf != null) {
609                         // nothing was read, release the buffer.
610                         byteBuf.release();
611                         byteBuf = null;
612                     }
613                     allDataRead = allocHandle.lastBytesRead() < 0;
614                     if (allDataRead) {
615                         // There is nothing left to read as we received an EOF.
616                         shutdownInput(true);
617                     }
618                     allocHandle.readComplete();
619                     pipeline.fireChannelReadComplete();
620                     return;
621                 }
622 
623                 allocHandle.incMessagesRead(1);
624                 pipeline.fireChannelRead(byteBuf);
625                 byteBuf = null;
626                 scheduleNextRead(pipeline, allocHandle, rearm, completeRead);
627             } catch (Throwable t) {
628                 handleReadException(pipeline, byteBuf, t, allDataRead, allocHandle);
629             }
630         }
631 
632         private void scheduleNextRead(ChannelPipeline pipeline, IoUringRecvByteAllocatorHandle allocHandle,
633                                       boolean rearm, boolean completeRead) {
634             if (allocHandle.continueReading() && !completeRead) {
635                 if (rearm) {
636                     // We only should schedule another read if we need to rearm.
637                     // See https://github.com/axboe/liburing/wiki/io_uring-and-networking-in-2023#multi-shot
638                     scheduleRead(false);
639                 }
640             } else {
641                 // We did not fill the whole ByteBuf so we should break the "read loop" and try again later.
642                 allocHandle.readComplete();
643                 pipeline.fireChannelReadComplete();
644             }
645         }
646 
647         protected final void handleReadException(ChannelPipeline pipeline, ByteBuf byteBuf,
648                                          Throwable cause, boolean allDataRead,
649                                          IoUringRecvByteAllocatorHandle allocHandle) {
650             if (byteBuf != null) {
651                 if (byteBuf.isReadable()) {
652                     pipeline.fireChannelRead(byteBuf);
653                 } else {
654                     byteBuf.release();
655                 }
656             }
657             allocHandle.readComplete();
658             pipeline.fireChannelReadComplete();
659             pipeline.fireExceptionCaught(cause);
660             if (allDataRead || cause instanceof IOException) {
661                 shutdownInput(true);
662             }
663         }
664 
665         private boolean handleWriteCompleteFileRegion(ChannelOutboundBuffer channelOutboundBuffer,
666                                                       IoUringFileRegion fileRegion, int res, short data) {
667             try {
668                 if (res == Native.ERRNO_ECANCELED_NEGATIVE) {
669                     return true;
670                 }
671                 int result = res >= 0 ? res : ioResult("io_uring splice", res);
672                 if (result == 0 && fileRegion.count() > 0) {
673                     validateFileRegion(fileRegion.fileRegion, fileRegion.transfered());
674                     return false;
675                 }
676                 int progress = fileRegion.handleResult(result, data);
677                 if (progress == -1) {
678                     // Done with writing
679                     channelOutboundBuffer.remove();
680                 } else if (progress > 0) {
681                     channelOutboundBuffer.progress(progress);
682                 }
683             } catch (Throwable cause) {
684                 handleWriteError(cause);
685             }
686             return true;
687         }
688 
689         @Override
690         boolean writeComplete0(byte op, int res, int flags, long data, int outstanding) {
691             if ((flags & Native.IORING_CQE_F_NOTIF) == 0) {
692                 // We only want to reset these if IORING_CQE_F_NOTIF is not set.
693                 // If it's set we know this is only an extra notification for a write but we already handled
694                 // the write completions before.
695                 // See https://man7.org/linux/man-pages/man2/io_uring_enter.2.html section: IORING_OP_SEND_ZC
696                 writeId = 0;
697                 writeOpCode = 0;
698                 // A completion that never went through the slot finds it inactive, which makes this a no-op.
699                 writeTracker.completeStream(flags);
700             }
701             ChannelOutboundBuffer channelOutboundBuffer = unsafe().outboundBuffer();
702             if (channelOutboundBuffer == null) {
703                 // The completion may arrive after close() or shutdownOutput() already dropped the buffer.
704                 releaseFileRegionChunkBuf();
705                 return true;
706             }
707             Object current = channelOutboundBuffer.current();
708             if (current instanceof IoUringFileRegion) {
709                 IoUringFileRegion fileRegion = (IoUringFileRegion) current;
710                 // A splice picks its own data to tell its two stages apart, so narrowing here can not drop bits.
711                 return handleWriteCompleteFileRegion(channelOutboundBuffer, fileRegion, res, (short) data);
712             }
713 
714             if (current instanceof FileRegion) {
715                 return handleWriteCompleteGenericFileRegion(
716                         channelOutboundBuffer, (FileRegion) current, res);
717             }
718 
719             if (res >= 0) {
720                 channelOutboundBuffer.removeBytes(res);
721             } else if (res == Native.ERRNO_ECANCELED_NEGATIVE) {
722                 return true;
723             } else {
724                 try {
725                     if (ioResult("io_uring write", res) == 0) {
726                         return false;
727                     }
728                 } catch (Throwable cause) {
729                     handleWriteError(cause);
730                 }
731             }
732             return true;
733         }
734 
735         // Returns true when the completion can be treated as "written all" for this SQE
736         // (the framework may still schedule further writes from the outbound buffer); returns
737         // false to signal the framework that POLLOUT should be armed so the chunk buffer is
738         // resubmitted once the socket becomes writable again.
739         private boolean handleWriteCompleteGenericFileRegion(
740                 ChannelOutboundBuffer channelOutboundBuffer, FileRegion region, int res) {
741             try {
742                 if (res == Native.ERRNO_ECANCELED_NEGATIVE) {
743                     releaseFileRegionChunkBuf();
744                     return true;
745                 }
746                 if (res >= 0) {
747                     ByteBuf buf = fileRegionChunkBuf;
748                     assert buf != null;
749                     buf.skipBytes(res);
750                     channelOutboundBuffer.progress(res);
751                     if (!buf.isReadable()) {
752                         // Chunk fully sent.
753                         releaseFileRegionChunkBuf();
754                         if (region.transferred() >= region.count()) {
755                             channelOutboundBuffer.remove();
756                         }
757                     } else {
758                         // Partial send -- schedule POLLOUT to re-send the remainder.
759                         return false;
760                     }
761                 } else {
762                     // Keep the chunk buffer -- on retryable errors (EAGAIN) ioResult returns 0
763                     // and scheduleWriteFileRegion() will re-submit the same fileRegionChunkBuf
764                     // once POLLOUT fires. On a non-retryable error ioResult throws, and the
765                     // outer catch releases the buffer.
766                     if (ioResult("io_uring write", res) == 0) {
767                         return false;
768                     }
769                 }
770             } catch (Throwable cause) {
771                 releaseFileRegionChunkBuf();
772                 handleWriteError(cause);
773             }
774             return true;
775         }
776 
777         private void releaseFileRegionChunkBuf() {
778             if (fileRegionChunkBuf != null) {
779                 fileRegionChunkBuf.release();
780                 fileRegionChunkBuf = null;
781             }
782         }
783 
784         @Override
785         public void unregistered() {
786             // Abandons the single slot through writeTracker.releaseAll() before the chunk buffer is
787             // dropped below, so a reference a shutdown retained on that buffer is released first.
788             super.unregistered();
789             assert readBuffer == null;
790             releaseFileRegionChunkBuf();
791         }
792     }
793 
794     @Override
795     protected final void cancelOutstandingReads(IoRegistration registration, int numOutstandingReads) {
796         if (readId != 0) {
797             // Let's try to cancel outstanding reads as these might be submitted and waiting for data (via fastpoll).
798             assert numOutstandingReads == 1 || numOutstandingReads == -1;
799             IoUringIoOps ops = IoUringIoOps.newAsyncCancel((byte) 0, readId, readOpCode);
800             long id = registration.submit(ops);
801             assert id != 0;
802             readId = 0;
803         }
804     }
805 
806     @Override
807     protected final void cancelOutstandingWrites(IoRegistration registration, int numOutstandingWrites) {
808         if (writeId != 0) {
809             // Let's try to cancel outstanding writes as these might be submitted and waiting to finish writing
810             // (via fastpoll).
811             assert numOutstandingWrites == 1;
812             assert writeOpCode != 0;
813             long id = registration.submit(IoUringIoOps.newAsyncCancel((byte) 0, writeId, writeOpCode));
814             assert id != 0;
815             writeId = 0;
816         }
817     }
818 
819     @Override
820     protected boolean socketIsEmpty(int flags) {
821         return IoUring.isCqeFSockNonEmptySupported() && (flags & Native.IORING_CQE_F_SOCK_NONEMPTY) == 0;
822     }
823 
824     protected boolean shouldCompleteReadLoop(int flags, boolean multishot) {
825         return socketIsEmpty(flags);
826     }
827 
828     @Override
829     boolean isPollInFirst() {
830         return bufferRing == null || !bufferRing.isUsable();
831     }
832 
833     /**
834      * A {@link WritableByteChannel} backed by a {@link ByteBuf}.
835      * Writes are capped to {@link ByteBuf#writableBytes()} to prevent overflow
836      * when {@link FileRegion#transferTo} writes more than the chunk size.
837      */
838     private static final class ByteBufWritableByteChannel implements WritableByteChannel {
839         private final ByteBuf buf;
840 
841         ByteBufWritableByteChannel(ByteBuf buf) {
842             this.buf = buf;
843         }
844 
845         @Override
846         public int write(ByteBuffer src) {
847             int toWrite = Math.min(src.remaining(), buf.writableBytes());
848             if (toWrite == 0) {
849                 return 0;
850             }
851             if (toWrite < src.remaining()) {
852                 int oldLimit = src.limit();
853                 src.limit(src.position() + toWrite);
854                 buf.writeBytes(src);
855                 src.limit(oldLimit);
856                 return toWrite;
857             }
858             buf.writeBytes(src);
859             return toWrite;
860         }
861 
862         @Override
863         public boolean isOpen() {
864             return true;
865         }
866 
867         @Override
868         public void close() {
869             // NOOP
870         }
871     }
872 }