2012-09-11 15:10:16 +02:00
|
|
|
|
/**
|
|
|
|
|
|
* Copyright (C) 2009-2012 Typesafe Inc. <http://www.typesafe.com>
|
|
|
|
|
|
*/
|
|
|
|
|
|
package akka.routing
|
|
|
|
|
|
|
|
|
|
|
|
import scala.collection.JavaConversions.iterableAsScalaIterable
|
|
|
|
|
|
import scala.util.control.NonFatal
|
|
|
|
|
|
import akka.actor.ActorRef
|
|
|
|
|
|
import akka.actor.SupervisorStrategy
|
|
|
|
|
|
import akka.actor.Props
|
|
|
|
|
|
import akka.dispatch.Dispatchers
|
|
|
|
|
|
import akka.event.Logging
|
|
|
|
|
|
import akka.serialization.SerializationExtension
|
2012-09-13 18:06:35 +02:00
|
|
|
|
import java.util.concurrent.atomic.AtomicReference
|
2012-09-11 15:10:16 +02:00
|
|
|
|
|
|
|
|
|
|
object ConsistentHashingRouter {
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Creates a new ConsistentHashingRouter, routing to the specified routees
|
|
|
|
|
|
*/
|
|
|
|
|
|
def apply(routees: Iterable[ActorRef]): ConsistentHashingRouter =
|
|
|
|
|
|
new ConsistentHashingRouter(routees = routees map (_.path.toString))
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Java API to create router with the supplied 'routees' actors.
|
|
|
|
|
|
*/
|
|
|
|
|
|
def create(routees: java.lang.Iterable[ActorRef]): ConsistentHashingRouter = {
|
|
|
|
|
|
import scala.collection.JavaConverters._
|
|
|
|
|
|
apply(routees.asScala)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
2012-09-17 13:24:13 +02:00
|
|
|
|
* If you don't define the `hashMapping` when
|
2012-09-14 13:47:58 +02:00
|
|
|
|
* constructing the [[akka.routing.ConsistentHashingRouter]]
|
|
|
|
|
|
* the messages need to implement this interface to define what
|
2012-09-11 15:10:16 +02:00
|
|
|
|
* data to use for the consistent hash key. Note that it's not
|
2012-09-14 13:47:58 +02:00
|
|
|
|
* the hash, but the data to be hashed.
|
|
|
|
|
|
*
|
|
|
|
|
|
* If returning an `Array[Byte]` or String it will be used as is,
|
|
|
|
|
|
* otherwise the configured [[akka.akka.serialization.Serializer]]
|
|
|
|
|
|
* will be applied to the returned data.
|
2012-09-11 15:10:16 +02:00
|
|
|
|
*
|
|
|
|
|
|
* If messages can't implement this interface themselves,
|
|
|
|
|
|
* it's possible to wrap the messages in
|
2012-09-14 13:47:58 +02:00
|
|
|
|
* [[akka.routing.ConsistentHashingRouter.ConsistentHashableEnvelope]],
|
|
|
|
|
|
* or use [[akka.routing.ConsistentHashingRouter.ConsistentHashableEnvelope]]
|
2012-09-11 15:10:16 +02:00
|
|
|
|
*/
|
|
|
|
|
|
trait ConsistentHashable {
|
|
|
|
|
|
def consistentHashKey: Any
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
2012-09-17 13:24:13 +02:00
|
|
|
|
* If you don't define the `hashMapping` when
|
2012-09-14 13:47:58 +02:00
|
|
|
|
* constructing the [[akka.routing.ConsistentHashingRouter]]
|
|
|
|
|
|
* and messages can't implement [[akka.routing.ConsistentHashingRouter.ConsistentHashable]]
|
2012-09-13 18:06:35 +02:00
|
|
|
|
* themselves they can we wrapped by this envelope instead. The
|
|
|
|
|
|
* router will only send the wrapped message to the destination,
|
|
|
|
|
|
* i.e. the envelope will be stripped off.
|
2012-09-11 15:10:16 +02:00
|
|
|
|
*/
|
2012-09-13 18:06:35 +02:00
|
|
|
|
@SerialVersionUID(1L)
|
2012-09-17 13:24:13 +02:00
|
|
|
|
final case class ConsistentHashableEnvelope(message: Any, hashKey: Any)
|
|
|
|
|
|
extends ConsistentHashable with RouterEnvelope {
|
|
|
|
|
|
override def consistentHashKey: Any = hashKey
|
|
|
|
|
|
}
|
2012-09-11 15:10:16 +02:00
|
|
|
|
|
2012-09-14 13:47:58 +02:00
|
|
|
|
/**
|
|
|
|
|
|
* Partial function from message to the data to
|
|
|
|
|
|
* use for the consistent hash key. Note that it's not
|
|
|
|
|
|
* the hash that is to be returned, but the data to be hashed.
|
|
|
|
|
|
*
|
|
|
|
|
|
* If returning an `Array[Byte]` or String it will be used as is,
|
|
|
|
|
|
* otherwise the configured [[akka.akka.serialization.Serializer]]
|
|
|
|
|
|
* will be applied to the returned data.
|
|
|
|
|
|
*/
|
2012-09-17 13:24:13 +02:00
|
|
|
|
type ConsistentHashMapping = PartialFunction[Any, Any]
|
2012-09-14 13:47:58 +02:00
|
|
|
|
|
|
|
|
|
|
@SerialVersionUID(1L)
|
2012-09-17 13:24:13 +02:00
|
|
|
|
object emptyConsistentHashMapping extends ConsistentHashMapping {
|
2012-09-14 13:47:58 +02:00
|
|
|
|
def isDefinedAt(x: Any) = false
|
2012-09-17 13:24:13 +02:00
|
|
|
|
def apply(x: Any) = throw new UnsupportedOperationException("Empty ConsistentHashMapping apply()")
|
2012-09-14 13:47:58 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* JAVA API
|
|
|
|
|
|
* Mapping from message to the data to use for the consistent hash key.
|
|
|
|
|
|
* Note that it's not the hash that is to be returned, but the data to be
|
|
|
|
|
|
* hashed.
|
|
|
|
|
|
*
|
2012-09-17 11:40:06 +02:00
|
|
|
|
* May return `null` to indicate that the message is not handled by
|
|
|
|
|
|
* this mapping.
|
|
|
|
|
|
*
|
2012-09-14 13:47:58 +02:00
|
|
|
|
* If returning an `Array[Byte]` or String it will be used as is,
|
|
|
|
|
|
* otherwise the configured [[akka.akka.serialization.Serializer]]
|
|
|
|
|
|
* will be applied to the returned data.
|
|
|
|
|
|
*/
|
2012-09-17 13:24:13 +02:00
|
|
|
|
trait ConsistentHashMapper {
|
|
|
|
|
|
def hashKey(message: Any): Any
|
2012-09-14 13:47:58 +02:00
|
|
|
|
}
|
2012-09-11 15:10:16 +02:00
|
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
|
|
|
* A Router that uses consistent hashing to select a connection based on the
|
2012-09-14 13:47:58 +02:00
|
|
|
|
* sent message.
|
|
|
|
|
|
*
|
|
|
|
|
|
* There is 3 ways to define what data to use for the consistent hash key.
|
|
|
|
|
|
*
|
2012-09-17 13:24:13 +02:00
|
|
|
|
* 1. You can define `hashMapping` / `withHashMapper`
|
2012-09-14 13:47:58 +02:00
|
|
|
|
* of the router to map incoming messages to their consistent hash key.
|
2012-09-17 11:40:06 +02:00
|
|
|
|
* This makes the decision transparent for the sender.
|
2012-09-14 13:47:58 +02:00
|
|
|
|
*
|
2012-09-17 11:40:06 +02:00
|
|
|
|
* 2. The messages may implement [[akka.routing.ConsistentHashingRouter.ConsistentHashable]].
|
2012-09-14 13:47:58 +02:00
|
|
|
|
* The key is part of the message and it's convenient to define it together
|
|
|
|
|
|
* with the message definition.
|
|
|
|
|
|
*
|
|
|
|
|
|
* 3. The messages can be be wrapped in a [[akka.routing.ConsistentHashingRouter.ConsistentHashableEnvelope]]
|
|
|
|
|
|
* to define what data to use for the consistent hash key. The sender knows
|
|
|
|
|
|
* the key to use.
|
|
|
|
|
|
*
|
|
|
|
|
|
* These ways to define the consistent hash key can be use together and at
|
2012-09-17 13:24:13 +02:00
|
|
|
|
* the same time for one router. The `hashMapping` is tried first.
|
2012-09-11 15:10:16 +02:00
|
|
|
|
*
|
|
|
|
|
|
* 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.
|
|
|
|
|
|
* <br>
|
|
|
|
|
|
* <b>The</b> 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.
|
|
|
|
|
|
*
|
|
|
|
|
|
* <h1>Supervision Setup</h1>
|
|
|
|
|
|
*
|
|
|
|
|
|
* 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.
|
|
|
|
|
|
*
|
|
|
|
|
|
* @param routees string representation of the actor paths of the routees that will be looked up
|
|
|
|
|
|
* using `actorFor` in [[akka.actor.ActorRefProvider]]
|
2012-09-13 18:06:35 +02:00
|
|
|
|
* @param virtualNodesFactor number of virtual nodes per node, used in [[akka.routing.ConsistantHash]]
|
2012-09-17 13:24:13 +02:00
|
|
|
|
* @param hashMapping partial function from message to the data to
|
2012-09-14 13:47:58 +02:00
|
|
|
|
* use for the consistent hash key
|
2012-09-11 15:10:16 +02:00
|
|
|
|
*/
|
|
|
|
|
|
@SerialVersionUID(1L)
|
|
|
|
|
|
case class ConsistentHashingRouter(
|
|
|
|
|
|
nrOfInstances: Int = 0, routees: Iterable[String] = Nil, override val resizer: Option[Resizer] = None,
|
|
|
|
|
|
val routerDispatcher: String = Dispatchers.DefaultDispatcherId,
|
|
|
|
|
|
val supervisorStrategy: SupervisorStrategy = Router.defaultSupervisorStrategy,
|
2012-09-14 13:47:58 +02:00
|
|
|
|
val virtualNodesFactor: Int = 0,
|
2012-09-17 13:24:13 +02:00
|
|
|
|
val hashMapping: ConsistentHashingRouter.ConsistentHashMapping = ConsistentHashingRouter.emptyConsistentHashMapping)
|
2012-09-11 15:10:16 +02:00
|
|
|
|
extends RouterConfig with ConsistentHashingLike {
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 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): ConsistentHashingRouter = copy(routerDispatcher = dispatcherId)
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Java API for setting the supervisor strategy to be used for the “head”
|
|
|
|
|
|
* Router actor.
|
|
|
|
|
|
*/
|
|
|
|
|
|
def withSupervisorStrategy(strategy: SupervisorStrategy): ConsistentHashingRouter = copy(supervisorStrategy = strategy)
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
2012-09-13 18:06:35 +02:00
|
|
|
|
* Java API for setting the number of virtual nodes per node, used in [[akka.routing.ConsistantHash]]
|
2012-09-11 15:10:16 +02:00
|
|
|
|
*/
|
2012-09-13 18:06:35 +02:00
|
|
|
|
def withVirtualNodesFactor(vnodes: Int): ConsistentHashingRouter = copy(virtualNodesFactor = vnodes)
|
2012-09-11 15:10:16 +02:00
|
|
|
|
|
2012-09-14 13:47:58 +02:00
|
|
|
|
/**
|
2012-09-17 11:40:06 +02:00
|
|
|
|
* Java API for setting the mapping from message to the data to use for the consistent hash key.
|
2012-09-14 13:47:58 +02:00
|
|
|
|
*/
|
2012-09-17 13:24:13 +02:00
|
|
|
|
def withHashMapper(mapping: ConsistentHashingRouter.ConsistentHashMapper) = {
|
|
|
|
|
|
copy(hashMapping = {
|
|
|
|
|
|
case message if (mapping.hashKey(message).asInstanceOf[AnyRef] ne null) ⇒
|
|
|
|
|
|
mapping.hashKey(message)
|
2012-09-14 13:47:58 +02:00
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2012-09-11 15:10:16 +02:00
|
|
|
|
/**
|
|
|
|
|
|
* Uses the resizer of the given RouterConfig if this RouterConfig
|
|
|
|
|
|
* doesn't have one, i.e. the resizer defined in code is used if
|
|
|
|
|
|
* resizer was not defined in config.
|
2012-09-17 13:24:13 +02:00
|
|
|
|
* Uses the the `hashMapping` defined in code, since
|
2012-09-14 13:47:58 +02:00
|
|
|
|
* that can't be defined in configuration.
|
2012-09-11 15:10:16 +02:00
|
|
|
|
*/
|
2012-09-14 13:47:58 +02:00
|
|
|
|
override def withFallback(other: RouterConfig): RouterConfig = other match {
|
2012-09-17 11:40:06 +02:00
|
|
|
|
case _: FromConfig ⇒ this
|
2012-09-14 13:47:58 +02:00
|
|
|
|
case otherRouter: ConsistentHashingRouter ⇒
|
|
|
|
|
|
val useResizer =
|
|
|
|
|
|
if (this.resizer.isEmpty && otherRouter.resizer.isDefined) otherRouter.resizer
|
|
|
|
|
|
else this.resizer
|
2012-09-17 13:24:13 +02:00
|
|
|
|
copy(resizer = useResizer, hashMapping = otherRouter.hashMapping)
|
2012-09-14 13:47:58 +02:00
|
|
|
|
case _ ⇒ throw new IllegalArgumentException("Expected ConsistentHashingRouter, got [%s]".format(other))
|
2012-09-11 15:10:16 +02:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2012-09-13 18:06:35 +02:00
|
|
|
|
/**
|
|
|
|
|
|
* The core pieces of the routing logic is located in this
|
|
|
|
|
|
* trait to be able to extend.
|
|
|
|
|
|
*/
|
2012-09-11 15:10:16 +02:00
|
|
|
|
trait ConsistentHashingLike { this: RouterConfig ⇒
|
|
|
|
|
|
|
|
|
|
|
|
import ConsistentHashingRouter._
|
|
|
|
|
|
|
|
|
|
|
|
def nrOfInstances: Int
|
|
|
|
|
|
|
|
|
|
|
|
def routees: Iterable[String]
|
|
|
|
|
|
|
2012-09-13 18:06:35 +02:00
|
|
|
|
def virtualNodesFactor: Int
|
2012-09-11 15:10:16 +02:00
|
|
|
|
|
2012-09-17 13:24:13 +02:00
|
|
|
|
def hashMapping: ConsistentHashMapping
|
2012-09-14 13:47:58 +02:00
|
|
|
|
|
2012-09-11 15:10:16 +02:00
|
|
|
|
override def createRoute(routeeProvider: RouteeProvider): Route = {
|
|
|
|
|
|
if (resizer.isEmpty) {
|
|
|
|
|
|
if (routees.isEmpty) routeeProvider.createRoutees(nrOfInstances)
|
|
|
|
|
|
else routeeProvider.registerRouteesFor(routees)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
val log = Logging(routeeProvider.context.system, routeeProvider.context.self)
|
2012-09-13 18:06:35 +02:00
|
|
|
|
val vnodes =
|
|
|
|
|
|
if (virtualNodesFactor == 0) routeeProvider.context.system.settings.DefaultVirtualNodesFactor
|
|
|
|
|
|
else virtualNodesFactor
|
2012-09-11 15:10:16 +02:00
|
|
|
|
|
2012-09-13 20:44:17 +02:00
|
|
|
|
// tuple of routees and the ConsistentHash, updated together in updateConsistentHash
|
|
|
|
|
|
val consistentHashRef = new AtomicReference[(IndexedSeq[ActorRef], ConsistentHash[ActorRef])]((null, null))
|
2012-09-13 18:06:35 +02:00
|
|
|
|
updateConsistentHash()
|
2012-09-11 15:10:16 +02:00
|
|
|
|
|
|
|
|
|
|
// update consistentHash when routees has changed
|
|
|
|
|
|
// changes to routees are rare and when no changes this is a quick operation
|
2012-09-13 18:06:35 +02:00
|
|
|
|
def updateConsistentHash(): ConsistentHash[ActorRef] = {
|
2012-09-13 20:44:17 +02:00
|
|
|
|
val oldConsistentHashTuple = consistentHashRef.get
|
|
|
|
|
|
val (oldConsistentHashRoutees, oldConsistentHash) = oldConsistentHashTuple
|
2012-09-13 22:04:22 +02:00
|
|
|
|
val currentRoutees = routeeProvider.routees
|
2012-09-13 20:44:17 +02:00
|
|
|
|
if (currentRoutees ne oldConsistentHashRoutees) {
|
|
|
|
|
|
// when other instance, same content, no need to re-hash, but try to set routees
|
|
|
|
|
|
val consistentHash =
|
|
|
|
|
|
if (currentRoutees == oldConsistentHashRoutees) oldConsistentHash
|
|
|
|
|
|
else ConsistentHash(currentRoutees, vnodes) // re-hash
|
2012-09-13 18:06:35 +02:00
|
|
|
|
// ignore, don't update, in case of CAS failure
|
2012-09-13 20:44:17 +02:00
|
|
|
|
consistentHashRef.compareAndSet(oldConsistentHashTuple, (currentRoutees, consistentHash))
|
|
|
|
|
|
consistentHash
|
|
|
|
|
|
} else oldConsistentHash
|
2012-09-11 15:10:16 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
def target(hashData: Any): ActorRef = try {
|
2012-09-13 18:06:35 +02:00
|
|
|
|
val currentConsistenHash = updateConsistentHash()
|
2012-09-11 15:10:16 +02:00
|
|
|
|
if (currentConsistenHash.isEmpty) routeeProvider.context.system.deadLetters
|
2012-09-17 09:51:15 +02:00
|
|
|
|
else hashData match {
|
|
|
|
|
|
case bytes: Array[Byte] ⇒ currentConsistenHash.nodeFor(bytes)
|
|
|
|
|
|
case str: String ⇒ currentConsistenHash.nodeFor(str)
|
|
|
|
|
|
case x: AnyRef ⇒ currentConsistenHash.nodeFor(SerializationExtension(routeeProvider.context.system).serialize(x).get)
|
|
|
|
|
|
}
|
2012-09-11 15:10:16 +02:00
|
|
|
|
} catch {
|
|
|
|
|
|
case NonFatal(e) ⇒
|
|
|
|
|
|
// serialization failed
|
2012-09-17 13:24:13 +02:00
|
|
|
|
log.warning("Couldn't route message with consistent hash key [{}] due to [{}]", hashData, e.getMessage)
|
2012-09-11 15:10:16 +02:00
|
|
|
|
routeeProvider.context.system.deadLetters
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
case (sender, message) ⇒
|
|
|
|
|
|
message match {
|
2012-09-14 13:47:58 +02:00
|
|
|
|
case Broadcast(msg) ⇒ toAll(sender, routeeProvider.routees)
|
2012-09-17 13:24:13 +02:00
|
|
|
|
case _ if hashMapping.isDefinedAt(message) ⇒
|
|
|
|
|
|
List(Destination(sender, target(hashMapping(message))))
|
2012-09-11 15:10:16 +02:00
|
|
|
|
case hashable: ConsistentHashable ⇒ List(Destination(sender, target(hashable.consistentHashKey)))
|
|
|
|
|
|
case other ⇒
|
2012-09-17 13:24:13 +02:00
|
|
|
|
log.warning("Message [{}] must be handled by hashMapping, or implement [{}] or be wrapped in [{}]",
|
2012-09-11 15:10:16 +02:00
|
|
|
|
message.getClass.getName, classOf[ConsistentHashable].getName,
|
|
|
|
|
|
classOf[ConsistentHashableEnvelope].getName)
|
|
|
|
|
|
List(Destination(sender, routeeProvider.context.system.deadLetters))
|
|
|
|
|
|
}
|
2012-09-14 13:47:58 +02:00
|
|
|
|
|
2012-09-11 15:10:16 +02:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|