2011-02-01 14:56:34 -08:00
|
|
|
/**
|
|
|
|
|
* Copyright (C) 2009-2011 Scalable Solutions AB <http://scalablesolutions.se>
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
package akka.routing
|
|
|
|
|
|
2011-03-18 23:04:48 +01:00
|
|
|
import akka.actor.{Actor, ActorRef, PoisonPill}
|
2011-02-01 14:56:34 -08:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Actor pooling
|
|
|
|
|
*
|
|
|
|
|
* An actor pool is an message router for a set of delegate actors. The pool is an actor itself.
|
|
|
|
|
* There are a handful of basic concepts that need to be understood when working with and defining your pool.
|
|
|
|
|
*
|
|
|
|
|
* Selectors - A selector is a trait that determines how and how many pooled actors will receive an incoming message.
|
|
|
|
|
* Capacitors - A capacitor is a trait that influences the size of pool. There are effectively two types.
|
2011-02-28 22:55:02 +01:00
|
|
|
* The first determines the size itself - either fixed or bounded.
|
|
|
|
|
* The second determines how to adjust of the pool according to some internal pressure characteristic.
|
2011-02-01 14:56:34 -08:00
|
|
|
* Filters - A filter can be used to refine the raw pressure value returned from a capacitor.
|
|
|
|
|
*
|
|
|
|
|
* It should be pointed out that all actors in the pool are treated as essentially equivalent. This is not to say
|
|
|
|
|
* that one couldn't instance different classes within the pool, only that the pool, when selecting and routing,
|
|
|
|
|
* will not take any type information into consideration.
|
|
|
|
|
*
|
|
|
|
|
* @author Garrick Evans
|
|
|
|
|
*/
|
|
|
|
|
|
2011-03-14 16:03:47 +01:00
|
|
|
object ActorPool {
|
|
|
|
|
case object Stat
|
|
|
|
|
case class Stats(size:Int)
|
2011-02-01 14:56:34 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Defines the nature of an actor pool.
|
|
|
|
|
*/
|
2011-03-14 16:03:47 +01:00
|
|
|
trait ActorPool {
|
2011-03-16 19:36:16 +01:00
|
|
|
def instance(): ActorRef //Question, Instance of what?
|
|
|
|
|
def capacity(delegates: Seq[ActorRef]): Int //Question, What is the semantics of this return value?
|
|
|
|
|
def select(delegates: Seq[ActorRef]): Tuple2[Iterator[ActorRef], Int] //Question, Why does select return this instead of an ordered Set?
|
2011-02-01 14:56:34 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* A default implementation of a pool, on each message to route,
|
2011-02-28 22:55:02 +01:00
|
|
|
* - checks the current capacity and adjusts accordingly if needed
|
|
|
|
|
* - routes the incoming message to a selection set of delegate actors
|
2011-02-01 14:56:34 -08:00
|
|
|
*/
|
2011-03-14 16:03:47 +01:00
|
|
|
trait DefaultActorPool extends ActorPool { this: Actor =>
|
|
|
|
|
import ActorPool._
|
|
|
|
|
import akka.actor.MaximumNumberOfRestartsWithinTimeRangeReached
|
|
|
|
|
|
2011-03-16 13:20:00 +01:00
|
|
|
protected var _delegates = Vector[ActorRef]()
|
2011-03-14 16:03:47 +01:00
|
|
|
private var _lastCapacityChange = 0
|
|
|
|
|
private var _lastSelectorCount = 0
|
|
|
|
|
|
2011-04-26 20:31:08 +02:00
|
|
|
override def postStop() = _delegates foreach {
|
2011-03-16 14:13:48 +01:00
|
|
|
delegate => try {
|
|
|
|
|
delegate ! PoisonPill
|
|
|
|
|
} catch { case e: Exception => } //Ignore any exceptions here
|
|
|
|
|
}
|
2011-03-14 16:03:47 +01:00
|
|
|
|
|
|
|
|
protected def _route(): Receive = {
|
|
|
|
|
// for testing...
|
|
|
|
|
case Stat =>
|
|
|
|
|
self reply_? Stats(_delegates length)
|
|
|
|
|
case max: MaximumNumberOfRestartsWithinTimeRangeReached =>
|
|
|
|
|
_delegates = _delegates filterNot { _.uuid == max.victim.uuid }
|
|
|
|
|
case msg =>
|
2011-03-16 14:13:48 +01:00
|
|
|
resizeIfAppropriate()
|
|
|
|
|
|
|
|
|
|
select(_delegates) match {
|
|
|
|
|
case (selectedDelegates, count) =>
|
|
|
|
|
_lastSelectorCount = count
|
2011-03-16 21:18:10 +01:00
|
|
|
selectedDelegates foreach { _ forward msg } //Should we really send the same message to several actors?
|
2011-03-16 14:13:48 +01:00
|
|
|
}
|
2011-03-14 16:03:47 +01:00
|
|
|
}
|
|
|
|
|
|
2011-03-16 14:13:48 +01:00
|
|
|
private def resizeIfAppropriate() {
|
2011-03-16 13:20:00 +01:00
|
|
|
val requestedCapacity = capacity(_delegates)
|
|
|
|
|
val newDelegates = requestedCapacity match {
|
|
|
|
|
case qty if qty > 0 =>
|
|
|
|
|
_delegates ++ { for (i <- 0 until requestedCapacity) yield {
|
2011-03-14 16:03:47 +01:00
|
|
|
val delegate = instance()
|
|
|
|
|
self startLink delegate
|
|
|
|
|
delegate
|
2011-02-28 22:55:02 +01:00
|
|
|
}
|
2011-03-14 16:03:47 +01:00
|
|
|
}
|
2011-03-16 13:20:00 +01:00
|
|
|
case qty if qty < 0 =>
|
|
|
|
|
_delegates.splitAt(_delegates.length + requestedCapacity) match {
|
|
|
|
|
case (keep, abandon) =>
|
|
|
|
|
abandon foreach { _ ! PoisonPill }
|
|
|
|
|
keep
|
|
|
|
|
}
|
|
|
|
|
case _ => _delegates //No change
|
2011-03-14 16:03:47 +01:00
|
|
|
}
|
2011-03-16 13:20:00 +01:00
|
|
|
|
|
|
|
|
_lastCapacityChange = requestedCapacity
|
|
|
|
|
_delegates = newDelegates
|
2011-03-14 16:03:47 +01:00
|
|
|
}
|
2011-02-01 14:56:34 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Selectors
|
2011-02-28 22:55:02 +01:00
|
|
|
* These traits define how, when a message needs to be routed, delegate(s) are chosen from the pool
|
2011-04-20 16:12:51 +12:00
|
|
|
*/
|
2011-02-01 14:56:34 -08:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Returns the set of delegates with the least amount of message backlog.
|
|
|
|
|
*/
|
2011-03-14 16:03:47 +01:00
|
|
|
trait SmallestMailboxSelector {
|
|
|
|
|
def selectionCount: Int
|
|
|
|
|
def partialFill: Boolean
|
|
|
|
|
|
|
|
|
|
def select(delegates: Seq[ActorRef]): Tuple2[Iterator[ActorRef], Int] = {
|
|
|
|
|
var set: Seq[ActorRef] = Nil
|
2011-03-16 19:31:54 +01:00
|
|
|
var take = if (partialFill) math.min(selectionCount, delegates.length) else selectionCount
|
2011-03-14 16:03:47 +01:00
|
|
|
|
|
|
|
|
while (take > 0) {
|
2011-03-16 19:31:54 +01:00
|
|
|
set = delegates.sortWith(_.mailboxSize < _.mailboxSize).take(take) ++ set //Question, doesn't this risk selecting the same actor multiple times?
|
2011-03-14 16:03:47 +01:00
|
|
|
take -= set.size
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
(set.iterator, set.size)
|
|
|
|
|
}
|
2011-02-01 14:56:34 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Returns the set of delegates that occur sequentially 'after' the last delegate from the previous selection
|
|
|
|
|
*/
|
2011-03-14 16:03:47 +01:00
|
|
|
trait RoundRobinSelector {
|
|
|
|
|
private var _last: Int = -1;
|
|
|
|
|
|
|
|
|
|
def selectionCount: Int
|
|
|
|
|
def partialFill: Boolean
|
|
|
|
|
|
|
|
|
|
def select(delegates:Seq[ActorRef]):Tuple2[Iterator[ActorRef], Int] = {
|
|
|
|
|
val length = delegates.length
|
|
|
|
|
val take = if (partialFill) math.min(selectionCount, length)
|
|
|
|
|
else selectionCount
|
|
|
|
|
|
|
|
|
|
val set =
|
2011-04-20 16:12:51 +12:00
|
|
|
for (i <- 0 until take) yield {
|
2011-03-14 16:03:47 +01:00
|
|
|
_last = (_last + 1) % length
|
|
|
|
|
delegates(_last)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
(set.iterator, set.size)
|
|
|
|
|
}
|
2011-02-01 14:56:34 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Capacitors
|
2011-02-28 22:55:02 +01:00
|
|
|
* These traits define how to alter the size of the pool
|
2011-02-01 14:56:34 -08:00
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Ensures a fixed number of delegates in the pool
|
|
|
|
|
*/
|
2011-03-14 16:03:47 +01:00
|
|
|
trait FixedSizeCapacitor {
|
|
|
|
|
def limit:Int
|
2011-03-16 19:31:54 +01:00
|
|
|
def capacity(delegates: Seq[ActorRef]): Int = (limit - delegates.size) max 0
|
2011-02-01 14:56:34 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Constrains the pool capacity to a bounded range
|
|
|
|
|
*/
|
2011-03-14 16:03:47 +01:00
|
|
|
trait BoundedCapacitor {
|
|
|
|
|
def lowerBound: Int
|
|
|
|
|
def upperBound: Int
|
|
|
|
|
|
|
|
|
|
def capacity(delegates: Seq[ActorRef]): Int = {
|
|
|
|
|
val current = delegates length
|
|
|
|
|
val delta = _eval(delegates)
|
|
|
|
|
val proposed = current + delta
|
|
|
|
|
|
|
|
|
|
if (proposed < lowerBound) delta + (lowerBound - proposed)
|
|
|
|
|
else if (proposed > upperBound) delta - (proposed - upperBound)
|
|
|
|
|
else delta
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected def _eval(delegates: Seq[ActorRef]): Int
|
2011-02-01 14:56:34 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Returns the number of delegates required to manage the current message backlogs
|
|
|
|
|
*/
|
2011-03-14 16:03:47 +01:00
|
|
|
trait MailboxPressureCapacitor {
|
|
|
|
|
def pressureThreshold:Int
|
|
|
|
|
def pressure(delegates: Seq[ActorRef]): Int =
|
|
|
|
|
delegates count { _.mailboxSize > pressureThreshold }
|
2011-02-01 14:56:34 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Returns the number of delegates required to respond to the number of pending futures
|
|
|
|
|
*/
|
2011-03-14 16:03:47 +01:00
|
|
|
trait ActiveFuturesPressureCapacitor {
|
|
|
|
|
def pressure(delegates: Seq[ActorRef]): Int =
|
|
|
|
|
delegates count { _.senderFuture.isDefined }
|
2011-02-01 14:56:34 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
*/
|
2011-03-14 16:03:47 +01:00
|
|
|
trait CapacityStrategy {
|
|
|
|
|
import ActorPool._
|
|
|
|
|
|
|
|
|
|
def pressure(delegates: Seq[ActorRef]): Int
|
|
|
|
|
def filter(pressure: Int, capacity: Int): Int
|
|
|
|
|
|
|
|
|
|
protected def _eval(delegates: Seq[ActorRef]): Int = filter(pressure(delegates), delegates.size)
|
2011-02-01 14:56:34 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
trait FixedCapacityStrategy extends FixedSizeCapacitor
|
|
|
|
|
trait BoundedCapacityStrategy extends CapacityStrategy with BoundedCapacitor
|
2011-02-15 15:58:43 -08:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Filters
|
|
|
|
|
* These traits refine the raw pressure reading into a more appropriate capacity delta.
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* The basic filter trait that composes ramp-up and and back-off subfiltering.
|
|
|
|
|
*/
|
2011-03-14 16:03:47 +01:00
|
|
|
trait Filter {
|
|
|
|
|
def rampup(pressure: Int, capacity: Int): Int
|
|
|
|
|
def backoff(pressure: Int, capacity: Int): Int
|
|
|
|
|
|
|
|
|
|
// pass through both filters just to be sure any internal counters
|
|
|
|
|
// are updated consistently. ramping up is always + and backing off
|
|
|
|
|
// is always - and each should return 0 otherwise...
|
|
|
|
|
def filter(pressure: Int, capacity: Int): Int =
|
|
|
|
|
rampup (pressure, capacity) + backoff (pressure, capacity)
|
2011-02-15 15:58:43 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
trait BasicFilter extends Filter with BasicRampup with BasicBackoff
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Filter performs steady incremental growth using only the basic ramp-up subfilter
|
|
|
|
|
*/
|
2011-03-14 16:03:47 +01:00
|
|
|
trait BasicNoBackoffFilter extends BasicRampup {
|
|
|
|
|
def filter(pressure: Int, capacity: Int): Int = rampup(pressure, capacity)
|
2011-02-15 15:58:43 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Basic incremental growth as a percentage of the current pool capacity
|
|
|
|
|
*/
|
2011-03-14 16:03:47 +01:00
|
|
|
trait BasicRampup {
|
|
|
|
|
def rampupRate: Double
|
2011-02-15 15:58:43 -08:00
|
|
|
|
2011-03-14 16:03:47 +01:00
|
|
|
def rampup(pressure: Int, capacity: Int): Int =
|
|
|
|
|
if (pressure < capacity) 0 else math.ceil(rampupRate * capacity) toInt
|
2011-02-15 15:58:43 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Basic decrement as a percentage of the current pool capacity
|
|
|
|
|
*/
|
2011-03-14 16:03:47 +01:00
|
|
|
trait BasicBackoff {
|
|
|
|
|
def backoffThreshold: Double
|
|
|
|
|
def backoffRate: Double
|
|
|
|
|
|
|
|
|
|
def backoff(pressure: Int, capacity: Int): Int =
|
|
|
|
|
if (capacity > 0 && pressure / capacity < backoffThreshold) math.ceil(-1.0 * backoffRate * capacity) toInt else 0
|
2011-02-15 15:58:43 -08:00
|
|
|
}
|
|
|
|
|
/**
|
|
|
|
|
* This filter tracks the average pressure over the lifetime of the pool (or since last reset) and
|
|
|
|
|
* will begin to reduce capacity once this value drops below the provided threshold. The number of
|
|
|
|
|
* delegates to cull from the pool is determined by some scaling factor (the backoffRate) multiplied
|
|
|
|
|
* by the difference in capacity and pressure.
|
|
|
|
|
*/
|
2011-03-14 16:03:47 +01:00
|
|
|
trait RunningMeanBackoff {
|
|
|
|
|
def backoffThreshold: Double
|
|
|
|
|
def backoffRate: Double
|
2011-02-15 15:58:43 -08:00
|
|
|
|
2011-03-14 16:03:47 +01:00
|
|
|
private var _pressure: Double = 0.0
|
|
|
|
|
private var _capacity: Double = 0.0
|
2011-02-15 15:58:43 -08:00
|
|
|
|
2011-03-14 16:03:47 +01:00
|
|
|
def backoff(pressure: Int, capacity: Int): Int = {
|
2011-02-15 15:58:43 -08:00
|
|
|
_pressure += pressure
|
|
|
|
|
_capacity += capacity
|
|
|
|
|
|
2011-03-16 19:31:54 +01:00
|
|
|
if (capacity > 0 && pressure / capacity < backoffThreshold
|
|
|
|
|
&& _capacity > 0 && _pressure / _capacity < backoffThreshold) //Why does the entire clause need to be true?
|
2011-02-15 15:58:43 -08:00
|
|
|
math.floor(-1.0 * backoffRate * (capacity-pressure)).toInt
|
2011-03-14 16:03:47 +01:00
|
|
|
else 0
|
2011-02-15 15:58:43 -08:00
|
|
|
}
|
|
|
|
|
|
2011-03-16 19:31:54 +01:00
|
|
|
def backoffReset {
|
|
|
|
|
_pressure = 0.0
|
2011-02-15 15:58:43 -08:00
|
|
|
_capacity = 0.0
|
|
|
|
|
}
|
|
|
|
|
}
|