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

akka.routing.Routing.scala Maven / Gradle / Ivy

There is a newer version: 2.0.5-protobuf-2.5-java-1.5
Show newest version
/**
 * Copyright (C) 2009-2012 Typesafe Inc. 
 */
package akka.routing

import akka.actor._
import akka.util.Duration
import akka.util.duration._
import akka.config.ConfigurationException
import akka.pattern.pipe
import akka.pattern.AskSupport
import com.typesafe.config.Config
import scala.collection.JavaConversions.iterableAsScalaIterable
import java.util.concurrent.atomic.{ AtomicLong, AtomicBoolean }
import java.util.concurrent.TimeUnit
import java.util.concurrent.locks.ReentrantLock
import akka.jsr166y.ThreadLocalRandom
import akka.util.Unsafe
import akka.dispatch.Dispatchers
import scala.annotation.tailrec
import scala.runtime.ScalaRunTime

/**
 * A RoutedActorRef is an ActorRef that has a set of connected ActorRef and it uses a Router to
 * send a message to on (or more) of these actors.
 */
private[akka] class RoutedActorRef(_system: ActorSystemImpl, _props: Props, _supervisor: InternalActorRef, _path: ActorPath)
  extends LocalActorRef(
    _system,
    _props.copy(creator = () ⇒ _props.routerConfig.createActor(), dispatcher = _props.routerConfig.routerDispatcher),
    _supervisor,
    _path) {

  // verify that a BalancingDispatcher is not used with a Router
  if (_props.routerConfig != NoRouter && _system.dispatchers.isBalancingDispatcher(_props.routerConfig.routerDispatcher))
    throw new ConfigurationException(
      "Configuration for actor [" + _path.toString +
        "] is invalid - you can not use a 'BalancingDispatcher' together with any type of 'Router'")

  /*
   * CAUTION: RoutedActorRef is PROBLEMATIC
   * ======================================
   * 
   * We are constructing/assembling the children outside of the scope of the 
   * Router actor, inserting them in its childrenRef list, which is not at all
   * synchronized. This is done exactly once at start-up, all other accesses
   * are done from the Router actor. This means that the only thing which is
   * really hairy is making sure that the Router does not touch its childrenRefs
   * before we are done with them: lock the monitor of the actor cell (hence the
   * override of newActorCell) and use that to block the Router constructor for
   * as long as it takes to setup the RoutedActorRef itself.
   */
  override def newActorCell(
    system: ActorSystemImpl,
    ref: InternalActorRef,
    props: Props,
    supervisor: InternalActorRef,
    receiveTimeout: Option[Duration]): ActorCell =
    {
      val cell = super.newActorCell(system, ref, props, supervisor, receiveTimeout)
      Unsafe.instance.monitorEnter(cell)
      cell
    }

  private[akka] val routerConfig = _props.routerConfig
  private[akka] val routeeProps = _props.copy(routerConfig = NoRouter)
  private[akka] val resizeInProgress = new AtomicBoolean
  private val resizeCounter = new AtomicLong

  @volatile
  private var _routees: IndexedSeq[ActorRef] = IndexedSeq.empty[ActorRef] // this MUST be initialized during createRoute
  def routees = _routees

  @volatile
  private var _routeeProvider: RouteeProvider = _
  def routeeProvider = _routeeProvider

  val route =
    try {
      _routeeProvider = routerConfig.createRouteeProvider(actorContext)
      val r = routerConfig.createRoute(routeeProps, routeeProvider)
      // initial resize, before message send
      routerConfig.resizer foreach { r ⇒
        if (r.isTimeForResize(resizeCounter.getAndIncrement()))
          r.resize(routeeProps, routeeProvider)
      }
      r
    } finally {
      assert(Thread.holdsLock(actorContext))
      Unsafe.instance.monitorExit(actorContext) // unblock Router’s constructor
    }

  if (routerConfig.resizer.isEmpty && _routees.isEmpty)
    throw new ActorInitializationException("router " + routerConfig + " did not register routees!")

  /*
   * end of construction
   */

  def applyRoute(sender: ActorRef, message: Any): Iterable[Destination] = message match {
    case _: AutoReceivedMessage ⇒ Destination(this, this) :: Nil
    case Terminated(_)          ⇒ Destination(this, this) :: Nil
    case CurrentRoutees ⇒
      sender ! RouterRoutees(_routees)
      Nil
    case _ ⇒
      if (route.isDefinedAt(sender, message)) route(sender, message)
      else Nil
  }

  /**
   * Adds the routees to existing routees.
   * Adds death watch of the routees so that they are removed when terminated.
   * Not thread safe, but intended to be called from protected points, such as
   * `RouterConfig.createRoute` and `Resizer.resize`
   */
  private[akka] def addRoutees(newRoutees: IndexedSeq[ActorRef]): Unit = {
    _routees = _routees ++ newRoutees
    // subscribe to Terminated messages for all route destinations, to be handled by Router actor
    newRoutees foreach underlying.watch
  }

  /**
   * Adds the routees to existing routees.
   * Removes death watch of the routees. Doesn't stop the routees.
   * Not thread safe, but intended to be called from protected points, such as
   * `Resizer.resize`
   */
  private[akka] def removeRoutees(abandonedRoutees: IndexedSeq[ActorRef]): Unit = {
    _routees = _routees diff abandonedRoutees
    abandonedRoutees foreach underlying.unwatch
  }

  override def !(message: Any)(implicit sender: ActorRef = null): Unit = {
    resize()

    val s = if (sender eq null) underlying.system.deadLetters else sender

    val msg = message match {
      case Broadcast(m) ⇒ m
      case m            ⇒ m
    }

    applyRoute(s, message) match {
      case Destination(_, x) :: Nil if x eq this ⇒ super.!(message)(s)
      case refs                                  ⇒ refs foreach (p ⇒ p.recipient.!(msg)(p.sender))
    }
  }

  def resize(): Unit = {
    for (r ← routerConfig.resizer) {
      if (r.isTimeForResize(resizeCounter.getAndIncrement()) && resizeInProgress.compareAndSet(false, true))
        super.!(Router.Resize)
    }
  }
}

/**
 * This trait represents a router factory: it produces the actual router actor
 * and creates the routing table (a function which determines the recipients
 * for each message which is to be dispatched). The resulting RoutedActorRef
 * optimizes the sending of the message so that it does NOT go through the
 * router’s mailbox unless the route returns an empty recipient set.
 *
 * '''Caution:''' This means
 * that the route function is evaluated concurrently without protection by
 * the RoutedActorRef: either provide a reentrant (i.e. pure) implementation or
 * do the locking yourself!
 *
 * '''Caution:''' Please note that the [[akka.routing.Router]] which needs to
 * be returned by `createActor()` should not send a message to itself in its
 * constructor or `preStart()` or publish its self reference from there: if
 * someone tries sending a message to that reference before the constructor of
 * RoutedActorRef has returned, there will be a `NullPointerException`!
 */
trait RouterConfig {

  def createRoute(routeeProps: Props, routeeProvider: RouteeProvider): Route

  def createRouteeProvider(context: ActorContext) = new RouteeProvider(context, resizer)

  def createActor(): Router = new Router {
    override def supervisorStrategy: SupervisorStrategy = RouterConfig.this.supervisorStrategy
  }

  /**
   * SupervisorStrategy for the created Router actor.
   */
  def supervisorStrategy: SupervisorStrategy

  /**
   * Dispatcher ID to use for running the “head” actor, i.e. the [[akka.routing.Router]].
   */
  def routerDispatcher: String

  /**
   * Overridable merge strategy, by default completely prefers “this” (i.e. no merge).
   */
  def withFallback(other: RouterConfig): RouterConfig = this

  protected def toAll(sender: ActorRef, routees: Iterable[ActorRef]): Iterable[Destination] = routees.map(Destination(sender, _))

  /**
   * Routers with dynamically resizable number of routees return the [[akka.routing.Resizer]]
   * to use.
   */
  def resizer: Option[Resizer] = None

}

/**
 * Factory and registry for routees of the router.
 * Uses `context.actorOf` to create routees from nrOfInstances property
 * and `context.actorFor` lookup routees from paths.
 */
class RouteeProvider(val context: ActorContext, val resizer: Option[Resizer]) {

  /**
   * Adds the routees to the router.
   * Adds death watch of the routees so that they are removed when terminated.
   * Not thread safe, but intended to be called from protected points, such as
   * `RouterConfig.createRoute` and `Resizer.resize`.
   */
  def registerRoutees(routees: IndexedSeq[ActorRef]): Unit = {
    routedRef.addRoutees(routees)
  }

  /**
   * Adds the routees to the router.
   * Adds death watch of the routees so that they are removed when terminated.
   * Not thread safe, but intended to be called from protected points, such as
   * `RouterConfig.createRoute` and `Resizer.resize`.
   * Java API.
   */
  def registerRoutees(routees: java.util.List[ActorRef]): Unit = {
    import scala.collection.JavaConverters._
    registerRoutees(routees.asScala.toIndexedSeq)
  }

  /**
   * Removes routees from the router. This method doesn't stop the routees.
   * Removes death watch of the routees.
   * Not thread safe, but intended to be called from protected points, such as
   * `Resizer.resize`.
   */
  def unregisterRoutees(routees: IndexedSeq[ActorRef]): Unit = {
    routedRef.removeRoutees(routees)
  }

  def createRoutees(props: Props, nrOfInstances: Int, routees: Iterable[String]): IndexedSeq[ActorRef] =
    (nrOfInstances, routees) match {
      case (x, Nil) if x <= 0 ⇒
        throw new IllegalArgumentException(
          "Must specify nrOfInstances or routees for [%s]" format context.self.path.toString)
      case (x, Nil) ⇒ (1 to x).map(_ ⇒ context.actorOf(props))(scala.collection.breakOut)
      case (_, xs)  ⇒ xs.map(context.actorFor(_))(scala.collection.breakOut)
    }

  def createAndRegisterRoutees(props: Props, nrOfInstances: Int, routees: Iterable[String]): Unit = {
    if (resizer.isEmpty) {
      registerRoutees(createRoutees(props, nrOfInstances, routees))
    }
  }

  /**
   * All routees of the router
   */
  def routees: IndexedSeq[ActorRef] = routedRef.routees

  private def routedRef = context.self.asInstanceOf[RoutedActorRef]

}

/**
 * Java API for a custom router factory.
 * @see akka.routing.RouterConfig
 */
abstract class CustomRouterConfig extends RouterConfig {
  override def createRoute(props: Props, routeeProvider: RouteeProvider): Route = {
    // as a bonus, this prevents closing of props and context in the returned Route PartialFunction
    val customRoute = createCustomRoute(props, routeeProvider)

    {
      case (sender, message) ⇒ customRoute.destinationsFor(sender, message)
    }
  }

  def createCustomRoute(props: Props, routeeProvider: RouteeProvider): CustomRoute

}

trait CustomRoute {
  def destinationsFor(sender: ActorRef, message: Any): java.lang.Iterable[Destination]
}

/**
 * Base trait for `Router` actors. Override `receive` to handle custom
 * messages which the corresponding [[akka.actor.RouterConfig]] lets
 * through by returning an empty route.
 */
trait Router extends Actor {

  // make sure that we synchronize properly to get the childrenRefs into our CPU cache
  val ref = context.synchronized {
    self match {
      case x: RoutedActorRef ⇒ x
      case _                 ⇒ throw new ActorInitializationException("Router actor can only be used in RoutedActorRef")
    }
  }

  final def receive = ({

    case Router.Resize ⇒
      try ref.routerConfig.resizer foreach (_.resize(ref.routeeProps, ref.routeeProvider))
      finally assert(ref.resizeInProgress.getAndSet(false))

    case Terminated(child) ⇒
      ref.removeRoutees(IndexedSeq(child))
      if (ref.routees.isEmpty) context.stop(self)

  }: Receive) orElse routerReceive

  def routerReceive: Receive = Actor.emptyBehavior

  override def preRestart(cause: Throwable, msg: Option[Any]): Unit = {
    // do not scrap children
  }
}

private object Router {

  case object Resize

  val defaultSupervisorStrategy: SupervisorStrategy = OneForOneStrategy() {
    case _ ⇒ SupervisorStrategy.Escalate
  }
}

/**
 * Used to broadcast a message to all connections in a router; only the
 * contained message will be forwarded, i.e. the `Broadcast(...)`
 * envelope will be stripped off.
 *
 * Router implementations may choose to handle this message differently.
 */
case class Broadcast(message: Any)

/**
 * Sending this message to a router will make it send back its currently used routees.
 * A RouterRoutees message is sent asynchronously to the "requester" containing information
 * about what routees the router is routing over.
 */
abstract class CurrentRoutees
case object CurrentRoutees extends CurrentRoutees {
  /**
   * Java API: get the singleton instance
   */
  def getInstance = this
}

/**
 * Message used to carry information about what routees the router is currently using.
 */
case class RouterRoutees(routees: Iterable[ActorRef])

/**
 * For every message sent to a router, its route determines a set of destinations,
 * where for each recipient a different sender may be specified; typically the
 * sender should match the sender of the original request, but e.g. the scatter-
 * gather router needs to receive the replies with an AskActorRef instead.
 */
case class Destination(sender: ActorRef, recipient: ActorRef)

/**
 * Routing configuration that indicates no routing; this is also the default
 * value which hence overrides the merge strategy in order to accept values
 * from lower-precedence sources. The decision whether or not to create a
 * router is taken in the LocalActorRefProvider based on Props.
 */
//TODO add @SerialVersionUID(1L) when SI-4804 is fixed
abstract class NoRouter extends RouterConfig
case object NoRouter extends NoRouter {
  def createRoute(props: Props, routeeProvider: RouteeProvider): Route = null
  def routerDispatcher: String = ""
  def supervisorStrategy = null
  override def withFallback(other: RouterConfig): RouterConfig = other

  /**
   * Java API: get the singleton instance
   */
  def getInstance = this
}

/**
 * Router configuration which has no default, i.e. external configuration is required.
 */
case object FromConfig extends FromConfig {
  /**
   * Java API: get the singleton instance
   */
  def getInstance = this
  @inline final def apply(routerDispatcher: String = Dispatchers.DefaultDispatcherId) = new FromConfig(routerDispatcher)
  @inline final def unapply(fc: FromConfig): Option[String] = Some(fc.routerDispatcher)
}

/**
 * Java API: Router configuration which has no default, i.e. external configuration is required.
 *
 * This can be used when the dispatcher to be used for the head Router needs to be configured
 * (defaults to default-dispatcher).
 */
//TODO add @SerialVersionUID(1L) when SI-4804 is fixed
class FromConfig(val routerDispatcher: String = Dispatchers.DefaultDispatcherId)
  extends RouterConfig
  with Product
  with Serializable
  with Equals {

  def this() = this(Dispatchers.DefaultDispatcherId)

  def createRoute(props: Props, routeeProvider: RouteeProvider): Route =
    throw new ConfigurationException("router " + routeeProvider.context.self + " needs external configuration from file (e.g. application.conf)")

  def supervisorStrategy: SupervisorStrategy = Router.defaultSupervisorStrategy

  // open-coded case class to preserve binary compatibility, all deprecated for 2.1
  @deprecated("FromConfig does not make sense as case class", "2.0.1")
  override def productPrefix = "FromConfig"

  @deprecated("FromConfig does not make sense as case class", "2.0.1")
  def productArity = 1

  @deprecated("FromConfig does not make sense as case class", "2.0.1")
  def productElement(x: Int) = x match {
    case 0 ⇒ routerDispatcher
    case _ ⇒ throw new IndexOutOfBoundsException(x.toString)
  }

  @deprecated("FromConfig does not make sense as case class", "2.0.1")
  def copy(d: String = Dispatchers.DefaultDispatcherId): FromConfig = new FromConfig(d)

  @deprecated("FromConfig does not make sense as case class", "2.0.1")
  def canEqual(o: Any) = o.isInstanceOf[FromConfig]

  @deprecated("FromConfig does not make sense as case class", "2.0.1")
  override def hashCode = ScalaRunTime._hashCode(this)

  @deprecated("FromConfig does not make sense as case class", "2.0.1")
  override def toString = "FromConfig(" + routerDispatcher + ")"

  @deprecated("FromConfig does not make sense as case class", "2.0.1")
  override def equals(other: Any): Boolean = other match {
    case FromConfig(x) ⇒ x == routerDispatcher
    case _             ⇒ false
  }

}

object RoundRobinRouter {
  def apply(routees: Iterable[ActorRef]) = new RoundRobinRouter(routees = routees map (_.path.toString))

  /**
   * Java API to create router with the supplied 'routees' actors.
   */
  def create(routees: java.lang.Iterable[ActorRef]): RoundRobinRouter = {
    import scala.collection.JavaConverters._
    apply(routees.asScala)
  }
}
/**
 * A Router that uses round-robin to select a connection. For concurrent calls, round robin is just a best effort.
 * 
* Please note that providing both 'nrOfInstances' and 'routees' does not make logical sense as this means * that the router should both create new actors and use the 'routees' actor(s). * In this case the 'nrOfInstances' will be ignored and the 'routees' will be used. *
* The configuration parameter trumps the constructor arguments. This means that * if you provide either 'nrOfInstances' or 'routees' during instantiation they will * be ignored if the router is defined in the configuration file for the actor being used. * *

Supervision Setup

* * The router creates a “head” actor which supervises and/or monitors the * routees. Instances are created as children of this actor, hence the * children are not supervised by the parent of the router. Common choices are * to always escalate (meaning that fault handling is always applied to all * children simultaneously; this is the default) or use the parent’s strategy, * which will result in routed children being treated individually, but it is * possible as well to use Routers to give different supervisor strategies to * different groups of children. * * {{{ * class MyActor extends Actor { * override val supervisorStrategy = ... * * val poolAsAWhole = context.actorOf(Props[SomeActor].withRouter(RoundRobinRouter(5))) * * val poolIndividuals = context.actorOf(Props[SomeActor].withRouter( * RoundRobinRouter(5, supervisorStrategy = this.supervisorStrategy))) * * val specialChild = context.actorOf(Props[SomeActor].withRouter( * RoundRobinRouter(5, supervisorStrategy = OneForOneStrategy() { * ... * }))) * } * }}} * * @param routees string representation of the actor paths of the routees that will be looked up * using `actorFor` in [[akka.actor.ActorRefProvider]] */ //TODO add @SerialVersionUID(1L) when SI-4804 is fixed case class RoundRobinRouter(nrOfInstances: Int = 0, routees: Iterable[String] = Nil, override val resizer: Option[Resizer] = None, val routerDispatcher: String = Dispatchers.DefaultDispatcherId, val supervisorStrategy: SupervisorStrategy = Router.defaultSupervisorStrategy) extends RouterConfig with RoundRobinLike { /** * Constructor that sets nrOfInstances to be created. * Java API */ def this(nr: Int) = { this(nrOfInstances = nr) } /** * Constructor that sets the routees to be used. * Java API * @param routeePaths string representation of the actor paths of the routees that will be looked up * using `actorFor` in [[akka.actor.ActorRefProvider]] */ def this(routeePaths: java.lang.Iterable[String]) = { this(routees = iterableAsScalaIterable(routeePaths)) } /** * Constructor that sets the resizer to be used. * Java API */ def this(resizer: Resizer) = this(resizer = Some(resizer)) /** * Java API for setting routerDispatcher */ def withDispatcher(dispatcherId: String) = copy(routerDispatcher = dispatcherId) /** * Java API for setting the supervisor strategy to be used for the “head” * Router actor. */ def withSupervisorStrategy(strategy: SupervisorStrategy) = copy(supervisorStrategy = strategy) } trait RoundRobinLike { this: RouterConfig ⇒ def nrOfInstances: Int def routees: Iterable[String] def createRoute(props: Props, routeeProvider: RouteeProvider): Route = { routeeProvider.createAndRegisterRoutees(props, nrOfInstances, routees) val next = new AtomicLong(0) def getNext(): ActorRef = { val currentRoutees = routeeProvider.routees if (currentRoutees.isEmpty) routeeProvider.context.system.deadLetters else currentRoutees((next.getAndIncrement % currentRoutees.size).asInstanceOf[Int]) } { case (sender, message) ⇒ message match { case Broadcast(msg) ⇒ toAll(sender, routeeProvider.routees) case msg ⇒ List(Destination(sender, getNext())) } } } } object RandomRouter { def apply(routees: Iterable[ActorRef]) = new RandomRouter(routees = routees map (_.path.toString)) /** * Java API to create router with the supplied 'routees' actors. */ def create(routees: java.lang.Iterable[ActorRef]): RandomRouter = { import scala.collection.JavaConverters._ apply(routees.asScala) } } /** * A Router that randomly selects one of the target connections to send a message to. *
* Please note that providing both 'nrOfInstances' and 'routees' does not make logical sense as this means * that the router should both create new actors and use the 'routees' actor(s). * In this case the 'nrOfInstances' will be ignored and the 'routees' will be used. *
* The configuration parameter trumps the constructor arguments. This means that * if you provide either 'nrOfInstances' or 'routees' during instantiation they will * be ignored if the router is defined in the configuration file for the actor being used. * *

Supervision Setup

* * The router creates a “head” actor which supervises and/or monitors the * routees. Instances are created as children of this actor, hence the * children are not supervised by the parent of the router. Common choices are * to always escalate (meaning that fault handling is always applied to all * children simultaneously; this is the default) or use the parent’s strategy, * which will result in routed children being treated individually, but it is * possible as well to use Routers to give different supervisor strategies to * different groups of children. * * {{{ * class MyActor extends Actor { * override val supervisorStrategy = ... * * val poolAsAWhole = context.actorOf(Props[SomeActor].withRouter(RoundRobinRouter(5))) * * val poolIndividuals = context.actorOf(Props[SomeActor].withRouter( * RoundRobinRouter(5, supervisorStrategy = this.supervisorStrategy))) * * val specialChild = context.actorOf(Props[SomeActor].withRouter( * RoundRobinRouter(5, supervisorStrategy = OneForOneStrategy() { * ... * }))) * } * }}} * * @param routees string representation of the actor paths of the routees that will be looked up * using `actorFor` in [[akka.actor.ActorRefProvider]] */ //TODO add @SerialVersionUID(1L) when SI-4804 is fixed case class RandomRouter(nrOfInstances: Int = 0, routees: Iterable[String] = Nil, override val resizer: Option[Resizer] = None, val routerDispatcher: String = Dispatchers.DefaultDispatcherId, val supervisorStrategy: SupervisorStrategy = Router.defaultSupervisorStrategy) extends RouterConfig with RandomLike { /** * Constructor that sets nrOfInstances to be created. * Java API */ def this(nr: Int) = { this(nrOfInstances = nr) } /** * Constructor that sets the routees to be used. * Java API * @param routeePaths string representation of the actor paths of the routees that will be looked up * using `actorFor` in [[akka.actor.ActorRefProvider]] */ def this(routeePaths: java.lang.Iterable[String]) = { this(routees = iterableAsScalaIterable(routeePaths)) } /** * Constructor that sets the resizer to be used. * Java API */ def this(resizer: Resizer) = this(resizer = Some(resizer)) /** * Java API for setting routerDispatcher */ def withDispatcher(dispatcherId: String) = copy(routerDispatcher = dispatcherId) /** * Java API for setting the supervisor strategy to be used for the “head” * Router actor. */ def withSupervisorStrategy(strategy: SupervisorStrategy) = copy(supervisorStrategy = strategy) } trait RandomLike { this: RouterConfig ⇒ def nrOfInstances: Int def routees: Iterable[String] def createRoute(props: Props, routeeProvider: RouteeProvider): Route = { routeeProvider.createAndRegisterRoutees(props, nrOfInstances, routees) def getNext(): ActorRef = { val currentRoutees = routeeProvider.routees if (currentRoutees.isEmpty) routeeProvider.context.system.deadLetters else currentRoutees(ThreadLocalRandom.current.nextInt(currentRoutees.size)) } { case (sender, message) ⇒ message match { case Broadcast(msg) ⇒ toAll(sender, routeeProvider.routees) case msg ⇒ List(Destination(sender, getNext())) } } } } object SmallestMailboxRouter { def apply(routees: Iterable[ActorRef]) = new SmallestMailboxRouter(routees = routees map (_.path.toString)) /** * Java API to create router with the supplied 'routees' actors. */ def create(routees: java.lang.Iterable[ActorRef]): SmallestMailboxRouter = { import scala.collection.JavaConverters._ apply(routees.asScala) } } /** * A Router that tries to send to the non-suspended routee with fewest messages in mailbox. * The selection is done in this order: *
    *
  • pick any idle routee (not processing message) with empty mailbox
  • *
  • pick any routee with empty mailbox
  • *
  • pick routee with fewest pending messages in mailbox
  • *
  • pick any remote routee, remote actors are consider lowest priority, * since their mailbox size is unknown
  • *
* *
* Please note that providing both 'nrOfInstances' and 'routees' does not make logical sense as this means * that the router should both create new actors and use the 'routees' actor(s). * In this case the 'nrOfInstances' will be ignored and the 'routees' will be used. *
* The configuration parameter trumps the constructor arguments. This means that * if you provide either 'nrOfInstances' or 'routees' during instantiation they will * be ignored if the router is defined in the configuration file for the actor being used. * *

Supervision Setup

* * The router creates a “head” actor which supervises and/or monitors the * routees. Instances are created as children of this actor, hence the * children are not supervised by the parent of the router. Common choices are * to always escalate (meaning that fault handling is always applied to all * children simultaneously; this is the default) or use the parent’s strategy, * which will result in routed children being treated individually, but it is * possible as well to use Routers to give different supervisor strategies to * different groups of children. * * {{{ * class MyActor extends Actor { * override val supervisorStrategy = ... * * val poolAsAWhole = context.actorOf(Props[SomeActor].withRouter(RoundRobinRouter(5))) * * val poolIndividuals = context.actorOf(Props[SomeActor].withRouter( * RoundRobinRouter(5, supervisorStrategy = this.supervisorStrategy))) * * val specialChild = context.actorOf(Props[SomeActor].withRouter( * RoundRobinRouter(5, supervisorStrategy = OneForOneStrategy() { * ... * }))) * } * }}} * * @param routees string representation of the actor paths of the routees that will be looked up * using `actorFor` in [[akka.actor.ActorRefProvider]] */ //TODO add @SerialVersionUID(1L) when SI-4804 is fixed case class SmallestMailboxRouter(nrOfInstances: Int = 0, routees: Iterable[String] = Nil, override val resizer: Option[Resizer] = None, val routerDispatcher: String = Dispatchers.DefaultDispatcherId, val supervisorStrategy: SupervisorStrategy = Router.defaultSupervisorStrategy) extends RouterConfig with SmallestMailboxLike { /** * Constructor that sets nrOfInstances to be created. * Java API */ def this(nr: Int) = { this(nrOfInstances = nr) } /** * Constructor that sets the routees to be used. * Java API * @param routeePaths string representation of the actor paths of the routees that will be looked up * using `actorFor` in [[akka.actor.ActorRefProvider]] */ def this(routeePaths: java.lang.Iterable[String]) = { this(routees = iterableAsScalaIterable(routeePaths)) } /** * Constructor that sets the resizer to be used. * Java API */ def this(resizer: Resizer) = this(resizer = Some(resizer)) /** * Java API for setting routerDispatcher */ def withDispatcher(dispatcherId: String) = copy(routerDispatcher = dispatcherId) /** * Java API for setting the supervisor strategy to be used for the “head” * Router actor. */ def withSupervisorStrategy(strategy: SupervisorStrategy) = copy(supervisorStrategy = strategy) } trait SmallestMailboxLike { this: RouterConfig ⇒ import java.security.SecureRandom def nrOfInstances: Int def routees: Iterable[String] /** * Returns true if the actor is currently processing a message. * It will always return false for remote actors. * Method is exposed to subclasses to be able to implement custom * routers based on mailbox and actor internal state. */ protected def isProcessingMessage(a: ActorRef): Boolean = a match { case x: LocalActorRef ⇒ val cell = x.underlying cell.mailbox.isScheduled && cell.currentMessage != null case _ ⇒ false } /** * Returns true if the actor currently has any pending messages * in the mailbox, i.e. the mailbox is not empty. * It will always return false for remote actors. * Method is exposed to subclasses to be able to implement custom * routers based on mailbox and actor internal state. */ protected def hasMessages(a: ActorRef): Boolean = a match { case x: LocalActorRef ⇒ x.underlying.mailbox.hasMessages case _ ⇒ false } /** * Returns true if the actor is currently suspended. * It will always return false for remote actors. * Method is exposed to subclasses to be able to implement custom * routers based on mailbox and actor internal state. */ protected def isSuspended(a: ActorRef): Boolean = a match { case x: LocalActorRef ⇒ x.underlying.mailbox.isSuspended case _ ⇒ false } /** * Returns the number of pending messages in the mailbox of the actor. * It will always return 0 for remote actors. * Method is exposed to subclasses to be able to implement custom * routers based on mailbox and actor internal state. */ protected def numberOfMessages(a: ActorRef): Int = a match { case x: LocalActorRef ⇒ x.underlying.mailbox.numberOfMessages case _ ⇒ 0 } def createRoute(props: Props, routeeProvider: RouteeProvider): Route = { routeeProvider.createAndRegisterRoutees(props, nrOfInstances, routees) // Worst-case a 2-pass inspection with mailbox size checking done on second pass, and only until no one empty is found. // Lowest score wins, score 0 is autowin // If no actor with score 0 is found, it will return that, or if it is terminated, a random of the entire set. // Why? Well, in case we had 0 viable actors and all we got was the default, which is the DeadLetters, anything else is better. // Order of interest, in ascending priority: // 1. The DeadLetterActorRef // 2. A Suspended ActorRef // 3. An ActorRef with unknown mailbox size but with one message being processed // 4. An ActorRef with unknown mailbox size that isn't processing anything // 5. An ActorRef with a known mailbox size // 6. An ActorRef without any messages @tailrec def getNext(targets: IndexedSeq[ActorRef] = routeeProvider.routees, proposedTarget: ActorRef = routeeProvider.context.system.deadLetters, currentScore: Long = Long.MaxValue, at: Int = 0, deep: Boolean = false): ActorRef = if (targets.isEmpty) routeeProvider.context.system.deadLetters else if (at >= targets.size) { if (deep) { if (proposedTarget.isTerminated) targets(ThreadLocalRandom.current.nextInt(targets.size)) else proposedTarget } else getNext(targets, proposedTarget, currentScore, 0, deep = true) } else { val target = targets(at) val newScore: Long = if (isSuspended(target)) Long.MaxValue - 1 else { //Just about better than the DeadLetters (if (isProcessingMessage(target)) 1l else 0l) + (if (!hasMessages(target)) 0l else { //Race between hasMessages and numberOfMessages here, unfortunate the numberOfMessages returns 0 if unknown val noOfMsgs: Long = if (deep) numberOfMessages(target) else 0 if (noOfMsgs > 0) noOfMsgs else Long.MaxValue - 3 //Just better than a suspended actorref }) } if (newScore == 0) target else if (newScore < 0 || newScore >= currentScore) getNext(targets, proposedTarget, currentScore, at + 1, deep) else getNext(targets, target, newScore, at + 1, deep) } { case (sender, message) ⇒ message match { case Broadcast(msg) ⇒ toAll(sender, routeeProvider.routees) case msg ⇒ List(Destination(sender, getNext())) } } } } object BroadcastRouter { def apply(routees: Iterable[ActorRef]) = new BroadcastRouter(routees = routees map (_.path.toString)) /** * Java API to create router with the supplied 'routees' actors. */ def create(routees: java.lang.Iterable[ActorRef]): BroadcastRouter = { import scala.collection.JavaConverters._ apply(routees.asScala) } } /** * A Router that uses broadcasts a message to all its connections. *
* Please note that providing both 'nrOfInstances' and 'routees' does not make logical sense as this means * that the router should both create new actors and use the 'routees' actor(s). * In this case the 'nrOfInstances' will be ignored and the 'routees' will be used. *
* The configuration parameter trumps the constructor arguments. This means that * if you provide either 'nrOfInstances' or 'routees' during instantiation they will * be ignored if the router is defined in the configuration file for the actor being used. * *

Supervision Setup

* * The router creates a “head” actor which supervises and/or monitors the * routees. Instances are created as children of this actor, hence the * children are not supervised by the parent of the router. Common choices are * to always escalate (meaning that fault handling is always applied to all * children simultaneously; this is the default) or use the parent’s strategy, * which will result in routed children being treated individually, but it is * possible as well to use Routers to give different supervisor strategies to * different groups of children. * * {{{ * class MyActor extends Actor { * override val supervisorStrategy = ... * * val poolAsAWhole = context.actorOf(Props[SomeActor].withRouter(RoundRobinRouter(5))) * * val poolIndividuals = context.actorOf(Props[SomeActor].withRouter( * RoundRobinRouter(5, supervisorStrategy = this.supervisorStrategy))) * * val specialChild = context.actorOf(Props[SomeActor].withRouter( * RoundRobinRouter(5, supervisorStrategy = OneForOneStrategy() { * ... * }))) * } * }}} * * @param routees string representation of the actor paths of the routees that will be looked up * using `actorFor` in [[akka.actor.ActorRefProvider]] */ //TODO add @SerialVersionUID(1L) when SI-4804 is fixed case class BroadcastRouter(nrOfInstances: Int = 0, routees: Iterable[String] = Nil, override val resizer: Option[Resizer] = None, val routerDispatcher: String = Dispatchers.DefaultDispatcherId, val supervisorStrategy: SupervisorStrategy = Router.defaultSupervisorStrategy) extends RouterConfig with BroadcastLike { /** * Constructor that sets nrOfInstances to be created. * Java API */ def this(nr: Int) = { this(nrOfInstances = nr) } /** * Constructor that sets the routees to be used. * Java API * @param routeePaths string representation of the actor paths of the routees that will be looked up * using `actorFor` in [[akka.actor.ActorRefProvider]] */ def this(routeePaths: java.lang.Iterable[String]) = { this(routees = iterableAsScalaIterable(routeePaths)) } /** * Constructor that sets the resizer to be used. * Java API */ def this(resizer: Resizer) = this(resizer = Some(resizer)) /** * Java API for setting routerDispatcher */ def withDispatcher(dispatcherId: String) = copy(routerDispatcher = dispatcherId) /** * Java API for setting the supervisor strategy to be used for the “head” * Router actor. */ def withSupervisorStrategy(strategy: SupervisorStrategy) = copy(supervisorStrategy = strategy) } trait BroadcastLike { this: RouterConfig ⇒ def nrOfInstances: Int def routees: Iterable[String] def createRoute(props: Props, routeeProvider: RouteeProvider): Route = { routeeProvider.createAndRegisterRoutees(props, nrOfInstances, routees) { case (sender, message) ⇒ toAll(sender, routeeProvider.routees) } } } object ScatterGatherFirstCompletedRouter { def apply(routees: Iterable[ActorRef], within: Duration) = new ScatterGatherFirstCompletedRouter(routees = routees map (_.path.toString), within = within) /** * Java API to create router with the supplied 'routees' actors. */ def create(routees: java.lang.Iterable[ActorRef], within: Duration): ScatterGatherFirstCompletedRouter = { import scala.collection.JavaConverters._ apply(routees.asScala, within) } } /** * Simple router that broadcasts the message to all routees, and replies with the first response. *
* You have to defin the 'within: Duration' parameter (f.e: within = 10 seconds). *
* Please note that providing both 'nrOfInstances' and 'routees' does not make logical sense as this means * that the router should both create new actors and use the 'routees' actor(s). * In this case the 'nrOfInstances' will be ignored and the 'routees' will be used. *
* The configuration parameter trumps the constructor arguments. This means that * if you provide either 'nrOfInstances' or 'routees' during instantiation they will * be ignored if the router is defined in the configuration file for the actor being used. * *

Supervision Setup

* * The router creates a “head” actor which supervises and/or monitors the * routees. Instances are created as children of this actor, hence the * children are not supervised by the parent of the router. Common choices are * to always escalate (meaning that fault handling is always applied to all * children simultaneously; this is the default) or use the parent’s strategy, * which will result in routed children being treated individually, but it is * possible as well to use Routers to give different supervisor strategies to * different groups of children. * * {{{ * class MyActor extends Actor { * override val supervisorStrategy = ... * * val poolAsAWhole = context.actorOf(Props[SomeActor].withRouter(RoundRobinRouter(5))) * * val poolIndividuals = context.actorOf(Props[SomeActor].withRouter( * RoundRobinRouter(5, supervisorStrategy = this.supervisorStrategy))) * * val specialChild = context.actorOf(Props[SomeActor].withRouter( * RoundRobinRouter(5, supervisorStrategy = OneForOneStrategy() { * ... * }))) * } * }}} * * @param routees string representation of the actor paths of the routees that will be looked up * using `actorFor` in [[akka.actor.ActorRefProvider]] */ //TODO add @SerialVersionUID(1L) when SI-4804 is fixed case class ScatterGatherFirstCompletedRouter(nrOfInstances: Int = 0, routees: Iterable[String] = Nil, within: Duration, override val resizer: Option[Resizer] = None, val routerDispatcher: String = Dispatchers.DefaultDispatcherId, val supervisorStrategy: SupervisorStrategy = Router.defaultSupervisorStrategy) extends RouterConfig with ScatterGatherFirstCompletedLike { if (within <= Duration.Zero) throw new IllegalArgumentException( "[within: Duration] can not be zero or negative, was [" + within + "]") /** * Constructor that sets nrOfInstances to be created. * Java API */ def this(nr: Int, w: Duration) = { this(nrOfInstances = nr, within = w) } /** * Constructor that sets the routees to be used. * Java API * @param routeePaths string representation of the actor paths of the routees that will be looked up * using `actorFor` in [[akka.actor.ActorRefProvider]] */ def this(routeePaths: java.lang.Iterable[String], w: Duration) = { this(routees = iterableAsScalaIterable(routeePaths), within = w) } /** * Constructor that sets the resizer to be used. * Java API */ def this(resizer: Resizer, w: Duration) = this(resizer = Some(resizer), within = w) /** * Java API for setting routerDispatcher */ def withDispatcher(dispatcherId: String) = copy(routerDispatcher = dispatcherId) /** * Java API for setting the supervisor strategy to be used for the “head” * Router actor. */ def withSupervisorStrategy(strategy: SupervisorStrategy) = copy(supervisorStrategy = strategy) } trait ScatterGatherFirstCompletedLike { this: RouterConfig ⇒ def nrOfInstances: Int def routees: Iterable[String] def within: Duration def createRoute(props: Props, routeeProvider: RouteeProvider): Route = { routeeProvider.createAndRegisterRoutees(props, nrOfInstances, routees) { case (sender, message) ⇒ val provider: ActorRefProvider = routeeProvider.context.asInstanceOf[ActorCell].systemImpl.provider val asker = akka.pattern.PromiseActorRef(provider, within) asker.result.pipeTo(sender) toAll(asker, routeeProvider.routees) } } } /** * Routers with dynamically resizable number of routees is implemented by providing a Resizer * implementation in [[akka.routing.RouterConfig]]. */ trait Resizer { /** * Is it time for resizing. Typically implemented with modulo of nth message, but * could be based on elapsed time or something else. The messageCounter starts with 0 * for the initial resize and continues with 1 for the first message. Make sure to perform * initial resize before first message (messageCounter == 0), because there is no guarantee * that resize will be done when concurrent messages are in play. * * CAUTION: this method is invoked from the thread which tries to send a * message to the pool, i.e. the ActorRef.!() method, hence it may be called * concurrently. */ def isTimeForResize(messageCounter: Long): Boolean /** * Decide if the capacity of the router need to be changed. Will be invoked when `isTimeForResize` * returns true and no other resize is in progress. * Create and register more routees with `routeeProvider.registerRoutees(newRoutees) * or remove routees with `routeeProvider.unregisterRoutees(abandonedRoutees)` and * sending [[akka.actor.PoisonPill]] to them. * * This method is invoked only in the context of the Router actor in order to safely * create/stop children. */ def resize(props: Props, routeeProvider: RouteeProvider) } case object DefaultResizer { def apply(resizerConfig: Config): DefaultResizer = DefaultResizer( lowerBound = resizerConfig.getInt("lower-bound"), upperBound = resizerConfig.getInt("upper-bound"), pressureThreshold = resizerConfig.getInt("pressure-threshold"), rampupRate = resizerConfig.getDouble("rampup-rate"), backoffThreshold = resizerConfig.getDouble("backoff-threshold"), backoffRate = resizerConfig.getDouble("backoff-rate"), stopDelay = Duration(resizerConfig.getMilliseconds("stop-delay"), TimeUnit.MILLISECONDS), messagesPerResize = resizerConfig.getInt("messages-per-resize")) } case class DefaultResizer( /** * The fewest number of routees the router should ever have. */ lowerBound: Int = 1, /** * The most number of routees the router should ever have. * Must be greater than or equal to `lowerBound`. */ upperBound: Int = 10, /** * Threshold to evaluate if routee is considered to be busy (under pressure). * Implementation depends on this value (default is 1). *
    *
  • 0: number of routees currently processing a message.
  • *
  • 1: number of routees currently processing a message has * some messages in mailbox.
  • *
  • > 1: number of routees with at least the configured `pressureThreshold` * messages in their mailbox. Note that estimating mailbox size of * default UnboundedMailbox is O(N) operation.
  • *
*/ pressureThreshold: Int = 1, /** * Percentage to increase capacity whenever all routees are busy. * For example, 0.2 would increase 20% (rounded up), i.e. if current * capacity is 6 it will request an increase of 2 more routees. */ rampupRate: Double = 0.2, /** * Minimum fraction of busy routees before backing off. * For example, if this is 0.3, then we'll remove some routees only when * less than 30% of routees are busy, i.e. if current capacity is 10 and * 3 are busy then the capacity is unchanged, but if 2 or less are busy * the capacity is decreased. * * Use 0.0 or negative to avoid removal of routees. */ backoffThreshold: Double = 0.3, /** * Fraction of routees to be removed when the resizer reaches the * backoffThreshold. * For example, 0.1 would decrease 10% (rounded up), i.e. if current * capacity is 9 it will request an decrease of 1 routee. */ backoffRate: Double = 0.1, /** * When the resizer reduce the capacity the abandoned routee actors are stopped * with PoisonPill after this delay. The reason for the delay is to give concurrent * messages a chance to be placed in mailbox before sending PoisonPill. * Use 0 seconds to skip delay. */ stopDelay: Duration = 1.second, /** * Number of messages between resize operation. * Use 1 to resize before each message. */ messagesPerResize: Int = 10) extends Resizer { /** * Java API constructor for default values except bounds. */ def this(lower: Int, upper: Int) = this(lowerBound = lower, upperBound = upper) if (lowerBound < 0) throw new IllegalArgumentException("lowerBound must be >= 0, was: [%s]".format(lowerBound)) if (upperBound < 0) throw new IllegalArgumentException("upperBound must be >= 0, was: [%s]".format(upperBound)) if (upperBound < lowerBound) throw new IllegalArgumentException("upperBound must be >= lowerBound, was: [%s] < [%s]".format(upperBound, lowerBound)) if (rampupRate < 0.0) throw new IllegalArgumentException("rampupRate must be >= 0.0, was [%s]".format(rampupRate)) if (backoffThreshold > 1.0) throw new IllegalArgumentException("backoffThreshold must be <= 1.0, was [%s]".format(backoffThreshold)) if (backoffRate < 0.0) throw new IllegalArgumentException("backoffRate must be >= 0.0, was [%s]".format(backoffRate)) if (messagesPerResize <= 0) throw new IllegalArgumentException("messagesPerResize must be > 0, was [%s]".format(messagesPerResize)) def isTimeForResize(messageCounter: Long): Boolean = (messageCounter % messagesPerResize == 0) def resize(props: Props, routeeProvider: RouteeProvider) { val currentRoutees = routeeProvider.routees val requestedCapacity = capacity(currentRoutees) if (requestedCapacity > 0) { val newRoutees = routeeProvider.createRoutees(props, requestedCapacity, Nil) routeeProvider.registerRoutees(newRoutees) } else if (requestedCapacity < 0) { val (keep, abandon) = currentRoutees.splitAt(currentRoutees.length + requestedCapacity) routeeProvider.unregisterRoutees(abandon) delayedStop(routeeProvider.context.system.scheduler, abandon) } } /** * Give concurrent messages a chance to be placed in mailbox before * sending PoisonPill. */ protected def delayedStop(scheduler: Scheduler, abandon: IndexedSeq[ActorRef]) { if (abandon.nonEmpty) { if (stopDelay <= Duration.Zero) { abandon foreach (_ ! PoisonPill) } else { scheduler.scheduleOnce(stopDelay) { abandon foreach (_ ! PoisonPill) } } } } /** * Returns the overall desired change in resizer capacity. Positive value will * add routees to the resizer. Negative value will remove routees from the * resizer. * @param routees The current actor in the resizer * @return the number of routees by which the resizer should be adjusted (positive, negative or zero) */ def capacity(routees: IndexedSeq[ActorRef]): Int = { val currentSize = routees.size val press = pressure(routees) val delta = filter(press, currentSize) val proposed = currentSize + delta if (proposed < lowerBound) delta + (lowerBound - proposed) else if (proposed > upperBound) delta - (proposed - upperBound) else delta } /** * Number of routees considered busy, or above 'pressure level'. * * Implementation depends on the value of `pressureThreshold` * (default is 1). *
    *
  • 0: number of routees currently processing a message.
  • *
  • 1: number of routees currently processing a message has * some messages in mailbox.
  • *
  • > 1: number of routees with at least the configured `pressureThreshold` * messages in their mailbox. Note that estimating mailbox size of * default UnboundedMailbox is O(N) operation.
  • *
* * @param routees the current resizer of routees * @return number of busy routees, between 0 and routees.size */ def pressure(routees: IndexedSeq[ActorRef]): Int = { routees count { case a: LocalActorRef ⇒ val cell = a.underlying pressureThreshold match { case 1 ⇒ cell.mailbox.isScheduled && cell.mailbox.hasMessages case i if i < 1 ⇒ cell.mailbox.isScheduled && cell.currentMessage != null case threshold ⇒ cell.mailbox.numberOfMessages >= threshold } case x ⇒ false } } /** * This method can be used to smooth the capacity delta by considering * the current pressure and current capacity. * * @param pressure current number of busy routees * @param capacity current number of routees * @return proposed change in the capacity */ def filter(pressure: Int, capacity: Int): Int = { rampup(pressure, capacity) + backoff(pressure, capacity) } /** * Computes a proposed positive (or zero) capacity delta using * the configured `rampupRate`. * @param pressure the current number of busy routees * @param capacity the current number of total routees * @return proposed increase in capacity */ def rampup(pressure: Int, capacity: Int): Int = if (pressure < capacity) 0 else math.ceil(rampupRate * capacity) toInt /** * Computes a proposed negative (or zero) capacity delta using * the configured `backoffThreshold` and `backoffRate` * @param pressure the current number of busy routees * @param capacity the current number of total routees * @return proposed decrease in capacity (as a negative number) */ def backoff(pressure: Int, capacity: Int): Int = if (backoffThreshold > 0.0 && backoffRate > 0.0 && capacity > 0 && pressure.toDouble / capacity < backoffThreshold) math.floor(-1.0 * backoffRate * capacity) toInt else 0 }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy