├── .gitignore ├── COPYING ├── LICENSE ├── README.md ├── activator.properties ├── build.sbt ├── project └── build.properties ├── src ├── main │ ├── resources │ │ └── application.conf │ └── scala │ │ └── chat │ │ ├── ChatClient.scala │ │ ├── Main.scala │ │ ├── MemberListener.scala │ │ └── RandomUser.scala └── test │ └── scala │ └── chat │ └── ChatClientSpec.scala └── tutorial └── index.html /.gitignore: -------------------------------------------------------------------------------- 1 | *# 2 | *.iml 3 | *.ipr 4 | *.iws 5 | *.pyc 6 | *.tm.epoch 7 | *.vim 8 | */project/boot 9 | */project/build/target 10 | */project/project.target.config-classes 11 | *-shim.sbt 12 | *~ 13 | .#* 14 | .*.swp 15 | .DS_Store 16 | .cache 17 | .cache 18 | .classpath 19 | .codefellow 20 | .ensime* 21 | .eprj 22 | .history 23 | .idea 24 | .manager 25 | .multi-jvm 26 | .project 27 | .scala_dependencies 28 | .scalastyle 29 | .settings 30 | .tags 31 | .tags_sorted_by_file 32 | .target 33 | .worksheet 34 | Makefile 35 | TAGS 36 | _akka_cluster/ 37 | _dump 38 | _mb 39 | activemq-data 40 | akka-contrib/rst_preprocessed/ 41 | akka-docs/_build/ 42 | akka-docs/exts/ 43 | akka-docs/rst_preprocessed/ 44 | akka-osgi/src/main/resources/*.conf 45 | akka.sublime-project 46 | akka.sublime-workspace 47 | akka.tmproj 48 | beanstalk/ 49 | bin/ 50 | data 51 | deploy/*.jar 52 | etags 53 | lib_managed 54 | logs 55 | manifest.mf 56 | mongoDB/ 57 | multiverse.log 58 | out 59 | project/akka-build.properties 60 | project/boot/* 61 | project/plugins/project 62 | redis/ 63 | reports 64 | run-codefellow 65 | schoir.props 66 | semantic.cache 67 | src_managed 68 | storage 69 | tags 70 | target 71 | tm*.lck 72 | tm*.log 73 | tm.out 74 | worker*.log 75 | -------------------------------------------------------------------------------- /COPYING: -------------------------------------------------------------------------------- 1 | Creative Commons Legal Code 2 | 3 | CC0 1.0 Universal 4 | 5 | CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE 6 | LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN 7 | ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS 8 | INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES 9 | REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS 10 | PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM 11 | THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED 12 | HEREUNDER. 13 | 14 | Statement of Purpose 15 | 16 | The laws of most jurisdictions throughout the world automatically confer 17 | exclusive Copyright and Related Rights (defined below) upon the creator 18 | and subsequent owner(s) (each and all, an "owner") of an original work of 19 | authorship and/or a database (each, a "Work"). 20 | 21 | Certain owners wish to permanently relinquish those rights to a Work for 22 | the purpose of contributing to a commons of creative, cultural and 23 | scientific works ("Commons") that the public can reliably and without fear 24 | of later claims of infringement build upon, modify, incorporate in other 25 | works, reuse and redistribute as freely as possible in any form whatsoever 26 | and for any purposes, including without limitation commercial purposes. 27 | These owners may contribute to the Commons to promote the ideal of a free 28 | culture and the further production of creative, cultural and scientific 29 | works, or to gain reputation or greater distribution for their Work in 30 | part through the use and efforts of others. 31 | 32 | For these and/or other purposes and motivations, and without any 33 | expectation of additional consideration or compensation, the person 34 | associating CC0 with a Work (the "Affirmer"), to the extent that he or she 35 | is an owner of Copyright and Related Rights in the Work, voluntarily 36 | elects to apply CC0 to the Work and publicly distribute the Work under its 37 | terms, with knowledge of his or her Copyright and Related Rights in the 38 | Work and the meaning and intended legal effect of CC0 on those rights. 39 | 40 | 1. Copyright and Related Rights. A Work made available under CC0 may be 41 | protected by copyright and related or neighboring rights ("Copyright and 42 | Related Rights"). Copyright and Related Rights include, but are not 43 | limited to, the following: 44 | 45 | i. the right to reproduce, adapt, distribute, perform, display, 46 | communicate, and translate a Work; 47 | ii. moral rights retained by the original author(s) and/or performer(s); 48 | iii. publicity and privacy rights pertaining to a person's image or 49 | likeness depicted in a Work; 50 | iv. rights protecting against unfair competition in regards to a Work, 51 | subject to the limitations in paragraph 4(a), below; 52 | v. rights protecting the extraction, dissemination, use and reuse of data 53 | in a Work; 54 | vi. database rights (such as those arising under Directive 96/9/EC of the 55 | European Parliament and of the Council of 11 March 1996 on the legal 56 | protection of databases, and under any national implementation 57 | thereof, including any amended or successor version of such 58 | directive); and 59 | vii. other similar, equivalent or corresponding rights throughout the 60 | world based on applicable law or treaty, and any national 61 | implementations thereof. 62 | 63 | 2. Waiver. To the greatest extent permitted by, but not in contravention 64 | of, applicable law, Affirmer hereby overtly, fully, permanently, 65 | irrevocably and unconditionally waives, abandons, and surrenders all of 66 | Affirmer's Copyright and Related Rights and associated claims and causes 67 | of action, whether now known or unknown (including existing as well as 68 | future claims and causes of action), in the Work (i) in all territories 69 | worldwide, (ii) for the maximum duration provided by applicable law or 70 | treaty (including future time extensions), (iii) in any current or future 71 | medium and for any number of copies, and (iv) for any purpose whatsoever, 72 | including without limitation commercial, advertising or promotional 73 | purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each 74 | member of the public at large and to the detriment of Affirmer's heirs and 75 | successors, fully intending that such Waiver shall not be subject to 76 | revocation, rescission, cancellation, termination, or any other legal or 77 | equitable action to disrupt the quiet enjoyment of the Work by the public 78 | as contemplated by Affirmer's express Statement of Purpose. 79 | 80 | 3. Public License Fallback. Should any part of the Waiver for any reason 81 | be judged legally invalid or ineffective under applicable law, then the 82 | Waiver shall be preserved to the maximum extent permitted taking into 83 | account Affirmer's express Statement of Purpose. In addition, to the 84 | extent the Waiver is so judged Affirmer hereby grants to each affected 85 | person a royalty-free, non transferable, non sublicensable, non exclusive, 86 | irrevocable and unconditional license to exercise Affirmer's Copyright and 87 | Related Rights in the Work (i) in all territories worldwide, (ii) for the 88 | maximum duration provided by applicable law or treaty (including future 89 | time extensions), (iii) in any current or future medium and for any number 90 | of copies, and (iv) for any purpose whatsoever, including without 91 | limitation commercial, advertising or promotional purposes (the 92 | "License"). The License shall be deemed effective as of the date CC0 was 93 | applied by Affirmer to the Work. Should any part of the License for any 94 | reason be judged legally invalid or ineffective under applicable law, such 95 | partial invalidity or ineffectiveness shall not invalidate the remainder 96 | of the License, and in such case Affirmer hereby affirms that he or she 97 | will not (i) exercise any of his or her remaining Copyright and Related 98 | Rights in the Work or (ii) assert any associated claims and causes of 99 | action with respect to the Work, in either case contrary to Affirmer's 100 | express Statement of Purpose. 101 | 102 | 4. Limitations and Disclaimers. 103 | 104 | a. No trademark or patent rights held by Affirmer are waived, abandoned, 105 | surrendered, licensed or otherwise affected by this document. 106 | b. Affirmer offers the Work as-is and makes no representations or 107 | warranties of any kind concerning the Work, express, implied, 108 | statutory or otherwise, including without limitation warranties of 109 | title, merchantability, fitness for a particular purpose, non 110 | infringement, or the absence of latent or other defects, accuracy, or 111 | the present or absence of errors, whether or not discoverable, all to 112 | the greatest extent permissible under applicable law. 113 | c. Affirmer disclaims responsibility for clearing rights of other persons 114 | that may apply to the Work or any use thereof, including without 115 | limitation any person's Copyright and Related Rights in the Work. 116 | Further, Affirmer disclaims responsibility for obtaining any necessary 117 | consents, permissions or other rights required for any use of the 118 | Work. 119 | d. Affirmer understands and acknowledges that Creative Commons is not a 120 | party to this document and has no duty or obligation with respect to 121 | this CC0 or use of the Work. 122 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Activator Template by Typesafe 2 | 3 | Licensed under Public Domain (CC0) 4 | 5 | To the extent possible under law, the person who associated CC0 with 6 | this Activator Tempate has waived all copyright and related or neighboring 7 | rights to this Activator Template. 8 | 9 | You should have received a copy of the CC0 legalcode along with this 10 | work. If not, see . 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Typesafe Activator template for Akka Cluster. -------------------------------------------------------------------------------- /activator.properties: -------------------------------------------------------------------------------- 1 | name=akka-clustering 2 | title=Akka Clustered PubSub with Scala! 3 | description=Illustrates publish-subscribe in a cluster of Akka nodes. 4 | tags=Basics,akka,starter 5 | -------------------------------------------------------------------------------- /build.sbt: -------------------------------------------------------------------------------- 1 | name := "akka-clustering" 2 | 3 | version := "0.1" 4 | 5 | scalaVersion := "2.11.7" 6 | 7 | lazy val akkaVersion = "2.4.0" 8 | 9 | libraryDependencies ++= Seq( 10 | "com.typesafe.akka" %% "akka-cluster" % akkaVersion, 11 | "com.typesafe.akka" %% "akka-cluster-tools" % akkaVersion, 12 | "com.typesafe.akka" %% "akka-testkit" % akkaVersion % "test", 13 | "org.scalatest" %% "scalatest" % "2.1.6" % "test") 14 | 15 | -------------------------------------------------------------------------------- /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=0.13.8 2 | -------------------------------------------------------------------------------- /src/main/resources/application.conf: -------------------------------------------------------------------------------- 1 | akka { 2 | 3 | actor.provider = "akka.cluster.ClusterActorRefProvider" 4 | 5 | remote.netty.tcp.port=0 6 | 7 | } 8 | -------------------------------------------------------------------------------- /src/main/scala/chat/ChatClient.scala: -------------------------------------------------------------------------------- 1 | package chat 2 | 3 | import akka.actor.Actor 4 | import akka.actor.Props 5 | import akka.cluster.pubsub.DistributedPubSub 6 | import akka.cluster.pubsub.DistributedPubSubMediator.{Publish, Subscribe} 7 | 8 | object ChatClient { 9 | def props(name: String): Props = Props(classOf[ChatClient], name) 10 | 11 | case class Publish(msg: String) 12 | case class Message(from: String, text: String) 13 | } 14 | 15 | class ChatClient(name: String) extends Actor { 16 | val mediator = DistributedPubSub(context.system).mediator 17 | val topic = "chatroom" 18 | mediator ! Subscribe(topic, self) 19 | println(s"$name joined chat room") 20 | 21 | def receive = { 22 | case ChatClient.Publish(msg) => 23 | mediator ! Publish(topic, ChatClient.Message(name, msg)) 24 | 25 | case ChatClient.Message(from, text) => 26 | val direction = if (sender == self) ">>>>" else s"<< $from:" 27 | println(s"$name $direction $text") 28 | } 29 | 30 | } -------------------------------------------------------------------------------- /src/main/scala/chat/Main.scala: -------------------------------------------------------------------------------- 1 | package chat 2 | 3 | import akka.actor.ActorSystem 4 | import akka.actor.Props 5 | import akka.cluster.Cluster 6 | 7 | object Main { 8 | def main(args: Array[String]): Unit = { 9 | val systemName = "ChatApp" 10 | val system1 = ActorSystem(systemName) 11 | val joinAddress = Cluster(system1).selfAddress 12 | Cluster(system1).join(joinAddress) 13 | system1.actorOf(Props[MemberListener], "memberListener") 14 | system1.actorOf(Props[RandomUser], "Ben") 15 | system1.actorOf(Props[RandomUser], "Kathy") 16 | 17 | Thread.sleep(5000) 18 | val system2 = ActorSystem(systemName) 19 | Cluster(system2).join(joinAddress) 20 | system2.actorOf(Props[RandomUser], "Skye") 21 | 22 | Thread.sleep(10000) 23 | val system3 = ActorSystem(systemName) 24 | Cluster(system3).join(joinAddress) 25 | system3.actorOf(Props[RandomUser], "Miguel") 26 | system3.actorOf(Props[RandomUser], "Tyler") 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/main/scala/chat/MemberListener.scala: -------------------------------------------------------------------------------- 1 | package chat 2 | 3 | import akka.actor.Actor 4 | import akka.actor.ActorLogging 5 | import akka.actor.Address 6 | import akka.cluster.Cluster 7 | import akka.cluster.ClusterEvent._ 8 | import akka.cluster.MemberStatus 9 | 10 | class MemberListener extends Actor with ActorLogging { 11 | 12 | val cluster = Cluster(context.system) 13 | 14 | override def preStart(): Unit = 15 | cluster.subscribe(self, classOf[MemberEvent]) 16 | 17 | override def postStop(): Unit = 18 | cluster unsubscribe self 19 | 20 | var nodes = Set.empty[Address] 21 | 22 | def receive = { 23 | case state: CurrentClusterState => 24 | nodes = state.members.collect { 25 | case m if m.status == MemberStatus.Up => m.address 26 | } 27 | case MemberUp(member) => 28 | nodes += member.address 29 | log.info("Member is Up: {}. {} nodes in cluster", 30 | member.address, nodes.size) 31 | case MemberRemoved(member, _) => 32 | nodes -= member.address 33 | log.info("Member is Removed: {}. {} nodes cluster", 34 | member.address, nodes.size) 35 | case _: MemberEvent => // ignore 36 | } 37 | } -------------------------------------------------------------------------------- /src/main/scala/chat/RandomUser.scala: -------------------------------------------------------------------------------- 1 | package chat 2 | 3 | import scala.concurrent.forkjoin.ThreadLocalRandom 4 | import scala.concurrent.duration._ 5 | import akka.actor.Actor 6 | import akka.actor.ActorRef 7 | import akka.actor.Props 8 | 9 | object RandomUser { 10 | case object Tick 11 | val phrases = Vector( 12 | "Creativity is allowing yourself to make mistakes. Art is knowing which ones to keep.", 13 | "The best way to compile inaccurate information that no one wants is to make it up.", 14 | "Decisions are made by people who have time, not people who have talent.", 15 | "Frankly, I'm suspicious of anyone who has a strong opinion on a complicated issue.", 16 | "Nothing inspires forgiveness quite like revenge.", 17 | "Free will is an illusion. People always choose the perceived path of greatest pleasure.", 18 | "The best things in life are silly.", 19 | "Remind people that profit is the difference between revenue and expense. This makes you look smart.", 20 | "Engineers like to solve problems. If there are no problems handily available, they will create their own problems.") 21 | } 22 | 23 | class RandomUser extends Actor { 24 | import RandomUser._ 25 | import context.dispatcher 26 | val client = context.actorOf(ChatClient.props(self.path.name), "client") 27 | 28 | def scheduler = context.system.scheduler 29 | def rnd = ThreadLocalRandom.current 30 | 31 | override def preStart(): Unit = 32 | scheduler.scheduleOnce(5.seconds, self, Tick) 33 | 34 | // override postRestart so we don't call preStart and schedule a new Tick 35 | override def postRestart(reason: Throwable): Unit = () 36 | 37 | def receive = { 38 | case Tick => 39 | scheduler.scheduleOnce(rnd.nextInt(5, 20).seconds, self, Tick) 40 | val msg = phrases(rnd.nextInt(phrases.size)) 41 | client ! ChatClient.Publish(msg) 42 | } 43 | 44 | } -------------------------------------------------------------------------------- /src/test/scala/chat/ChatClientSpec.scala: -------------------------------------------------------------------------------- 1 | package chat 2 | 3 | import org.scalatest.{ BeforeAndAfterAll, FlatSpecLike, Matchers } 4 | import akka.actor.ActorSystem 5 | import akka.testkit.TestKit 6 | import akka.cluster.pubsub.DistributedPubSub 7 | import akka.cluster.pubsub.DistributedPubSubMediator.Subscribe 8 | 9 | import scala.concurrent.Await 10 | import scala.concurrent.duration.Duration 11 | 12 | class ChatClientSpec(_system: ActorSystem) 13 | extends TestKit(_system) 14 | with Matchers 15 | with FlatSpecLike 16 | with BeforeAndAfterAll { 17 | 18 | def this() = this(ActorSystem("ChatClientSpec")) 19 | 20 | override def afterAll: Unit = Await.ready(system.terminate(), Duration.Inf) 21 | 22 | "A ChatClient" should "publish messages to chatroom topic" in { 23 | val mediator = DistributedPubSub(system).mediator 24 | mediator ! Subscribe("chatroom", testActor) 25 | val chatClient = system.actorOf(ChatClient.props("user1")) 26 | chatClient ! ChatClient.Publish("hello") 27 | val msg = expectMsgType[ChatClient.Message] 28 | msg.from should be("user1") 29 | msg.text should be("hello") 30 | } 31 | 32 | } -------------------------------------------------------------------------------- /tutorial/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Akka Cluster - Activator Template 4 | 5 | 6 |
7 |

Run the Application

8 | 9 |

10 | This tutorial will demonstrate a chat room application. Chat users 11 | communicate with publish-subscribe to other users somewhere in 14 | the cluster. 15 |

16 | 17 |

18 | Open the Run tab. On the 19 | left-hand side we can see the console output, which is logging output 20 | from nodes joining the cluster and the fictive chat users publishing 21 | random Scott Adams quotes to the chat room as illustrated with the 22 | >>>> markers. Other chat users in the room 23 | receive those messages as illustrated by the << 24 | markers. 25 |

26 | 27 |
28 |
29 |

Explore the Code - Startup

30 | 31 |

32 | Let's look at the Main.scala file. 34 |

35 | 36 |

37 | The application starts in the main method. Normally you 38 | would run cluster nodes in separate JVMs, typically on different physical 39 | machines. Here we simulate that by starting 3 separate actor systems in 40 | the same JVM. The actor systems use remoting to communicate with each other, 41 | so the code would be the same for a real distributed application. 42 |

43 | 44 |

45 | The configuration of the actor systems is located in application.conf. 47 | The 48 | ClusterActorRefProvider 49 | is the only mandatory configuration to enable Akka clustering. 50 |

51 |

52 | port=0 53 | means that the remoting of each actor system will listen on a random 54 | available port. This is important when running several actor systems 55 | on the same host, since they cannot bind to the same port. 56 |

57 | 58 |

A few chat user actors are created in each actor system.

59 | 60 |

To form a cluster the nodes must join some other node in the 61 | cluster. The first node joins itself, the other 2 nodes join the 62 | first node.

63 | 64 |

65 | Note that there is a delay between the startup of each actor system 66 | and that explains why the first messages are not delivered to Miguel 67 | and Tyler. An Akka cluster is elastic in its nature. Nodes may join 68 | and exit over time and that is discovered by other members of the 69 | cluster. Go through the log output in the Run tab again and make sure you see that the 3 71 | nodes become members. Look for "Member is Up". If the log has grown too 72 | much you can restart the app. 73 |

74 |
75 |
76 |

Explore the Code - Chat Client Actor

77 | 78 |

79 | Let's look at the RandomUser.scala file. 81 |

82 | 83 | 84 |

85 | The RandomUser simulates a chat user. It schedules a 86 | Tick message to itself with a random interval. For each tick 87 | it picks a Scott Adams quote and publishes it to the chat room via the 88 | ChatClient, which is a child actor to RandomUser. 89 |

90 | 91 | 92 | 93 |

94 | Open the ChatClient.scala 95 | file. 96 |

97 | 98 |

99 | The ChatClient receives Publish messages from the 100 | RandomUser and sends those to the chat room topic via the 101 | DistributedPubSubMediator. It subscribes to the same topic to 102 | receive messages from itself and other chat users. Note that the chat users 103 | are completely decoupled from each other and only know about the publish-subscribe 104 | topic that is managed by the mediator. The sender of the message is still the 105 | original sender, which makes it possible to reply directly to that actor. 106 |

107 | 108 |

109 | DistributedPubSubMediator facilitates this 111 | publish-subscribe communication pattern. It is built on top of Akka cluster and packaged in the 112 | Akka Cluster Tools Module. 113 | The mediator actor keeps track of registered subscribers, but there is no central 114 | broker. One mediator actor is running on each node, and it replicates the actor 115 | references to peer mediators at other cluster members. This distributed registry 116 | is eventually consistent, i.e. changes are not immediately visible at other nodes, 117 | but typically they will be fully replicated to all other nodes after a few seconds. 118 |

119 | 120 |
121 |
122 |

Cluster Membership

123 | 124 |

125 | Discovery of new nodes in the cluster, as well as detection of 126 | unreachable and removed nodes, is important to be able to build 127 | cluster aware features, such as the DistributedPubSubMediator. 128 | The API for cluster membership is event based. 129 |

130 | 131 |

132 | The MemberListener.scala 133 | illustrates how to subscribe to such cluster membership events and 134 | keep track of addresses of the current members in the cluster. 135 |

136 | 137 |

138 | A snapshot of the full state, CurrentClusterState, is sent to the subscriber as 139 | the first event. Here we are only interested in members with status Up and grab the 140 | Address of those members. When there are changes to the members of the cluster 141 | other events are sent to the subscriber. MemberUp is received when 142 | a new member is added to the cluster. MemberRemoved is received when a member 143 | is removed from the cluster due to graceful exit or failure. 144 |

145 | 146 |

147 | Using the Address information you can send messages to actors at those nodes. 148 |

149 |
150 | 			
151 |   import akka.actor.RootActorPath
152 |   val service = context.actorSelection(RootActorPath(member.address) /
153 |     "user" / "backendService")
154 |   service ! Request("..")
155 | 
156 | 		
157 |
158 |
159 |

Next Steps

160 | 161 |

162 | With the presented publish-subscribe feature and brief understanding of cluster membership you can develop applications 167 | that scale out. Of course there are more things in Akka cluster to 168 | take advantage of, such as Cluster Aware Routers and the Cluster Singleton. 173 |

174 |

175 | In depth documentation can be found in the Cluster Specification and in the Cluster Usage documentation. 180 |

181 | 182 |
183 | 184 | 185 | 186 | --------------------------------------------------------------------------------