=sam #3843 Use ClusterSingletonProxy in cluster samples
This commit is contained in:
parent
f457e0a30c
commit
ad18405877
11 changed files with 41 additions and 173 deletions
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -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");
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -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")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue