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

com.twitter.finagle.mux.Server.scala Maven / Gradle / Ivy

There is a newer version: 6.39.0
Show newest version
package com.twitter.finagle.mux

import com.twitter.app.GlobalFlag
import com.twitter.conversions.time._
import com.twitter.finagle._
import com.twitter.finagle.context.{Contexts, RemoteInfo}
import com.twitter.finagle.mux.lease.exp.{Lessee, Lessor, nackOnExpiredLease}
import com.twitter.finagle.mux.transport.Message
import com.twitter.finagle.stats.{NullStatsReceiver, StatsReceiver}
import com.twitter.finagle.tracing.{NullTracer, Trace, Tracer}
import com.twitter.finagle.transport.Transport
import com.twitter.finagle.util.DefaultTimer
import com.twitter.logging.HasLogLevel
import com.twitter.util._
import java.util.concurrent.atomic.{AtomicInteger, AtomicReference}
import java.util.concurrent.ConcurrentHashMap
import java.util.logging.{Level, Logger}
import scala.annotation.tailrec
import scala.collection.JavaConverters._

/**
 * Indicates that a client requested that a given request be discarded.
 *
 * This implies that the client issued a Tdiscarded message for a given tagged
 * request, as per [[com.twitter.finagle.mux]].
 */
case class ClientDiscardedRequestException(why: String)
  extends Exception(why)
  with HasLogLevel
{
  def logLevel: com.twitter.logging.Level = com.twitter.logging.Level.DEBUG
}

object gracefulShutdownEnabled extends GlobalFlag(true, "Graceful shutdown enabled. " +
  "Temporary measure to allow servers to deploy without hurting clients.")

/**
 * A tracker is responsible for tracking pending transactions
 * and coordinating draining.
 */
private class Tracker[T] {
  private[this] val pending = new ConcurrentHashMap[Int, Future[Unit]]
  private[this] val _drained: Promise[Unit] = new Promise

  // The state of a tracker is a single integer. Its absolute
  // value minus one indicates the number of pending requests.
  // A negative value indicates that the tracker is draining.
  // Negative values cannot transition to positive values.
  private[this] val state = new AtomicInteger(1)

  /**
   * Try to enter a transaction, returning false if the
   * tracker is draining.
   */
  @tailrec
  private[this] def enter(): Boolean = {
    val n = state.get
    if (n <= 0) false
    else if (!state.compareAndSet(n, n+1)) enter()
    else true
  }

  /**
   * Exit an entered transaction.
   */
  @tailrec
  private[this] def exit(): Unit = {
    val n = state.get
    if (n < 0) {
      if (state.incrementAndGet() == -1)
        _drained.setDone()
    } else if (!state.compareAndSet(n, n-1)) exit()
  }

  /**
   * Track a transaction. `track` manages the lifetime of a tag
   * including its reply and write. Function `process` handles the result
   * of `reply`. The tag is freed once a client receives the reply, and, since
   * write completion is not synchronous with processing the next
   * request, there is a race between acknowledging the write and
   * receiving the next request from the client (which may then reuse
   * the tag); We also can't complete draining until we've acknowledged
   * the write for the last request processed.
   *
   * @note `track` isn't synchronized across threads so this may have
   * races in a multithreaded environment. In our case, each instance
   * is owned by exactly one thread (i.e. we inherit netty's threading
   * model).
   */
  def track(tag: Int, reply: Future[T])(process: Try[T] => Future[Unit]): Future[Unit] = {
    if (!enter()) return reply.transform(process)

    val f = reply.transform(process)
    pending.put(tag, f)
    f.ensure {
      pending.remove(tag)
      exit()
    }
  }

  /**
   * Retrieve the value for the pending transaction matching `tag`.
   */
  def get(tag: Int): Option[Future[Unit]] =
    Option(pending.get(tag))

  /**
   * Returns the set of current tags.
   */
  def tags: Set[Int] =
    pending.keySet.asScala.toSet

  /**
   * Initiate the draining protocol. After `drain` is called, future
   * requests for tracking are dropped. [[drained]] is satisified
   * when the number of pending requests reaches 0.
   */
  @tailrec
  final def drain(): Unit = {
    val n = state.get
    if (n < 0) return

    if (!state.compareAndSet(n, -n)) drain()
    else if (n == 1) _drained.setDone()
  }

  /**
   * True when the tracker is in draining state.
   */
  def isDraining: Boolean = state.get < 0

  /**
   * Satisifed when the tracker has completed the draining protocol,
   * as described in [[drain]].
   */
  def drained: Future[Unit] = _drained

  /**
   * Tests whether the given tag is actively tracked.
   */
  def isTracking(tag: Int): Boolean = pending.containsKey(tag)

  /**
   * The number of tracked tags.
   */
  def npending: Int =
    math.abs(state.get)-1
}

private[twitter] object ServerDispatcher {
  /**
   * Construct a new request-response dispatcher.
   */
  def newRequestResponse(
    trans: Transport[Message, Message],
    service: Service[Request, Response],
    lessor: Lessor,
    tracer: Tracer,
    statsReceiver: StatsReceiver
  ): ServerDispatcher =
    new ServerDispatcher(trans, Processor andThen service, lessor, tracer, statsReceiver)

  /**
   * Construct a new request-response dispatcher with a
   * null lessor, tracer, and statsReceiver.
   */
  def newRequestResponse(
    trans: Transport[Message, Message],
    service: Service[Request, Response]
  ): ServerDispatcher =
    newRequestResponse(trans, service, Lessor.nil, NullTracer, NullStatsReceiver)

  /**
   * Used when comparing the difference between leases.
   */
  val Epsilon = 1.second

  object State extends Enumeration {
    val Open, Draining, Closed = Value
  }
}

/**
 * A dispatcher for the Mux protocol. In addition to multiplexing, the dispatcher
 * handles concerns of leasing and draining.
 */
private[twitter] class ServerDispatcher(
    trans: Transport[Message, Message],
    service: Service[Message, Message],
    lessor: Lessor, // the lessor that the dispatcher should register with in order to get leases
    tracer: Tracer,
    statsReceiver: StatsReceiver
) extends Closable with Lessee {
  import ServerDispatcher.State

  private[this] implicit val injectTimer = DefaultTimer.twitter
  private[this] val tracker = new Tracker[Message]
  private[this] val log = Logger.getLogger(getClass.getName)
  private[this] val duplicateTagCounter = statsReceiver.counter("duplicate_tag")
  private[this] val orphanedTdiscardCounter = statsReceiver.counter("orphaned_tdiscard")

  private[this] val state: AtomicReference[State.Value] =
    new AtomicReference(State.Open)

  @volatile private[this] var lease = Message.Tlease.MaxLease
  @volatile private[this] var curElapsed = NilStopwatch.start()
  lessor.register(this)

  private[this] def write(m: Message): Future[Unit] =
    trans.write(m)

  private[this] def isAccepting: Boolean =
    !tracker.isDraining && (!nackOnExpiredLease() || (lease > Duration.Zero))

  private[this] def process(m: Message): Unit = m match {
    case (_: Message.Tdispatch | _: Message.Treq) if isAccepting =>
      lessor.observeArrival()
      val elapsed = Stopwatch.start()

      val reply: Try[Message] => Future[Unit] = {
        case Return(rep) =>
          lessor.observe(elapsed())
          write(rep)
        case Throw(exc) =>
          log.log(Level.WARNING, s"Error processing message $m", exc)
          write(Message.Rerr(m.tag, exc.toString))
      }

      if (!tracker.isTracking(m.tag)) {
        tracker.track(m.tag, service(m))(reply)
      } else {
        // This can mean two things:
        //
        // 1. We have a pathalogical client which is sending duplicate tags.
        // We push the responsibility of resolving the duplicate on the client
        // and service the request.
        //
        // 2. We lost a race with the client where it reused a tag before we were
        // able to cleanup the tracker. This is possible since we cleanup state on
        // write closures which can be executed on a separate thread from the event
        // loop thread (in netty3). We take extra precaution in the `ChannelTransport.write`
        // to a avoid this, but technically it isn't guaranteed by netty3.
        //
        // In both cases, we forfeit the ability to track (and thus drain or interrupt)
        // the request, but we can still service it.
        log.fine(s"Received duplicate tag ${m.tag} from client ${trans.remoteAddress}")
        duplicateTagCounter.incr()
        service(m).transform(reply)
      }

    // Dispatch when !isAccepting
    case d: Message.Tdispatch =>
      write(Message.RdispatchNack(d.tag, Nil))
    case r: Message.Treq =>
      write(Message.RreqNack(r.tag))

    case _: Message.Tping =>
      service(m).respond {
        case Return(rep) => write(rep)
        case Throw(exc) => write(Message.Rerr(m.tag, exc.toString))
      }

    case Message.Tdiscarded(tag, why) =>
      tracker.get(tag) match {
        case Some(reply) =>
          reply.raise(new ClientDiscardedRequestException(why))
        case None =>
          orphanedTdiscardCounter.incr()
      }

    case Message.Rdrain(1) if state.get == State.Draining =>
      tracker.drain()

    case m: Message =>
      val rerr = Message.Rerr(m.tag, s"Unexpected mux message type ${m.typ}")
      write(rerr)
  }

  private[this] def loop(): Unit =
    Future.each(trans.read) { msg =>
      val save = Local.save()
      process(msg)
      Local.restore(save)
    } ensure { hangup(Time.now) }

  Local.letClear {
    Trace.letTracer(tracer) {
      Contexts.local.let(RemoteInfo.Upstream.AddressCtx, trans.remoteAddress) {
        trans.peerCertificate match {
          case None => loop()
          case Some(cert) => Contexts.local.let(Transport.peerCertCtx, cert) {
            loop()
          }
        }
      }
    }
  }

  trans.onClose respond { res =>
    val exc = res match {
      case Return(exc) => exc
      case Throw(exc) => exc
    }
    val cancelledExc = new CancelledRequestException(exc)
    for (tag <- tracker.tags; f <- tracker.get(tag))
      f.raise(cancelledExc)

    service.close()
    lessor.unregister(this)

    state.get match {
      case State.Open =>
        statsReceiver.counter("clienthangup").incr()
      case (State.Draining | State.Closed) =>
        statsReceiver.counter("serverhangup").incr()
    }
  }

  @tailrec
  private[this] def hangup(deadline: Time): Future[Unit] = state.get match {
    case State.Closed => Future.Done
    case s@(State.Draining | State.Open) =>
      if (!state.compareAndSet(s, State.Closed)) hangup(deadline) else {
        trans.close(deadline)
      }
    }

  def close(deadline: Time): Future[Unit] = {
    if (!state.compareAndSet(State.Open, State.Draining))
      return trans.onClose.unit

    if (!gracefulShutdownEnabled()) {
      // In theory, we can do slightly better here.
      // (i.e., at least try to wait for requests to drain)
      // but instead we should just disable this flag.
      return hangup(deadline)
    }

    statsReceiver.counter("draining").incr()
    val done = write(Message.Tdrain(1)) before
      tracker.drained.by(deadline) before
      trans.close(deadline)
    done.transform {
      case Return(_) =>
        statsReceiver.counter("drained").incr()
        Future.Done
      case Throw(_: ChannelClosedException) =>
        Future.Done
      case Throw(_) =>
        hangup(deadline)
    }
  }

  /**
    * Emit a lease to the clients of this server.  If howlong is less than or
    * equal to 0, also nack all requests until a new lease is issued.
    */
  def issue(howlong: Duration): Unit = {
    require(howlong >= Message.Tlease.MinLease)

    synchronized {
      val diff = (lease - curElapsed()).abs
      if (diff > ServerDispatcher.Epsilon) {
        curElapsed = Stopwatch.start()
        lease = howlong
        write(Message.Tlease(howlong min Message.Tlease.MaxLease))
      } else if ((howlong < Duration.Zero) && (lease > Duration.Zero)) {
        curElapsed = Stopwatch.start()
        lease = howlong
      }
    }
  }

  def npending: Int = tracker.npending
}

/**
 * Processor handles request, dispatch, and ping messages. Request
 * and dispatch messages are passed onto the request-response in the
 * filter chain. Pings are answered immediately in the affirmative.
 *
 * (This arrangement permits interpositioning other filters to modify ping
 * or dispatch behavior, e.g., for testing.)
 */
private[finagle] object Processor extends Filter[Message, Message, Request, Response] {
  import Message._

  private[this] def dispatch(
    tdispatch: Message.Tdispatch,
    service: Service[Request, Response]
  ): Future[Message] = {

    Contexts.broadcast.letUnmarshal(tdispatch.contexts) {
      if (tdispatch.dtab.nonEmpty)
        Dtab.local ++= tdispatch.dtab
      service(Request(tdispatch.dst, tdispatch.req)).transform {
        case Return(rep) =>
          Future.value(RdispatchOk(tdispatch.tag, Nil, rep.body))

        case Throw(f: Failure) if f.isFlagged(Failure.Restartable) =>
          Future.value(RdispatchNack(tdispatch.tag, Nil))

        case Throw(exc) =>
          Future.value(RdispatchError(tdispatch.tag, Nil, exc.toString))
      }
    }
  }

  private[this] def dispatch(
    treq: Message.Treq,
    service: Service[Request, Response]
  ): Future[Message] = {
    Trace.letIdOption(treq.traceId) {
      service(Request(Path.empty, treq.req)).transform {
        case Return(rep) =>
          Future.value(RreqOk(treq.tag, rep.body))

        case Throw(f: Failure) if f.isFlagged(Failure.Restartable) =>
          Future.value(Message.RreqNack(treq.tag))

        case Throw(exc) =>
          Future.value(Message.RreqError(treq.tag, exc.toString))
      }
    }
  }

  def apply(req: Message, service: Service[Request, Response]): Future[Message] = req match {
    case d: Message.Tdispatch => dispatch(d, service)
    case r: Message.Treq => dispatch(r, service)
    case Message.Tping(tag) => Future.value(Message.Rping(tag))
    case m => Future.exception(new IllegalArgumentException(s"Cannot process message $m"))
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy