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