docs: protected synchronized void flushSenders() {
          if(flushingSenders) {
              throw UndertowMessages.MESSAGES.recursiveCallToFlushingSenders();
          }
          flushingSenders = true;
          try {
              int toSend = 0;
              while (!newFrames.isEmpty()) {
                  S frame = newFrames.poll();
                  frame.preWrite();
                  if (framePriority.insertFrame(frame, pendingFrames)) {
                      if (!heldFrames.isEmpty()) {
                          framePriority.frameAdded(frame, pendingFrames, heldFrames);
                      }
                  } else {
                      heldFrames.add(frame);
                  }
              }
  
              boolean finalFrame = false;
              ListIterator<S> it = pendingFrames.listIterator();
              while (it.hasNext()) {
                  S sender = it.next();
                  if (sender.isReadyForFlush()) {
                      ++toSend;
                  } else {
                      break;
                  }
                  if (sender.isLastFrame()) {
                      finalFrame = true;
                  }
              }
              if (toSend == 0) {
                  //if there is nothing to send we just attempt a flush on the underlying channel
                  try {
                      if(channel.getSinkChannel().flush()) {
                          channel.getSinkChannel().suspendWrites();
                      }
                  } catch (Throwable e) {
                      safeClose(channel);
                      markWritesBroken(e);
                  }
                  return;
              }
              ByteBuffer[] data = new ByteBuffer[toSend * 3];
              int j = 0;
              it = pendingFrames.listIterator();
              try {
                  while (j < toSend) {
                      S next = it.next();
                      //todo: rather than adding empty buffers just store the offsets
                      SendFrameHeader frameHeader = next.getFrameHeader();
                      PooledByteBuffer frameHeaderByteBuffer = frameHeader.getByteBuffer();
                      ByteBuffer frameTrailerBuffer = frameHeader.getTrailer();
                      data[j * 3] = frameHeaderByteBuffer != null
                              ? frameHeaderByteBuffer.getBuffer()
                              : Buffers.EMPTY_BYTE_BUFFER;
                      data[(j * 3) + 1] = next.getBuffer() == null ? Buffers.EMPTY_BYTE_BUFFER : next.getBuffer();
                      data[(j * 3) + 2] = frameTrailerBuffer != null ? frameTrailerBuffer : Buffers.EMPTY_BYTE_BUFFER;
                      ++j;
                  }
                  long toWrite = Buffers.remaining(data);
                  long res;
                  do {
                      res = channel.getSinkChannel().write(data);
                      toWrite -= res;
                  } while (res > 0 && toWrite > 0);
                  int max = toSend;
  
                  while (max > 0) {
                      S sinkChannel = pendingFrames.get(0);
                      PooledByteBuffer frameHeaderByteBuffer = sinkChannel.getFrameHeader().getByteBuffer();
                      ByteBuffer frameTrailerBuffer = sinkChannel.getFrameHeader().getTrailer();
                      if (frameHeaderByteBuffer != null && frameHeaderByteBuffer.getBuffer().hasRemaining()
                              || sinkChannel.getBuffer() != null && sinkChannel.getBuffer().hasRemaining()
                              || frameTrailerBuffer != null && frameTrailerBuffer.hasRemaining()) {
                          break;
                      }
                      sinkChannel.flushComplete();
                      pendingFrames.remove(sinkChannel);
                      max--;
                  }
                  if (!pendingFrames.isEmpty() || !channel.getSinkChannel().flush()) {
                      channel.getSinkChannel().resumeWrites();
                  } else {
                      channel.getSinkChannel().suspendWrites();
                  }
                  if (pendingFrames.isEmpty() && finalFrame) {
                      //all data has been sent. Close gracefully
                      channel.getSinkChannel().shutdownWrites();
                      if (!channel.getSinkChannel().flush()) {
                          channel.getSinkChannel().setWriteListener(ChannelListeners.flushingChannelListener(null, null));
                          channel.getSinkChannel().resumeWrites();
                      }
                  }
  
              } catch (IOException|RuntimeException|Error e) {
                  safeClose(channel);
                  markWritesBroken(e);
              }
          } finally {
              flushingSenders = false;
              if(!newFrames.isEmpty()) {
                  runInIoThread(new Runnable() {
                      @Override
                      public void run() {
                          flushSenders();
                      }
                  });
              }
          }
      }