=sam #3843 Use ClusterSingletonProxy in cluster samples

This commit is contained in:
Patrik Nordwall 2014-03-14 16:32:54 +01:00
parent f457e0a30c
commit ad18405877
11 changed files with 41 additions and 173 deletions

View file

@ -519,14 +519,14 @@ in the contrib module. The ``ClusterSingletonManager`` is started on each node.
.. includecode:: ../../../akka-samples/akka-sample-cluster-java/src/main/java/sample/cluster/stats/StatsSampleOneMasterMain.java#create-singleton-manager .. includecode:: ../../../akka-samples/akka-sample-cluster-java/src/main/java/sample/cluster/stats/StatsSampleOneMasterMain.java#create-singleton-manager
We also need an actor on each node that keeps track of where current single master exists and We also need an actor on each node that keeps track of where current single master exists and
delegates jobs to the ``StatsService``. delegates jobs to the ``StatsService``. That is provided by the ``ClusterSingletonProxy``.
.. includecode:: ../../../akka-samples/akka-sample-cluster-java/src/main/java/sample/cluster/stats/StatsFacade.java#facade .. includecode:: ../../../akka-samples/akka-sample-cluster-java/src/main/java/sample/cluster/stats/StatsSampleOneMasterMain.java#singleton-proxy
The ``StatsFacade`` receives text from users and delegates to the current ``StatsService``, the single The ``ClusterSingletonProxy`` receives text from users and delegates to the current ``StatsService``, the single
master. It listens to cluster events to lookup the ``StatsService`` on the oldest node. master. It listens to cluster events to lookup the ``StatsService`` on the oldest node.
All nodes start ``StatsFacade`` and the ``ClusterSingletonManager``. The router is now configured like this: All nodes start ``ClusterSingletonProxy`` and the ``ClusterSingletonManager``. The router is now configured like this:
.. includecode:: ../../../akka-samples/akka-sample-cluster-java/src/main/resources/stats2.conf#config-router-deploy .. includecode:: ../../../akka-samples/akka-sample-cluster-java/src/main/resources/stats2.conf#config-router-deploy

View file

@ -514,14 +514,14 @@ in the contrib module. The ``ClusterSingletonManager`` is started on each node.
.. includecode:: ../../../akka-samples/akka-sample-cluster-scala/src/main/scala/sample/cluster/stats/StatsSampleOneMaster.scala#create-singleton-manager .. includecode:: ../../../akka-samples/akka-sample-cluster-scala/src/main/scala/sample/cluster/stats/StatsSampleOneMaster.scala#create-singleton-manager
We also need an actor on each node that keeps track of where current single master exists and We also need an actor on each node that keeps track of where current single master exists and
delegates jobs to the ``StatsService``. delegates jobs to the ``StatsService``. That is provided by the ``ClusterSingletonProxy``.
.. includecode:: ../../../akka-samples/akka-sample-cluster-scala/src/main/scala/sample/cluster/stats/StatsFacade.scala#facade .. includecode:: ../../../akka-samples/akka-sample-cluster-scala/src/main/scala/sample/cluster/stats/StatsSampleOneMaster.scala#singleton-proxy
The ``StatsFacade`` receives text from users and delegates to the current ``StatsService``, the single The ``ClusterSingletonProxy`` receives text from users and delegates to the current ``StatsService``, the single
master. It listens to cluster events to lookup the ``StatsService`` on the oldest node. master. It listens to cluster events to lookup the ``StatsService`` on the oldest node.
All nodes start ``StatsFacade`` and the ``ClusterSingletonManager``. The router is now configured like this: All nodes start ``ClusterSingletonProxy`` and the ``ClusterSingletonManager``. The router is now configured like this:
.. includecode:: ../../../akka-samples/akka-sample-cluster-scala/src/main/resources/stats2.conf#config-router-deploy .. includecode:: ../../../akka-samples/akka-sample-cluster-scala/src/main/resources/stats2.conf#config-router-deploy

View file

@ -1,95 +0,0 @@
package sample.cluster.stats;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.SortedSet;
import java.util.TreeSet;
import sample.cluster.stats.StatsMessages.JobFailed;
import sample.cluster.stats.StatsMessages.StatsJob;
import akka.actor.ActorSelection;
import akka.actor.UntypedActor;
import akka.cluster.Cluster;
import akka.cluster.ClusterEvent.CurrentClusterState;
import akka.cluster.ClusterEvent.MemberEvent;
import akka.cluster.ClusterEvent.MemberUp;
import akka.cluster.ClusterEvent.MemberRemoved;
import akka.cluster.Member;
import akka.event.Logging;
import akka.event.LoggingAdapter;
//#facade
public class StatsFacade extends UntypedActor {
final LoggingAdapter log = Logging.getLogger(getContext().system(), this);
final Cluster cluster = Cluster.get(getContext().system());
final Comparator<Member> ageComparator = new Comparator<Member>() {
public int compare(Member a, Member b) {
if (a.isOlderThan(b))
return -1;
else if (b.isOlderThan(a))
return 1;
else
return 0;
}
};
final SortedSet<Member> membersByAge = new TreeSet<Member>(ageComparator);
//subscribe to cluster changes
@Override
public void preStart() {
cluster.subscribe(getSelf(), MemberEvent.class);
}
//re-subscribe when restart
@Override
public void postStop() {
cluster.unsubscribe(getSelf());
}
@Override
public void onReceive(Object message) {
if (message instanceof StatsJob && membersByAge.isEmpty()) {
getSender().tell(new JobFailed("Service unavailable, try again later"),
getSelf());
} else if (message instanceof StatsJob) {
currentMaster().tell(message, getSender());
} else if (message instanceof CurrentClusterState) {
CurrentClusterState state = (CurrentClusterState) message;
List<Member> members = new ArrayList<Member>();
for (Member m : state.getMembers()) {
if (m.hasRole("compute"))
members.add(m);
}
membersByAge.clear();
membersByAge.addAll(members);
} else if (message instanceof MemberUp) {
Member m = ((MemberUp) message).member();
if (m.hasRole("compute"))
membersByAge.add(m);
} else if (message instanceof MemberRemoved) {
Member m = ((MemberRemoved) message).member();
if (m.hasRole("compute"))
membersByAge.remove(m);
} else if (message instanceof MemberEvent) {
// not interesting
} else {
unhandled(message);
}
}
ActorSelection currentMaster() {
return getContext().actorSelection(
membersByAge.first().address() + "/user/singleton/statsService");
}
}
//#facade

View file

@ -11,7 +11,7 @@ public class StatsSampleOneMasterClientMain {
// note that client is not a compute node, role not defined // note that client is not a compute node, role not defined
ActorSystem system = ActorSystem.create("ClusterSystem", ActorSystem system = ActorSystem.create("ClusterSystem",
ConfigFactory.load("stats2")); ConfigFactory.load("stats2"));
system.actorOf(Props.create(StatsSampleClient.class, "/user/statsFacade"), system.actorOf(Props.create(StatsSampleClient.class, "/user/statsServiceProxy"),
"client"); "client");
} }

View file

@ -7,6 +7,7 @@ import akka.actor.ActorSystem;
import akka.actor.PoisonPill; import akka.actor.PoisonPill;
import akka.actor.Props; import akka.actor.Props;
import akka.contrib.pattern.ClusterSingletonManager; import akka.contrib.pattern.ClusterSingletonManager;
import akka.contrib.pattern.ClusterSingletonProxy;
public class StatsSampleOneMasterMain { public class StatsSampleOneMasterMain {
@ -36,7 +37,10 @@ public class StatsSampleOneMasterMain {
PoisonPill.getInstance(), "compute"), "singleton"); PoisonPill.getInstance(), "compute"), "singleton");
//#create-singleton-manager //#create-singleton-manager
system.actorOf(Props.create(StatsFacade.class), "statsFacade"); //#singleton-proxy
system.actorOf(ClusterSingletonProxy.defaultProps("/user/singleton/statsService",
"compute"), "statsServiceProxy");
//#singleton-proxy
} }
} }

View file

@ -15,6 +15,7 @@ import akka.cluster.MemberStatus
import akka.cluster.ClusterEvent.CurrentClusterState import akka.cluster.ClusterEvent.CurrentClusterState
import akka.cluster.ClusterEvent.MemberUp import akka.cluster.ClusterEvent.MemberUp
import akka.contrib.pattern.ClusterSingletonManager import akka.contrib.pattern.ClusterSingletonManager
import akka.contrib.pattern.ClusterSingletonProxy
import akka.remote.testkit.MultiNodeConfig import akka.remote.testkit.MultiNodeConfig
import akka.remote.testkit.MultiNodeSpec import akka.remote.testkit.MultiNodeSpec
import akka.testkit.ImplicitSender import akka.testkit.ImplicitSender
@ -91,18 +92,19 @@ abstract class StatsSampleSingleMasterSpec extends MultiNodeSpec(StatsSampleSing
terminationMessage = PoisonPill, terminationMessage = PoisonPill,
role = null), name = "singleton") role = null), name = "singleton")
system.actorOf(Props[StatsFacade], "statsFacade") system.actorOf(ClusterSingletonProxy.defaultProps("/user/singleton/statsService",
"compute"), "statsServiceProxy");
testConductor.enter("all-up") testConductor.enter("all-up")
} }
"show usage of the statsFacade" in within(40 seconds) { "show usage of the statsServiceProxy" in within(40 seconds) {
val facade = system.actorSelection(RootActorPath(node(third).address) / "user" / "statsFacade") val proxy = system.actorSelection(RootActorPath(node(third).address) / "user" / "statsServiceProxy")
// eventually the service should be ok, // eventually the service should be ok,
// service and worker nodes might not be up yet // service and worker nodes might not be up yet
awaitAssert { awaitAssert {
facade ! new StatsJob("this is the text that will be analyzed") proxy ! new StatsJob("this is the text that will be analyzed")
expectMsgType[StatsResult](1.second).getMeanWordLength should be(3.875 +- 0.001) expectMsgType[StatsResult](1.second).getMeanWordLength should be(3.875 +- 0.001)
} }

View file

@ -352,17 +352,16 @@ in the contrib module. The <code>ClusterSingletonManager</code> is started on ea
<p> <p>
We also need an actor on each node that keeps track of where current single master exists and We also need an actor on each node that keeps track of where current single master exists and
delegates jobs to the <code>StatsService</code>. That is handled by the delegates jobs to the <code>StatsService</code>. That is provided by the <code>ClusterSingletonProxy</code>.
<a href="#code/src/main/java/sample/cluster/stats/StatsFacade.java" class="shortcut">StatsFacade.java</a>
</p> </p>
<p> <p>
The <code>StatsFacade</code> receives text from users and delegates to the current <code>StatsService</code>, the single The <code>ClusterSingletonProxy</code> receives text from users and delegates to the current <code>StatsService</code>, the single
master. It listens to cluster events to lookup the <code>StatsService</code> on the oldest node. master. It listens to cluster events to lookup the <code>StatsService</code> on the oldest node.
</p> </p>
<p> <p>
All nodes start <code>StatsFacade</code> and the <code>ClusterSingletonManager</code>. The router is now configured in All nodes start <code>ClusterSingletonProxy</code> and the <code>ClusterSingletonManager</code>. The router is now configured in
<a href="#code/src/main/resources/stats2.conf" class="shortcut">stats2.conf</a> <a href="#code/src/main/resources/stats2.conf" class="shortcut">stats2.conf</a>
</p> </p>

View file

@ -1,48 +0,0 @@
package sample.cluster.stats
import scala.collection.immutable
import akka.actor.Actor
import akka.actor.ActorLogging
import akka.actor.ActorSelection
import akka.actor.RootActorPath
import akka.cluster.Cluster
import akka.cluster.ClusterEvent.CurrentClusterState
import akka.cluster.ClusterEvent.MemberEvent
import akka.cluster.ClusterEvent.MemberRemoved
import akka.cluster.ClusterEvent.MemberUp
import akka.cluster.Member
//#facade
class StatsFacade extends Actor with ActorLogging {
import context.dispatcher
val cluster = Cluster(context.system)
// sort by age, oldest first
val ageOrdering = Ordering.fromLessThan[Member] { (a, b) => a.isOlderThan(b) }
var membersByAge: immutable.SortedSet[Member] = immutable.SortedSet.empty(ageOrdering)
// subscribe to cluster changes
// re-subscribe when restart
override def preStart(): Unit = cluster.subscribe(self, classOf[MemberEvent])
override def postStop(): Unit = cluster.unsubscribe(self)
def receive = {
case job: StatsJob if membersByAge.isEmpty =>
sender() ! JobFailed("Service unavailable, try again later")
case job: StatsJob =>
currentMaster.tell(job, sender())
case state: CurrentClusterState =>
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
}
def currentMaster: ActorSelection =
context.actorSelection(RootActorPath(membersByAge.head.address) /
"user" / "singleton" / "statsService")
}
//#facade

View file

@ -5,6 +5,7 @@ import akka.actor.ActorSystem
import akka.actor.PoisonPill import akka.actor.PoisonPill
import akka.actor.Props import akka.actor.Props
import akka.contrib.pattern.ClusterSingletonManager import akka.contrib.pattern.ClusterSingletonManager
import akka.contrib.pattern.ClusterSingletonProxy
object StatsSampleOneMaster { object StatsSampleOneMaster {
def main(args: Array[String]): Unit = { def main(args: Array[String]): Unit = {
@ -32,7 +33,11 @@ object StatsSampleOneMaster {
terminationMessage = PoisonPill, role = Some("compute")), terminationMessage = PoisonPill, role = Some("compute")),
name = "singleton") name = "singleton")
//#create-singleton-manager //#create-singleton-manager
system.actorOf(Props[StatsFacade], name = "statsFacade")
//#singleton-proxy
system.actorOf(ClusterSingletonProxy.props(singletonPath = "/user/singleton/statsService",
role = Some("compute")), name = "statsServiceProxy")
//#singleton-proxy
} }
} }
} }
@ -41,7 +46,7 @@ object StatsSampleOneMasterClient {
def main(args: Array[String]): Unit = { def main(args: Array[String]): Unit = {
// note that client is not a compute node, role not defined // note that client is not a compute node, role not defined
val system = ActorSystem("ClusterSystem") val system = ActorSystem("ClusterSystem")
system.actorOf(Props(classOf[StatsSampleClient], "/user/statsFacade"), "client") system.actorOf(Props(classOf[StatsSampleClient], "/user/statsServiceProxy"), "client")
} }
} }

View file

@ -10,6 +10,7 @@ import akka.actor.PoisonPill
import akka.actor.Props import akka.actor.Props
import akka.actor.RootActorPath import akka.actor.RootActorPath
import akka.contrib.pattern.ClusterSingletonManager import akka.contrib.pattern.ClusterSingletonManager
import akka.contrib.pattern.ClusterSingletonProxy
import akka.cluster.Cluster import akka.cluster.Cluster
import akka.cluster.Member import akka.cluster.Member
import akka.cluster.MemberStatus import akka.cluster.MemberStatus
@ -88,18 +89,19 @@ abstract class StatsSampleSingleMasterSpec extends MultiNodeSpec(StatsSampleSing
singletonProps = Props[StatsService], singletonName = "statsService", singletonProps = Props[StatsService], singletonName = "statsService",
terminationMessage = PoisonPill, role = Some("compute")), name = "singleton") terminationMessage = PoisonPill, role = Some("compute")), name = "singleton")
system.actorOf(Props[StatsFacade], "statsFacade") system.actorOf(ClusterSingletonProxy.props(singletonPath = "/user/singleton/statsService",
role = Some("compute")), name = "statsServiceProxy")
testConductor.enter("all-up") testConductor.enter("all-up")
} }
"show usage of the statsFacade" in within(40 seconds) { "show usage of the statsServiceProxy" in within(40 seconds) {
val facade = system.actorSelection(RootActorPath(node(third).address) / "user" / "statsFacade") val proxy = system.actorSelection(RootActorPath(node(third).address) / "user" / "statsServiceProxy")
// eventually the service should be ok, // eventually the service should be ok,
// service and worker nodes might not be up yet // service and worker nodes might not be up yet
awaitAssert { awaitAssert {
facade ! StatsJob("this is the text that will be analyzed") proxy ! StatsJob("this is the text that will be analyzed")
expectMsgType[StatsResult](1.second).meanWordLength should be( expectMsgType[StatsResult](1.second).meanWordLength should be(
3.875 +- 0.001) 3.875 +- 0.001)
} }

View file

@ -351,17 +351,16 @@ in the contrib module. The <code>ClusterSingletonManager</code> is started on ea
<p> <p>
We also need an actor on each node that keeps track of where current single master exists and We also need an actor on each node that keeps track of where current single master exists and
delegates jobs to the <code>StatsService</code>. That is handled by the delegates jobs to the <code>StatsService</code>. That is provided by the <code>ClusterSingletonProxy</code>.
<a href="#code/src/main/scala/sample/cluster/stats/StatsFacade.scala" class="shortcut">StatsFacade.scala</a>
</p> </p>
<p> <p>
The <code>StatsFacade</code> receives text from users and delegates to the current <code>StatsService</code>, the single The <code>ClusterSingletonProxy</code> receives text from users and delegates to the current <code>StatsService</code>, the single
master. It listens to cluster events to lookup the <code>StatsService</code> on the oldest node. master. It listens to cluster events to lookup the <code>StatsService</code> on the oldest node.
</p> </p>
<p> <p>
All nodes start <code>StatsFacade</code> and the <code>ClusterSingletonManager</code>. The router is now configured in All nodes start <code>ClusterSingletonProxy</code> and the <code>ClusterSingletonManager</code>. The router is now configured in
<a href="#code/src/main/resources/stats2.conf" class="shortcut">stats2.conf</a> <a href="#code/src/main/resources/stats2.conf" class="shortcut">stats2.conf</a>
</p> </p>