diff --git a/akka-docs/cluster/cluster-usage.rst b/akka-docs/cluster/cluster-usage.rst index b0ec8f08b7..d370f8b7ac 100644 --- a/akka-docs/cluster/cluster-usage.rst +++ b/akka-docs/cluster/cluster-usage.rst @@ -141,6 +141,8 @@ Be aware of that using auto-down implies that two separate clusters will automatically be formed in case of network partition. That might be desired by some applications but not by others. +.. _cluster_subscriber: + Subscribe to Cluster Events ^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -339,6 +341,64 @@ service nodes and 1 client:: .. note:: The above example, especially the last part, will be simplified when the cluster handles automatic actor partitioning. + +How to Test +^^^^^^^^^^^ + +:ref:`multi_node_testing` is useful for testing cluster applications. + +Set up your project according to the instructions in :ref:`multi_node_testing` and :ref:`multi_jvm_testing`, i.e. +add the ``sbt-multi-jvm`` plugin and the dependency to ``akka-remote-tests-experimental``. + +First, as described in :ref:`multi_node_testing`, we need some scaffolding to configure the ``MultiNodeSpec``. +Define the participating roles and their :ref:`cluster_configuration` in an object extending ``MultiNodeConfig``: + +.. includecode:: ../../akka-samples/akka-sample-cluster/src/multi-jvm/scala/sample/cluster/stats/StatsSampleSpec.scala + :include: MultiNodeConfig + :exclude: router-lookup-config + +Define one concrete test class for each role/node. These will be instantiated on the different nodes (JVMs). They can be +implemented differently, but often they are the same and extend an abstract test class, as illustrated here. + +.. includecode:: ../../akka-samples/akka-sample-cluster/src/multi-jvm/scala/sample/cluster/stats/StatsSampleSpec.scala#concrete-tests + +Note the naming convention of these classes. The name of the classes must end with ``MultiJvmNode1``, ``MultiJvmNode2`` +and so on. It's possible to define another suffix to be used by the ``sbt-multi-jvm``, but the default should be +fine in most cases. + +Then the abstract ``MultiNodeSpec``, which takes the ``MultiNodeConfig`` as constructor parameter. + +.. includecode:: ../../akka-samples/akka-sample-cluster/src/multi-jvm/scala/sample/cluster/stats/StatsSampleSpec.scala#abstract-test + +Most of this can of course be extracted to a separate trait to avoid repeating this in all your tests. This example +is using `Scalatest `_, but similar can be done with other testing frameworks. + +Typically you begin your test by starting up the cluster and let the members join, and create some actors. +That can be done like this: + +.. includecode:: ../../akka-samples/akka-sample-cluster/src/multi-jvm/scala/sample/cluster/stats/StatsSampleSpec.scala#startup-cluster + +From the test you interact with the cluster using the ``Cluster`` extension, e.g. ``join``. + +.. includecode:: ../../akka-samples/akka-sample-cluster/src/multi-jvm/scala/sample/cluster/stats/StatsSampleSpec.scala#join + +Notice how the `testActor` from :ref:`testkit ` is added as :ref:`subscriber ` +to cluster changes and then waiting for certain events, such as in this case all members becoming 'Up'. + +The above code was running for all roles (JVMs). ``runOn`` is a convenient utility to declare that a certain block +of code should only run for a specific role. + +.. includecode:: ../../akka-samples/akka-sample-cluster/src/multi-jvm/scala/sample/cluster/stats/StatsSampleSpec.scala#test-statsService + +Once again we take advantage of the facilities in :ref:`testkit ` to verify expected behavior. +Here using ``testActor`` as sender (via ``ImplicitSender``) and verifing the reply with ``expectMsgPF``. + +In the above code you can see ``node(third)``, which is useful facility to get the root actor reference of +the actor system for a specific role. This can also be used to grab the ``akka.actor.Address`` of that node. + +.. includecode:: ../../akka-samples/akka-sample-cluster/src/multi-jvm/scala/sample/cluster/stats/StatsSampleSpec.scala#addresses + + .. _cluster_jmx: JMX diff --git a/akka-samples/akka-sample-cluster/src/multi-jvm/scala/sample/cluster/stats/StatsSampleSingleMasterSpec.scala b/akka-samples/akka-sample-cluster/src/multi-jvm/scala/sample/cluster/stats/StatsSampleSingleMasterSpec.scala index b1d27cd7a3..9f7010f7cd 100644 --- a/akka-samples/akka-sample-cluster/src/multi-jvm/scala/sample/cluster/stats/StatsSampleSingleMasterSpec.scala +++ b/akka-samples/akka-sample-cluster/src/multi-jvm/scala/sample/cluster/stats/StatsSampleSingleMasterSpec.scala @@ -5,8 +5,10 @@ import scala.concurrent.util.duration._ import com.typesafe.config.ConfigFactory -import StatsSampleSpec.first -import StatsSampleSpec.third +import org.scalatest.BeforeAndAfterAll +import org.scalatest.WordSpec +import org.scalatest.matchers.MustMatchers + import akka.actor.Props import akka.actor.RootActorPath import akka.cluster.Cluster @@ -16,10 +18,9 @@ import akka.cluster.ClusterEvent.CurrentClusterState import akka.cluster.ClusterEvent.MemberUp import akka.remote.testkit.MultiNodeConfig import akka.remote.testkit.MultiNodeSpec -import akka.remote.testkit.STMultiNodeSpec import akka.testkit.ImplicitSender -object StatsSampleSingleMasterSpec extends MultiNodeConfig { +object StatsSampleSingleMasterSpecConfig extends MultiNodeConfig { // register the named roles (nodes) of the test val first = role("first") val second = role("second") @@ -50,17 +51,21 @@ object StatsSampleSingleMasterSpec extends MultiNodeConfig { } // need one concrete test class per node -class StatsSampleSingleMasterMultiJvmNode1 extends StatsSampleSingleMasterSpec -class StatsSampleSingleMasterMultiJvmNode2 extends StatsSampleSingleMasterSpec -class StatsSampleSingleMasterMultiJvmNode3 extends StatsSampleSingleMasterSpec +class StatsSampleSingleMasterSpecMultiJvmNode1 extends StatsSampleSingleMasterSpec +class StatsSampleSingleMasterSpecMultiJvmNode2 extends StatsSampleSingleMasterSpec +class StatsSampleSingleMasterSpecMultiJvmNode3 extends StatsSampleSingleMasterSpec -abstract class StatsSampleSingleMasterSpec extends MultiNodeSpec(StatsSampleSingleMasterSpec) - with STMultiNodeSpec with ImplicitSender { +abstract class StatsSampleSingleMasterSpec extends MultiNodeSpec(StatsSampleSingleMasterSpecConfig) + with WordSpec with MustMatchers with BeforeAndAfterAll with ImplicitSender { - import StatsSampleSpec._ + import StatsSampleSingleMasterSpecConfig._ override def initialParticipants = roles.size + override def beforeAll() = multiNodeSpecBeforeAll() + + override def afterAll() = multiNodeSpecAfterAll() + "The stats sample with single master" must { "illustrate how to startup cluster" in within(10 seconds) { Cluster(system).subscribe(testActor, classOf[MemberUp]) diff --git a/akka-samples/akka-sample-cluster/src/multi-jvm/scala/sample/cluster/stats/StatsSampleSpec.scala b/akka-samples/akka-sample-cluster/src/multi-jvm/scala/sample/cluster/stats/StatsSampleSpec.scala index 9f88597051..b1141e587f 100644 --- a/akka-samples/akka-sample-cluster/src/multi-jvm/scala/sample/cluster/stats/StatsSampleSpec.scala +++ b/akka-samples/akka-sample-cluster/src/multi-jvm/scala/sample/cluster/stats/StatsSampleSpec.scala @@ -2,7 +2,6 @@ package sample.cluster.stats import language.postfixOps import scala.concurrent.util.duration._ -import com.typesafe.config.ConfigFactory import akka.actor.Props import akka.actor.RootActorPath @@ -11,12 +10,12 @@ import akka.cluster.Member import akka.cluster.MemberStatus import akka.cluster.ClusterEvent.CurrentClusterState import akka.cluster.ClusterEvent.MemberUp -import akka.remote.testkit.MultiNodeConfig -import akka.remote.testkit.MultiNodeSpec -import akka.remote.testkit.STMultiNodeSpec -import akka.testkit.ImplicitSender -object StatsSampleSpec extends MultiNodeConfig { +//#MultiNodeConfig +import akka.remote.testkit.MultiNodeConfig +import com.typesafe.config.ConfigFactory + +object StatsSampleSpecConfig extends MultiNodeConfig { // register the named roles (nodes) of the test val first = role("first") val second = role("second") @@ -44,49 +43,95 @@ object StatsSampleSpec extends MultiNodeConfig { """)) } +//#MultiNodeConfig +//#concrete-tests // need one concrete test class per node -class StatsSampleMultiJvmNode1 extends StatsSampleSpec -class StatsSampleMultiJvmNode2 extends StatsSampleSpec -class StatsSampleMultiJvmNode3 extends StatsSampleSpec +class StatsSampleSpecMultiJvmNode1 extends StatsSampleSpec +class StatsSampleSpecMultiJvmNode2 extends StatsSampleSpec +class StatsSampleSpecMultiJvmNode3 extends StatsSampleSpec +//#concrete-tests -abstract class StatsSampleSpec extends MultiNodeSpec(StatsSampleSpec) - with STMultiNodeSpec with ImplicitSender { +//#abstract-test +import org.scalatest.BeforeAndAfterAll +import org.scalatest.WordSpec +import org.scalatest.matchers.MustMatchers +import akka.remote.testkit.MultiNodeSpec +import akka.testkit.ImplicitSender - import StatsSampleSpec._ +abstract class StatsSampleSpec extends MultiNodeSpec(StatsSampleSpecConfig) + with WordSpec with MustMatchers with BeforeAndAfterAll + with ImplicitSender { + + import StatsSampleSpecConfig._ override def initialParticipants = roles.size + override def beforeAll() = multiNodeSpecBeforeAll() + + override def afterAll() = multiNodeSpecAfterAll() + +//#abstract-test + "The stats sample" must { + + //#startup-cluster "illustrate how to startup cluster" in within(10 seconds) { Cluster(system).subscribe(testActor, classOf[MemberUp]) expectMsgClass(classOf[CurrentClusterState]) - Cluster(system) join node(first).address + //#addresses + val firstAddress = node(first).address + val secondAddress = node(second).address + val thirdAddress = node(third).address + //#addresses + + //#join + Cluster(system) join firstAddress + //#join + system.actorOf(Props[StatsWorker], "statsWorker") system.actorOf(Props[StatsService], "statsService") expectMsgAllOf( - MemberUp(Member(node(first).address, MemberStatus.Up)), - MemberUp(Member(node(second).address, MemberStatus.Up)), - MemberUp(Member(node(third).address, MemberStatus.Up))) + MemberUp(Member(firstAddress, MemberStatus.Up)), + MemberUp(Member(secondAddress, MemberStatus.Up)), + MemberUp(Member(thirdAddress, MemberStatus.Up))) Cluster(system).unsubscribe(testActor) testConductor.enter("all-up") } + //#startup-cluster - "show usage of the statsService" in within(5 seconds) { - val service = system.actorFor(RootActorPath(node(third).address) / "user" / "statsService") + //#test-statsService + "show usage of the statsService from one node" in within(5 seconds) { + runOn(second) { + val service = system.actorFor(node(third) / "user" / "statsService") + service ! StatsJob("this is the text that will be analyzed") + val meanWordLength = expectMsgPF() { + case StatsResult(meanWordLength) ⇒ meanWordLength + } + meanWordLength must be(3.875 plusOrMinus 0.001) + } + + testConductor.enter("done-2") + } + //#test-statsService + + "show usage of the statsService from all nodes" in within(5 seconds) { + val service = system.actorFor(node(third) / "user" / "statsService") service ! StatsJob("this is the text that will be analyzed") val meanWordLength = expectMsgPF() { case StatsResult(meanWordLength) ⇒ meanWordLength } meanWordLength must be(3.875 plusOrMinus 0.001) - testConductor.enter("done") + testConductor.enter("done-2") } + + } } \ No newline at end of file diff --git a/akka-samples/akka-sample-cluster/src/multi-jvm/scala/sample/cluster/transformation/TransformationSampleSpec.scala b/akka-samples/akka-sample-cluster/src/multi-jvm/scala/sample/cluster/transformation/TransformationSampleSpec.scala index 98b4cb6526..1c3176ee16 100644 --- a/akka-samples/akka-sample-cluster/src/multi-jvm/scala/sample/cluster/transformation/TransformationSampleSpec.scala +++ b/akka-samples/akka-sample-cluster/src/multi-jvm/scala/sample/cluster/transformation/TransformationSampleSpec.scala @@ -5,14 +5,17 @@ import scala.concurrent.util.duration._ import com.typesafe.config.ConfigFactory +import org.scalatest.BeforeAndAfterAll +import org.scalatest.WordSpec +import org.scalatest.matchers.MustMatchers + import akka.actor.Props import akka.cluster.Cluster import akka.remote.testkit.MultiNodeConfig import akka.remote.testkit.MultiNodeSpec -import akka.remote.testkit.STMultiNodeSpec import akka.testkit.ImplicitSender -object TransformationSampleSpec extends MultiNodeConfig { +object TransformationSampleSpecConfig extends MultiNodeConfig { // register the named roles (nodes) of the test val frontend1 = role("frontend1") val frontend2 = role("frontend2") @@ -31,19 +34,23 @@ object TransformationSampleSpec extends MultiNodeConfig { } // need one concrete test class per node -class TransformationSampleMultiJvmNode1 extends TransformationSampleSpec -class TransformationSampleMultiJvmNode2 extends TransformationSampleSpec -class TransformationSampleMultiJvmNode3 extends TransformationSampleSpec -class TransformationSampleMultiJvmNode4 extends TransformationSampleSpec -class TransformationSampleMultiJvmNode5 extends TransformationSampleSpec +class TransformationSampleSpecMultiJvmNode1 extends TransformationSampleSpec +class TransformationSampleSpecMultiJvmNode2 extends TransformationSampleSpec +class TransformationSampleSpecMultiJvmNode3 extends TransformationSampleSpec +class TransformationSampleSpecMultiJvmNode4 extends TransformationSampleSpec +class TransformationSampleSpecMultiJvmNode5 extends TransformationSampleSpec -abstract class TransformationSampleSpec extends MultiNodeSpec(TransformationSampleSpec) - with STMultiNodeSpec with ImplicitSender { +abstract class TransformationSampleSpec extends MultiNodeSpec(TransformationSampleSpecConfig) + with WordSpec with MustMatchers with BeforeAndAfterAll with ImplicitSender { - import TransformationSampleSpec._ + import TransformationSampleSpecConfig._ override def initialParticipants = roles.size + override def beforeAll() = multiNodeSpecBeforeAll() + + override def afterAll() = multiNodeSpecAfterAll() + "The transformation sample" must { "illustrate how to start first frontend" in { runOn(frontend1) { diff --git a/project/AkkaBuild.scala b/project/AkkaBuild.scala index 8972ed5cc8..9a11c119c8 100644 --- a/project/AkkaBuild.scala +++ b/project/AkkaBuild.scala @@ -330,14 +330,14 @@ object AkkaBuild extends Build { lazy val clusterSample = Project( id = "akka-sample-cluster-experimental", base = file("akka-samples/akka-sample-cluster"), - dependencies = Seq(cluster, remoteTests % "compile;test->test;multi-jvm->multi-jvm", testkit % "test->test"), + dependencies = Seq(cluster, remoteTests % "test", testkit % "test"), settings = defaultSettings ++ multiJvmSettings ++ Seq( + libraryDependencies ++= Dependencies.clusterSample, // disable parallel tests parallelExecution in Test := false, extraOptions in MultiJvm <<= (sourceDirectory in MultiJvm) { src => (name: String) => (src ** (name + ".conf")).get.headOption.map("-Dakka.config=" + _.absolutePath).toSeq }, - scalatestOptions in MultiJvm := defaultMultiJvmScalatestOptions, jvmOptions in MultiJvm := defaultMultiJvmOptions, publishArtifact in Compile := false ) @@ -594,6 +594,8 @@ object Dependencies { val docs = Seq(Test.scalatest, Test.junit, Test.junitIntf) val zeroMQ = Seq(protobuf, zeroMQClient, Test.scalatest, Test.junit) + + val clusterSample = Seq(Test.scalatest) } object Dependency {