2012-09-13 10:54:14 +02:00
|
|
|
package sample.cluster.stats
|
|
|
|
|
|
|
|
|
|
//#imports
|
|
|
|
|
import language.postfixOps
|
2013-04-28 22:05:40 +02:00
|
|
|
import scala.collection.immutable
|
2012-09-13 10:54:14 +02:00
|
|
|
import scala.concurrent.forkjoin.ThreadLocalRandom
|
2012-09-21 14:50:06 +02:00
|
|
|
import scala.concurrent.duration._
|
2012-09-13 10:54:14 +02:00
|
|
|
import com.typesafe.config.ConfigFactory
|
|
|
|
|
import akka.actor.Actor
|
|
|
|
|
import akka.actor.ActorLogging
|
|
|
|
|
import akka.actor.ActorRef
|
2013-03-26 18:17:50 +01:00
|
|
|
import akka.actor.ActorSelection
|
2012-09-13 10:54:14 +02:00
|
|
|
import akka.actor.ActorSystem
|
|
|
|
|
import akka.actor.Address
|
2013-01-14 14:09:53 +01:00
|
|
|
import akka.actor.PoisonPill
|
2012-09-13 10:54:14 +02:00
|
|
|
import akka.actor.Props
|
|
|
|
|
import akka.actor.ReceiveTimeout
|
|
|
|
|
import akka.actor.RelativeActorPath
|
|
|
|
|
import akka.actor.RootActorPath
|
|
|
|
|
import akka.cluster.Cluster
|
2012-11-27 18:07:37 +01:00
|
|
|
import akka.cluster.ClusterEvent._
|
2012-09-13 10:54:14 +02:00
|
|
|
import akka.cluster.MemberStatus
|
2013-04-28 22:05:40 +02:00
|
|
|
import akka.cluster.Member
|
2013-01-14 14:09:53 +01:00
|
|
|
import akka.contrib.pattern.ClusterSingletonManager
|
2012-09-13 10:54:14 +02:00
|
|
|
import akka.routing.FromConfig
|
2012-09-20 12:58:51 +02:00
|
|
|
import akka.routing.ConsistentHashingRouter.ConsistentHashableEnvelope
|
2012-09-13 10:54:14 +02:00
|
|
|
//#imports
|
|
|
|
|
|
|
|
|
|
//#messages
|
|
|
|
|
case class StatsJob(text: String)
|
|
|
|
|
case class StatsResult(meanWordLength: Double)
|
|
|
|
|
case class JobFailed(reason: String)
|
|
|
|
|
//#messages
|
|
|
|
|
|
|
|
|
|
//#service
|
|
|
|
|
class StatsService extends Actor {
|
|
|
|
|
val workerRouter = context.actorOf(Props[StatsWorker].withRouter(FromConfig),
|
|
|
|
|
name = "workerRouter")
|
|
|
|
|
|
|
|
|
|
def receive = {
|
|
|
|
|
case StatsJob(text) if text != "" ⇒
|
|
|
|
|
val words = text.split(" ")
|
|
|
|
|
val replyTo = sender // important to not close over sender
|
2012-10-04 14:12:48 +02:00
|
|
|
// create actor that collects replies from workers
|
2012-10-01 20:35:19 +02:00
|
|
|
val aggregator = context.actorOf(Props(
|
2013-04-17 22:14:19 +02:00
|
|
|
classOf[StatsAggregator], words.size, replyTo))
|
2012-09-20 12:58:51 +02:00
|
|
|
words foreach { word ⇒
|
|
|
|
|
workerRouter.tell(
|
|
|
|
|
ConsistentHashableEnvelope(word, word), aggregator)
|
|
|
|
|
}
|
2012-09-13 10:54:14 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
class StatsAggregator(expectedResults: Int, replyTo: ActorRef) extends Actor {
|
|
|
|
|
var results = IndexedSeq.empty[Int]
|
2012-11-22 16:09:19 +01:00
|
|
|
context.setReceiveTimeout(3 seconds)
|
2012-09-13 10:54:14 +02:00
|
|
|
|
|
|
|
|
def receive = {
|
|
|
|
|
case wordCount: Int ⇒
|
|
|
|
|
results = results :+ wordCount
|
|
|
|
|
if (results.size == expectedResults) {
|
|
|
|
|
val meanWordLength = results.sum.toDouble / results.size
|
|
|
|
|
replyTo ! StatsResult(meanWordLength)
|
|
|
|
|
context.stop(self)
|
|
|
|
|
}
|
|
|
|
|
case ReceiveTimeout ⇒
|
|
|
|
|
replyTo ! JobFailed("Service unavailable, try again later")
|
|
|
|
|
context.stop(self)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
//#service
|
|
|
|
|
|
|
|
|
|
//#worker
|
|
|
|
|
class StatsWorker extends Actor {
|
2012-09-20 12:58:51 +02:00
|
|
|
var cache = Map.empty[String, Int]
|
2012-09-13 10:54:14 +02:00
|
|
|
def receive = {
|
2012-09-20 12:58:51 +02:00
|
|
|
case word: String ⇒
|
|
|
|
|
val length = cache.get(word) match {
|
|
|
|
|
case Some(x) ⇒ x
|
|
|
|
|
case None ⇒
|
|
|
|
|
val x = word.length
|
|
|
|
|
cache += (word -> x)
|
|
|
|
|
x
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
sender ! length
|
2012-09-13 10:54:14 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
//#worker
|
|
|
|
|
|
|
|
|
|
//#facade
|
|
|
|
|
class StatsFacade extends Actor with ActorLogging {
|
2012-10-24 17:59:49 +02:00
|
|
|
import context.dispatcher
|
2012-09-13 10:54:14 +02:00
|
|
|
val cluster = Cluster(context.system)
|
|
|
|
|
|
2013-04-28 22:05:40 +02:00
|
|
|
// sort by age, oldest first
|
|
|
|
|
val ageOrdering = Ordering.fromLessThan[Member] { (a, b) ⇒ a.isOlderThan(b) }
|
|
|
|
|
var membersByAge: immutable.SortedSet[Member] = immutable.SortedSet.empty(ageOrdering)
|
2012-09-13 10:54:14 +02:00
|
|
|
|
2013-04-28 22:05:40 +02:00
|
|
|
// subscribe to cluster changes
|
2012-09-13 10:54:14 +02:00
|
|
|
// re-subscribe when restart
|
2013-04-28 22:05:40 +02:00
|
|
|
override def preStart(): Unit = cluster.subscribe(self, classOf[MemberEvent])
|
2012-09-13 10:54:14 +02:00
|
|
|
override def postStop(): Unit = cluster.unsubscribe(self)
|
|
|
|
|
|
|
|
|
|
def receive = {
|
2013-04-28 22:05:40 +02:00
|
|
|
case job: StatsJob if membersByAge.isEmpty ⇒
|
2012-09-13 10:54:14 +02:00
|
|
|
sender ! JobFailed("Service unavailable, try again later")
|
|
|
|
|
case job: StatsJob ⇒
|
2013-04-28 22:05:40 +02:00
|
|
|
currentMaster.tell(job, sender)
|
2013-03-14 20:32:43 +01:00
|
|
|
case state: CurrentClusterState ⇒
|
2013-04-28 22:05:40 +02:00
|
|
|
membersByAge = immutable.SortedSet.empty(ageOrdering) ++ state.members.collect {
|
|
|
|
|
case m if m.hasRole("compute") ⇒ m
|
|
|
|
|
}
|
|
|
|
|
case MemberUp(m) ⇒ if (m.hasRole("compute")) membersByAge += m
|
|
|
|
|
case MemberRemoved(m) ⇒ if (m.hasRole("compute")) membersByAge -= m
|
|
|
|
|
case _: MemberEvent ⇒ // not interesting
|
2013-03-26 18:17:50 +01:00
|
|
|
}
|
|
|
|
|
|
2013-04-28 22:05:40 +02:00
|
|
|
def currentMaster: ActorSelection =
|
|
|
|
|
context.actorSelection(RootActorPath(membersByAge.head.address) /
|
|
|
|
|
"user" / "singleton" / "statsService")
|
2012-09-13 10:54:14 +02:00
|
|
|
|
|
|
|
|
}
|
|
|
|
|
//#facade
|
|
|
|
|
|
|
|
|
|
object StatsSample {
|
|
|
|
|
def main(args: Array[String]): Unit = {
|
2013-03-14 20:32:43 +01:00
|
|
|
// Override the configuration of the port when specified as program argument
|
|
|
|
|
val config =
|
|
|
|
|
(if (args.nonEmpty) ConfigFactory.parseString(s"akka.remote.netty.tcp.port=${args(0)}")
|
|
|
|
|
else ConfigFactory.empty).withFallback(
|
|
|
|
|
ConfigFactory.parseString("akka.cluster.roles = [compute]")).
|
|
|
|
|
withFallback(ConfigFactory.load())
|
2012-09-13 10:54:14 +02:00
|
|
|
|
2013-03-14 20:32:43 +01:00
|
|
|
val system = ActorSystem("ClusterSystem", config)
|
2012-09-13 10:54:14 +02:00
|
|
|
|
|
|
|
|
system.actorOf(Props[StatsWorker], name = "statsWorker")
|
|
|
|
|
system.actorOf(Props[StatsService], name = "statsService")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
object StatsSampleOneMaster {
|
|
|
|
|
def main(args: Array[String]): Unit = {
|
2013-03-14 20:32:43 +01:00
|
|
|
// Override the configuration of the port when specified as program argument
|
|
|
|
|
val config =
|
|
|
|
|
(if (args.nonEmpty) ConfigFactory.parseString(s"akka.remote.netty.tcp.port=${args(0)}")
|
|
|
|
|
else ConfigFactory.empty).withFallback(
|
|
|
|
|
ConfigFactory.parseString("akka.cluster.roles = [compute]")).
|
|
|
|
|
withFallback(ConfigFactory.load())
|
2012-09-13 10:54:14 +02:00
|
|
|
|
2013-03-14 20:32:43 +01:00
|
|
|
val system = ActorSystem("ClusterSystem", config)
|
2012-09-13 10:54:14 +02:00
|
|
|
|
2013-01-14 14:09:53 +01:00
|
|
|
//#create-singleton-manager
|
2013-04-17 22:14:19 +02:00
|
|
|
system.actorOf(ClusterSingletonManager.props(
|
2013-01-14 14:09:53 +01:00
|
|
|
singletonProps = _ ⇒ Props[StatsService], singletonName = "statsService",
|
2013-04-17 22:14:19 +02:00
|
|
|
terminationMessage = PoisonPill, role = Some("compute")),
|
2013-03-14 20:32:43 +01:00
|
|
|
name = "singleton")
|
2013-01-14 14:09:53 +01:00
|
|
|
//#create-singleton-manager
|
2012-09-13 10:54:14 +02:00
|
|
|
system.actorOf(Props[StatsFacade], name = "statsFacade")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
object StatsSampleClient {
|
|
|
|
|
def main(args: Array[String]): Unit = {
|
2013-03-14 20:32:43 +01:00
|
|
|
// note that client is not a compute node, role not defined
|
2012-09-13 10:54:14 +02:00
|
|
|
val system = ActorSystem("ClusterSystem")
|
2013-04-17 22:14:19 +02:00
|
|
|
system.actorOf(Props(classOf[StatsSampleClient], "/user/statsService"), "client")
|
2012-09-13 10:54:14 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
object StatsSampleOneMasterClient {
|
|
|
|
|
def main(args: Array[String]): Unit = {
|
2013-03-14 20:32:43 +01:00
|
|
|
// note that client is not a compute node, role not defined
|
2012-09-13 10:54:14 +02:00
|
|
|
val system = ActorSystem("ClusterSystem")
|
2013-04-17 22:14:19 +02:00
|
|
|
system.actorOf(Props(classOf[StatsSampleClient], "/user/statsFacade"), "client")
|
2012-09-13 10:54:14 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
class StatsSampleClient(servicePath: String) extends Actor {
|
|
|
|
|
val cluster = Cluster(context.system)
|
|
|
|
|
val servicePathElements = servicePath match {
|
|
|
|
|
case RelativeActorPath(elements) ⇒ elements
|
|
|
|
|
case _ ⇒ throw new IllegalArgumentException(
|
|
|
|
|
"servicePath [%s] is not a valid relative actor path" format servicePath)
|
|
|
|
|
}
|
|
|
|
|
import context.dispatcher
|
|
|
|
|
val tickTask = context.system.scheduler.schedule(2 seconds, 2 seconds, self, "tick")
|
|
|
|
|
|
|
|
|
|
var nodes = Set.empty[Address]
|
|
|
|
|
|
2012-11-27 18:07:37 +01:00
|
|
|
override def preStart(): Unit = {
|
|
|
|
|
cluster.subscribe(self, classOf[MemberEvent])
|
|
|
|
|
cluster.subscribe(self, classOf[UnreachableMember])
|
|
|
|
|
}
|
2012-09-13 10:54:14 +02:00
|
|
|
override def postStop(): Unit = {
|
|
|
|
|
cluster.unsubscribe(self)
|
|
|
|
|
tickTask.cancel()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
def receive = {
|
|
|
|
|
case "tick" if nodes.nonEmpty ⇒
|
|
|
|
|
// just pick any one
|
|
|
|
|
val address = nodes.toIndexedSeq(ThreadLocalRandom.current.nextInt(nodes.size))
|
2013-03-26 18:17:50 +01:00
|
|
|
val service = context.actorSelection(RootActorPath(address) / servicePathElements)
|
2012-09-18 10:49:41 +02:00
|
|
|
service ! StatsJob("this is the text that will be analyzed")
|
2012-09-13 10:54:14 +02:00
|
|
|
case result: StatsResult ⇒
|
|
|
|
|
println(result)
|
|
|
|
|
case failed: JobFailed ⇒
|
|
|
|
|
println(failed)
|
|
|
|
|
case state: CurrentClusterState ⇒
|
2013-03-14 20:32:43 +01:00
|
|
|
nodes = state.members.collect {
|
|
|
|
|
case m if m.hasRole("compute") && m.status == MemberStatus.Up ⇒ m.address
|
|
|
|
|
}
|
|
|
|
|
case MemberUp(m) if m.hasRole("compute") ⇒ nodes += m.address
|
|
|
|
|
case other: MemberEvent ⇒ nodes -= other.member.address
|
|
|
|
|
case UnreachableMember(m) ⇒ nodes -= m.address
|
2012-09-13 10:54:14 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// not used, only for documentation
|
|
|
|
|
abstract class StatsService2 extends Actor {
|
|
|
|
|
//#router-lookup-in-code
|
|
|
|
|
import akka.cluster.routing.ClusterRouterConfig
|
|
|
|
|
import akka.cluster.routing.ClusterRouterSettings
|
2012-09-20 12:58:51 +02:00
|
|
|
import akka.routing.ConsistentHashingRouter
|
2012-09-13 10:54:14 +02:00
|
|
|
|
|
|
|
|
val workerRouter = context.actorOf(Props[StatsWorker].withRouter(
|
2012-09-20 12:58:51 +02:00
|
|
|
ClusterRouterConfig(ConsistentHashingRouter(), ClusterRouterSettings(
|
2012-09-13 10:54:14 +02:00
|
|
|
totalInstances = 100, routeesPath = "/user/statsWorker",
|
2013-03-14 20:32:43 +01:00
|
|
|
allowLocalRoutees = true, useRole = Some("compute")))),
|
2012-09-13 10:54:14 +02:00
|
|
|
name = "workerRouter2")
|
|
|
|
|
//#router-lookup-in-code
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// not used, only for documentation
|
|
|
|
|
abstract class StatsService3 extends Actor {
|
|
|
|
|
//#router-deploy-in-code
|
|
|
|
|
import akka.cluster.routing.ClusterRouterConfig
|
|
|
|
|
import akka.cluster.routing.ClusterRouterSettings
|
2012-09-20 12:58:51 +02:00
|
|
|
import akka.routing.ConsistentHashingRouter
|
2012-09-13 10:54:14 +02:00
|
|
|
|
|
|
|
|
val workerRouter = context.actorOf(Props[StatsWorker].withRouter(
|
2012-09-20 12:58:51 +02:00
|
|
|
ClusterRouterConfig(ConsistentHashingRouter(), ClusterRouterSettings(
|
2012-09-13 10:54:14 +02:00
|
|
|
totalInstances = 100, maxInstancesPerNode = 3,
|
2013-03-14 20:32:43 +01:00
|
|
|
allowLocalRoutees = false, useRole = None))),
|
2012-09-13 10:54:14 +02:00
|
|
|
name = "workerRouter3")
|
|
|
|
|
//#router-deploy-in-code
|
|
|
|
|
}
|