All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.twitter.finagle.netty4.transport.buoyant.BufferingChannelTransport.scala Maven / Gradle / Ivy

package com.twitter.finagle.netty4.transport.buoyant

import com.twitter.concurrent.AsyncQueue
import com.twitter.finagle.ChannelException
import com.twitter.finagle.netty4.transport.ChannelTransport
import com.twitter.util.{Future, Promise}
import io.netty.channel.ChannelFuture
import io.netty.{channel => nettyChan}
import java.util
import java.util.concurrent.LinkedBlockingQueue
import java.util.concurrent.atomic.AtomicBoolean

/**
 * A Transport implementation based on Netty's Channel which buffers writes.  This Transport buffers
 * writes to a queue and schedules an event on the channel's event loop to write the items and then
 * flush them.  This allows writes to be batched and flushed together rather than needing to flush
 * after each individual write.  The result is fewer syscalls which leads to better performance.
 * This approach was highly inspired by grpc-java's WriteQueue:
 * https://github.com/grpc/grpc-java/pull/431
 *
 * @param ch the underlying netty channel
 *
 * @param readQueue the queue used to buffer inbound messages
 *
 * @note During the construction, a `ChannelTransport` inserts the terminating
 *       inbound channel handler into the channel's pipeline so any inbound channel
 *       handlers inserted after that won't get any of the inbound traffic.
 */
class BufferingChannelTransport(
  ch: nettyChan.Channel,
  readQueue: AsyncQueue[Any] = new AsyncQueue[Any],
  omitStackTraceOnInactive: Boolean = false
) extends ChannelTransport(ch, readQueue, omitStackTraceOnInactive) {

  // Satisfy the done promise when the write completes.
  private case class WriteItem(msg: Any, done: Promise[Unit])

  // Always flush after this many messages.
  // This value was cargo-culted from
  // https://github.com/grpc/grpc-java/pull/431/files#diff-7f048858dab93d58f2bcac583626abddR49
  // This is mostly just a safety net to ensure that we don't buffer up arbitrarily large writes
  // without flushing. In most cases I would expect us to flush before hitting this limit.
  private[this] val MaxFlushSize = 128
  private[this] val flushScheduled = new AtomicBoolean(false)
  private[this] val writeQueue = new LinkedBlockingQueue[WriteItem]()
  private[this] val writeChunk = new util.ArrayDeque[WriteItem](MaxFlushSize)

  override def write(msg: Any): Future[Unit] = {
    val p = new Promise[Unit]()
    writeQueue.add(WriteItem(msg, p))
    scheduleFlush()
    p
  }

  private[this] def scheduleFlush(): Unit = {
    if (flushScheduled.compareAndSet(false, true)) {
      ch.eventLoop().execute(flush)
    }
  }

  private[this] val flush: Runnable = { () =>
    var flushed = false
    while (writeQueue.drainTo(writeChunk, MaxFlushSize) > 0) {
      while (writeChunk.size > 0) {
        val item = writeChunk.poll()
        val f = toFuture(ch.write(item.msg))
        item.done.become(f)
      }
      flushed = true
      ch.flush()
    }
    // Always flush at least once.
    if (!flushed) {
      ch.flush()
    }

    flushScheduled.set(false)
    if (!writeQueue.isEmpty) {
      scheduleFlush()
    }
  }

  private[this] def toFuture(op: ChannelFuture): Future[Unit] = {
    val p = new Promise[Unit]
    op.addListener { f: ChannelFuture =>
      if (f.isSuccess) {
        p.setDone(); ()
      } else {
        p.setException(ChannelException(f.cause, context.remoteAddress))
      }
    }
    p
  }
}





© 2015 - 2024 Weber Informatics LLC | Privacy Policy