/**
   * Requests buffers from the buffer provider and triggers asynchronous read requests to fill them.
   *
   * <p>The number of requested buffers/triggered I/O read requests per call depends on the
   * configured size of batch reads.
   */
  private void readNextBatchAsync() throws IOException {
    // This does not need to be fully synchronized with actually reaching EOF as long as
    // we eventually notice it. In the worst case, we trigger some discarded reads and
    // notice it when the buffers are returned.
    //
    // We only trigger reads if the current batch size is 0.
    if (hasReachedEndOfFile || currentBatchSize.get() != 0) {
      return;
    }

    // Number of successful buffer requests or callback registrations. The call back will
    // trigger the read as soon as a buffer becomes available again.
    int i = 0;

    while (i < readBatchSize) {
      final Buffer buffer = bufferProvider.requestBuffer();

      if (buffer == null) {
        // Listen for buffer availability.
        currentBatchSize.incrementAndGet();

        if (bufferProvider.addListener(bufferAvailabilityListener)) {
          i++;
        } else if (bufferProvider.isDestroyed()) {
          currentBatchSize.decrementAndGet();
          return;
        } else {
          // Buffer available again
          currentBatchSize.decrementAndGet();
        }
      } else {
        currentBatchSize.incrementAndGet();

        asyncFileReader.readInto(buffer);
      }
    }
  }
    private boolean waitForBuffer(
        BufferProvider bufferProvider, NettyMessage.BufferResponse bufferResponse) {

      stagedBufferResponse = bufferResponse;

      if (bufferProvider.addListener(this)) {
        if (ctx.channel().config().isAutoRead()) {
          ctx.channel().config().setAutoRead(false);
        }

        return true;
      } else {
        stagedBufferResponse = null;

        return false;
      }
    }
  private boolean decodeBufferOrEvent(
      RemoteInputChannel inputChannel, NettyMessage.BufferResponse bufferOrEvent) throws Throwable {
    boolean releaseNettyBuffer = true;

    try {
      if (bufferOrEvent.isBuffer()) {
        // ---- Buffer ------------------------------------------------

        // Early return for empty buffers. Otherwise Netty's readBytes() throws an
        // IndexOutOfBoundsException.
        if (bufferOrEvent.getSize() == 0) {
          inputChannel.onEmptyBuffer(bufferOrEvent.sequenceNumber);
          return true;
        }

        BufferProvider bufferProvider = inputChannel.getBufferProvider();

        if (bufferProvider == null) {

          cancelRequestFor(bufferOrEvent.receiverId);

          return false; // receiver has been cancelled/failed
        }

        while (true) {
          Buffer buffer = bufferProvider.requestBuffer();

          if (buffer != null) {
            buffer.setSize(bufferOrEvent.getSize());
            bufferOrEvent.getNettyBuffer().readBytes(buffer.getNioBuffer());

            inputChannel.onBuffer(buffer, bufferOrEvent.sequenceNumber);

            return true;
          } else if (bufferListener.waitForBuffer(bufferProvider, bufferOrEvent)) {
            releaseNettyBuffer = false;

            return false;
          } else if (bufferProvider.isDestroyed()) {
            return false;
          }
        }
      } else {
        // ---- Event -------------------------------------------------
        // TODO We can just keep the serialized data in the Netty buffer and release it later at the
        // reader
        byte[] byteArray = new byte[bufferOrEvent.getSize()];
        bufferOrEvent.getNettyBuffer().readBytes(byteArray);

        Buffer buffer =
            new Buffer(new MemorySegment(byteArray), FreeingBufferRecycler.INSTANCE, false);

        inputChannel.onBuffer(buffer, bufferOrEvent.sequenceNumber);

        return true;
      }
    } finally {
      if (releaseNettyBuffer) {
        bufferOrEvent.releaseBuffer();
      }
    }
  }