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

com.twitter.finagle.dispatch.PipeliningDispatcher.scala Maven / Gradle / Ivy

There is a newer version: 7.0.0
Show newest version
package com.twitter.finagle.dispatch

import com.twitter.concurrent.{AsyncQueue, AsyncMutex}
import com.twitter.conversions.time._
import com.twitter.finagle.Failure
import com.twitter.finagle.stats.StatsReceiver
import com.twitter.finagle.transport.Transport
import com.twitter.logging.Logger
import com.twitter.util.{Future, Promise, Timer}

/**
 * A generic pipelining dispatcher, which assumes that servers will
 * respect normal pipelining semantics, and that replies will be sent
 * in the same order as requests were sent.  Exploits
 * [[GenSerialClientDispatcher]] to serialize requests.
 *
 * Because many requests might be sharing the same transport,
 * [[com.twitter.util.Future Futures]] returned by PipeliningDispatcher#apply
 * are masked, and will only propagate the interrupt if the future doesn't
 * return after 10 seconds after the interruption.  This ensures that
 * interrupting a Future in one request won't change the result of another
 * request unless the connection is stuck, and does not look like it will make
 * progress.
 *
 * @param statsReceiver typically scoped to `clientName/dispatcher`
 */
private[finagle] class PipeliningDispatcher[Req, Rep](
    trans: Transport[Req, Rep],
    statsReceiver: StatsReceiver,
    timer: Timer)
  extends GenSerialClientDispatcher[Req, Rep, Req, Rep](
    trans,
    statsReceiver) { self =>
  import PipeliningDispatcher._

  // thread-safety provided by synchronization on this
  private[this] var stalled = false
  private[this] val q = new AsyncQueue[Promise[Rep]]

  private[this] val queueSize =
    statsReceiver.scope("pipelining").addGauge("pending") {
      q.size
    }

  private[this] val transRead: Promise[Rep] => Unit =
    p =>
      trans.read().respond { res =>
        try p.update(res)
        finally loop()
      }

  // this is unbounded because we assume a higher layer bounds how many
  // concurrent requests we can have
  private[this] val mutex = new AsyncMutex()

  private[this] def loop(): Unit =
    q.poll().onSuccess(transRead)

  loop()

  protected def dispatch(req: Req, p: Promise[Rep]): Future[Unit] =
    mutex.acquire().flatMap { permit =>
      // we must map on offering so that we don't relinquish the mutex until we
      // have enqueued the promise, so we don't have to worry about out of order
      // Promises
      trans.write(req).before { q.offer(p); Future.Done }.ensure {
        permit.release()
      }
    }

  override def apply(req: Req): Future[Rep] = {
    val f = super.apply(req)
    val p = Promise[Rep]()
    f.proxyTo(p)

    p.setInterruptHandler {
      case t: Throwable =>
        f.raiseWithin(TimeToWaitForStalledPipeline, StalledPipelineException)(timer)
        self.synchronized {
          // we check stalled so that we log exactly once per failed pipeline
          if (!stalled) {
            stalled = true
            val addr = trans.remoteAddress
            PipeliningDispatcher.log.warning(
              s"pipelined connection stalled with ${q.size} items, talking to $addr")
          }
        }
    }
    p
  }
}

private object PipeliningDispatcher {
  val log = Logger.get(getClass.getName)

  val TimeToWaitForStalledPipeline = 10.seconds

  val StalledPipelineException =
    Failure(
      s"The connection pipeline could not make progress in $TimeToWaitForStalledPipeline",
      Failure.Interrupted)

  object Timeout {
    def unapply(t: Throwable): Option[Throwable] = t match {
      case exc: com.twitter.util.TimeoutException => Some(exc)
      case exc: com.twitter.finagle.TimeoutException => Some(exc)
      case _ => None
    }
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy