├── .gitignore ├── README.md ├── pom.xml ├── run.sh └── src └── main ├── java └── eivindw │ ├── ClusterApp.java │ ├── actors │ ├── MasterActor.java │ └── WorkerActor.java │ └── messages │ └── ConstantMessages.java └── resources └── application.conf /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .idea 3 | target/ 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Akka Cluster Example 2 | ==================== 3 | 4 | This is an example of creating a master distributing work to a set of competing workers. It is highly inspired by this blog post: http://letitcrash.com/post/29044669086/balancing-workload-across-nodes-with-akka-2 5 | 6 | Clone the repo and run a seed node (seed nodes are 1337 and 1338): 7 | ```bash 8 | ./run.sh 1337 9 | ``` 10 | Additional nodes can be run without specifying a port: 11 | ```bash 12 | ./run.sh 13 | ``` 14 | The master on the seed node should start handing out work to workers. If a node is killed it should be registered by the master. If the node running the master is killed a new master should be started on another node. 15 | 16 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | Akka Cluster Example 8 | eivindw 9 | akka-cluster-example 10 | jar 11 | 1.0-SNAPSHOT 12 | 13 | UTF-8 14 | 2.3.3 15 | 16 | 17 | 18 | 19 | com.typesafe.akka 20 | akka-cluster_2.10 21 | ${akka.version} 22 | 23 | 24 | com.typesafe.akka 25 | akka-contrib_2.10 26 | ${akka.version} 27 | 28 | 29 | com.typesafe.akka 30 | akka-persistence-experimental_2.10 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | org.apache.maven.plugins 40 | maven-compiler-plugin 41 | 3.0 42 | 43 | 1.8 44 | 1.8 45 | 46 | 47 | 48 | org.codehaus.mojo 49 | exec-maven-plugin 50 | 1.2.1 51 | 52 | eivindw.ClusterApp 53 | 54 | 55 | 56 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | mvn compile exec:java -Dexec.args="$1" 3 | -------------------------------------------------------------------------------- /src/main/java/eivindw/ClusterApp.java: -------------------------------------------------------------------------------- 1 | package eivindw; 2 | 3 | import akka.actor.ActorSystem; 4 | import akka.actor.PoisonPill; 5 | import akka.actor.Props; 6 | import akka.contrib.pattern.ClusterSingletonManager; 7 | import akka.routing.RandomPool; 8 | import eivindw.actors.MasterActor; 9 | import eivindw.actors.WorkerActor; 10 | 11 | import static eivindw.messages.ConstantMessages.TOPIC_WORKERS; 12 | 13 | public class ClusterApp { 14 | 15 | public static void main(String[] args) { 16 | if (args.length > 0) { 17 | System.setProperty("akka.remote.netty.tcp.port", args[0]); 18 | } 19 | 20 | final ActorSystem actorSystem = ActorSystem.create("ClusterExample"); 21 | 22 | actorSystem.actorOf(ClusterSingletonManager.defaultProps( 23 | Props.create(MasterActor.class), 24 | "master", 25 | PoisonPill.getInstance(), 26 | null 27 | ), "singleton"); 28 | 29 | actorSystem.actorOf( 30 | new RandomPool(5).props(Props.create(WorkerActor.class)), 31 | TOPIC_WORKERS 32 | ); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/eivindw/actors/MasterActor.java: -------------------------------------------------------------------------------- 1 | package eivindw.actors; 2 | 3 | import akka.actor.AbstractActor; 4 | import akka.actor.ActorRef; 5 | import akka.actor.Terminated; 6 | import akka.contrib.pattern.DistributedPubSubExtension; 7 | import akka.contrib.pattern.DistributedPubSubMediator; 8 | import akka.event.Logging; 9 | import akka.event.LoggingAdapter; 10 | import akka.japi.pf.ReceiveBuilder; 11 | import eivindw.messages.ConstantMessages; 12 | import scala.concurrent.duration.Duration; 13 | 14 | import java.util.Random; 15 | import java.util.concurrent.TimeUnit; 16 | 17 | public class MasterActor extends AbstractActor implements ConstantMessages { 18 | 19 | private LoggingAdapter log = Logging.getLogger(getContext().system(), this); 20 | 21 | private final ActorRef mediator = DistributedPubSubExtension.get(getContext().system()).mediator(); 22 | 23 | private static final Random RANDOM = new Random(); 24 | 25 | public MasterActor() { 26 | log.info("Starting master!"); 27 | 28 | receive(ReceiveBuilder 29 | .matchEquals(MSG_WAKE_UP, msg -> { 30 | log.info("[Master] Scheduled wake-up!"); 31 | mediator.tell(new DistributedPubSubMediator.Publish(TOPIC_WORKERS, MSG_WORK_AVAILABLE), self()); 32 | scheduleWakeUp(); 33 | }) 34 | .matchEquals(MSG_GIVE_WORK, msg -> { 35 | if (RANDOM.nextBoolean()) { // obtain real work here 36 | getContext().watch(sender()); 37 | sender().tell(MSG_WORK, self()); 38 | } 39 | }) 40 | .matchEquals(MSG_WORK_DONE, msg -> getContext().unwatch(sender())) 41 | .match(Terminated.class, msg -> log.info("Active worker crashed: " + msg.getActor())) 42 | .build() 43 | ); 44 | 45 | scheduleWakeUp(); 46 | } 47 | 48 | private void scheduleWakeUp() { 49 | context().system().scheduler().scheduleOnce( 50 | Duration.create(5, TimeUnit.SECONDS), 51 | self(), 52 | MSG_WAKE_UP, 53 | context().dispatcher(), 54 | null 55 | ); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/main/java/eivindw/actors/WorkerActor.java: -------------------------------------------------------------------------------- 1 | package eivindw.actors; 2 | 3 | import akka.actor.AbstractActor; 4 | import akka.actor.ActorRef; 5 | import akka.contrib.pattern.DistributedPubSubExtension; 6 | import akka.contrib.pattern.DistributedPubSubMediator; 7 | import akka.dispatch.Futures; 8 | import akka.event.Logging; 9 | import akka.event.LoggingAdapter; 10 | import akka.japi.pf.ReceiveBuilder; 11 | import eivindw.messages.ConstantMessages; 12 | 13 | public class WorkerActor extends AbstractActor implements ConstantMessages { 14 | 15 | private LoggingAdapter log = Logging.getLogger(getContext().system(), this); 16 | 17 | private final ActorRef mediator = DistributedPubSubExtension.get(getContext().system()).mediator(); 18 | 19 | private boolean working = false; 20 | 21 | public WorkerActor() { 22 | receive(ReceiveBuilder 23 | .matchEquals(MSG_WORK_AVAILABLE, msg -> { 24 | if (!working) { 25 | sender().tell(MSG_GIVE_WORK, self()); 26 | } 27 | }) 28 | .matchEquals(MSG_WORK, msg -> { 29 | final ActorRef master = sender(); 30 | log.info("Got work!"); 31 | working = true; 32 | Futures.future(() -> { 33 | Thread.sleep(10000); // real work code goes here 34 | working = false; 35 | master.tell(MSG_WORK_DONE, self()); 36 | return null; 37 | }, getContext().dispatcher()); 38 | }) 39 | .match(DistributedPubSubMediator.SubscribeAck.class, msg -> 40 | log.info("Subscribed to 'workers'!")) 41 | .build() 42 | ); 43 | } 44 | 45 | @Override 46 | public void preStart() { 47 | mediator.tell(new DistributedPubSubMediator.Subscribe(TOPIC_WORKERS, self()), self()); 48 | } 49 | 50 | @Override 51 | public void postStop() { 52 | mediator.tell(new DistributedPubSubMediator.Unsubscribe(TOPIC_WORKERS, self()), self()); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/main/java/eivindw/messages/ConstantMessages.java: -------------------------------------------------------------------------------- 1 | package eivindw.messages; 2 | 3 | public interface ConstantMessages { 4 | 5 | String TOPIC_WORKERS = "workers"; 6 | 7 | String MSG_WAKE_UP = "WakeUp"; 8 | String MSG_WORK_AVAILABLE = "WorkAvailable"; 9 | String MSG_GIVE_WORK = "GiveMeWork"; 10 | String MSG_WORK = "Work"; 11 | String MSG_WORK_DONE = "WorkDone"; 12 | } 13 | -------------------------------------------------------------------------------- /src/main/resources/application.conf: -------------------------------------------------------------------------------- 1 | akka { 2 | actor { 3 | provider = "akka.cluster.ClusterActorRefProvider" 4 | } 5 | remote { 6 | log-remote-lifecycle-events = off 7 | netty.tcp { 8 | hostname = "127.0.0.1" 9 | port = 0 10 | } 11 | } 12 | cluster { 13 | seed-nodes = [ 14 | "akka.tcp://ClusterExample@127.0.0.1:1337", 15 | "akka.tcp://ClusterExample@127.0.0.1:1338" 16 | ] 17 | auto-down = on 18 | } 19 | scheduler { 20 | tick-duration = 33ms 21 | } 22 | extensions = [ 23 | "akka.contrib.pattern.DistributedPubSubExtension" 24 | ] 25 | log-dead-letters = 0 26 | } --------------------------------------------------------------------------------