From 2e50bb8f524e2b72f4b32a7714dde0d8f2791a93 Mon Sep 17 00:00:00 2001 From: Viktor Klang Date: Wed, 6 Mar 2013 15:10:59 +0100 Subject: [PATCH 01/47] Removing the redundant field that is RemoteActorRefProvider from RARP as it is transitively availabe through the transport --- .../akka/remote/RemoteActorRefProvider.scala | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/akka-remote/src/main/scala/akka/remote/RemoteActorRefProvider.scala b/akka-remote/src/main/scala/akka/remote/RemoteActorRefProvider.scala index a9856c238a..2066aacc58 100644 --- a/akka-remote/src/main/scala/akka/remote/RemoteActorRefProvider.scala +++ b/akka-remote/src/main/scala/akka/remote/RemoteActorRefProvider.scala @@ -242,7 +242,7 @@ private[akka] class RemoteActorRefProvider( try { val localAddress = transport.localAddressForRemote(addr) val rpath = RootActorPath(addr) / "remote" / localAddress.protocol / localAddress.hostPort / path.elements - new RemoteActorRef(this, transport, localAddress, rpath, supervisor, Some(props), Some(d)) + new RemoteActorRef(transport, localAddress, rpath, supervisor, Some(props), Some(d)) } catch { case NonFatal(e) ⇒ log.error(e, "Error while looking up address {}", addr) @@ -258,7 +258,7 @@ private[akka] class RemoteActorRefProvider( def actorFor(path: ActorPath): InternalActorRef = { if (hasAddress(path.address)) actorFor(rootGuardian, path.elements) else try { - new RemoteActorRef(this, transport, transport.localAddressForRemote(path.address), + new RemoteActorRef(transport, transport.localAddressForRemote(path.address), path, Nobody, props = None, deploy = None) } catch { case NonFatal(e) ⇒ @@ -270,7 +270,7 @@ private[akka] class RemoteActorRefProvider( def actorFor(ref: InternalActorRef, path: String): InternalActorRef = path match { case ActorPathExtractor(address, elems) ⇒ if (hasAddress(address)) actorFor(rootGuardian, elems) - else new RemoteActorRef(this, transport, transport.localAddressForRemote(address), + else new RemoteActorRef(transport, transport.localAddressForRemote(address), new RootActorPath(address) / elems, Nobody, props = None, deploy = None) case _ ⇒ local.actorFor(ref, path) } @@ -283,7 +283,7 @@ private[akka] class RemoteActorRefProvider( def actorForWithLocalAddress(ref: InternalActorRef, path: String, localAddress: Address): InternalActorRef = path match { case ActorPathExtractor(address, elems) ⇒ if (hasAddress(address)) actorFor(rootGuardian, elems) - else new RemoteActorRef(this, transport, localAddress, + else new RemoteActorRef(transport, localAddress, new RootActorPath(address) / elems, Nobody, props = None, deploy = None) case _ ⇒ local.actorFor(ref, path) } @@ -325,7 +325,6 @@ private[akka] trait RemoteRef extends ActorRefScope { * This reference is network-aware (remembers its origin) and immutable. */ private[akka] class RemoteActorRef private[akka] ( - val provider: RemoteActorRefProvider, remote: RemoteTransport, val localAddressToUse: Address, val path: ActorPath, @@ -339,7 +338,7 @@ private[akka] class RemoteActorRef private[akka] ( s.headOption match { case None ⇒ this case Some("..") ⇒ getParent getChild name - case _ ⇒ new RemoteActorRef(provider, remote, localAddressToUse, path / s, Nobody, props = None, deploy = None) + case _ ⇒ new RemoteActorRef(remote, localAddressToUse, path / s, Nobody, props = None, deploy = None) } } @@ -360,7 +359,10 @@ private[akka] class RemoteActorRef private[akka] ( try remote.send(message, Option(sender), this) catch handleException } - def start(): Unit = if (props.isDefined && deploy.isDefined) provider.useActorOnNode(path, props.get, deploy.get, getParent) + override def provider: RemoteActorRefProvider = remote.provider + + def start(): Unit = + if (props.isDefined && deploy.isDefined) remote.provider.useActorOnNode(path, props.get, deploy.get, getParent) def suspend(): Unit = sendSystemMessage(Suspend()) From 7933fa5d0db5a65359fe291aed34fa5418f2d9e0 Mon Sep 17 00:00:00 2001 From: Roland Date: Thu, 7 Mar 2013 19:59:59 +0100 Subject: [PATCH 02/47] make config switchable in initialCommands for SBT console --- project/AkkaBuild.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/project/AkkaBuild.scala b/project/AkkaBuild.scala index 517adebe92..eb9a9fc117 100644 --- a/project/AkkaBuild.scala +++ b/project/AkkaBuild.scala @@ -596,8 +596,8 @@ object AkkaBuild extends Build { |import com.typesafe.config.ConfigFactory |import scala.concurrent.duration._ |import akka.util.Timeout - |val config = ConfigFactory.parseString("akka.stdout-loglevel=INFO,akka.loglevel=DEBUG,pinned{type=PinnedDispatcher,executor=thread-pool-executor,throughput=1000}") - |val remoteConfig = ConfigFactory.parseString("akka.remote.netty{port=0,use-dispatcher-for-io=akka.actor.default-dispatcher,execution-pool-size=0},akka.actor.provider=akka.remote.RemoteActorRefProvider").withFallback(config) + |var config = ConfigFactory.parseString("akka.stdout-loglevel=INFO,akka.loglevel=DEBUG,pinned{type=PinnedDispatcher,executor=thread-pool-executor,throughput=1000}") + |var remoteConfig = ConfigFactory.parseString("akka.remote.netty{port=0,use-dispatcher-for-io=akka.actor.default-dispatcher,execution-pool-size=0},akka.actor.provider=akka.remote.RemoteActorRefProvider").withFallback(config) |var system: ActorSystem = null |implicit def _system = system |def startSystem(remoting: Boolean = false) { system = ActorSystem("repl", if(remoting) remoteConfig else config); println("don’t forget to system.shutdown()!") } From f6a14cb5ac068849b3da12ef7325dc395b4c0948 Mon Sep 17 00:00:00 2001 From: Rich Dougherty Date: Thu, 28 Feb 2013 15:45:12 +1300 Subject: [PATCH 03/47] Dump diagnostics (Coroner's Report) if a test takes too long. Fixes #3110 --- .../test/scala/akka/actor/SchedulerSpec.scala | 2 + .../akka/cluster/MultiNodeClusterSpec.scala | 9 +- .../scala/akka/cluster/StressSpec.scala | 4 + .../test/scala/akka/testkit/AkkaSpec.scala | 7 +- .../src/test/scala/akka/testkit/Coroner.scala | 160 ++++++++++++++++++ .../test/scala/akka/testkit/CoronerSpec.scala | 125 ++++++++++++++ 6 files changed, 305 insertions(+), 2 deletions(-) create mode 100644 akka-testkit/src/test/scala/akka/testkit/Coroner.scala create mode 100644 akka-testkit/src/test/scala/akka/testkit/CoronerSpec.scala diff --git a/akka-actor-tests/src/test/scala/akka/actor/SchedulerSpec.scala b/akka-actor-tests/src/test/scala/akka/actor/SchedulerSpec.scala index 0cba96ffcc..c815518b80 100644 --- a/akka-actor-tests/src/test/scala/akka/actor/SchedulerSpec.scala +++ b/akka-actor-tests/src/test/scala/akka/actor/SchedulerSpec.scala @@ -324,6 +324,8 @@ class DefaultSchedulerSpec extends AkkaSpec(SchedulerSpec.testConf) with Schedul } } } + + override def expectedTestDuration = 5 minutes } class LightArrayRevolverSchedulerSpec extends AkkaSpec(SchedulerSpec.testConfRevolver) with SchedulerSpec { diff --git a/akka-cluster/src/multi-jvm/scala/akka/cluster/MultiNodeClusterSpec.scala b/akka-cluster/src/multi-jvm/scala/akka/cluster/MultiNodeClusterSpec.scala index 9401013558..e3861ca61c 100644 --- a/akka-cluster/src/multi-jvm/scala/akka/cluster/MultiNodeClusterSpec.scala +++ b/akka-cluster/src/multi-jvm/scala/akka/cluster/MultiNodeClusterSpec.scala @@ -50,16 +50,23 @@ object MultiNodeClusterSpec { """) } -trait MultiNodeClusterSpec extends Suite with STMultiNodeSpec { self: MultiNodeSpec ⇒ +trait MultiNodeClusterSpec extends Suite with STMultiNodeSpec with WatchedByCoroner { self: MultiNodeSpec ⇒ override def initialParticipants = roles.size private val cachedAddresses = new ConcurrentHashMap[RoleName, Address] override def atStartup(): Unit = { + startCoroner() muteLog() } + override def afterTermination(): Unit = { + stopCoroner() + } + + override def expectedTestDuration = 60.seconds + def muteLog(sys: ActorSystem = system): Unit = { if (!sys.log.isDebugEnabled) { Seq(".*Metrics collection has started successfully.*", diff --git a/akka-cluster/src/multi-jvm/scala/akka/cluster/StressSpec.scala b/akka-cluster/src/multi-jvm/scala/akka/cluster/StressSpec.scala index 1875a0d48d..0d2d55e917 100644 --- a/akka-cluster/src/multi-jvm/scala/akka/cluster/StressSpec.scala +++ b/akka-cluster/src/multi-jvm/scala/akka/cluster/StressSpec.scala @@ -85,6 +85,7 @@ object StressMultiJvmSpec extends MultiNodeConfig { high-throughput-duration = 10s supervision-duration = 10s supervision-one-iteration = 1s + expected-test-duration = 600s # actors are created in a tree structure defined # by tree-width (number of children for each actor) and # tree-levels, total number of actors can be calculated by @@ -169,6 +170,7 @@ object StressMultiJvmSpec extends MultiNodeConfig { val highThroughputDuration = getDuration("high-throughput-duration") * dFactor val supervisionDuration = getDuration("supervision-duration") * dFactor val supervisionOneIteration = getDuration("supervision-one-iteration") * dFactor + val expectedTestDuration = getDuration("expected-test-duration") * dFactor val treeWidth = getInt("tree-width") val treeLevels = getInt("tree-levels") val reportMetricsInterval = getDuration("report-metrics-interval") @@ -617,6 +619,8 @@ abstract class StressSpec override def beforeEach(): Unit = { step += 1 } + override def expectedTestDuration = settings.expectedTestDuration + override def muteLog(sys: ActorSystem = system): Unit = { super.muteLog(sys) sys.eventStream.publish(Mute(EventFilter[RuntimeException](pattern = ".*Simulated exception.*"))) diff --git a/akka-testkit/src/test/scala/akka/testkit/AkkaSpec.scala b/akka-testkit/src/test/scala/akka/testkit/AkkaSpec.scala index 4a2e24dbd2..4cc59eca6e 100644 --- a/akka-testkit/src/test/scala/akka/testkit/AkkaSpec.scala +++ b/akka-testkit/src/test/scala/akka/testkit/AkkaSpec.scala @@ -52,7 +52,7 @@ object AkkaSpec { } abstract class AkkaSpec(_system: ActorSystem) - extends TestKit(_system) with WordSpec with MustMatchers with BeforeAndAfterAll { + extends TestKit(_system) with WordSpec with MustMatchers with BeforeAndAfterAll with WatchedByCoroner { def this(config: Config) = this(ActorSystem(AkkaSpec.getCallerName(getClass), ConfigFactory.load(config.withFallback(AkkaSpec.testConf)))) @@ -66,6 +66,7 @@ abstract class AkkaSpec(_system: ActorSystem) val log: LoggingAdapter = Logging(system, this.getClass) final override def beforeAll { + startCoroner atStartup() } @@ -78,6 +79,7 @@ abstract class AkkaSpec(_system: ActorSystem) println(system.asInstanceOf[ActorSystemImpl].printTree) } afterTermination() + stopCoroner() } protected def atStartup() {} @@ -88,4 +90,7 @@ abstract class AkkaSpec(_system: ActorSystem) def spawn(dispatcherId: String = Dispatchers.DefaultDispatcherId)(body: ⇒ Unit): Unit = Future(body)(system.dispatchers.lookup(dispatcherId)) + + override def expectedTestDuration: FiniteDuration = 60 seconds + } diff --git a/akka-testkit/src/test/scala/akka/testkit/Coroner.scala b/akka-testkit/src/test/scala/akka/testkit/Coroner.scala new file mode 100644 index 0000000000..601c23abd4 --- /dev/null +++ b/akka-testkit/src/test/scala/akka/testkit/Coroner.scala @@ -0,0 +1,160 @@ +/** + * Copyright (C) 2009-2013 Typesafe Inc. + */ +package akka.testkit + +import java.io.PrintStream +import java.lang.management.{ ManagementFactory, ThreadInfo } +import java.util.Date +import java.util.concurrent.CountDownLatch +import org.scalatest.{ BeforeAndAfterAll, Suite } +import scala.annotation.tailrec +import scala.concurrent.duration._ +import scala.util.control.NonFatal + +/** + * The Coroner can be used to print a diagnostic report of the JVM state, + * including stack traces and deadlocks. A report can be printed directly, by + * calling `printReport`. Alternatively, the Coroner can be asked to `watch` + * the JVM and generate a report at a later time - unless the Coroner is cancelled + * by that time. + * + * The latter method is useful for printing diagnostics in the event that, for + * example, a unit test stalls and fails to cancel the Coroner in time. The + * Coroner will assume that the test has "died" and print a report to aid in + * debugging. + */ +object Coroner { + + /** + * Used to cancel the Coroner after calling `watch`. + */ + trait WatchHandle { + def cancel(): Unit + } + + /** + * Ask the Coroner to print a report if it is not cancelled by the given deadline. + * The returned handle can be used to perform the cancellation. + */ + def watch(deadline: Deadline, reportTitle: String, out: PrintStream): WatchHandle = { + val duration = deadline.timeLeft // Store for later reporting + val cancelLatch = new CountDownLatch(1) + + @tailrec def watchLoop() { + if (deadline.isOverdue) { + triggerReport() + } else { + val cancelled = try { + cancelLatch.await(deadline.timeLeft.length, deadline.timeLeft.unit) + } catch { + case _: InterruptedException ⇒ false + } + if (cancelled) { + // Our job is finished, let the thread stop + } else { + watchLoop() + } + } + } + + def triggerReport() { + out.println(s"Coroner not cancelled after ${duration.toMillis}ms. Looking for signs of foul play...") + try { + printReport(reportTitle, out) + } catch { + case NonFatal(ex) ⇒ { + out.println("Error displaying Coroner's Report") + ex.printStackTrace(out) + } + } + } + + val thread = new Thread(new Runnable { def run = watchLoop() }, "Coroner") + thread.start() // Must store thread in val to work around SI-7203 + + new WatchHandle { + def cancel() { cancelLatch.countDown() } + } + } + + /** + * Print a report containing diagnostic information. + */ + def printReport(reportTitle: String, out: PrintStream) { + import out.println + + val osMx = ManagementFactory.getOperatingSystemMXBean() + val rtMx = ManagementFactory.getRuntimeMXBean() + val memMx = ManagementFactory.getMemoryMXBean() + val threadMx = ManagementFactory.getThreadMXBean() + + println(s"""#Coroner's Report: $reportTitle + #OS Architecture: ${osMx.getArch()} + #Available processors: ${osMx.getAvailableProcessors()} + #System load (last minute): ${osMx.getSystemLoadAverage()} + #VM start time: ${new Date(rtMx.getStartTime())} + #VM uptime: ${rtMx.getUptime()}ms + #Heap usage: ${memMx.getHeapMemoryUsage()} + #Non-heap usage: ${memMx.getNonHeapMemoryUsage()}""".stripMargin('#')) + + def dumpAllThreads(): Seq[ThreadInfo] = { + threadMx.dumpAllThreads( + threadMx.isObjectMonitorUsageSupported(), + threadMx.isSynchronizerUsageSupported()) + } + + def findDeadlockedThreads(): (Seq[ThreadInfo], String) = { + val (ids, desc) = if (threadMx.isSynchronizerUsageSupported()) { + (threadMx.findDeadlockedThreads(), "monitors and ownable synchronizers") + } else { + (threadMx.findMonitorDeadlockedThreads(), "monitors, but NOT ownable synchronizers") + } + if (ids == null) { + (Seq.empty, desc) + } else { + val maxTraceDepth = 1000 // Seems deep enough + (threadMx.getThreadInfo(ids, maxTraceDepth), desc) + } + } + + def printThreadInfo(threadInfos: Seq[ThreadInfo]) = { + if (threadInfos.isEmpty) { + println("None") + } else { + for (ti ← threadInfos.sortBy(_.getThreadName)) { println(ti) } + } + } + + println("All threads:") + printThreadInfo(dumpAllThreads()) + + val (deadlockedThreads, deadlockDesc) = findDeadlockedThreads() + println(s"Deadlocks found for $deadlockDesc:") + printThreadInfo(deadlockedThreads) + } + +} + +/** + * Mixin for tests that should be watched by the Coroner. The `startCoroner` + * and `stopCoroner` methods should be called before and after the test runs. + * The Coroner will display its report if the test takes longer than the + * (dilated) `expectedTestDuration` to run. + */ +trait WatchedByCoroner { + self: TestKit ⇒ + + @volatile private var coronerWatch: Coroner.WatchHandle = _ + + final def startCoroner() { + coronerWatch = Coroner.watch(expectedTestDuration.dilated.fromNow, getClass.getName, System.err) + } + + final def stopCoroner() { + coronerWatch.cancel() + coronerWatch = null + } + + def expectedTestDuration: FiniteDuration +} \ No newline at end of file diff --git a/akka-testkit/src/test/scala/akka/testkit/CoronerSpec.scala b/akka-testkit/src/test/scala/akka/testkit/CoronerSpec.scala new file mode 100644 index 0000000000..fb0ffa9222 --- /dev/null +++ b/akka-testkit/src/test/scala/akka/testkit/CoronerSpec.scala @@ -0,0 +1,125 @@ +/** + * Copyright (C) 2009-2013 Typesafe Inc. + */ +package akka.testkit + +import java.io._ +import java.lang.management.ManagementFactory +import java.util.concurrent.Semaphore +import java.util.concurrent.locks.ReentrantLock +import org.scalatest.WordSpec +import org.scalatest.matchers.MustMatchers +import scala.concurrent.duration._ + +@org.junit.runner.RunWith(classOf[org.scalatest.junit.JUnitRunner]) +class CoronerSpec extends WordSpec with MustMatchers { + + private def captureOutput[A](f: PrintStream ⇒ A): (A, String) = { + val bytes = new ByteArrayOutputStream() + val out = new PrintStream(bytes) + val result = f(out) + (result, new String(bytes.toByteArray())) + } + + "A Coroner" must { + + "generate a report if enough time passes" in { + val (_, report) = captureOutput(out ⇒ { + val handle = Coroner.watch(500.milliseconds.fromNow, "XXXX", out) + Thread.sleep(1000.milliseconds.toMillis) + }) + report must include("Coroner's Report") + report must include("XXXX") + } + + "not generate a report if cancelled early" in { + val (_, report) = captureOutput(out ⇒ { + val coroner = Coroner.watch(500.milliseconds.fromNow, "XXXX", out) + coroner.cancel() + Thread.sleep(1000.milliseconds.toMillis) + }) + report must be("") + } + + "display deadlock information in its report" in { + + // Create two threads that each recursively synchronize on a list of + // objects. Give each thread the same objects, but in reversed order. + // Control execution of the threads so that they each hold an object + // that the other wants to synchronize on. BOOM! Deadlock. Generate a + // report, then clean up and check the report contents. + + case class LockingThread(name: String, thread: Thread, ready: Semaphore, proceed: Semaphore) + + def lockingThread(name: String, initialLocks: List[ReentrantLock]): LockingThread = { + val ready = new Semaphore(0) + val proceed = new Semaphore(0) + val t = new Thread(new Runnable { + def run = try recursiveLock(initialLocks) catch { case _: InterruptedException ⇒ () } + + def recursiveLock(locks: List[ReentrantLock]) { + locks match { + case Nil ⇒ () + case lock :: rest ⇒ { + ready.release() + proceed.acquire() + lock.lockInterruptibly() // Allows us to break deadlock and free threads + try { + recursiveLock(rest) + } finally { + lock.unlock() + } + } + } + } + }, name) + t.start() + LockingThread(name, t, ready, proceed) + } + + val x = new ReentrantLock() + val y = new ReentrantLock() + val a = lockingThread("deadlock-thread-a", List(x, y)) + val b = lockingThread("deadlock-thread-b", List(y, x)) + + // Walk threads into deadlock + a.ready.acquire() + b.ready.acquire() + a.proceed.release() + b.proceed.release() + a.ready.acquire() + b.ready.acquire() + a.proceed.release() + b.proceed.release() + + val (_, report) = captureOutput(Coroner.printReport("Deadlock test", _)) + + a.thread.interrupt() + b.thread.interrupt() + + report must include("Coroner's Report") + + // Split test based on JVM capabilities. Not all JVMs can detect + // deadlock between ReentrantLocks. However, we need to use + // ReentrantLocks because normal, monitor-based locks cannot be + // un-deadlocked once this test is finished. + + val threadMx = ManagementFactory.getThreadMXBean() + if (threadMx.isSynchronizerUsageSupported()) { + val sectionHeading = "Deadlocks found for monitors and ownable synchronizers" + report must include(sectionHeading) + val deadlockSection = report.split(sectionHeading)(1) + deadlockSection must include("deadlock-thread-a") + deadlockSection must include("deadlock-thread-b") + } else { + val sectionHeading = "Deadlocks found for monitors, but NOT ownable synchronizers" + report must include(sectionHeading) + val deadlockSection = report.split(sectionHeading)(1) + deadlockSection must include("None") + deadlockSection must not include ("deadlock-thread-a") + deadlockSection must not include ("deadlock-thread-b") + } + } + + } +} From b614b14803298aae5408c6156e62d4d3e5699b1c Mon Sep 17 00:00:00 2001 From: Patrik Nordwall Date: Fri, 8 Mar 2013 08:59:35 +0100 Subject: [PATCH 04/47] Increase within timeout in MBeanSpec, see #3133 * 5 seconds for detecting unreachable is not always enough --- akka-cluster/src/multi-jvm/scala/akka/cluster/MBeanSpec.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/akka-cluster/src/multi-jvm/scala/akka/cluster/MBeanSpec.scala b/akka-cluster/src/multi-jvm/scala/akka/cluster/MBeanSpec.scala index 5c3fd3d5f6..ad8c41124a 100644 --- a/akka-cluster/src/multi-jvm/scala/akka/cluster/MBeanSpec.scala +++ b/akka-cluster/src/multi-jvm/scala/akka/cluster/MBeanSpec.scala @@ -95,7 +95,7 @@ abstract class MBeanSpec enterBarrier("after-4") } - "support down" taggedAs LongRunningTest in { + "support down" taggedAs LongRunningTest in within(20 seconds) { val fourthAddress = address(fourth) runOn(first) { testConductor.shutdown(fourth, 0).await From e00c551550e6d342593bbdbb2dc3a59344bc1410 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Antonsson?= Date: Fri, 8 Mar 2013 09:05:51 +0100 Subject: [PATCH 05/47] TransitioinsSpec can't use address ordering to check who's leader. See #3129 --- .../scala/akka/cluster/TransitionSpec.scala | 25 ++++++++++--------- 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/akka-cluster/src/multi-jvm/scala/akka/cluster/TransitionSpec.scala b/akka-cluster/src/multi-jvm/scala/akka/cluster/TransitionSpec.scala index 602d242b63..9df1268b93 100644 --- a/akka-cluster/src/multi-jvm/scala/akka/cluster/TransitionSpec.scala +++ b/akka-cluster/src/multi-jvm/scala/akka/cluster/TransitionSpec.scala @@ -11,7 +11,6 @@ import akka.remote.testkit.MultiNodeConfig import akka.remote.testkit.MultiNodeSpec import akka.testkit._ import akka.actor.Address -import akka.pattern.ask import akka.remote.testconductor.RoleName import MemberStatus._ import InternalClusterAction._ @@ -138,14 +137,14 @@ abstract class TransitionSpec } enterBarrier("convergence-joining-2") - runOn(leader(first, second)) { + runOn(first) { leaderActions() awaitMemberStatus(first, Up) awaitMemberStatus(second, Joining) } enterBarrier("leader-actions-2") - leader(first, second) gossipTo nonLeader(first, second).head + first gossipTo second runOn(first, second) { // gossip chat will synchronize the views awaitMemberStatus(second, Up) @@ -187,7 +186,9 @@ abstract class TransitionSpec enterBarrier("convergence-joining-3") - runOn(leader(first, second, third)) { + val leader12 = leader(first, second) + val (other1, other2) = { val tmp = roles.filterNot(_ == leader12); (tmp.head, tmp.tail.head) } + runOn(leader12) { leaderActions() awaitMemberStatus(first, Up) awaitMemberStatus(second, Up) @@ -196,25 +197,25 @@ abstract class TransitionSpec enterBarrier("leader-actions-3") // leader gossipTo first non-leader - leader(first, second, third) gossipTo nonLeader(first, second, third).head - runOn(nonLeader(first, second, third).head) { + leader12 gossipTo other1 + runOn(other1) { awaitMemberStatus(third, Up) - awaitCond(seenLatestGossip == Set(leader(first, second, third), myself)) + awaitCond(seenLatestGossip == Set(leader12, myself)) } // first non-leader gossipTo the other non-leader - nonLeader(first, second, third).head gossipTo nonLeader(first, second, third).tail.head - runOn(nonLeader(first, second, third).head) { + other1 gossipTo other2 + runOn(other1) { // send gossip - cluster.clusterCore ! InternalClusterAction.SendGossipTo(nonLeader(first, second, third).tail.head) + cluster.clusterCore ! InternalClusterAction.SendGossipTo(other2) } - runOn(nonLeader(first, second, third).tail.head) { + runOn(other2) { awaitMemberStatus(third, Up) awaitCond(seenLatestGossip == Set(first, second, third)) } // first non-leader gossipTo the leader - nonLeader(first, second, third).head gossipTo leader(first, second, third) + other1 gossipTo leader12 runOn(first, second, third) { awaitMemberStatus(first, Up) awaitMemberStatus(second, Up) From 386bf87f0e3b162c3cb9a7818ff482e0a6848dc8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Antonsson?= Date: Thu, 7 Mar 2013 13:20:50 +0100 Subject: [PATCH 06/47] Don't increment vector-clock on merge and merge locally. See #3076 --- .../scala/akka/cluster/ClusterDaemon.scala | 131 +++++------------- .../src/main/scala/akka/cluster/Gossip.scala | 63 ++++----- .../test/scala/akka/cluster/GossipSpec.scala | 26 +--- 3 files changed, 64 insertions(+), 156 deletions(-) diff --git a/akka-cluster/src/main/scala/akka/cluster/ClusterDaemon.scala b/akka-cluster/src/main/scala/akka/cluster/ClusterDaemon.scala index 4ebf64c1c7..3ca4c343b7 100644 --- a/akka-cluster/src/main/scala/akka/cluster/ClusterDaemon.scala +++ b/akka-cluster/src/main/scala/akka/cluster/ClusterDaemon.scala @@ -289,7 +289,6 @@ private[cluster] final class ClusterCoreDaemon(publisher: ActorRef) extends Acto def initialized: Actor.Receive = { case msg: GossipEnvelope ⇒ receiveGossip(msg) - case msg: GossipMergeConflict ⇒ receiveGossipMerge(msg) case GossipTick ⇒ gossip() case ReapUnreachableTick ⇒ reapUnreachableMembers() case LeaderActionsTick ⇒ leaderActions() @@ -506,35 +505,6 @@ private[cluster] final class ClusterCoreDaemon(publisher: ActorRef) extends Acto publish(latestGossip) } - /** - * When conflicting versions of received and local [[akka.cluster.Gossip]] is detected - * it's forwarded to the leader for conflict resolution. Trying to simultaneously - * resolving conflicts at several nodes creates new conflicts. Therefore the leader resolves - * conflicts to limit divergence. To avoid overload there is also a configurable rate - * limit of how many conflicts that are handled by second. If the limit is - * exceeded the conflicting gossip messages are dropped and will reappear later. - */ - def receiveGossipMerge(merge: GossipMergeConflict): Unit = { - stats = stats.incrementMergeConflictCount - val rate = mergeRate(stats.mergeConflictCount) - if (rate <= MaxGossipMergeRate) { - receiveGossip(merge.a.copy(conversation = false)) - receiveGossip(merge.b.copy(conversation = false)) - - // use one-way gossip from leader to reduce load of leader - def sendBack(to: Address): Unit = { - if (to != selfAddress && !latestGossip.overview.unreachable.exists(_.address == to)) - oneWayGossipTo(to) - } - - sendBack(merge.a.from) - sendBack(merge.b.from) - - } else { - log.debug("Dropping gossip merge conflict due to rate [{}] / s ", rate) - } - } - /** * Receive new gossip. */ @@ -548,70 +518,47 @@ private[cluster] final class ClusterCoreDaemon(publisher: ActorRef) extends Acto } else if (localGossip.overview.isNonDownUnreachable(from)) { log.debug("Ignoring received gossip from unreachable [{}] ", from) } else { - - // leader handles merge conflicts, or when they have different views of how is leader - val handleMerge = localGossip.leader == Some(selfAddress) || localGossip.leader != remoteGossip.leader val comparison = remoteGossip.version tryCompareTo localGossip.version val conflict = comparison.isEmpty - if (conflict && !handleMerge) { - // delegate merge resolution to leader to reduce number of simultaneous resolves, - // which will result in new conflicts + val (winningGossip, talkback, newStats) = comparison match { + case None ⇒ + // conflicting versions, merge + (remoteGossip merge localGossip, true, stats.incrementMergeCount) + case Some(0) ⇒ + // same version + (remoteGossip mergeSeen localGossip, !remoteGossip.seenByAddress(selfAddress), stats.incrementSameCount) + case Some(x) if x < 0 ⇒ + // local is newer + (localGossip, true, stats.incrementNewerCount) + case _ ⇒ + // remote is newer + (remoteGossip, !remoteGossip.seenByAddress(selfAddress), stats.incrementOlderCount) + } - stats = stats.incrementMergeDetectedCount - log.debug("Merge conflict [{}] detected [{}] <> [{}]", stats.mergeDetectedCount, selfAddress, from) + stats = newStats + latestGossip = winningGossip seen selfAddress - stats = stats.incrementMergeConflictCount - val rate = mergeRate(stats.mergeConflictCount) + // for all new joining nodes we remove them from the failure detector + (latestGossip.members -- localGossip.members).foreach { + node ⇒ if (node.status == Joining) failureDetector.remove(node.address) + } - if (rate <= MaxGossipMergeRate) - localGossip.leader foreach { clusterCore(_) ! GossipMergeConflict(GossipEnvelope(selfAddress, localGossip), envelope) } - else - log.debug("Skipping gossip merge conflict due to rate [{}] / s ", rate) + log.debug("Cluster Node [{}] - Receiving gossip from [{}]", selfAddress, from) - } else { + if (conflict) { + log.debug( + """Couldn't establish a causal relationship between "remote" gossip and "local" gossip - Remote[{}] - Local[{}] - merged them into [{}]""", + remoteGossip, localGossip, winningGossip) + } - val (winningGossip, talkback, newStats) = comparison match { - case None ⇒ - // conflicting versions, merge, and new version - ((remoteGossip merge localGossip) :+ vclockNode, true, stats) - case Some(0) ⇒ - // same version - // TODO optimize talkback based on how the merged seen differs - (remoteGossip mergeSeen localGossip, !remoteGossip.hasSeen(selfAddress), stats.incrementSameCount) - case Some(x) if x < 0 ⇒ - // local is newer - (localGossip, true, stats.incrementNewerCount) - case _ ⇒ - // remote is newer - (remoteGossip, !remoteGossip.hasSeen(selfAddress), stats.incrementOlderCount) - } + stats = stats.incrementReceivedGossipCount + publish(latestGossip) - stats = newStats - latestGossip = winningGossip seen selfAddress - - // for all new joining nodes we remove them from the failure detector - (latestGossip.members -- localGossip.members).foreach { - node ⇒ if (node.status == Joining) failureDetector.remove(node.address) - } - - log.debug("Cluster Node [{}] - Receiving gossip from [{}]", selfAddress, from) - - if (conflict) { - stats = stats.incrementMergeCount - log.debug( - """Couldn't establish a causal relationship between "remote" gossip and "local" gossip - Remote[{}] - Local[{}] - merged them into [{}]""", - remoteGossip, localGossip, winningGossip) - } - - stats = stats.incrementReceivedGossipCount - publish(latestGossip) - - if (envelope.conversation && talkback) { - // send back gossip to sender when sender had different view, i.e. merge, or sender had - // older or sender had newer - gossipTo(from) - } + if (envelope.conversation && talkback) { + // send back gossip to sender when sender had different view, i.e. merge, or sender had + // older or sender had newer + gossipTo(from) } } } @@ -622,8 +569,6 @@ private[cluster] final class ClusterCoreDaemon(publisher: ActorRef) extends Acto * Initiates a new round of gossip. */ def gossip(): Unit = { - stats = stats.copy(mergeConflictCount = 0) - log.debug("Cluster Node [{}] - Initiating new round of gossip", selfAddress) if (!isSingletonCluster && isAvailable) { @@ -1049,9 +994,7 @@ private[cluster] class OnMemberUpListener(callback: Runnable) extends Actor with */ private[cluster] case class ClusterStats( receivedGossipCount: Long = 0L, - mergeConflictCount: Long = 0L, mergeCount: Long = 0L, - mergeDetectedCount: Long = 0L, sameCount: Long = 0L, newerCount: Long = 0L, olderCount: Long = 0L) { @@ -1059,15 +1002,9 @@ private[cluster] case class ClusterStats( def incrementReceivedGossipCount(): ClusterStats = copy(receivedGossipCount = receivedGossipCount + 1) - def incrementMergeConflictCount(): ClusterStats = - copy(mergeConflictCount = mergeConflictCount + 1) - def incrementMergeCount(): ClusterStats = copy(mergeCount = mergeCount + 1) - def incrementMergeDetectedCount(): ClusterStats = - copy(mergeDetectedCount = mergeDetectedCount + 1) - def incrementSameCount(): ClusterStats = copy(sameCount = sameCount + 1) @@ -1080,9 +1017,7 @@ private[cluster] case class ClusterStats( def :+(that: ClusterStats): ClusterStats = { ClusterStats( this.receivedGossipCount + that.receivedGossipCount, - this.mergeConflictCount + that.mergeConflictCount, this.mergeCount + that.mergeCount, - this.mergeDetectedCount + that.mergeDetectedCount, this.sameCount + that.sameCount, this.newerCount + that.newerCount, this.olderCount + that.olderCount) @@ -1091,9 +1026,7 @@ private[cluster] case class ClusterStats( def :-(that: ClusterStats): ClusterStats = { ClusterStats( this.receivedGossipCount - that.receivedGossipCount, - this.mergeConflictCount - that.mergeConflictCount, this.mergeCount - that.mergeCount, - this.mergeDetectedCount - that.mergeDetectedCount, this.sameCount - that.sameCount, this.newerCount - that.newerCount, this.olderCount - that.olderCount) diff --git a/akka-cluster/src/main/scala/akka/cluster/Gossip.scala b/akka-cluster/src/main/scala/akka/cluster/Gossip.scala index 46b250101d..23fc6c8885 100644 --- a/akka-cluster/src/main/scala/akka/cluster/Gossip.scala +++ b/akka-cluster/src/main/scala/akka/cluster/Gossip.scala @@ -43,7 +43,7 @@ private[cluster] object Gossip { * When a `Gossip` is received the version (vector clock) is used to determine if the * received `Gossip` is newer or older than the current local `Gossip`. The received `Gossip` * and local `Gossip` is merged in case of conflicting version, i.e. vector clocks without - * same history. When merged the seen table is cleared. + * same history. * * When a node is told by the user to leave the cluster the leader will move it to `Leaving` * and then rebalance and repartition the cluster and start hand-off by migrating the actors @@ -100,7 +100,7 @@ private[cluster] case class Gossip( * Map with the VectorClock (version) for the new gossip. */ def seen(address: Address): Gossip = { - if (hasSeen(address)) this + if (seenByAddress(address)) this else this copy (overview = overview copy (seen = overview.seen + (address -> version))) } @@ -116,24 +116,29 @@ private[cluster] case class Gossip( /** * Has this Gossip been seen by this address. */ - def hasSeen(address: Address): Boolean = { + def seenByAddress(address: Address): Boolean = { overview.seen.get(address).exists(_ == version) } + private def mergeSeenTables(allowed: immutable.Set[Member], one: Map[Address, VectorClock], another: Map[Address, VectorClock]): Map[Address, VectorClock] = { + (one.filter { case (a, v) ⇒ allowed.exists(_.address == a) } /: another) { + case (merged, (address, oneVersion)) ⇒ + if (allowed.exists(_.address == address)) { + val anotherVersion = merged.getOrElse(address, oneVersion) + anotherVersion tryCompareTo oneVersion match { + case None ⇒ merged - address + case Some(x) if x > 0 ⇒ merged + (address -> anotherVersion) + case _ ⇒ merged + (address -> oneVersion) + } + } else merged + } + } + /** * Merges the seen table of two Gossip instances. */ - def mergeSeen(that: Gossip): Gossip = { - val mergedSeen = (overview.seen /: that.overview.seen) { - case (merged, (address, version)) ⇒ - val curr = merged.getOrElse(address, version) - if (curr > version) - merged + (address -> curr) - else - merged + (address -> version) - } - this copy (overview = overview copy (seen = mergedSeen)) - } + def mergeSeen(that: Gossip): Gossip = + this copy (overview = overview copy (seen = mergeSeenTables(members, overview.seen, that.overview.seen))) /** * Merges two Gossip instances including membership tables, and the VectorClock histories. @@ -151,8 +156,8 @@ private[cluster] case class Gossip( // and exclude unreachable val mergedMembers = Gossip.emptyMembers ++ Member.pickHighestPriority(this.members, that.members).filterNot(mergedUnreachable.contains) - // 4. fresh seen table - val mergedSeen = Map.empty[Address, VectorClock] + // 4. merge seen table + val mergedSeen = mergeSeenTables(mergedMembers, overview.seen, that.overview.seen) Gossip(mergedMembers, GossipOverview(mergedSeen, mergedUnreachable), mergedVClock) } @@ -171,22 +176,12 @@ private[cluster] case class Gossip( // 1. we don't have any members that are unreachable, or // 2. all unreachable members in the set have status DOWN // Else we can't continue to check for convergence - // When that is done we check that all the entries in the 'seen' table have the same vector clock version - // and that all members exists in seen table + // When that is done we check that all members exists in the seen table and + // have the latest vector clock version val hasUnreachable = unreachable.nonEmpty && unreachable.exists { _.status != Down } - def allMembersInSeen = members.forall(m ⇒ seen.contains(m.address)) + def allMembersInSeenHasLatest = members.forall(m ⇒ seen.get(m.address).exists(_ == version)) - def seenSame: Boolean = - if (seen.isEmpty) { - // if both seen and members are empty, then every(no)body has seen the same thing - members.isEmpty - } else { - val values = seen.values - val seenHead = values.head - values.forall(_ == seenHead) - } - - !hasUnreachable && allMembersInSeen && seenSame + !hasUnreachable && allMembersInSeenHasLatest } def isLeader(address: Address): Boolean = leader == Some(address) @@ -240,11 +235,3 @@ private[cluster] case class GossipOverview( * Envelope adding a sender address to the gossip. */ private[cluster] case class GossipEnvelope(from: Address, gossip: Gossip, conversation: Boolean = true) extends ClusterMessage - -/** - * INTERNAL API - * When conflicting versions of received and local [[akka.cluster.Gossip]] is detected - * it's forwarded to the leader for conflict resolution. - */ -private[cluster] case class GossipMergeConflict(a: GossipEnvelope, b: GossipEnvelope) extends ClusterMessage - diff --git a/akka-cluster/src/test/scala/akka/cluster/GossipSpec.scala b/akka-cluster/src/test/scala/akka/cluster/GossipSpec.scala index 2978ba6373..9b677141d3 100644 --- a/akka-cluster/src/test/scala/akka/cluster/GossipSpec.scala +++ b/akka-cluster/src/test/scala/akka/cluster/GossipSpec.scala @@ -79,18 +79,6 @@ class GossipSpec extends WordSpec with MustMatchers { } - "start with fresh seen table after merge" in { - val g1 = Gossip(members = SortedSet(a1, e1)).seen(a1.address).seen(e1.address) - val g2 = Gossip(members = SortedSet(a2, e2)).seen(a2.address).seen(e2.address) - - val merged1 = g1 merge g2 - merged1.overview.seen.isEmpty must be(true) - - val merged2 = g2 merge g1 - merged2.overview.seen.isEmpty must be(true) - - } - "not have node in both members and unreachable" in intercept[IllegalArgumentException] { Gossip(members = SortedSet(a1, b1), overview = GossipOverview(unreachable = Set(b2))) } @@ -121,17 +109,17 @@ class GossipSpec extends WordSpec with MustMatchers { keys.length must be(4) keys.toSet must be(Set(a1.address, b1.address, c1.address, d1.address)) - merged hasSeen (a1.address) must be(true) - merged hasSeen (b1.address) must be(false) - merged hasSeen (c1.address) must be(true) - merged hasSeen (d1.address) must be(true) - merged hasSeen (e1.address) must be(false) + merged seenByAddress (a1.address) must be(true) + merged seenByAddress (b1.address) must be(false) + merged seenByAddress (c1.address) must be(true) + merged seenByAddress (d1.address) must be(true) + merged seenByAddress (e1.address) must be(false) merged.overview.seen(b1.address) must be(g1.version) } - checkMerged(g3 mergeSeen g2) - checkMerged(g2 mergeSeen g3) + checkMerged(g3 merge g2) + checkMerged(g2 merge g3) } } } From 01bfb9378e79f8381d3ee079dd0b8d7b356b5940 Mon Sep 17 00:00:00 2001 From: Patrik Nordwall Date: Fri, 8 Mar 2013 15:47:03 +0100 Subject: [PATCH 07/47] Logging of joining --- akka-cluster/src/main/scala/akka/cluster/ClusterDaemon.scala | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/akka-cluster/src/main/scala/akka/cluster/ClusterDaemon.scala b/akka-cluster/src/main/scala/akka/cluster/ClusterDaemon.scala index 4ebf64c1c7..d35c77a19a 100644 --- a/akka-cluster/src/main/scala/akka/cluster/ClusterDaemon.scala +++ b/akka-cluster/src/main/scala/akka/cluster/ClusterDaemon.scala @@ -405,8 +405,7 @@ private[cluster] final class ClusterCoreDaemon(publisher: ActorRef) extends Acto latestGossip = seenVersionedGossip - log.debug("Cluster Node [{}] - Node [{}] is JOINING", selfAddress, node) - // treat join as initial heartbeat, so that it becomes unavailable if nothing more happens + log.info("Cluster Node [{}] - Node [{}] is JOINING", selfAddress, node) if (node != selfAddress) { gossipTo(node) } From 63b8c33483ab07ce77b4e6f5c376c0e3678e489b Mon Sep 17 00:00:00 2001 From: Viktor Klang Date: Mon, 11 Mar 2013 11:39:07 +0100 Subject: [PATCH 08/47] #3114 - Replace props on ActorCell termination to avoid leakage if one holds onto LocalActorRefs. --- .../actor/LocalActorRefProviderSpec.scala | 18 ++++++++ .../src/main/scala/akka/actor/ActorCell.scala | 45 ++++++++++--------- .../akka/actor/dungeon/FaultHandling.scala | 7 +-- .../src/main/scala/akka/routing/Routing.scala | 4 +- 4 files changed, 46 insertions(+), 28 deletions(-) diff --git a/akka-actor-tests/src/test/scala/akka/actor/LocalActorRefProviderSpec.scala b/akka-actor-tests/src/test/scala/akka/actor/LocalActorRefProviderSpec.scala index 314739e78e..8fc765f9d8 100644 --- a/akka-actor-tests/src/test/scala/akka/actor/LocalActorRefProviderSpec.scala +++ b/akka-actor-tests/src/test/scala/akka/actor/LocalActorRefProviderSpec.scala @@ -57,6 +57,24 @@ class LocalActorRefProviderSpec extends AkkaSpec(LocalActorRefProviderSpec.confi } + "A LocalActorRef's ActorCell" must { + "not retain its original Props when terminated" in { + val GetChild = "GetChild" + val a = watch(system.actorOf(Props(new Actor { + val child = context.actorOf(Props.empty) + def receive = { case `GetChild` ⇒ sender ! child } + }))) + a.tell(GetChild, testActor) + val child = expectMsgType[ActorRef] + child.asInstanceOf[LocalActorRef].underlying.props must be theSameInstanceAs Props.empty + system stop a + expectMsgType[Terminated] + val childProps = child.asInstanceOf[LocalActorRef].underlying.props + childProps must not be theSameInstanceAs(Props.empty) + childProps must be theSameInstanceAs ActorCell.terminatedProps + } + } + "An ActorRefFactory" must { implicit val ec = system.dispatcher "only create one instance of an actor with a specific address in a concurrent environment" in { diff --git a/akka-actor/src/main/scala/akka/actor/ActorCell.scala b/akka-actor/src/main/scala/akka/actor/ActorCell.scala index a48d5c4c45..d7d0338bd2 100644 --- a/akka-actor/src/main/scala/akka/actor/ActorCell.scala +++ b/akka-actor/src/main/scala/akka/actor/ActorCell.scala @@ -304,6 +304,8 @@ private[akka] object ActorCell { final val emptyBehaviorStack: List[Actor.Receive] = Nil final val emptyActorRefSet: Set[ActorRef] = immutable.TreeSet.empty + + final val terminatedProps: Props = Props(() ⇒ throw new IllegalActorStateException("This Actor has been terminated")) } //ACTORCELL IS 64bytes and should stay that way unless very good reason not to (machine sympathy, cache line fit) @@ -521,36 +523,35 @@ private[akka] class ActorCell( case _ ⇒ } + @tailrec private final def lookupAndSetField(clazz: Class[_], instance: AnyRef, name: String, value: Any): Boolean = { + if (try { + val field = clazz.getDeclaredField(name) + field.setAccessible(true) + field.set(instance, value) + true + } catch { + case e: NoSuchFieldException ⇒ false + }) true + else if (clazz.getSuperclass eq null) false + else lookupAndSetField(clazz.getSuperclass, instance, name, value) + } + + final protected def clearActorCellFields(cell: ActorCell): Unit = + if (!lookupAndSetField(cell.getClass, cell, "props", ActorCell.terminatedProps)) + throw new IllegalArgumentException("ActorCell has no props field") + final protected def clearActorFields(actorInstance: Actor): Unit = { setActorFields(actorInstance, context = null, self = system.deadLetters) currentMessage = null behaviorStack = emptyBehaviorStack } - final protected def setActorFields(actorInstance: Actor, context: ActorContext, self: ActorRef) { - @tailrec - def lookupAndSetField(clazz: Class[_], actor: Actor, name: String, value: Any): Boolean = { - val success = try { - val field = clazz.getDeclaredField(name) - field.setAccessible(true) - field.set(actor, value) - true - } catch { - case e: NoSuchFieldException ⇒ false - } - - if (success) true - else { - val parent: Class[_] = clazz.getSuperclass - if (parent eq null) throw new IllegalActorStateException(actorInstance.getClass + " is not an Actor since it have not mixed in the 'Actor' trait") - lookupAndSetField(parent, actor, name, value) - } - } + final protected def setActorFields(actorInstance: Actor, context: ActorContext, self: ActorRef): Unit = if (actorInstance ne null) { - lookupAndSetField(actorInstance.getClass, actorInstance, "context", context) - lookupAndSetField(actorInstance.getClass, actorInstance, "self", self) + if (!lookupAndSetField(actorInstance.getClass, actorInstance, "context", context) + || !lookupAndSetField(actorInstance.getClass, actorInstance, "self", self)) + throw new IllegalActorStateException(actorInstance.getClass + " is not an Actor since it have not mixed in the 'Actor' trait") } - } // logging is not the main purpose, and if it fails there’s nothing we can do protected final def publish(e: LogEvent): Unit = try system.eventStream.publish(e) catch { case NonFatal(_) ⇒ } diff --git a/akka-actor/src/main/scala/akka/actor/dungeon/FaultHandling.scala b/akka-actor/src/main/scala/akka/actor/dungeon/FaultHandling.scala index 053765296a..78100eae1a 100644 --- a/akka-actor/src/main/scala/akka/actor/dungeon/FaultHandling.scala +++ b/akka-actor/src/main/scala/akka/actor/dungeon/FaultHandling.scala @@ -195,9 +195,8 @@ private[akka] trait FaultHandling { this: ActorCell ⇒ * specific order. */ try if (a ne null) a.postStop() - catch handleNonFatalOrInterruptedException { e ⇒ - publish(Error(e, self.path.toString, clazz(a), e.getMessage)) - } finally try dispatcher.detach(this) + catch handleNonFatalOrInterruptedException { e ⇒ publish(Error(e, self.path.toString, clazz(a), e.getMessage)) } + finally try dispatcher.detach(this) finally try parent.sendSystemMessage(ChildTerminated(self)) finally try parent ! NullMessage // read ScalaDoc of NullMessage to see why finally try tellWatchersWeDied(a) @@ -205,7 +204,9 @@ private[akka] trait FaultHandling { this: ActorCell ⇒ finally { if (system.settings.DebugLifecycle) publish(Debug(self.path.toString, clazz(a), "stopped")) + clearActorFields(a) + clearActorCellFields(this) actor = null } } diff --git a/akka-actor/src/main/scala/akka/routing/Routing.scala b/akka-actor/src/main/scala/akka/routing/Routing.scala index 2d2a84c444..f09168ed73 100644 --- a/akka-actor/src/main/scala/akka/routing/Routing.scala +++ b/akka-actor/src/main/scala/akka/routing/Routing.scala @@ -33,9 +33,7 @@ private[akka] class RoutedActorRef(_system: ActorSystemImpl, _props: Props, _sup throw new ConfigurationException( "Configuration for " + this + " is invalid - you can not use a 'BalancingDispatcher' as a Router's dispatcher, you can however use it for the routees.") - } - - _props.routerConfig.verifyConfig() + } else _props.routerConfig.verifyConfig() override def newCell(old: UnstartedCell): Cell = new RoutedActorCell(system, this, props, supervisor).init(old.uid, sendSupervise = false) From 7ed6b3d4eeaef1f08557f2fa00397f252a24497d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Antonsson?= Date: Fri, 8 Mar 2013 13:26:50 +0100 Subject: [PATCH 09/47] Fixes according review. See #3076 --- .../src/main/resources/reference.conf | 4 --- .../scala/akka/cluster/ClusterDaemon.scala | 4 +-- .../scala/akka/cluster/ClusterSettings.scala | 1 - .../src/main/scala/akka/cluster/Gossip.scala | 33 +++++++++---------- .../scala/akka/cluster/StressSpec.scala | 2 +- .../akka/cluster/ClusterConfigSpec.scala | 1 - 6 files changed, 19 insertions(+), 26 deletions(-) diff --git a/akka-cluster/src/main/resources/reference.conf b/akka-cluster/src/main/resources/reference.conf index 5458285245..b2126409c1 100644 --- a/akka-cluster/src/main/resources/reference.conf +++ b/akka-cluster/src/main/resources/reference.conf @@ -65,10 +65,6 @@ akka { # Probability value is between 0.0 and 1.0. 0.0 means never, 1.0 means always. gossip-different-view-probability = 0.8 - # Limit number of merge conflicts per second that are handled. If the limit is - # exceeded the conflicting gossip messages are dropped and will reappear later. - max-gossip-merge-rate = 5.0 - # Settings for the Phi accrual failure detector (http://ddg.jaist.ac.jp/pub/HDY+04.pdf # [Hayashibara et al]) used by the cluster subsystem to detect unreachable members. failure-detector { diff --git a/akka-cluster/src/main/scala/akka/cluster/ClusterDaemon.scala b/akka-cluster/src/main/scala/akka/cluster/ClusterDaemon.scala index 3ca4c343b7..21e423c974 100644 --- a/akka-cluster/src/main/scala/akka/cluster/ClusterDaemon.scala +++ b/akka-cluster/src/main/scala/akka/cluster/ClusterDaemon.scala @@ -540,8 +540,8 @@ private[cluster] final class ClusterCoreDaemon(publisher: ActorRef) extends Acto latestGossip = winningGossip seen selfAddress // for all new joining nodes we remove them from the failure detector - (latestGossip.members -- localGossip.members).foreach { - node ⇒ if (node.status == Joining) failureDetector.remove(node.address) + latestGossip.members foreach { + node ⇒ if (node.status == Joining && !localGossip.members(node)) failureDetector.remove(node.address) } log.debug("Cluster Node [{}] - Receiving gossip from [{}]", selfAddress, from) diff --git a/akka-cluster/src/main/scala/akka/cluster/ClusterSettings.scala b/akka-cluster/src/main/scala/akka/cluster/ClusterSettings.scala index e4b36d6f5a..27bde94a7f 100644 --- a/akka-cluster/src/main/scala/akka/cluster/ClusterSettings.scala +++ b/akka-cluster/src/main/scala/akka/cluster/ClusterSettings.scala @@ -59,7 +59,6 @@ class ClusterSettings(val config: Config, val systemName: String) { case id ⇒ id } final val GossipDifferentViewProbability: Double = cc.getDouble("gossip-different-view-probability") - final val MaxGossipMergeRate: Double = cc.getDouble("max-gossip-merge-rate") final val SchedulerTickDuration: FiniteDuration = Duration(cc.getMilliseconds("scheduler.tick-duration"), MILLISECONDS) final val SchedulerTicksPerWheel: Int = cc.getInt("scheduler.ticks-per-wheel") final val MetricsEnabled: Boolean = cc.getBoolean("metrics.enabled") diff --git a/akka-cluster/src/main/scala/akka/cluster/Gossip.scala b/akka-cluster/src/main/scala/akka/cluster/Gossip.scala index 23fc6c8885..2d3ba52570 100644 --- a/akka-cluster/src/main/scala/akka/cluster/Gossip.scala +++ b/akka-cluster/src/main/scala/akka/cluster/Gossip.scala @@ -120,17 +120,21 @@ private[cluster] case class Gossip( overview.seen.get(address).exists(_ == version) } - private def mergeSeenTables(allowed: immutable.Set[Member], one: Map[Address, VectorClock], another: Map[Address, VectorClock]): Map[Address, VectorClock] = { - (one.filter { case (a, v) ⇒ allowed.exists(_.address == a) } /: another) { - case (merged, (address, oneVersion)) ⇒ - if (allowed.exists(_.address == address)) { - val anotherVersion = merged.getOrElse(address, oneVersion) - anotherVersion tryCompareTo oneVersion match { - case None ⇒ merged - address - case Some(x) if x > 0 ⇒ merged + (address -> anotherVersion) - case _ ⇒ merged + (address -> oneVersion) - } - } else merged + private def mergeSeenTables(allowed: Set[Member], one: Map[Address, VectorClock], another: Map[Address, VectorClock]): Map[Address, VectorClock] = { + (Map.empty[Address, VectorClock] /: allowed) { + (merged, member) ⇒ + val address = member.address + (one.get(address), another.get(address)) match { + case (None, None) ⇒ merged + case (Some(v1), None) ⇒ merged.updated(address, v1) + case (None, Some(v2)) ⇒ merged.updated(address, v2) + case (Some(v1), Some(v2)) ⇒ + v1 tryCompareTo v2 match { + case None ⇒ merged + case Some(x) if x > 0 ⇒ merged.updated(address, v1) + case _ ⇒ merged.updated(address, v2) + } + } } } @@ -169,19 +173,14 @@ private[cluster] case class Gossip( * @return true if convergence have been reached and false if not */ def convergence: Boolean = { - val unreachable = overview.unreachable - val seen = overview.seen - // First check that: // 1. we don't have any members that are unreachable, or // 2. all unreachable members in the set have status DOWN // Else we can't continue to check for convergence // When that is done we check that all members exists in the seen table and // have the latest vector clock version - val hasUnreachable = unreachable.nonEmpty && unreachable.exists { _.status != Down } - def allMembersInSeenHasLatest = members.forall(m ⇒ seen.get(m.address).exists(_ == version)) - !hasUnreachable && allMembersInSeenHasLatest + overview.unreachable.forall(_.status == Down) && members.forall(m ⇒ seenByAddress(m.address)) } def isLeader(address: Address): Boolean = leader == Some(address) diff --git a/akka-cluster/src/multi-jvm/scala/akka/cluster/StressSpec.scala b/akka-cluster/src/multi-jvm/scala/akka/cluster/StressSpec.scala index c0902eeffd..ec851b1594 100644 --- a/akka-cluster/src/multi-jvm/scala/akka/cluster/StressSpec.scala +++ b/akka-cluster/src/multi-jvm/scala/akka/cluster/StressSpec.scala @@ -84,7 +84,7 @@ object StressMultiJvmSpec extends MultiNodeConfig { normal-throughput-duration = 30s high-throughput-duration = 10s supervision-duration = 10s - supervision-one-iteration = 1s + supervision-one-iteration = 2.5s expected-test-duration = 600s # actors are created in a tree structure defined # by tree-width (number of children for each actor) and diff --git a/akka-cluster/src/test/scala/akka/cluster/ClusterConfigSpec.scala b/akka-cluster/src/test/scala/akka/cluster/ClusterConfigSpec.scala index 39e273d345..9cdb47aeb2 100644 --- a/akka-cluster/src/test/scala/akka/cluster/ClusterConfigSpec.scala +++ b/akka-cluster/src/test/scala/akka/cluster/ClusterConfigSpec.scala @@ -42,7 +42,6 @@ class ClusterConfigSpec extends AkkaSpec { JmxEnabled must be(true) UseDispatcher must be(Dispatchers.DefaultDispatcherId) GossipDifferentViewProbability must be(0.8 plusOrMinus 0.0001) - MaxGossipMergeRate must be(5.0 plusOrMinus 0.0001) SchedulerTickDuration must be(33 millis) SchedulerTicksPerWheel must be(512) MetricsEnabled must be(true) From d98a7ef1e878827cb4075ddbb2c6efcfd8da1e9d Mon Sep 17 00:00:00 2001 From: Patrik Nordwall Date: Fri, 8 Mar 2013 09:39:48 +0100 Subject: [PATCH 10/47] Cluster singleton failure due to down-removed, see #3130 * The scenario was that previous leader left. * The problem was that the new leader got MemberRemoved before it got the HandOverDone and therefore missed the hand over data. * Solved by not changing the singleton to leader when receiving MemberRemoved and instead do that on normal HandOverDone or in failure cases after retry timeout. * The reason for this bug was the new transition from Down to Removed and that there is now no MemberDowned event. Previously this was only triggered by MemberDowned (not MemberRemoved) and that was safe because that was "always" preceeded by unreachable. * The new solution means that it will take longer for new singleton to startup in case of unreachable previous leader, but I don't want to trigger it on MemberUnreachable because it might in the future be possible to switch it back to reachable. --- .../scala/akka/cluster/ClusterDaemon.scala | 21 ++++++++++++------- .../pattern/ClusterSingletonManager.scala | 6 +++--- .../pattern/ClusterSingletonManagerSpec.scala | 6 +++--- 3 files changed, 19 insertions(+), 14 deletions(-) diff --git a/akka-cluster/src/main/scala/akka/cluster/ClusterDaemon.scala b/akka-cluster/src/main/scala/akka/cluster/ClusterDaemon.scala index d35c77a19a..a779f4a2fc 100644 --- a/akka-cluster/src/main/scala/akka/cluster/ClusterDaemon.scala +++ b/akka-cluster/src/main/scala/akka/cluster/ClusterDaemon.scala @@ -428,7 +428,7 @@ private[cluster] final class ClusterCoreDaemon(publisher: ActorRef) extends Acto latestGossip = seenVersionedGossip - log.info("Cluster Node [{}] - Marked address [{}] as LEAVING", selfAddress, address) + log.info("Cluster Node [{}] - Marked address [{}] as [{}]", selfAddress, address, Leaving) publish(latestGossip) } } @@ -437,7 +437,7 @@ private[cluster] final class ClusterCoreDaemon(publisher: ActorRef) extends Acto * State transition to EXITING. */ def exiting(address: Address): Unit = { - log.info("Cluster Node [{}] - Marked node [{}] as EXITING", selfAddress, address) + log.info("Cluster Node [{}] - Marked node [{}] as [{}]", selfAddress, address, Exiting) // FIXME implement when we implement hand-off } @@ -475,7 +475,7 @@ private[cluster] final class ClusterCoreDaemon(publisher: ActorRef) extends Acto val newMembers = downedMember match { case Some(m) ⇒ - log.info("Cluster Node [{}] - Marking node [{}] as DOWN", selfAddress, m.address) + log.info("Cluster Node [{}] - Marking node [{}] as [{}]", selfAddress, m.address, Down) localMembers - m case None ⇒ localMembers } @@ -485,7 +485,7 @@ private[cluster] final class ClusterCoreDaemon(publisher: ActorRef) extends Acto localUnreachableMembers.map { member ⇒ // no need to DOWN members already DOWN if (member.address == address && member.status != Down) { - log.info("Cluster Node [{}] - Marking unreachable node [{}] as DOWN", selfAddress, member.address) + log.info("Cluster Node [{}] - Marking unreachable node [{}] as [{}]", selfAddress, member.address, Down) member copy (status = Down) } else member } @@ -786,25 +786,30 @@ private[cluster] final class ClusterCoreDaemon(publisher: ActorRef) extends Acto // ---------------------- // log the move of members from joining to up - upMembers foreach { member ⇒ log.info("Cluster Node [{}] - Leader is moving node [{}] from JOINING to UP", selfAddress, member.address) } + upMembers foreach { member ⇒ + log.info("Cluster Node [{}] - Leader is moving node [{}] from [{}] to [{}]", + selfAddress, member.address, member.status, Up) + } // tell all removed members to remove and shut down themselves removedMembers foreach { member ⇒ val address = member.address - log.info("Cluster Node [{}] - Leader is moving node [{}] from EXITING to REMOVED - and removing node from node ring", selfAddress, address) + log.info("Cluster Node [{}] - Leader is moving node [{}] from [{}] to [{}] - and removing node from node ring", + selfAddress, address, member.status, Removed) clusterCore(address) ! ClusterLeaderAction.Remove(address) } // tell all exiting members to exit exitingMembers foreach { member ⇒ val address = member.address - log.info("Cluster Node [{}] - Leader is moving node [{}] from LEAVING to EXITING", selfAddress, address) + log.info("Cluster Node [{}] - Leader is moving node [{}] from [{}] to [{}]", + selfAddress, address, member.status, Exiting) clusterCore(address) ! ClusterLeaderAction.Exit(address) // FIXME should use ? to await completion of handoff? } // log the auto-downing of the unreachable nodes unreachableButNotDownedMembers foreach { member ⇒ - log.info("Cluster Node [{}] - Leader is marking unreachable node [{}] as DOWN", selfAddress, member.address) + log.info("Cluster Node [{}] - Leader is marking unreachable node [{}] as [{}]", selfAddress, member.address, Down) } publish(latestGossip) diff --git a/akka-contrib/src/main/scala/akka/contrib/pattern/ClusterSingletonManager.scala b/akka-contrib/src/main/scala/akka/contrib/pattern/ClusterSingletonManager.scala index ebb57512c1..be5dfa4717 100644 --- a/akka-contrib/src/main/scala/akka/contrib/pattern/ClusterSingletonManager.scala +++ b/akka-contrib/src/main/scala/akka/contrib/pattern/ClusterSingletonManager.scala @@ -422,7 +422,7 @@ class ClusterSingletonManager( case Event(MemberRemoved(m), BecomingLeaderData(Some(previousLeader))) if m.address == previousLeader ⇒ logInfo("Previous leader [{}] removed", previousLeader) addRemoved(m.address) - gotoLeader(None) + stay case Event(TakeOverFromMe, BecomingLeaderData(None)) ⇒ sender ! HandOverToMe @@ -439,10 +439,10 @@ class ClusterSingletonManager( logInfo("Retry [{}], sending HandOverToMe to [{}]", count, previousLeaderOption) previousLeaderOption foreach { peer(_) ! HandOverToMe } setTimer(HandOverRetryTimer, HandOverRetry(count + 1), retryInterval, repeat = false) - } else if (previousLeaderOption.isEmpty) { + } else if (previousLeaderOption forall removed.contains) { // can't send HandOverToMe, previousLeader unknown for new node (or restart) // previous leader might be down or removed, so no TakeOverFromMe message is received - logInfo("Timeout in BecomingLeader. Previous leader unknown and no TakeOver request.") + logInfo("Timeout in BecomingLeader. Previous leader unknown, removed and no TakeOver request.") gotoLeader(None) } else throw new ClusterSingletonManagerIsStuck( diff --git a/akka-contrib/src/multi-jvm/scala/akka/contrib/pattern/ClusterSingletonManagerSpec.scala b/akka-contrib/src/multi-jvm/scala/akka/contrib/pattern/ClusterSingletonManagerSpec.scala index 8e56c2d7b7..835a78290b 100644 --- a/akka-contrib/src/multi-jvm/scala/akka/contrib/pattern/ClusterSingletonManagerSpec.scala +++ b/akka-contrib/src/multi-jvm/scala/akka/contrib/pattern/ClusterSingletonManagerSpec.scala @@ -314,7 +314,7 @@ class ClusterSingletonManagerSpec extends MultiNodeSpec(ClusterSingletonManagerS enterBarrier("after-leave") } - "take over when leader crashes in 5 nodes cluster" in within(35 seconds) { + "take over when leader crashes in 5 nodes cluster" in within(60 seconds) { system.eventStream.publish(Mute(EventFilter.warning(pattern = ".*received dead letter from.*"))) system.eventStream.publish(Mute(EventFilter.error(pattern = ".*Disassociated.*"))) system.eventStream.publish(Mute(EventFilter.error(pattern = ".*Association failed.*"))) @@ -324,12 +324,12 @@ class ClusterSingletonManagerSpec extends MultiNodeSpec(ClusterSingletonManagerS verify(sortedClusterRoles(2), msg = 8, expectedCurrent = 0) } - "take over when two leaders crash in 3 nodes cluster" in within(45 seconds) { + "take over when two leaders crash in 3 nodes cluster" in within(60 seconds) { crash(sortedClusterRoles(2), sortedClusterRoles(3)) verify(sortedClusterRoles(4), msg = 9, expectedCurrent = 0) } - "take over when leader crashes in 2 nodes cluster" in within(25 seconds) { + "take over when leader crashes in 2 nodes cluster" in within(60 seconds) { crash(sortedClusterRoles(4)) verify(sortedClusterRoles(5), msg = 10, expectedCurrent = 0) } From 1e4b2585c7fceae8b4c409dfae0faa0b9c37fffc Mon Sep 17 00:00:00 2001 From: Patrik Nordwall Date: Mon, 11 Mar 2013 12:41:15 +0100 Subject: [PATCH 11/47] Publish LeaderChanged when first seen, see #3131 * The problem in ClusterSingletonManagerChaosSpec was that node 4 doesn't publish LeaderChanged, because there is never convergence on node 4 of the new Up state for the three new nodes before they are shutdown. When it becomes convergence on node 4 prevConvergedGossip and newGossip have same leader (i.e. no change). * LeaderChanged is now published when the new leader is first seen, i.e. same as member events. This makes sense now when leader can't be in Joining state. --- .../scala/akka/cluster/ClusterEvent.scala | 20 +++--------- .../ClusterDomainEventPublisherSpec.scala | 31 +++---------------- 2 files changed, 9 insertions(+), 42 deletions(-) diff --git a/akka-cluster/src/main/scala/akka/cluster/ClusterEvent.scala b/akka-cluster/src/main/scala/akka/cluster/ClusterEvent.scala index bf27cb186f..6b61a5c7d8 100644 --- a/akka-cluster/src/main/scala/akka/cluster/ClusterEvent.scala +++ b/akka-cluster/src/main/scala/akka/cluster/ClusterEvent.scala @@ -96,7 +96,8 @@ object ClusterEvent { } /** - * Leader of the cluster members changed. Only published after convergence. + * Leader of the cluster members changed. Published when the state change + * is first seen on a node. */ case class LeaderChanged(leader: Option[Address]) extends ClusterDomainEvent { /** @@ -210,8 +211,6 @@ private[cluster] final class ClusterDomainEventPublisher extends Actor with Acto import InternalClusterAction._ var latestGossip: Gossip = Gossip.empty - var latestConvergedGossip: Gossip = Gossip.empty - var bufferedEvents: immutable.IndexedSeq[ClusterDomainEvent] = Vector.empty override def preRestart(reason: Throwable, message: Option[Any]) { // don't postStop when restarted, no children to stop @@ -243,7 +242,7 @@ private[cluster] final class ClusterDomainEventPublisher extends Actor with Acto members = latestGossip.members, unreachable = latestGossip.overview.unreachable, seenBy = latestGossip.seenBy, - leader = latestConvergedGossip.leader) + leader = latestGossip.leader) receiver match { case Some(ref) ⇒ ref ! state case None ⇒ publish(state) @@ -275,15 +274,7 @@ private[cluster] final class ClusterDomainEventPublisher extends Actor with Acto case _ ⇒ publish(event) } } - // buffer up the LeaderChanged waiting for convergence - bufferedEvents ++= diffLeader(oldGossip, newGossip) - // if we have convergence then publish the MemberEvents and LeaderChanged - if (newGossip.convergence) { - val previousConvergedGossip = latestConvergedGossip - latestConvergedGossip = newGossip - bufferedEvents foreach publish - bufferedEvents = Vector.empty - } + diffLeader(oldGossip, newGossip) foreach publish // publish internal SeenState for testing purposes diffSeen(oldGossip, newGossip) foreach publish } @@ -293,13 +284,12 @@ private[cluster] final class ClusterDomainEventPublisher extends Actor with Acto def publish(event: AnyRef): Unit = eventStream publish event def publishStart(): Unit = - if ((latestGossip ne Gossip.empty) || (latestConvergedGossip ne Gossip.empty)) { + if (latestGossip ne Gossip.empty) { clearState() publishCurrentClusterState(None) } def clearState(): Unit = { latestGossip = Gossip.empty - latestConvergedGossip = Gossip.empty } } diff --git a/akka-cluster/src/test/scala/akka/cluster/ClusterDomainEventPublisherSpec.scala b/akka-cluster/src/test/scala/akka/cluster/ClusterDomainEventPublisherSpec.scala index 196308e2e6..3d3ce0ac3e 100644 --- a/akka-cluster/src/test/scala/akka/cluster/ClusterDomainEventPublisherSpec.scala +++ b/akka-cluster/src/test/scala/akka/cluster/ClusterDomainEventPublisherSpec.scala @@ -67,25 +67,13 @@ class ClusterDomainEventPublisherSpec extends AkkaSpec memberSubscriber.expectMsg(MemberUp(cUp)) } - "publish leader changed when new leader after convergence" in { + "publish leader changed" in { publisher ! PublishChanges(g4) memberSubscriber.expectMsg(MemberUp(dUp)) memberSubscriber.expectMsg(MemberUp(bUp)) memberSubscriber.expectMsg(MemberUp(cUp)) + memberSubscriber.expectMsg(LeaderChanged(Some(dUp.address))) memberSubscriber.expectNoMsg(1 second) - - publisher ! PublishChanges(g5) - memberSubscriber.expectMsg(LeaderChanged(Some(dUp.address))) - } - - "publish leader changed when new leader and convergence both before and after" in { - // convergence both before and after - publisher ! PublishChanges(g3) - memberSubscriber.expectMsg(MemberUp(bUp)) - memberSubscriber.expectMsg(MemberUp(cUp)) - publisher ! PublishChanges(g5) - memberSubscriber.expectMsg(MemberUp(dUp)) - memberSubscriber.expectMsg(LeaderChanged(Some(dUp.address))) } "publish leader changed when old leader leaves and is removed" in { @@ -96,34 +84,23 @@ class ClusterDomainEventPublisherSpec extends AkkaSpec memberSubscriber.expectNoMsg(1 second) publisher ! PublishChanges(g7) memberSubscriber.expectMsg(MemberExited(aExiting)) + memberSubscriber.expectMsg(LeaderChanged(Some(bUp.address))) memberSubscriber.expectNoMsg(1 second) // at the removed member a an empty gossip is the last thing publisher ! PublishChanges(Gossip.empty) memberSubscriber.expectMsg(MemberRemoved(aRemoved)) memberSubscriber.expectMsg(MemberRemoved(bRemoved)) memberSubscriber.expectMsg(MemberRemoved(cRemoved)) - memberSubscriber.expectMsg(LeaderChanged(Some(bUp.address))) memberSubscriber.expectMsg(LeaderChanged(None)) } - "not publish leader changed when not convergence" in { + "not publish leader changed when same leader" in { publisher ! PublishChanges(g4) memberSubscriber.expectMsg(MemberUp(dUp)) memberSubscriber.expectMsg(MemberUp(bUp)) memberSubscriber.expectMsg(MemberUp(cUp)) - memberSubscriber.expectNoMsg(1 second) - } - - "not publish leader changed when changed convergence but still same leader" in { - publisher ! PublishChanges(g5) - memberSubscriber.expectMsg(MemberUp(dUp)) - memberSubscriber.expectMsg(MemberUp(bUp)) - memberSubscriber.expectMsg(MemberUp(cUp)) memberSubscriber.expectMsg(LeaderChanged(Some(dUp.address))) - publisher ! PublishChanges(g4) - memberSubscriber.expectNoMsg(1 second) - publisher ! PublishChanges(g5) memberSubscriber.expectNoMsg(1 second) } From f4d59383d785494c0eb2efedd417ccf1357aee18 Mon Sep 17 00:00:00 2001 From: Patrik Nordwall Date: Thu, 7 Mar 2013 12:10:30 +0100 Subject: [PATCH 12/47] Log actor failures in supervisor, see #2824 * To make it possible to override with application specific logging, or mute logging for certain failures * Changed log level of Resume failures to WARNING, which caused all the changes to the tests --- .../test/scala/akka/actor/ActorDSLSpec.scala | 30 ++++--- .../akka/actor/SupervisorHierarchySpec.scala | 46 +++++------ .../scala/akka/actor/SupervisorSpec.scala | 6 +- .../scala/akka/event/LoggingReceiveSpec.scala | 6 +- .../test/scala/akka/routing/RoutingSpec.scala | 6 +- .../src/main/scala/akka/actor/ActorCell.scala | 4 +- .../main/scala/akka/actor/FaultHandling.scala | 79 +++++++++++++++++-- .../akka/actor/dungeon/FaultHandling.scala | 24 +++--- .../akka/camel/ConsumerIntegrationTest.scala | 2 +- akka-docs/rst/java/fault-tolerance.rst | 23 ++++++ akka-docs/rst/java/untyped-actors.rst | 2 +- akka-docs/rst/scala/actors.rst | 2 +- .../docs/actor/FaultHandlingDocSpec.scala | 4 +- akka-docs/rst/scala/fault-tolerance.rst | 16 ++++ .../remote/testconductor/BarrierSpec.scala | 3 +- .../scala/akka/remote/RemoteRouterSpec.scala | 4 +- .../test/scala/akka/remote/RemotingSpec.scala | 8 +- 17 files changed, 185 insertions(+), 80 deletions(-) diff --git a/akka-actor-tests/src/test/scala/akka/actor/ActorDSLSpec.scala b/akka-actor-tests/src/test/scala/akka/actor/ActorDSLSpec.scala index da26490a42..bdefe3e7b7 100644 --- a/akka-actor-tests/src/test/scala/akka/actor/ActorDSLSpec.scala +++ b/akka-actor-tests/src/test/scala/akka/actor/ActorDSLSpec.scala @@ -67,18 +67,22 @@ class ActorDSLSpec extends AkkaSpec { "have a maximum queue size" in { val i = inbox() system.eventStream.subscribe(testActor, classOf[Warning]) - for (_ ← 1 to 1000) i.receiver ! 0 - expectNoMsg(1 second) - EventFilter.warning(start = "dropping message", occurrences = 1) intercept { + try { + for (_ ← 1 to 1000) i.receiver ! 0 + expectNoMsg(1 second) + EventFilter.warning(start = "dropping message", occurrences = 1) intercept { + i.receiver ! 42 + } + expectMsgType[Warning] i.receiver ! 42 - } - expectMsgType[Warning] - i.receiver ! 42 - expectNoMsg(1 second) - val gotit = for (_ ← 1 to 1000) yield i.receive() - gotit must be((1 to 1000) map (_ ⇒ 0)) - intercept[TimeoutException] { - i.receive(1 second) + expectNoMsg(1 second) + val gotit = for (_ ← 1 to 1000) yield i.receive() + gotit must be((1 to 1000) map (_ ⇒ 0)) + intercept[TimeoutException] { + i.receive(1 second) + } + } finally { + system.eventStream.unsubscribe(testActor, classOf[Warning]) } } @@ -188,8 +192,8 @@ class ActorDSLSpec extends AkkaSpec { } }) a ! testActor - EventFilter[Exception](occurrences = 1) intercept { - a ! new Exception + EventFilter.warning("hi", occurrences = 1) intercept { + a ! new Exception("hi") } expectNoMsg(1 second) EventFilter[Exception]("hello", occurrences = 1) intercept { diff --git a/akka-actor-tests/src/test/scala/akka/actor/SupervisorHierarchySpec.scala b/akka-actor-tests/src/test/scala/akka/actor/SupervisorHierarchySpec.scala index 095305dce4..7200c4cd7e 100644 --- a/akka-actor-tests/src/test/scala/akka/actor/SupervisorHierarchySpec.scala +++ b/akka-actor-tests/src/test/scala/akka/actor/SupervisorHierarchySpec.scala @@ -233,8 +233,8 @@ object SupervisorHierarchySpec { abort("invariant violated: " + state.kids.size + " != " + context.children.size) } cause match { - case f: Failure if f.failPost > 0 ⇒ f.failPost -= 1; throw f - case PostRestartException(`self`, f: Failure, _) if f.failPost > 0 ⇒ f.failPost -= 1; throw f + case f: Failure if f.failPost > 0 ⇒ { f.failPost -= 1; throw f } + case PostRestartException(`self`, f: Failure, _) if f.failPost > 0 ⇒ { f.failPost -= 1; throw f } case _ ⇒ } } @@ -272,12 +272,12 @@ object SupervisorHierarchySpec { setFlags(f.directive) stateCache.put(self, stateCache.get(self).copy(failConstr = f.copy())) throw f - case "ping" ⇒ Thread.sleep((Random.nextFloat * 1.03).toLong); sender ! "pong" + case "ping" ⇒ { Thread.sleep((Random.nextFloat * 1.03).toLong); sender ! "pong" } case Dump(0) ⇒ abort("dump") case Dump(level) ⇒ context.children foreach (_ ! Dump(level - 1)) case Terminated(ref) ⇒ /* - * It might be that we acted upon this death already in postRestart + * It might be that we acted upon this death already in postRestart * (if the unwatch() came too late), so just ignore in this case. */ val name = ref.path.name @@ -332,17 +332,17 @@ object SupervisorHierarchySpec { * This stress test will construct a supervision hierarchy of configurable * depth and breadth and then randomly fail and check its actors. The actors * perform certain checks internally (verifying that they do not run when - * suspended, for example), and they are checked for health by the test + * suspended, for example), and they are checked for health by the test * procedure. - * + * * Execution happens in phases (which is the reason for FSM): - * + * * Idle: * - upon reception of Init message, construct hierary and go to Init state - * + * * Init: * - receive refs of all contained actors - * + * * Stress: * - deal out actions (Fail or "ping"), keeping the hierarchy busy * - whenever all actors are in the "pinged" list (i.e. have not yet @@ -353,29 +353,29 @@ object SupervisorHierarchySpec { * - make sure to remove all actors which die in the course of the test * from the pinged and idle sets (others will be spawned from within the * hierarchy) - * + * * Finishing: * - after dealing out the last action, wait for the outstanding "pong" * messages * - when last "pong" is received, goto LastPing state * - upon state timeout, stop the hierarchy and go to the Failed state - * + * * LastPing: * - upon entering this state, send a "ping" to all actors * - when last "pong" is received, goto Stopping state * - upon state timeout, stop the hierarchy and go to the Failed state - * + * * Stopping: * - upon entering this state, stop the hierarchy * - upon termination of the hierarchy send back successful result - * + * * Whenever an ErrorLog is received, goto Failed state - * + * * Failed: * - accumulate ErrorLog messages * - upon termination of the hierarchy send back failed result and print * the logs, merged and in chronological order. - * + * * Remark about test failures which lead to stopping: * The FSM needs to know not the send more things to the dead guy, but it * also must not watch all targets, because the dead guy’s supervisor also @@ -558,10 +558,10 @@ object SupervisorHierarchySpec { stop } else if (false) { /* - * This part of the test is normally disabled, because it does not + * This part of the test is normally disabled, because it does not * work reliably: even though I found only these weak references * using YourKit just now, GC wouldn’t collect them and the test - * failed. I’m leaving this code in so that manual inspection remains + * failed. I’m leaving this code in so that manual inspection remains * an option (by setting the above condition to “true”). */ val weak = children map (new WeakReference(_)) @@ -756,7 +756,7 @@ class SupervisorHierarchySpec extends AkkaSpec(SupervisorHierarchySpec.config) w val worker = expectMsgType[ActorRef] worker ! "ping" expectMsg("pong") - EventFilter[Exception]("expected", occurrences = 1) intercept { + EventFilter.warning("expected", occurrences = 1) intercept { middle ! "fail" } middle ! "ping" @@ -781,13 +781,13 @@ class SupervisorHierarchySpec extends AkkaSpec(SupervisorHierarchySpec.config) w val worker = expectMsgType[ActorRef] worker ! "ping" expectMsg("pong") - EventFilter[Exception]("expected", occurrences = 1) intercept { + EventFilter.warning("expected", occurrences = 1) intercept { boss ! "fail" + awaitCond(worker.asInstanceOf[LocalActorRef].underlying.mailbox.isSuspended) + worker ! "ping" + expectNoMsg(2 seconds) + latch.countDown() } - awaitCond(worker.asInstanceOf[LocalActorRef].underlying.mailbox.isSuspended) - worker ! "ping" - expectNoMsg(2 seconds) - latch.countDown() expectMsg("pong") } diff --git a/akka-actor-tests/src/test/scala/akka/actor/SupervisorSpec.scala b/akka-actor-tests/src/test/scala/akka/actor/SupervisorSpec.scala index 7b7c36e0a5..10576187d2 100644 --- a/akka-actor-tests/src/test/scala/akka/actor/SupervisorSpec.scala +++ b/akka-actor-tests/src/test/scala/akka/actor/SupervisorSpec.scala @@ -171,7 +171,7 @@ class SupervisorSpec extends AkkaSpec with BeforeAndAfterEach with ImplicitSende override def preStart() { preStarts += 1; testActor ! ("preStart" + preStarts) } override def postStop() { postStops += 1; testActor ! ("postStop" + postStops) } def receive = { - case "crash" ⇒ testActor ! "crashed"; throw new RuntimeException("Expected") + case "crash" ⇒ { testActor ! "crashed"; throw new RuntimeException("Expected") } case "ping" ⇒ sender ! "pong" } } @@ -385,7 +385,7 @@ class SupervisorSpec extends AkkaSpec with BeforeAndAfterEach with ImplicitSende val child = context.watch(context.actorOf(Props(new Actor { override def postRestart(reason: Throwable): Unit = testActor ! "child restarted" def receive = { - case l: TestLatch ⇒ Await.ready(l, 5 seconds); throw new IllegalStateException("OHNOES") + case l: TestLatch ⇒ { Await.ready(l, 5 seconds); throw new IllegalStateException("OHNOES") } case "test" ⇒ sender ! "child green" } }), "child")) @@ -403,7 +403,7 @@ class SupervisorSpec extends AkkaSpec with BeforeAndAfterEach with ImplicitSende val latch = TestLatch() parent ! latch parent ! "testchild" - EventFilter[IllegalStateException]("OHNOES", occurrences = 2) intercept { + EventFilter[IllegalStateException]("OHNOES", occurrences = 1) intercept { latch.countDown() } expectMsg("parent restarted") diff --git a/akka-actor-tests/src/test/scala/akka/event/LoggingReceiveSpec.scala b/akka-actor-tests/src/test/scala/akka/event/LoggingReceiveSpec.scala index d61fd1496b..5fb2092d86 100644 --- a/akka-actor-tests/src/test/scala/akka/event/LoggingReceiveSpec.scala +++ b/akka-actor-tests/src/test/scala/akka/event/LoggingReceiveSpec.scala @@ -192,9 +192,9 @@ class LoggingReceiveSpec extends WordSpec with BeforeAndAfterEach with BeforeAnd EventFilter[ActorKilledException](occurrences = 1) intercept { actor ! Kill val set = receiveWhile(messages = 3) { - case Logging.Error(_: ActorKilledException, `aname`, `aclass`, "Kill") ⇒ 1 - case Logging.Debug(`aname`, `aclass`, "restarting") ⇒ 2 - case Logging.Debug(`aname`, `aclass`, "restarted") ⇒ 3 + case Logging.Error(_: ActorKilledException, `aname`, _, "Kill") ⇒ 1 + case Logging.Debug(`aname`, `aclass`, "restarting") ⇒ 2 + case Logging.Debug(`aname`, `aclass`, "restarted") ⇒ 3 }.toSet expectNoMsg(Duration.Zero) assert(set == Set(1, 2, 3), set + " was not Set(1, 2, 3)") diff --git a/akka-actor-tests/src/test/scala/akka/routing/RoutingSpec.scala b/akka-actor-tests/src/test/scala/akka/routing/RoutingSpec.scala index 23445ae1a0..f2b913d962 100644 --- a/akka-actor-tests/src/test/scala/akka/routing/RoutingSpec.scala +++ b/akka-actor-tests/src/test/scala/akka/routing/RoutingSpec.scala @@ -169,14 +169,14 @@ class RoutingSpec extends AkkaSpec(RoutingSpec.config) with DefaultTimeout with RoundRobinRouter(1, supervisorStrategy = escalator))) //#supervision router ! CurrentRoutees - EventFilter[ActorKilledException](occurrences = 2) intercept { + EventFilter[ActorKilledException](occurrences = 1) intercept { expectMsgType[RouterRoutees].routees.head ! Kill } expectMsgType[ActorKilledException] val router2 = system.actorOf(Props.empty.withRouter(RoundRobinRouter(1).withSupervisorStrategy(escalator))) router2 ! CurrentRoutees - EventFilter[ActorKilledException](occurrences = 2) intercept { + EventFilter[ActorKilledException](occurrences = 1) intercept { expectMsgType[RouterRoutees].routees.head ! Kill } expectMsgType[ActorKilledException] @@ -194,7 +194,7 @@ class RoutingSpec extends AkkaSpec(RoutingSpec.config) with DefaultTimeout with override def postRestart(reason: Throwable): Unit = testActor ! "restarted" }).withRouter(RoundRobinRouter(3)) val router = expectMsgType[ActorRef] - EventFilter[Exception]("die", occurrences = 2) intercept { + EventFilter[Exception]("die", occurrences = 1) intercept { router ! "die" } expectMsgType[Exception].getMessage must be("die") diff --git a/akka-actor/src/main/scala/akka/actor/ActorCell.scala b/akka-actor/src/main/scala/akka/actor/ActorCell.scala index a48d5c4c45..a1101f1936 100644 --- a/akka-actor/src/main/scala/akka/actor/ActorCell.scala +++ b/akka-actor/src/main/scala/akka/actor/ActorCell.scala @@ -383,7 +383,7 @@ private[akka] class ActorCell( case NoMessage ⇒ // only here to suppress warning } } catch handleNonFatalOrInterruptedException { e ⇒ - handleInvokeFailure(Nil, e, "error while processing " + message) + handleInvokeFailure(Nil, e) } if (todo != null) systemInvoke(todo) } @@ -398,7 +398,7 @@ private[akka] class ActorCell( } currentMessage = null // reset current message after successful invocation } catch handleNonFatalOrInterruptedException { e ⇒ - handleInvokeFailure(Nil, e, e.getMessage) + handleInvokeFailure(Nil, e) } finally { checkReceiveTimeout // Reschedule receive timeout } diff --git a/akka-actor/src/main/scala/akka/actor/FaultHandling.scala b/akka-actor/src/main/scala/akka/actor/FaultHandling.scala index 0249f8da9d..171290b4d1 100644 --- a/akka-actor/src/main/scala/akka/actor/FaultHandling.scala +++ b/akka-actor/src/main/scala/akka/actor/FaultHandling.scala @@ -4,13 +4,16 @@ package akka.actor import language.implicitConversions - import java.lang.{ Iterable ⇒ JIterable } import java.util.concurrent.TimeUnit import akka.japi.Util.immutableSeq import scala.collection.mutable.ArrayBuffer import scala.collection.immutable import scala.concurrent.duration.Duration +import akka.event.Logging.LogEvent +import akka.event.Logging.Error +import akka.event.Logging.Warning +import scala.util.control.NonFatal /** * INTERNAL API @@ -35,7 +38,7 @@ case class ChildRestartStats(child: ActorRef, var maxNrOfRetriesCount: Int = 0, def requestRestartPermission(retriesWindow: (Option[Int], Option[Int])): Boolean = retriesWindow match { case (Some(retries), _) if retries < 1 ⇒ false - case (Some(retries), None) ⇒ maxNrOfRetriesCount += 1; maxNrOfRetriesCount <= retries + case (Some(retries), None) ⇒ { maxNrOfRetriesCount += 1; maxNrOfRetriesCount <= retries } case (x, Some(window)) ⇒ retriesInWindowOkay(if (x.isDefined) x.get else 1, window) case (None, _) ⇒ true } @@ -273,18 +276,64 @@ abstract class SupervisorStrategy { * failure, which will lead to this actor re-throwing the exception which * caused the failure. The exception will not be wrapped. * + * This method calls [[akka.actor.SupervisorStrategy#logFailure]], which will + * log the failure unless it is escalated. You can customize the logging by + * setting [[akka.actor.SupervisorStrategy#loggingEnabled]] to `false` and + * do the logging inside the `decider` or override the `logFailure` method. + * * @param children is a lazy collection (a view) */ def handleFailure(context: ActorContext, child: ActorRef, cause: Throwable, stats: ChildRestartStats, children: Iterable[ChildRestartStats]): Boolean = { val directive = decider.applyOrElse(cause, escalateDefault) directive match { - case Resume ⇒ resumeChild(child, cause); true - case Restart ⇒ processFailure(context, true, child, cause, stats, children); true - case Stop ⇒ processFailure(context, false, child, cause, stats, children); true - case Escalate ⇒ false + case d @ Resume ⇒ + logFailure(context, child, cause, d) + resumeChild(child, cause) + true + case d @ Restart ⇒ + logFailure(context, child, cause, d) + processFailure(context, true, child, cause, stats, children) + true + case d @ Stop ⇒ + logFailure(context, child, cause, d) + processFailure(context, false, child, cause, stats, children) + true + case d @ Escalate ⇒ + logFailure(context, child, cause, d) + false } } + /** + * Logging of actor failures is done when this is `true`. + */ + protected def loggingEnabled: Boolean = true + + /** + * Default logging of actor failures when + * [[akka.actor.SupervisorStrategy#loggingEnabled]] is `true`. + * `Escalate` failures are not logged here, since they are supposed + * to be handled at a level higher up in the hierarchy. + * `Resume` failures are logged at `Warning` level. + * `Stop` and `Restart` failures are logged at `Error` level. + */ + protected def logFailure(context: ActorContext, child: ActorRef, cause: Throwable, decision: Directive): Unit = + if (loggingEnabled) { + val logMessage = cause match { + case e: ActorInitializationException ⇒ e.getCause.getMessage + case e ⇒ e.getMessage + } + decision match { + case Resume ⇒ publish(context, Warning(child.path.toString, getClass, logMessage)) + case Escalate ⇒ // don't log here + case _ ⇒ publish(context, Error(cause, child.path.toString, getClass, logMessage)) + } + } + + // logging is not the main purpose, and if it fails there’s nothing we can do + private def publish(context: ActorContext, logEvent: LogEvent): Unit = + try context.system.eventStream.publish(logEvent) catch { case NonFatal(_) ⇒ } + /** * Resume the previously failed child: do never apply this to a child which * is not the currently failing child. Suspend/resume needs to be done in @@ -319,12 +368,19 @@ abstract class SupervisorStrategy { * @param withinTimeRange duration of the time window for maxNrOfRetries, Duration.Inf means no window * @param decider mapping from Throwable to [[akka.actor.SupervisorStrategy.Directive]], you can also use a * `Seq` of Throwables which maps the given Throwables to restarts, otherwise escalates. + * @param loggingEnabled the strategy logs the failure if this is enabled (true), by default it is enabled */ -case class AllForOneStrategy(maxNrOfRetries: Int = -1, withinTimeRange: Duration = Duration.Inf)(val decider: SupervisorStrategy.Decider) +case class AllForOneStrategy( + maxNrOfRetries: Int = -1, + withinTimeRange: Duration = Duration.Inf, + override val loggingEnabled: Boolean = true)(val decider: SupervisorStrategy.Decider) extends SupervisorStrategy { import SupervisorStrategy._ + def this(maxNrOfRetries: Int, withinTimeRange: Duration, decider: SupervisorStrategy.JDecider, loggingEnabled: Boolean) = + this(maxNrOfRetries, withinTimeRange, loggingEnabled)(SupervisorStrategy.makeDecider(decider)) + def this(maxNrOfRetries: Int, withinTimeRange: Duration, decider: SupervisorStrategy.JDecider) = this(maxNrOfRetries, withinTimeRange)(SupervisorStrategy.makeDecider(decider)) @@ -358,10 +414,17 @@ case class AllForOneStrategy(maxNrOfRetries: Int = -1, withinTimeRange: Duration * @param withinTimeRange duration of the time window for maxNrOfRetries, Duration.Inf means no window * @param decider mapping from Throwable to [[akka.actor.SupervisorStrategy.Directive]], you can also use a * `Seq` of Throwables which maps the given Throwables to restarts, otherwise escalates. + * @param loggingEnabled the strategy logs the failure if this is enabled (true), by default it is enabled */ -case class OneForOneStrategy(maxNrOfRetries: Int = -1, withinTimeRange: Duration = Duration.Inf)(val decider: SupervisorStrategy.Decider) +case class OneForOneStrategy( + maxNrOfRetries: Int = -1, + withinTimeRange: Duration = Duration.Inf, + override val loggingEnabled: Boolean = true)(val decider: SupervisorStrategy.Decider) extends SupervisorStrategy { + def this(maxNrOfRetries: Int, withinTimeRange: Duration, decider: SupervisorStrategy.JDecider, loggingEnabled: Boolean) = + this(maxNrOfRetries, withinTimeRange, loggingEnabled)(SupervisorStrategy.makeDecider(decider)) + def this(maxNrOfRetries: Int, withinTimeRange: Duration, decider: SupervisorStrategy.JDecider) = this(maxNrOfRetries, withinTimeRange)(SupervisorStrategy.makeDecider(decider)) diff --git a/akka-actor/src/main/scala/akka/actor/dungeon/FaultHandling.scala b/akka-actor/src/main/scala/akka/actor/dungeon/FaultHandling.scala index 053765296a..cfe2dbf9c7 100644 --- a/akka-actor/src/main/scala/akka/actor/dungeon/FaultHandling.scala +++ b/akka-actor/src/main/scala/akka/actor/dungeon/FaultHandling.scala @@ -24,11 +24,11 @@ private[akka] trait FaultHandling { this: ActorCell ⇒ /* ================= * T H E R U L E S * ================= - * + * * Actors can be suspended for two reasons: * - they fail * - their supervisor gets suspended - * + * * In particular they are not suspended multiple times because of cascading * own failures, i.e. while currentlyFailed() they do not fail again. In case * of a restart, failures in constructor/preStart count as new failures. @@ -163,15 +163,14 @@ private[akka] trait FaultHandling { this: ActorCell ⇒ } } - final def handleInvokeFailure(childrenNotToSuspend: immutable.Iterable[ActorRef], t: Throwable, message: String): Unit = { - publish(Error(t, self.path.toString, clazz(actor), message)) + final def handleInvokeFailure(childrenNotToSuspend: immutable.Iterable[ActorRef], t: Throwable): Unit = { // prevent any further messages to be processed until the actor has been restarted if (!isFailed) try { suspendNonRecursive() // suspend children val skip: Set[ActorRef] = currentMessage match { - case Envelope(Failed(_, _), child) ⇒ setFailed(child); Set(child) - case _ ⇒ setFailed(self); Set.empty + case Envelope(Failed(_, _), child) ⇒ { setFailed(child); Set(child) } + case _ ⇒ { setFailed(self); Set.empty } } suspendChildren(exceptFor = skip ++ childrenNotToSuspend) t match { @@ -233,7 +232,7 @@ private[akka] trait FaultHandling { this: ActorCell ⇒ }) } catch handleNonFatalOrInterruptedException { e ⇒ clearActorFields(actor) // in order to prevent preRestart() from happening again - handleInvokeFailure(survivors, new PostRestartException(self, e, cause), e.getMessage) + handleInvokeFailure(survivors, new PostRestartException(self, e, cause)) } } @@ -256,14 +255,15 @@ private[akka] trait FaultHandling { this: ActorCell ⇒ final protected def handleChildTerminated(child: ActorRef): SystemMessage = { val status = removeChildAndGetStateChange(child) /* - * if this fails, we do nothing in case of terminating/restarting state, + * if this fails, we do nothing in case of terminating/restarting state, * otherwise tell the supervisor etc. (in that second case, the match * below will hit the empty default case, too) */ if (actor != null) { try actor.supervisorStrategy.handleChildTerminated(this, child, children) catch handleNonFatalOrInterruptedException { e ⇒ - handleInvokeFailure(Nil, e, "handleChildTerminated failed") + publish(Error(e, self.path.toString, clazz(actor), "handleChildTerminated failed")) + handleInvokeFailure(Nil, e) } } /* @@ -271,9 +271,9 @@ private[akka] trait FaultHandling { this: ActorCell ⇒ * then we are continuing the previously suspended recreate/create/terminate action */ status match { - case Some(c @ ChildrenContainer.Recreation(cause)) ⇒ finishRecreate(cause, actor); c.dequeueAll() - case Some(c @ ChildrenContainer.Creation()) ⇒ finishCreate(); c.dequeueAll() - case Some(ChildrenContainer.Termination) ⇒ finishTerminate(); null + case Some(c @ ChildrenContainer.Recreation(cause)) ⇒ { finishRecreate(cause, actor); c.dequeueAll() } + case Some(c @ ChildrenContainer.Creation()) ⇒ { finishCreate(); c.dequeueAll() } + case Some(ChildrenContainer.Termination) ⇒ { finishTerminate(); null } case _ ⇒ null } } diff --git a/akka-camel/src/test/scala/akka/camel/ConsumerIntegrationTest.scala b/akka-camel/src/test/scala/akka/camel/ConsumerIntegrationTest.scala index 4d2c3e7b48..d84e722fed 100644 --- a/akka-camel/src/test/scala/akka/camel/ConsumerIntegrationTest.scala +++ b/akka-camel/src/test/scala/akka/camel/ConsumerIntegrationTest.scala @@ -29,7 +29,7 @@ class ConsumerIntegrationTest extends WordSpec with MustMatchers with NonSharedC implicit def ec: ExecutionContext = system.dispatcher "Consumer must throw FailedToCreateRouteException, while awaiting activation, if endpoint is invalid" in { - filterEvents(EventFilter[ActorActivationException](occurrences = 1)) { + filterEvents(EventFilter.warning(pattern = "failed to activate.*", occurrences = 1)) { val actorRef = system.actorOf(Props(new TestActor(uri = "some invalid uri")), "invalidActor") intercept[FailedToCreateRouteException] { Await.result(camel.activationFutureFor(actorRef), defaultTimeoutDuration) diff --git a/akka-docs/rst/java/fault-tolerance.rst b/akka-docs/rst/java/fault-tolerance.rst index 74b5a0cb5f..1c93bbdba3 100644 --- a/akka-docs/rst/java/fault-tolerance.rst +++ b/akka-docs/rst/java/fault-tolerance.rst @@ -52,6 +52,13 @@ restarts per minute. ``-1`` and ``Duration.Inf()`` means that the respective lim does not apply, leaving the possibility to specify an absolute upper limit on the restarts or to make the restarts work infinitely. +.. note:: + + If the strategy is declared inside the supervising actor (as opposed to + a separate class) its decider has access to all internal state of + the actor in a thread-safe fashion, including obtaining a reference to the + currently failed child (available as the ``getSender`` of the failure message). + Default Supervisor Strategy ^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -78,6 +85,22 @@ loss of the child. This strategy is also provided pre-packaged as :class:`StoppingSupervisorStrategy` configurator to be used when you want the ``"/user"`` guardian to apply it. +Logging of Actor Failures +^^^^^^^^^^^^^^^^^^^^^^^^^ + +By default the ``SupervisorStrategy`` logs failures unless they are escalated. +Escalated failures are supposed to be handled, and potentially logged, at a level +higher in the hierarchy. + +You can mute the default logging of a ``SupervisorStrategy`` by setting +``loggingEnabled`` to ``false`` when instantiating it. Customized logging +can be done inside the ``Decider``. Note that the reference to the currently +failed child is available as the ``getSender`` when the ``SupervisorStrategy`` is +declared inside the supervising actor. + +You may also customize the logging in your own ``SupervisorStrategy`` implementation +by overriding the ``logFailure`` method. + Supervision of Top-Level Actors ------------------------------- diff --git a/akka-docs/rst/java/untyped-actors.rst b/akka-docs/rst/java/untyped-actors.rst index ab01e31216..5365aa5130 100644 --- a/akka-docs/rst/java/untyped-actors.rst +++ b/akka-docs/rst/java/untyped-actors.rst @@ -739,7 +739,7 @@ Please note, that the child actors are *still restarted*, but no new ``ActorRef` the same principles for the children, ensuring that their ``preStart()`` method is called only at the creation of their refs. -For more information see :ref:`what-restarting-means-scala`. +For more information see :ref:`supervision-restart`. Initialization via message passing ---------------------------------- diff --git a/akka-docs/rst/scala/actors.rst b/akka-docs/rst/scala/actors.rst index fcf7ad0763..8ec241d046 100644 --- a/akka-docs/rst/scala/actors.rst +++ b/akka-docs/rst/scala/actors.rst @@ -882,7 +882,7 @@ Please note, that the child actors are *still restarted*, but no new ``ActorRef` the same principles for the children, ensuring that their ``preStart()`` method is called only at the creation of their refs. -For more information see :ref:`what-restarting-means-scala`. +For more information see :ref:`supervision-restart`. Initialization via message passing ---------------------------------- diff --git a/akka-docs/rst/scala/code/docs/actor/FaultHandlingDocSpec.scala b/akka-docs/rst/scala/code/docs/actor/FaultHandlingDocSpec.scala index aa029823a4..fe1e5b87de 100644 --- a/akka-docs/rst/scala/code/docs/actor/FaultHandlingDocSpec.scala +++ b/akka-docs/rst/scala/code/docs/actor/FaultHandlingDocSpec.scala @@ -93,7 +93,7 @@ class FaultHandlingDocSpec extends AkkaSpec with ImplicitSender { supervisor ! Props[Child] val child = expectMsgType[ActorRef] // retrieve answer from TestKit’s testActor //#create - EventFilter[ArithmeticException](occurrences = 1) intercept { + EventFilter.warning(occurrences = 1) intercept { //#resume child ! 42 // set state to 42 child ! "get" @@ -121,7 +121,7 @@ class FaultHandlingDocSpec extends AkkaSpec with ImplicitSender { child.isTerminated must be(true) //#stop } - EventFilter[Exception]("CRASH", occurrences = 4) intercept { + EventFilter[Exception]("CRASH", occurrences = 2) intercept { //#escalate-kill supervisor ! Props[Child] // create new child val child2 = expectMsgType[ActorRef] diff --git a/akka-docs/rst/scala/fault-tolerance.rst b/akka-docs/rst/scala/fault-tolerance.rst index 9a2d36fbe1..1e705b4b0b 100644 --- a/akka-docs/rst/scala/fault-tolerance.rst +++ b/akka-docs/rst/scala/fault-tolerance.rst @@ -82,6 +82,22 @@ loss of the child. This strategy is also provided pre-packaged as :class:`StoppingSupervisorStrategy` configurator to be used when you want the ``"/user"`` guardian to apply it. +Logging of Actor Failures +^^^^^^^^^^^^^^^^^^^^^^^^^ + +By default the ``SupervisorStrategy`` logs failures unless they are escalated. +Escalated failures are supposed to be handled, and potentially logged, at a level +higher in the hierarchy. + +You can mute the default logging of a ``SupervisorStrategy`` by setting +``loggingEnabled`` to ``false`` when instantiating it. Customized logging +can be done inside the ``Decider``. Note that the reference to the currently +failed child is available as the ``sender`` when the ``SupervisorStrategy`` is +declared inside the supervising actor. + +You may also customize the logging in your own ``SupervisorStrategy`` implementation +by overriding the ``logFailure`` method. + Supervision of Top-Level Actors ------------------------------- diff --git a/akka-remote-tests/src/test/scala/akka/remote/testconductor/BarrierSpec.scala b/akka-remote-tests/src/test/scala/akka/remote/testconductor/BarrierSpec.scala index b33b67025e..696a29f24b 100644 --- a/akka-remote-tests/src/test/scala/akka/remote/testconductor/BarrierSpec.scala +++ b/akka-remote-tests/src/test/scala/akka/remote/testconductor/BarrierSpec.scala @@ -240,9 +240,10 @@ class BarrierSpec extends AkkaSpec(BarrierSpec.config) with ImplicitSender { expectMsg(ToClient(Done)) b ! Remove(B) b ! Remove(A) - EventFilter[BarrierEmpty](occurrences = 1) intercept { + EventFilter.warning(start = "cannot remove", occurrences = 1) intercept { b ! Remove(A) } + Thread.sleep(5000) } } diff --git a/akka-remote/src/test/scala/akka/remote/RemoteRouterSpec.scala b/akka-remote/src/test/scala/akka/remote/RemoteRouterSpec.scala index 7b029d3b4f..a993511b21 100644 --- a/akka-remote/src/test/scala/akka/remote/RemoteRouterSpec.scala +++ b/akka-remote/src/test/scala/akka/remote/RemoteRouterSpec.scala @@ -210,9 +210,7 @@ akka.actor.deployment { router ! CurrentRoutees EventFilter[ActorKilledException](occurrences = 1) intercept { - EventFilter[ActorKilledException](occurrences = 1).intercept { - expectMsgType[RouterRoutees].routees.head ! Kill - }(otherSystem) + expectMsgType[RouterRoutees].routees.head ! Kill } expectMsgType[ActorKilledException] } diff --git a/akka-remote/src/test/scala/akka/remote/RemotingSpec.scala b/akka-remote/src/test/scala/akka/remote/RemotingSpec.scala index 6284eb0780..e1f22f1379 100644 --- a/akka-remote/src/test/scala/akka/remote/RemotingSpec.scala +++ b/akka-remote/src/test/scala/akka/remote/RemotingSpec.scala @@ -197,7 +197,7 @@ class RemotingSpec extends AkkaSpec(RemotingSpec.cfg) with ImplicitSender with D expectMsg(42) EventFilter[Exception]("crash", occurrences = 1).intercept { r ! new Exception("crash") - }(other) + } expectMsg("preRestart") r ! 42 expectMsg(42) @@ -242,7 +242,7 @@ class RemotingSpec extends AkkaSpec(RemotingSpec.cfg) with ImplicitSender with D expectMsg(42) EventFilter[Exception]("crash", occurrences = 1).intercept { r ! new Exception("crash") - }(other) + } expectMsg("preRestart") r ! 42 expectMsg(42) @@ -258,7 +258,7 @@ class RemotingSpec extends AkkaSpec(RemotingSpec.cfg) with ImplicitSender with D expectMsg(10.seconds, 42) EventFilter[Exception]("crash", occurrences = 1).intercept { r ! new Exception("crash") - }(other) + } expectMsg("preRestart") r ! 42 expectMsg(42) @@ -274,7 +274,7 @@ class RemotingSpec extends AkkaSpec(RemotingSpec.cfg) with ImplicitSender with D expectMsg(10.seconds, 42) EventFilter[Exception]("crash", occurrences = 1).intercept { r ! new Exception("crash") - }(other) + } expectMsg("preRestart") r ! 42 expectMsg(42) From a67fa18f8d1b7f55c2f2345dcb25dca98805cb1f Mon Sep 17 00:00:00 2001 From: Patrik Nordwall Date: Thu, 7 Mar 2013 18:08:07 +0100 Subject: [PATCH 13/47] Reduce unwanted logging from remoting, see #2826 * Handle logging in EndpointManager supervisorStrategy * Added some more exception types to be able to differentiate failures --- .../akka/cluster/MultiNodeClusterSpec.scala | 3 ++ .../src/main/scala/akka/remote/Endpoint.scala | 19 ++++++-- .../src/main/scala/akka/remote/Remoting.scala | 44 ++++++++++++------- .../test/scala/akka/remote/RemotingSpec.scala | 7 +-- 4 files changed, 49 insertions(+), 24 deletions(-) diff --git a/akka-cluster/src/multi-jvm/scala/akka/cluster/MultiNodeClusterSpec.scala b/akka-cluster/src/multi-jvm/scala/akka/cluster/MultiNodeClusterSpec.scala index 0f3fae3c7c..f5d07b8724 100644 --- a/akka-cluster/src/multi-jvm/scala/akka/cluster/MultiNodeClusterSpec.scala +++ b/akka-cluster/src/multi-jvm/scala/akka/cluster/MultiNodeClusterSpec.scala @@ -81,6 +81,9 @@ trait MultiNodeClusterSpec extends Suite with STMultiNodeSpec with WatchedByCoro Seq(".*received dead letter from.*ClientDisconnected", ".*received dead letter from.*deadLetters.*PoisonPill", + ".*received dead letter from.*Disassociated", + ".*received dead letter from.*DisassociateUnderlying", + ".*received dead letter from.*HandleListenerRegistered", ".*installing context org.jboss.netty.channel.DefaultChannelPipeline.*") foreach { s ⇒ sys.eventStream.publish(Mute(EventFilter.warning(pattern = s))) } diff --git a/akka-remote/src/main/scala/akka/remote/Endpoint.scala b/akka-remote/src/main/scala/akka/remote/Endpoint.scala index b5759409c9..fe3f96f6ca 100644 --- a/akka-remote/src/main/scala/akka/remote/Endpoint.scala +++ b/akka-remote/src/main/scala/akka/remote/Endpoint.scala @@ -124,9 +124,22 @@ private[remote] class EndpointException(msg: String, cause: Throwable) extends A /** * INTERNAL API */ +@SerialVersionUID(1L) private[remote] case class InvalidAssociation(localAddress: Address, remoteAddress: Address, cause: Throwable) extends EndpointException("Invalid address: " + remoteAddress, cause) +/** + * INTERNAL API + */ +@SerialVersionUID(1L) +private[remote] class EndpointDisassociatedException(msg: String) extends EndpointException(msg) + +/** + * INTERNAL API + */ +@SerialVersionUID(1L) +private[remote] class EndpointAssociationException(msg: String, cause: Throwable) extends EndpointException(msg, cause) + /** * INTERNAL API */ @@ -184,11 +197,9 @@ private[remote] class EndpointWriter( stash() stay() case Event(Status.Failure(e: InvalidAssociationException), _) ⇒ - log.error("Tried to associate with invalid remote address [{}]. " + - "Address is now quarantined, all messages to this address will be delivered to dead letters.", remoteAddress) publishAndThrow(new InvalidAssociation(localAddress, remoteAddress, e)) case Event(Status.Failure(e), _) ⇒ - publishAndThrow(new EndpointException(s"Association failed with [$remoteAddress]", e)) + publishAndThrow(new EndpointAssociationException(s"Association failed with [$remoteAddress]", e)) case Event(inboundHandle: AssociationHandle, _) ⇒ // Assert handle == None? handle = Some(inboundHandle) @@ -246,7 +257,7 @@ private[remote] class EndpointWriter( } whenUnhandled { - case Event(Terminated(r), _) if Some(r) == reader ⇒ publishAndThrow(new EndpointException("Disassociated")) + case Event(Terminated(r), _) if r == reader.orNull ⇒ publishAndThrow(new EndpointDisassociatedException("Disassociated")) case Event(TakeOver(newHandle), _) ⇒ // Shutdown old reader handle foreach { _.disassociate() } diff --git a/akka-remote/src/main/scala/akka/remote/Remoting.scala b/akka-remote/src/main/scala/akka/remote/Remoting.scala index 1be99f6f86..1fe6fc21a7 100644 --- a/akka-remote/src/main/scala/akka/remote/Remoting.scala +++ b/akka-remote/src/main/scala/akka/remote/Remoting.scala @@ -352,24 +352,34 @@ private[remote] class EndpointManager(conf: Config, log: LoggingAdapter) extends Some(context.system.scheduler.schedule(pruneInterval, pruneInterval, self, Prune)) else None - override val supervisorStrategy = OneForOneStrategy(settings.MaximumRetriesInWindow, settings.RetryWindow) { - case InvalidAssociation(localAddress, remoteAddress, e) ⇒ - endpoints.markAsFailed(sender, Deadline.now + settings.UnknownAddressGateClosedFor) - Stop - - case NonFatal(e) ⇒ - // Retrying immediately if the retry gate is disabled, and it is an endpoint used for writing. - if (!retryGateEnabled && endpoints.isWritable(sender)) { - // This strategy keeps all the messages in the stash of the endpoint so restart will transfer the queue - // to the restarted endpoint -- thus no messages are lost - Restart - } else { - // This strategy throws away all the messages enqueued in the endpoint (in its stash), registers the time of failure, - // keeps throwing away messages until the retry gate becomes open (time specified in RetryGateClosedFor) - endpoints.markAsFailed(sender, Deadline.now + settings.RetryGateClosedFor) + override val supervisorStrategy = + OneForOneStrategy(settings.MaximumRetriesInWindow, settings.RetryWindow, loggingEnabled = false) { + case InvalidAssociation(localAddress, remoteAddress, _) ⇒ + log.error("Tried to associate with invalid remote address [{}]. " + + "Address is now quarantined, all messages to this address will be delivered to dead letters.", remoteAddress) + endpoints.markAsFailed(sender, Deadline.now + settings.UnknownAddressGateClosedFor) Stop - } - } + + case NonFatal(e) ⇒ + + // logging + e match { + case _: EndpointDisassociatedException | _: EndpointAssociationException ⇒ // no logging + case _ ⇒ log.error(e, e.getMessage) + } + + // Retrying immediately if the retry gate is disabled, and it is an endpoint used for writing. + if (!retryGateEnabled && endpoints.isWritable(sender)) { + // This strategy keeps all the messages in the stash of the endpoint so restart will transfer the queue + // to the restarted endpoint -- thus no messages are lost + Restart + } else { + // This strategy throws away all the messages enqueued in the endpoint (in its stash), registers the time of failure, + // keeps throwing away messages until the retry gate becomes open (time specified in RetryGateClosedFor) + endpoints.markAsFailed(sender, Deadline.now + settings.RetryGateClosedFor) + Stop + } + } def receive = { case Listen(addressesPromise) ⇒ diff --git a/akka-remote/src/test/scala/akka/remote/RemotingSpec.scala b/akka-remote/src/test/scala/akka/remote/RemotingSpec.scala index e1f22f1379..7ae8c33df1 100644 --- a/akka-remote/src/test/scala/akka/remote/RemotingSpec.scala +++ b/akka-remote/src/test/scala/akka/remote/RemotingSpec.scala @@ -137,9 +137,10 @@ class RemotingSpec extends AkkaSpec(RemotingSpec.cfg) with ImplicitSender with D } "send error message for wrong address" in { - filterEvents(EventFilter[EndpointException](occurrences = 6), EventFilter.error(start = "Association", occurrences = 6)) { - system.actorFor("akka.test://nonexistingsystem@localhost:12346/user/echo") ! "ping" - } + filterEvents(EventFilter.error(start = "Association", occurrences = 6), + EventFilter.warning(pattern = ".*dead letter.*echo.*", occurrences = 1)) { + system.actorFor("akka.test://nonexistingsystem@localhost:12346/user/echo") ! "ping" + } } "support ask" in { From fb35a3d8e795a88725f921b981d8887d1a4d8c79 Mon Sep 17 00:00:00 2001 From: Patrik Nordwall Date: Mon, 11 Mar 2013 13:25:32 +0100 Subject: [PATCH 14/47] Change log-remote-lifecycle-events=on, see #2826 --- akka-remote/src/main/resources/reference.conf | 2 +- .../scala/akka/remote/RemoteConfigSpec.scala | 63 +++++++------------ 2 files changed, 22 insertions(+), 43 deletions(-) diff --git a/akka-remote/src/main/resources/reference.conf b/akka-remote/src/main/resources/reference.conf index 1835d02173..26deb8aa89 100644 --- a/akka-remote/src/main/resources/reference.conf +++ b/akka-remote/src/main/resources/reference.conf @@ -126,7 +126,7 @@ akka { # If this is "on", Akka will log all RemoteLifeCycleEvents at the level # defined for each, if off then they are not logged. Failures to deserialize # received messages also fall under this flag. - log-remote-lifecycle-events = off + log-remote-lifecycle-events = on ### Failure detection and recovery diff --git a/akka-remote/src/test/scala/akka/remote/RemoteConfigSpec.scala b/akka-remote/src/test/scala/akka/remote/RemoteConfigSpec.scala index afdaa0ce2e..87b19886a7 100644 --- a/akka-remote/src/test/scala/akka/remote/RemoteConfigSpec.scala +++ b/akka-remote/src/test/scala/akka/remote/RemoteConfigSpec.scala @@ -21,24 +21,30 @@ class RemoteConfigSpec extends AkkaSpec( // FIXME: These tests are ignored as it tests configuration specific to the old remoting. "Remoting" must { - "be able to parse generic remote config elements" in { - val settings = RARP(system).provider.remoteSettings - import settings._ + "contain correct configuration values in reference.conf" in { + val remoteSettings = RARP(system).provider.remoteSettings + import remoteSettings._ - StartupTimeout must be === Timeout(10.seconds) - ShutdownTimeout must be === Timeout(10.seconds) - FlushWait must be === 2.seconds - UsePassiveConnections must be(true) - UntrustedMode must be(false) - LogRemoteLifecycleEvents must be(false) LogReceive must be(false) LogSend must be(false) - RetryGateClosedFor must be === 0.seconds - UnknownAddressGateClosedFor must be === 60.seconds - MaximumRetriesInWindow must be === 5 - RetryWindow must be === 3.seconds - BackoffPeriod must be === 10.milliseconds - CommandAckTimeout must be === Timeout(30.seconds) + UntrustedMode must be(false) + LogRemoteLifecycleEvents must be(true) + ShutdownTimeout.duration must be(10 seconds) + FlushWait must be(2 seconds) + StartupTimeout.duration must be(10 seconds) + RetryGateClosedFor must be(Duration.Zero) + UnknownAddressGateClosedFor must be(1 minute) + UsePassiveConnections must be(true) + MaximumRetriesInWindow must be(5) + RetryWindow must be(3 seconds) + BackoffPeriod must be(10 millis) + CommandAckTimeout.duration must be(30 seconds) + Transports.size must be(1) + Transports.head._1 must be(classOf[akka.remote.transport.netty.NettyTransport].getName) + Transports.head._2 must be(Nil) + Adapters must be(Map( + "gremlin" -> classOf[akka.remote.transport.FailureInjectorProvider].getName, + "trttl" -> classOf[akka.remote.transport.ThrottlerProvider].getName)) } @@ -59,33 +65,6 @@ class RemoteConfigSpec extends AkkaSpec( } - "contain correct configuration values in reference.conf" in { - val remoteSettings = RARP(system).provider.remoteSettings - import remoteSettings._ - - LogReceive must be(false) - LogSend must be(false) - UntrustedMode must be(false) - LogRemoteLifecycleEvents must be(false) - ShutdownTimeout.duration must be(10 seconds) - FlushWait must be(2 seconds) - StartupTimeout.duration must be(10 seconds) - RetryGateClosedFor must be(Duration.Zero) - UnknownAddressGateClosedFor must be(1 minute) - UsePassiveConnections must be(true) - MaximumRetriesInWindow must be(5) - RetryWindow must be(3 seconds) - BackoffPeriod must be(10 millis) - CommandAckTimeout.duration must be(30 seconds) - Transports.size must be(1) - Transports.head._1 must be(classOf[akka.remote.transport.netty.NettyTransport].getName) - Transports.head._2 must be(Nil) - Adapters must be(Map( - "gremlin" -> classOf[akka.remote.transport.FailureInjectorProvider].getName, - "trttl" -> classOf[akka.remote.transport.ThrottlerProvider].getName)) - - } - "contain correct socket worker pool configuration values in reference.conf" in { val c = RARP(system).provider.remoteSettings.config.getConfig("akka.remote.netty.tcp") From 852afeb565e2bf9e4e91962b5a97e795d5391fe0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Endre=20S=C3=A1ndor=20Varga?= Date: Tue, 5 Mar 2013 14:22:21 +0100 Subject: [PATCH 15/47] Set TCPNODELAY as default. --- akka-actor/src/main/scala/akka/io/Tcp.scala | 2 ++ akka-actor/src/main/scala/akka/io/TcpConnection.scala | 2 ++ 2 files changed, 4 insertions(+) diff --git a/akka-actor/src/main/scala/akka/io/Tcp.scala b/akka-actor/src/main/scala/akka/io/Tcp.scala index 989c1e05f1..8c5150cc83 100644 --- a/akka-actor/src/main/scala/akka/io/Tcp.scala +++ b/akka-actor/src/main/scala/akka/io/Tcp.scala @@ -50,6 +50,8 @@ object Tcp extends ExtensionKey[TcpExt] { * [[akka.io.Inet.SocketOption]] to enable or disable TCP_NODELAY * (disable or enable Nagle's algorithm) * + * Please note, that TCP_NODELAY is enabled by default. + * * For more information see [[java.net.Socket.setTcpNoDelay]] */ case class TcpNoDelay(on: Boolean) extends SocketOption { diff --git a/akka-actor/src/main/scala/akka/io/TcpConnection.scala b/akka-actor/src/main/scala/akka/io/TcpConnection.scala index 0af6497668..ec866ae75f 100644 --- a/akka-actor/src/main/scala/akka/io/TcpConnection.scala +++ b/akka-actor/src/main/scala/akka/io/TcpConnection.scala @@ -109,6 +109,8 @@ private[io] abstract class TcpConnection(val channel: SocketChannel, /** used in subclasses to start the common machinery above once a channel is connected */ def completeConnect(commander: ActorRef, options: immutable.Traversable[SocketOption]): Unit = { + // Turn off Nagle's algorithm by default + channel.socket.setTcpNoDelay(true) options.foreach(_.afterConnect(channel.socket)) commander ! Connected( From e8e5845d178949a5e49314dda6705e02add77b43 Mon Sep 17 00:00:00 2001 From: Roland Date: Tue, 12 Mar 2013 17:55:58 +0100 Subject: [PATCH 16/47] genjavadoc is now off by default, see #3141 but it complains if you then try to build the docs --- project/Unidoc.scala | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/project/Unidoc.scala b/project/Unidoc.scala index 63af1cbaa5..8ffe19275e 100644 --- a/project/Unidoc.scala +++ b/project/Unidoc.scala @@ -8,14 +8,21 @@ object Unidoc { lazy val JavaDoc = config("genjavadoc") extend Compile - lazy val javadocSettings = inConfig(JavaDoc)(Defaults.configSettings) ++ Seq( - libraryDependencies += Dependencies.Compile.genjavadoc, - scalacOptions <+= target map (t => "-P:genjavadoc:out=" + t + "/java"), - packageDoc in Compile <<= packageDoc in JavaDoc, - sources in JavaDoc <<= (target, compile in Compile, sources in Compile) map ((t, c, s) => (t / "java" ** "*.java").get ++ s.filter(_.getName.endsWith(".java"))), - javacOptions in JavaDoc := Seq(), - artifactName in packageDoc in JavaDoc := ((sv, mod, art) => "" + mod.name + "_" + sv.binary + "-" + mod.revision + "-javadoc.jar") - ) + lazy val GenJavaDocEnabled = Option(sys.props("akka.genjavadoc.enabled")) filter (_.toLowerCase == "true") map (_ => true) getOrElse false + + lazy val javadocSettings = + inConfig(JavaDoc)(Defaults.configSettings) ++ Seq( + packageDoc in Compile <<= packageDoc in JavaDoc, + sources in JavaDoc <<= (target, compile in Compile, sources in Compile) map ((t, c, s) => + if (GenJavaDocEnabled) (t / "java" ** "*.java").get ++ s.filter(_.getName.endsWith(".java")) + else throw new RuntimeException("cannot build java docs without -Dakka.genjavadoc.enabled=true") + ), + javacOptions in JavaDoc := Seq(), + artifactName in packageDoc in JavaDoc := ((sv, mod, art) => "" + mod.name + "_" + sv.binary + "-" + mod.revision + "-javadoc.jar") + ) ++ (if (GenJavaDocEnabled) Seq( + libraryDependencies += Dependencies.Compile.genjavadoc, + scalacOptions <+= target map (t => "-P:genjavadoc:out=" + t + "/java") + ) else Nil) val unidocDirectory = SettingKey[File]("unidoc-directory") val unidocExclude = SettingKey[Seq[String]]("unidoc-exclude") From 565f3ce571b29965198269ea763ddfe7001cf8ca Mon Sep 17 00:00:00 2001 From: 2beaucoup Date: Thu, 14 Mar 2013 14:24:40 +0100 Subject: [PATCH 17/47] some IO doc fixes --- akka-docs/rst/scala/io.rst | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/akka-docs/rst/scala/io.rst b/akka-docs/rst/scala/io.rst index 6d9b5f259a..e6fd0d1456 100644 --- a/akka-docs/rst/scala/io.rst +++ b/akka-docs/rst/scala/io.rst @@ -90,7 +90,7 @@ together they are both stored within the resulting ``ByteString`` instead of cop such as ``drop`` and ``take`` return ``ByteString``\s that still reference the original ``Array``, but just change the offset and length that is visible. Great care has also been taken to make sure that the internal ``Array`` cannot be modified. Whenever a potentially unsafe ``Array`` is used to create a new ``ByteString`` a defensive copy is created. If -you require a ``ByteString`` that only blocks a much memory as necessary for it's content, use the ``compact`` method to +you require a ``ByteString`` that only blocks as much memory as necessary for it's content, use the ``compact`` method to get a ``CompactByteString`` instance. If the ``ByteString`` represented only a slice of the original array, this will result in copying all bytes in that slice. @@ -102,7 +102,7 @@ result in copying all bytes in that slice. Compatibility with java.io .......................... -A ``ByteStringBuilder`` can be wrapped in a ``java.io.OutputStream`` via the ``asOutputStream`` method. Likewise, ``ByteIterator`` can we wrapped in a ``java.io.InputStream`` via ``asInputStream``. Using these, ``akka.io`` applications can integrate legacy code based on ``java.io`` streams. +A ``ByteStringBuilder`` can be wrapped in a ``java.io.OutputStream`` via the ``asOutputStream`` method. Likewise, ``ByteIterator`` can be wrapped in a ``java.io.InputStream`` via ``asInputStream``. Using these, ``akka.io`` applications can integrate legacy code based on ``java.io`` streams. Encoding and decoding binary data .................................... @@ -127,7 +127,7 @@ Decoding of such frames can be efficiently implemented in the following fashion: .. includecode:: code/docs/io/BinaryCoding.scala :include: decoding -This implementation naturally follows the example data format. In a true Scala application, one might, of course, want use specialized immutable Short/Long/Double containers instead of mutable Arrays. +This implementation naturally follows the example data format. In a true Scala application one might of course want to use specialized immutable ``Short``/``Long``/``Double`` containers instead of mutable Arrays. After extracting data from a ``ByteIterator``, the remaining content can also be turned back into a ``ByteString`` using the ``toSeq`` method. No bytes are copied. Because of immutability the underlying bytes can be shared between both the @@ -136,7 +136,7 @@ the ``toSeq`` method. No bytes are copied. Because of immutability the underlyin .. includecode:: code/docs/io/BinaryCoding.scala :include: rest-to-seq -In general, conversions from ByteString to ByteIterator and vice versa are O(1) for non-chunked ByteStrings and (at worst) O(nChunks) for chunked ByteStrings. +In general, conversions from ``ByteString`` to ``ByteIterator`` and vice versa are O(1) for non-chunked ``ByteString``s and (at worst) O(nChunks) for chunked ``ByteString``s. Encoding of data also is very natural, using ``ByteStringBuilder`` @@ -210,7 +210,7 @@ connection close events, see :ref:`closing-connections-scala` below. Accepting connections ^^^^^^^^^^^^^^^^^^^^^ -To create a TCP server and listen for inbound connection, a ``Bind`` command has to be sent to the TCP manager. +To create a TCP server and listen for inbound connections, a ``Bind`` command has to be sent to the TCP manager. This will instruct the TCP manager to listen for TCP connections on a particular address. .. code-block:: scala @@ -237,7 +237,7 @@ has to be sent to the connection actor with the listener ``ActorRef`` as a param connectionActor ! Register(listener) Upon registration, the connection actor will watch the listener actor provided in the ``listener`` parameter. -If the listener stops, the connection is closed, and all resources allocated for the connection released. During the +If the listener stops, the connection is closed, and all resources allocated for the connection are released. During the connection lifetime the listener will receive various event notifications in the same way as in the outbound connection case. @@ -412,7 +412,7 @@ will always be the endpoint we originally connected to. .. note:: There is a small performance benefit in using connection based UDP API over the connectionless one. If there is a SecurityManager enabled on the system, every connectionless message send has to go through a security - check, while in the case of connection-based UDP the security check is cached after connect, thus writes does + check, while in the case of connection-based UDP the security check is cached after connect, thus writes do not suffer an additional performance penalty. Throttling Reads and Writes From 0a5d93a1947ab5589e25adab342868f8a7f71cbc Mon Sep 17 00:00:00 2001 From: Viktor Klang Date: Thu, 14 Mar 2013 15:20:38 +0100 Subject: [PATCH 18/47] #3147 - correcting ScalaDoc comment for ActorRef.isTerminated --- akka-actor/src/main/scala/akka/actor/ActorRef.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/akka-actor/src/main/scala/akka/actor/ActorRef.scala b/akka-actor/src/main/scala/akka/actor/ActorRef.scala index 843b750ca3..99aaaa69fa 100644 --- a/akka-actor/src/main/scala/akka/actor/ActorRef.scala +++ b/akka-actor/src/main/scala/akka/actor/ActorRef.scala @@ -118,7 +118,7 @@ abstract class ActorRef extends java.lang.Comparable[ActorRef] with Serializable /** * Is the actor shut down? * The contract is that if this method returns true, then it will never be false again. - * But you cannot rely on that it is alive if it returns true, since this by nature is a racy method. + * But you cannot rely on that it is alive if it returns false, since this by nature is a racy method. */ def isTerminated: Boolean From 0e2ecc8be4ccafd1c7d2e7717b43189f2dac28e6 Mon Sep 17 00:00:00 2001 From: Roland Date: Thu, 14 Mar 2013 19:36:18 +0100 Subject: [PATCH 19/47] remove occurrences of List.size==0 --- .../src/main/scala/akka/channels/macros/Tell.scala | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/akka-channels/src/main/scala/akka/channels/macros/Tell.scala b/akka-channels/src/main/scala/akka/channels/macros/Tell.scala index 01bf698a2c..edead29061 100644 --- a/akka-channels/src/main/scala/akka/channels/macros/Tell.scala +++ b/akka-channels/src/main/scala/akka/channels/macros/Tell.scala @@ -76,7 +76,7 @@ object Tell { import c.universe._ replyChannel.tpe match { case TypeRef(_, _, param :: Nil) ⇒ - (param, replyChannel, c.Expr(Select(replyChannel, "actorRef"))(c.universe.weakTypeTag[ActorRef])) + (param, replyChannel, c.Expr(Select(replyChannel, newTermName("actorRef")))(c.universe.weakTypeTag[ActorRef])) } } else abort(c, "no implicit sender ChannelRef found") } @@ -89,12 +89,12 @@ object Tell { if (msg.nonEmpty) { val u: c.universe.type = c.universe val replies = msg map (m ⇒ m -> (replyChannels(u)(chT, m) map (t ⇒ ignoreUnknown(t)))) - val missing = replies collect { case (k, v) if v.size == 0 ⇒ k } + val missing = replies collect { case (k, v) if v.isEmpty ⇒ k } if (missing.nonEmpty) error(c, s"target ChannelRef does not support messages of types ${missing mkString ", "} (at depth $depth)") else { val nextSend = replies.map(_._2).flatten map (m ⇒ m -> (replyChannels(u)(sndT, m) map (t ⇒ ignoreUnknown(t)))) - val nextMissing = nextSend collect { case (k, v) if v.size == 0 ⇒ k } + val nextMissing = nextSend collect { case (k, v) if v.isEmpty ⇒ k } if (nextMissing.nonEmpty) error(c, s"implicit sender `$sender` does not support messages of the reply types ${nextMissing mkString ", "} (at depth $depth)") else { From 70dfcb37a9bf548d52c80e8b65adee56257fb59f Mon Sep 17 00:00:00 2001 From: Roland Date: Thu, 14 Mar 2013 19:43:05 +0100 Subject: [PATCH 20/47] update to genjavadoc 0.4 and fix path concat in Unidoc, see #3144 --- project/AkkaBuild.scala | 2 +- project/Unidoc.scala | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/project/AkkaBuild.scala b/project/AkkaBuild.scala index eb9a9fc117..949f8184ee 100644 --- a/project/AkkaBuild.scala +++ b/project/AkkaBuild.scala @@ -879,7 +879,7 @@ object Dependencies { val sigar = "org.fusesource" % "sigar" % "1.6.4" // ApacheV2 // Compiler plugins - val genjavadoc = compilerPlugin("com.typesafe.genjavadoc" %% "genjavadoc-plugin" % "0.3" cross CrossVersion.full) // ApacheV2 + val genjavadoc = compilerPlugin("com.typesafe.genjavadoc" %% "genjavadoc-plugin" % "0.4" cross CrossVersion.full) // ApacheV2 // Test diff --git a/project/Unidoc.scala b/project/Unidoc.scala index 8ffe19275e..17a5861a7d 100644 --- a/project/Unidoc.scala +++ b/project/Unidoc.scala @@ -21,7 +21,7 @@ object Unidoc { artifactName in packageDoc in JavaDoc := ((sv, mod, art) => "" + mod.name + "_" + sv.binary + "-" + mod.revision + "-javadoc.jar") ) ++ (if (GenJavaDocEnabled) Seq( libraryDependencies += Dependencies.Compile.genjavadoc, - scalacOptions <+= target map (t => "-P:genjavadoc:out=" + t + "/java") + scalacOptions <+= target map (t => "-P:genjavadoc:out=" + (t / "java")) ) else Nil) val unidocDirectory = SettingKey[File]("unidoc-directory") From 950d19a377eb38225cb76c6e6322c9d98df50291 Mon Sep 17 00:00:00 2001 From: Viktor Klang Date: Thu, 14 Mar 2013 16:48:23 +0100 Subject: [PATCH 21/47] #3153 - 'Fixing' the ByteStringSpec by not having it generate now illegal combinations of data for copyToArray --- akka-actor-tests/src/test/scala/akka/util/ByteStringSpec.scala | 2 +- akka-sbt-plugin/sample/project/Build.scala | 2 +- project/AkkaBuild.scala | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/akka-actor-tests/src/test/scala/akka/util/ByteStringSpec.scala b/akka-actor-tests/src/test/scala/akka/util/ByteStringSpec.scala index 117de28242..6f94770dcc 100644 --- a/akka-actor-tests/src/test/scala/akka/util/ByteStringSpec.scala +++ b/akka-actor-tests/src/test/scala/akka/util/ByteStringSpec.scala @@ -39,7 +39,7 @@ class ByteStringSpec extends WordSpec with MustMatchers with Checkers { implicit val arbitraryByteStringSlice: Arbitrary[ByteStringSlice] = Arbitrary { for { xs ← arbitraryByteString.arbitrary - from ← choose(0, xs.length) + from ← choose(0, xs.length - 1) until ← choose(from, xs.length) } yield (xs, from, until) } diff --git a/akka-sbt-plugin/sample/project/Build.scala b/akka-sbt-plugin/sample/project/Build.scala index c4c37dde1b..7c505d9125 100644 --- a/akka-sbt-plugin/sample/project/Build.scala +++ b/akka-sbt-plugin/sample/project/Build.scala @@ -7,7 +7,7 @@ import akka.sbt.AkkaKernelPlugin.{ Dist, outputDirectory, distJvmOptions} object HelloKernelBuild extends Build { val Organization = "akka.sample" val Version = "2.2-SNAPSHOT" - val ScalaVersion = "2.10.0" + val ScalaVersion = "2.10.1" lazy val HelloKernel = Project( id = "hello-kernel", diff --git a/project/AkkaBuild.scala b/project/AkkaBuild.scala index eb9a9fc117..7974cc6951 100644 --- a/project/AkkaBuild.scala +++ b/project/AkkaBuild.scala @@ -39,7 +39,7 @@ object AkkaBuild extends Build { organization := "com.typesafe.akka", version := "2.2-SNAPSHOT", // Also change ScalaVersion in akka-sbt-plugin/sample/project/Build.scala - scalaVersion := System.getProperty("akka.scalaVersion", "2.10.0") + scalaVersion := System.getProperty("akka.scalaVersion", "2.10.1") ) lazy val akka = Project( From 267060a2950e95d07b96032d7c444f80edeb7fdf Mon Sep 17 00:00:00 2001 From: Roland Date: Fri, 15 Mar 2013 09:07:37 +0100 Subject: [PATCH 22/47] filter resume-warning in SupervisorHierarchySpec, see #3151 --- .../src/test/scala/akka/actor/SupervisorHierarchySpec.scala | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/akka-actor-tests/src/test/scala/akka/actor/SupervisorHierarchySpec.scala b/akka-actor-tests/src/test/scala/akka/actor/SupervisorHierarchySpec.scala index 7200c4cd7e..a36af3d895 100644 --- a/akka-actor-tests/src/test/scala/akka/actor/SupervisorHierarchySpec.scala +++ b/akka-actor-tests/src/test/scala/akka/actor/SupervisorHierarchySpec.scala @@ -61,7 +61,7 @@ object SupervisorHierarchySpec { case class Event(msg: Any, identity: Long) { val time: Long = System.nanoTime } case class ErrorLog(msg: String, log: Vector[Event]) case class Failure(directive: Directive, stop: Boolean, depth: Int, var failPre: Int, var failPost: Int, val failConstr: Int, stopKids: Int) - extends RuntimeException with NoStackTrace { + extends RuntimeException("Failure") with NoStackTrace { override def toString = productPrefix + productIterator.mkString("(", ",", ")") } case class Dump(level: Int) @@ -844,6 +844,7 @@ class SupervisorHierarchySpec extends AkkaSpec(SupervisorHierarchySpec.config) w "survive being stressed" in { system.eventStream.publish(Mute( EventFilter[Failure](), + EventFilter.warning("Failure"), EventFilter[ActorInitializationException](), EventFilter[NoSuchElementException]("head of empty list"), EventFilter.error(start = "changing Resume into Restart"), From 7804df7743512876e4df39bee1b393c649b3aad7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Endre=20S=C3=A1ndor=20Varga?= Date: Fri, 15 Mar 2013 11:08:42 +0100 Subject: [PATCH 23/47] Updated documentation --- akka-docs/rst/java/io.rst | 5 +++++ akka-docs/rst/scala/io.rst | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/akka-docs/rst/java/io.rst b/akka-docs/rst/java/io.rst index e6984de46d..ac78ee052e 100644 --- a/akka-docs/rst/java/io.rst +++ b/akka-docs/rst/java/io.rst @@ -127,6 +127,11 @@ When connecting, it is also possible to set various socket options or specify a .. includecode:: code/docs/io/IODocTest.java#connect-with-options +.. note:: + The SO_NODELAY (TCP_NODELAY on Windows) socket option defaults to true in Akka, independently of the OS default + settings. This setting disables Nagle's algorithm considerably improving latency for most applications. This setting + could be overridden by passing ``SO.TcpNoDelay(false)`` in the list of socket options of the ``Connect`` message. + After issuing the ``Connect`` command the TCP manager spawns a worker actor to handle commands related to the connection. This worker actor will reveal itself by replying with a ``Connected`` message to the actor who sent the ``Connect`` command. diff --git a/akka-docs/rst/scala/io.rst b/akka-docs/rst/scala/io.rst index 6d9b5f259a..fa91868e81 100644 --- a/akka-docs/rst/scala/io.rst +++ b/akka-docs/rst/scala/io.rst @@ -176,6 +176,11 @@ When connecting, it is also possible to set various socket options or specify a IO(Tcp) ! Connect(remoteSocketAddress, Some(localSocketAddress), List(SO.KeepAlive(true))) +.. note:: + The SO_NODELAY (TCP_NODELAY on Windows) socket option defaults to true in Akka, independently of the OS default + settings. This setting disables Nagle's algorithm considerably improving latency for most applications. This setting + could be overridden by passing ``SO.TcpNoDelay(false)`` in the list of socket options of the ``Connect`` message. + After issuing the ``Connect`` command the TCP manager spawns a worker actor to handle commands related to the connection. This worker actor will reveal itself by replying with a ``Connected`` message to the actor who sent the ``Connect`` command. From db5594b08b9ea0117eb7d88fd328ff61b39d2038 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Antonsson?= Date: Fri, 15 Mar 2013 13:11:13 +0100 Subject: [PATCH 24/47] DOC Change the HTTP section. See #3155 --- akka-docs/rst/modules/http.rst | 24 ++++++------------------ 1 file changed, 6 insertions(+), 18 deletions(-) diff --git a/akka-docs/rst/modules/http.rst b/akka-docs/rst/modules/http.rst index 9a5f1f3a47..6c52779936 100644 --- a/akka-docs/rst/modules/http.rst +++ b/akka-docs/rst/modules/http.rst @@ -3,27 +3,15 @@ HTTP #### -Play2 Mini -========== +Play +==== -The Akka team recommends the `Play2 Mini `_ framework when building RESTful -service applications that integrates with Akka. It provides a REST API on top of `Play2 `_. +The `Play framework `_ is built using Akka, and is well suited for building both full web applications as well as REST services. -Getting started ---------------- - -Easiest way to get started with `Play2 Mini `_ is to use the -G8 project templates, as described in the `Play2 Mini Documentation `_. - -If you already have an Akka project and want to add Play2 Mini, you must first add the following to -your ``libraryDependencies``:: - - libraryDependencies += "com.typesafe" %% "play-mini" % "" - -In case you need to start Play2 Mini programatically you can use:: - - play.core.server.NettyServer.main(Array()) +Spray +===== +The `Spray toolkit `_ is built using Akka, and is a minimalistic HTTP/REST layer. Akka Mist ========= From ed40dff7d7353c5cb15b7408ec049116081cb1fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Antonsson?= Date: Mon, 18 Mar 2013 11:44:54 +0100 Subject: [PATCH 25/47] Release should build and copy javadoc automatically. See #3158 --- project/AkkaBuild.scala | 5 ++--- project/Dist.scala | 2 +- project/Release.scala | 3 ++- project/Unidoc.scala | 10 +++++++--- project/scripts/release | 4 ++-- 5 files changed, 14 insertions(+), 10 deletions(-) diff --git a/project/AkkaBuild.scala b/project/AkkaBuild.scala index 2af01d35ec..218dc0c188 100644 --- a/project/AkkaBuild.scala +++ b/project/AkkaBuild.scala @@ -25,7 +25,7 @@ import java.io.{PrintWriter, InputStreamReader, FileInputStream, File} import java.nio.charset.Charset import java.util.Properties import annotation.tailrec -import Unidoc.{ JavaDoc, javadocSettings, junidocSources, unidoc, unidocExclude } +import Unidoc.{ JavaDoc, javadocSettings, junidocSources, sunidoc, unidocExclude } object AkkaBuild extends Build { System.setProperty("akka.mode", "test") // Is there better place for this? @@ -52,7 +52,6 @@ object AkkaBuild extends Build { parallelExecution in GlobalScope := System.getProperty("akka.parallelExecution", "false").toBoolean, Publish.defaultPublishTo in ThisBuild <<= crossTarget / "repository", unidocExclude := Seq(samples.id, channelsTests.id, remoteTests.id), - unidoc <<= (unidoc, doc in JavaDoc) map ((u, d) => u), sources in JavaDoc <<= junidocSources, javacOptions in JavaDoc := Seq(), artifactName in packageDoc in JavaDoc := ((sv, mod, art) => "" + mod.name + "_" + sv.binary + "-" + mod.revision + "-javadoc.jar"), @@ -719,7 +718,7 @@ object AkkaBuild extends Build { lazy val unidocScaladocSettings: Seq[sbt.Setting[_]]= { Seq(scalacOptions in doc ++= scaladocOptions) ++ (if (scaladocDiagramsEnabled) - Seq(unidoc ~= scaladocVerifier) + Seq(sunidoc ~= scaladocVerifier) else Seq.empty) } diff --git a/project/Dist.scala b/project/Dist.scala index 86d4263346..5a7bf45b52 100644 --- a/project/Dist.scala +++ b/project/Dist.scala @@ -29,7 +29,7 @@ object Dist { distLibJars <<= (thisProjectRef, buildStructure, distExclude) flatMap aggregated(packageBin.task in Compile), distSrcJars <<= (thisProjectRef, buildStructure, distExclude) flatMap aggregated(packageSrc.task in Compile), distDocJars <<= (thisProjectRef, buildStructure, distExclude) flatMap aggregated(packageDoc.task in Compile), - distSources <<= (distDependencies, distLibJars, distSrcJars, distDocJars, Unidoc.unidoc, generate in Sphinx in docsProject) map DistSources, + distSources <<= (distDependencies, distLibJars, distSrcJars, distDocJars, Unidoc.sunidoc, generate in Sphinx in docsProject) map DistSources, distDirectory <<= crossTarget / "dist", distUnzipped <<= distDirectory / "unzipped", distFile <<= (distDirectory, version) { (dir, v) => dir / ("akka-" + v + ".zip") }, diff --git a/project/Release.scala b/project/Release.scala index 6365dc0285..697fbb39e6 100644 --- a/project/Release.scala +++ b/project/Release.scala @@ -23,13 +23,14 @@ object Release { val projectRef = extracted.get(thisProjectRef) val repo = extracted.get(Publish.defaultPublishTo) val state1 = extracted.runAggregated(publish in projectRef, state) - val (state2, api) = extracted.runTask(Unidoc.unidoc, state1) + val (state2, (api, japi)) = extracted.runTask(Unidoc.unidoc, state1) val (state3, docs) = extracted.runTask(generate in Sphinx, state2) val (state4, dist) = extracted.runTask(Dist.dist, state3) IO.delete(release) IO.createDirectory(release) IO.copyDirectory(repo, release / "releases") IO.copyDirectory(api, release / "api" / "akka" / releaseVersion) + IO.copyDirectory(japi, release / "japi" / "akka" / releaseVersion) IO.copyDirectory(docs, release / "docs" / "akka" / releaseVersion) IO.copyFile(dist, release / "downloads" / dist.name) state4 diff --git a/project/Unidoc.scala b/project/Unidoc.scala index 17a5861a7d..31fb756b3e 100644 --- a/project/Unidoc.scala +++ b/project/Unidoc.scala @@ -30,7 +30,9 @@ object Unidoc { val unidocSources = TaskKey[Seq[File]]("unidoc-sources") val unidocAllClasspaths = TaskKey[Seq[Classpath]]("unidoc-all-classpaths") val unidocClasspath = TaskKey[Seq[File]]("unidoc-classpath") - val unidoc = TaskKey[File]("unidoc", "Create unified scaladoc for all aggregates") + val unidoc = TaskKey[(File, File)]("unidoc", "Create unified scaladoc and javadoc for all aggregates") + val sunidoc = TaskKey[File]("sunidoc", "Create unified scaladoc for all aggregates") + val junidoc = TaskKey[File]("junidoc", "Create unified javadoc for all aggregates") val junidocAllSources = TaskKey[Seq[Seq[File]]]("junidoc-all-sources") val junidocSources = TaskKey[Seq[File]]("junidoc-sources") @@ -43,7 +45,9 @@ object Unidoc { unidocClasspath <<= unidocAllClasspaths map { _.flatten.map(_.data).distinct }, junidocAllSources <<= (thisProjectRef, buildStructure, unidocExclude) flatMap allSources(JavaDoc), junidocSources <<= junidocAllSources map { _.flatten }, - unidoc <<= unidocTask + sunidoc <<= sunidocTask, + junidoc <<= (doc in JavaDoc), + unidoc <<= (sunidoc, junidoc) map ((s, t) ⇒ (s, t)) ) def allSources(conf: Configuration)(projectRef: ProjectRef, structure: Load.BuildStructure, exclude: Seq[String]): Task[Seq[Seq[File]]] = { @@ -64,7 +68,7 @@ object Unidoc { } } - def unidocTask: Initialize[Task[File]] = { + def sunidocTask: Initialize[Task[File]] = { (compilers, cacheDirectory, unidocSources, unidocClasspath, unidocDirectory, scalacOptions in doc, streams) map { (compilers, cache, sources, classpath, target, options, s) => { val scaladoc = new Scaladoc(100, compilers.scalac) diff --git a/project/scripts/release b/project/scripts/release index c660aaeeda..8ba071196f 100755 --- a/project/scripts/release +++ b/project/scripts/release @@ -290,9 +290,9 @@ fi # build the release echolog "Building the release..." if [ ! $dry_run ]; then - RELEASE_OPT="-Dpublish.maven.central=true" + RELEASE_OPT="-Dakka.genjavadoc.enabled=true -Dpublish.maven.central=true" else - RELEASE_OPT="" + RELEASE_OPT="-Dakka.genjavadoc.enabled=true" fi try sbt $RELEASE_OPT build-release echolog "Creating gzipped tar download..." From 7eac88f372e08ce4812703cb0726e6c0453609d4 Mon Sep 17 00:00:00 2001 From: Patrik Nordwall Date: Thu, 14 Mar 2013 20:32:43 +0100 Subject: [PATCH 26/47] Cluster node roles, see #3049 * Config of node roles cluster.role * Cluster router configurable with use-role * RoleLeaderChanged event * Cluster singleton per role * Cluster only starts once all required per-role node counts are reached, role..min-nr-of-members config * Update documentation and make use of the roles in the examples --- .../src/main/resources/reference.conf | 20 +++ .../src/main/scala/akka/cluster/Cluster.scala | 11 ++ .../cluster/ClusterActorRefProvider.scala | 4 +- .../scala/akka/cluster/ClusterDaemon.scala | 50 +++--- .../scala/akka/cluster/ClusterEvent.scala | 58 ++++++- .../scala/akka/cluster/ClusterReadView.scala | 7 +- .../scala/akka/cluster/ClusterSettings.scala | 8 + .../src/main/scala/akka/cluster/Gossip.scala | 16 +- .../src/main/scala/akka/cluster/Member.scala | 28 ++-- .../cluster/routing/ClusterRouterConfig.scala | 45 ++++-- .../akka/cluster/MinMembersBeforeUpSpec.scala | 147 ++++++++++++------ .../scala/akka/cluster/TransitionSpec.scala | 6 +- .../AdaptiveLoadBalancingRouterSpec.scala | 2 +- .../ClusterConsistentHashingRouterSpec.scala | 4 +- .../ClusterRoundRobinRoutedActorSpec.scala | 40 ++++- .../akka/cluster/ClusterConfigSpec.scala | 2 + .../akka/cluster/ClusterDeployerSpec.scala | 4 +- .../ClusterDomainEventPublisherSpec.scala | 32 ++-- .../akka/cluster/ClusterDomainEventSpec.scala | 91 +++++++---- .../test/scala/akka/cluster/GossipSpec.scala | 24 +-- .../akka/cluster/MemberOrderingSpec.scala | 62 ++++---- akka-contrib/docs/cluster-singleton.rst | 33 ++-- .../pattern/ClusterSingletonManager.scala | 66 ++++++-- .../ClusterSingletonManagerChaosSpec.scala | 3 +- .../pattern/ClusterSingletonManagerSpec.scala | 86 ++++++---- akka-docs/rst/cluster/cluster-usage-java.rst | 69 +++++--- akka-docs/rst/cluster/cluster-usage-scala.rst | 54 ++++--- .../factorial/japi/FactorialBackendMain.java | 14 +- .../factorial/japi/FactorialFrontend.java | 14 +- .../factorial/japi/FactorialFrontendMain.java | 8 +- .../cluster/stats/japi/StatsFacade.java | 15 +- .../cluster/stats/japi/StatsSampleClient.java | 5 +- .../stats/japi/StatsSampleClientMain.java | 1 + .../cluster/stats/japi/StatsSampleMain.java | 32 ++-- .../japi/StatsSampleOneMasterClientMain.java | 16 +- .../stats/japi/StatsSampleOneMasterMain.java | 33 ++-- .../cluster/stats/japi/StatsService.java | 6 +- .../japi/TransformationBackend.java | 5 +- .../src/main/resources/application.conf | 31 ++++ .../src/main/resources/factorial.conf | 9 +- .../cluster/factorial/FactorialSample.scala | 24 +-- .../sample/cluster/stats/StatsSample.scala | 87 ++++------- .../transformation/TransformationSample.scala | 33 ++-- .../stats/StatsSampleSingleMasterSpec.scala | 10 +- .../cluster/stats/StatsSampleSpec.scala | 8 +- .../stats/japi/StatsSampleJapiSpec.scala | 8 +- .../StatsSampleSingleMasterJapiSpec.scala | 9 +- .../TransformationSampleSpec.scala | 5 + .../japi/TransformationSampleJapiSpec.scala | 6 + 49 files changed, 870 insertions(+), 481 deletions(-) diff --git a/akka-cluster/src/main/resources/reference.conf b/akka-cluster/src/main/resources/reference.conf index b2126409c1..1f5c5cebe9 100644 --- a/akka-cluster/src/main/resources/reference.conf +++ b/akka-cluster/src/main/resources/reference.conf @@ -28,6 +28,23 @@ akka { # formed in case of network partition. auto-down = off + # The roles of this member. List of strings, e.g. roles = ["A", "B"]. + # The roles are part of the membership information and can be used by + # routers or other services to distribute work to certain member types, + # e.g. front-end and back-end nodes. + roles = [] + + role { + # Minimum required number of members of a certain role before the leader changes + # member status of 'Joining' members to 'Up'. Typically used together with + # 'Cluster.registerOnMemberUp' to defer some action, such as starting actors, + # until the cluster has reached a certain size. + # E.g. to require 2 nodes with role 'frontend' and 3 nodes with role 'backend': + # frontend.min-nr-of-members = 2 + # backend.min-nr-of-members = 3 + #.min-nr-of-members = 1 + } + # Minimum required number of members before the leader changes member status # of 'Joining' members to 'Up'. Typically used together with # 'Cluster.registerOnMemberUp' to defer some action, such as starting actors, @@ -201,6 +218,9 @@ akka { # when routees-path is defined. routees-path = "" + # Use members with specified role, or all members if undefined. + use-role = "" + } } diff --git a/akka-cluster/src/main/scala/akka/cluster/Cluster.scala b/akka-cluster/src/main/scala/akka/cluster/Cluster.scala index da34727370..9221d81295 100644 --- a/akka-cluster/src/main/scala/akka/cluster/Cluster.scala +++ b/akka-cluster/src/main/scala/akka/cluster/Cluster.scala @@ -73,6 +73,17 @@ class Cluster(val system: ExtendedActorSystem) extends Extension { format(system, other.getClass.getName)) } + /** + * roles that this member has + */ + def selfRoles: Set[String] = settings.Roles + + /** + * Java API: roles that this member has + */ + def getSelfRoles: java.util.Set[String] = + scala.collection.JavaConverters.setAsJavaSetConverter(selfRoles).asJava + private val _isTerminated = new AtomicBoolean(false) private val log = Logging(system, "Cluster") diff --git a/akka-cluster/src/main/scala/akka/cluster/ClusterActorRefProvider.scala b/akka-cluster/src/main/scala/akka/cluster/ClusterActorRefProvider.scala index 5fb8376eea..0d06d4d64b 100644 --- a/akka-cluster/src/main/scala/akka/cluster/ClusterActorRefProvider.scala +++ b/akka-cluster/src/main/scala/akka/cluster/ClusterActorRefProvider.scala @@ -116,11 +116,13 @@ private[akka] class ClusterDeployer(_settings: ActorSystem.Settings, _pm: Dynami if (deploy.routerConfig.isInstanceOf[RemoteRouterConfig]) throw new ConfigurationException("Cluster deployment can't be combined with [%s]".format(deploy.routerConfig)) + import ClusterRouterSettings.useRoleOption val clusterRouterSettings = ClusterRouterSettings( totalInstances = deploy.config.getInt("nr-of-instances"), maxInstancesPerNode = deploy.config.getInt("cluster.max-nr-of-instances-per-node"), allowLocalRoutees = deploy.config.getBoolean("cluster.allow-local-routees"), - routeesPath = deploy.config.getString("cluster.routees-path")) + routeesPath = deploy.config.getString("cluster.routees-path"), + useRole = useRoleOption(deploy.config.getString("cluster.use-role"))) Some(deploy.copy( routerConfig = ClusterRouterConfig(deploy.routerConfig, clusterRouterSettings), scope = ClusterScope)) diff --git a/akka-cluster/src/main/scala/akka/cluster/ClusterDaemon.scala b/akka-cluster/src/main/scala/akka/cluster/ClusterDaemon.scala index 6a7416476e..64854f243b 100644 --- a/akka-cluster/src/main/scala/akka/cluster/ClusterDaemon.scala +++ b/akka-cluster/src/main/scala/akka/cluster/ClusterDaemon.scala @@ -37,7 +37,7 @@ object ClusterUserAction { * Command to join the cluster. Sent when a node (represented by 'address') * wants to join another node (the receiver). */ - case class Join(address: Address) extends ClusterMessage + case class Join(address: Address, roles: Set[String]) extends ClusterMessage /** * Command to leave the cluster. @@ -288,20 +288,20 @@ private[cluster] final class ClusterCoreDaemon(publisher: ActorRef) extends Acto } def initialized: Actor.Receive = { - case msg: GossipEnvelope ⇒ receiveGossip(msg) - case GossipTick ⇒ gossip() - case ReapUnreachableTick ⇒ reapUnreachableMembers() - case LeaderActionsTick ⇒ leaderActions() - case PublishStatsTick ⇒ publishInternalStats() - case InitJoin ⇒ initJoin() - case JoinTo(address) ⇒ join(address) - case ClusterUserAction.Join(address) ⇒ joining(address) - case ClusterUserAction.Down(address) ⇒ downing(address) - case ClusterUserAction.Leave(address) ⇒ leaving(address) - case Exit(address) ⇒ exiting(address) - case Remove(address) ⇒ removing(address) - case SendGossipTo(address) ⇒ gossipTo(address) - case msg: SubscriptionMessage ⇒ publisher forward msg + case msg: GossipEnvelope ⇒ receiveGossip(msg) + case GossipTick ⇒ gossip() + case ReapUnreachableTick ⇒ reapUnreachableMembers() + case LeaderActionsTick ⇒ leaderActions() + case PublishStatsTick ⇒ publishInternalStats() + case InitJoin ⇒ initJoin() + case JoinTo(address) ⇒ join(address) + case ClusterUserAction.Join(address, roles) ⇒ joining(address, roles) + case ClusterUserAction.Down(address) ⇒ downing(address) + case ClusterUserAction.Leave(address) ⇒ leaving(address) + case Exit(address) ⇒ exiting(address) + case Remove(address) ⇒ removing(address) + case SendGossipTo(address) ⇒ gossipTo(address) + case msg: SubscriptionMessage ⇒ publisher forward msg } @@ -366,16 +366,16 @@ private[cluster] final class ClusterCoreDaemon(publisher: ActorRef) extends Acto context.become(initialized) if (address == selfAddress) - joining(address) + joining(address, cluster.selfRoles) else - clusterCore(address) ! ClusterUserAction.Join(selfAddress) + clusterCore(address) ! ClusterUserAction.Join(selfAddress, cluster.selfRoles) } } /** * State transition to JOINING - new node joining. */ - def joining(node: Address): Unit = { + def joining(node: Address, roles: Set[String]): Unit = { if (node.protocol != selfAddress.protocol) log.warning("Member with wrong protocol tried to join, but was ignored, expected [{}] but was [{}]", selfAddress.protocol, node.protocol) @@ -396,7 +396,7 @@ private[cluster] final class ClusterCoreDaemon(publisher: ActorRef) extends Acto // add joining node as Joining // add self in case someone else joins before self has joined (Set discards duplicates) - val newMembers = localMembers + Member(node, Joining) + Member(selfAddress, Joining) + val newMembers = localMembers + Member(node, Joining, roles) + Member(selfAddress, Joining, cluster.selfRoles) val newGossip = latestGossip copy (members = newMembers) val versionedGossip = newGossip :+ vclockNode @@ -404,7 +404,7 @@ private[cluster] final class ClusterCoreDaemon(publisher: ActorRef) extends Acto latestGossip = seenVersionedGossip - log.info("Cluster Node [{}] - Node [{}] is JOINING", selfAddress, node) + log.info("Cluster Node [{}] - Node [{}] is JOINING, roles [{}]", selfAddress, node, roles.mkString(", ")) if (node != selfAddress) { gossipTo(node) } @@ -419,7 +419,7 @@ private[cluster] final class ClusterCoreDaemon(publisher: ActorRef) extends Acto */ def leaving(address: Address): Unit = { if (latestGossip.members.exists(_.address == address)) { // only try to update if the node is available (in the member ring) - val newMembers = latestGossip.members map { member ⇒ if (member.address == address) Member(address, Leaving) else member } // mark node as LEAVING + val newMembers = latestGossip.members map { m ⇒ if (m.address == address) m.copy(status = Leaving) else m } // mark node as LEAVING val newGossip = latestGossip copy (members = newMembers) val versionedGossip = newGossip :+ vclockNode @@ -636,8 +636,12 @@ private[cluster] final class ClusterCoreDaemon(publisher: ActorRef) extends Acto if (localGossip.convergence) { // we have convergence - so we can't have unreachable nodes - val numberOfMembers = localMembers.size - def isJoiningToUp(m: Member): Boolean = m.status == Joining && numberOfMembers >= MinNrOfMembers + def enoughMembers: Boolean = { + localMembers.size >= MinNrOfMembers && MinNrOfMembersOfRole.forall { + case (role, threshold) ⇒ localMembers.count(_.hasRole(role)) >= threshold + } + } + def isJoiningToUp(m: Member): Boolean = m.status == Joining && enoughMembers // transform the node member ring val newMembers = localMembers collect { diff --git a/akka-cluster/src/main/scala/akka/cluster/ClusterEvent.scala b/akka-cluster/src/main/scala/akka/cluster/ClusterEvent.scala index 6b61a5c7d8..84b174ee99 100644 --- a/akka-cluster/src/main/scala/akka/cluster/ClusterEvent.scala +++ b/akka-cluster/src/main/scala/akka/cluster/ClusterEvent.scala @@ -35,7 +35,8 @@ object ClusterEvent { members: immutable.SortedSet[Member] = immutable.SortedSet.empty, unreachable: Set[Member] = Set.empty, seenBy: Set[Address] = Set.empty, - leader: Option[Address] = None) extends ClusterDomainEvent { + leader: Option[Address] = None, + roleLeaderMap: Map[String, Option[Address]] = Map.empty) extends ClusterDomainEvent { /** * Java API: get current member list. @@ -61,6 +62,28 @@ object ClusterEvent { * Java API: get address of current leader, or null if none */ def getLeader: Address = leader orNull + + /** + * All node roles in the cluster + */ + def allRoles: Set[String] = roleLeaderMap.keySet + + /** + * Java API: All node roles in the cluster + */ + def getAllRoles: java.util.Set[String] = + scala.collection.JavaConverters.setAsJavaSetConverter(allRoles).asJava + + /** + * get address of current leader, if any, within the role set + */ + def roleLeader(role: String): Option[Address] = roleLeaderMap.getOrElse(role, None) + + /** + * Java API: get address of current leader within the role set, + * or null if no node with that role + */ + def getRoleLeader(role: String): Address = roleLeaderMap.get(role).flatten.orNull } /** @@ -107,6 +130,18 @@ object ClusterEvent { def getLeader: Address = leader orNull } + /** + * First member (leader) of the members within a role set changed. + * Published when the state change is first seen on a node. + */ + case class RoleLeaderChanged(role: String, leader: Option[Address]) extends ClusterDomainEvent { + /** + * Java API + * @return address of current leader, or null if none + */ + def getLeader: Address = leader orNull + } + /** * A member is considered as unreachable by the failure detector. */ @@ -184,9 +219,22 @@ object ClusterEvent { /** * INTERNAL API */ - private[cluster] def diffLeader(oldGossip: Gossip, newGossip: Gossip): immutable.Seq[LeaderChanged] = - if (newGossip.leader != oldGossip.leader) List(LeaderChanged(newGossip.leader)) + private[cluster] def diffLeader(oldGossip: Gossip, newGossip: Gossip): immutable.Seq[LeaderChanged] = { + val newLeader = newGossip.leader + if (newLeader != oldGossip.leader) List(LeaderChanged(newLeader)) else Nil + } + + /** + * INTERNAL API + */ + private[cluster] def diffRolesLeader(oldGossip: Gossip, newGossip: Gossip): Set[RoleLeaderChanged] = { + for { + role ← (oldGossip.allRoles ++ newGossip.allRoles) + newLeader = newGossip.roleLeader(role) + if newLeader != oldGossip.roleLeader(role) + } yield RoleLeaderChanged(role, newLeader) + } /** * INTERNAL API @@ -242,7 +290,8 @@ private[cluster] final class ClusterDomainEventPublisher extends Actor with Acto members = latestGossip.members, unreachable = latestGossip.overview.unreachable, seenBy = latestGossip.seenBy, - leader = latestGossip.leader) + leader = latestGossip.leader, + roleLeaderMap = latestGossip.allRoles.map(r ⇒ r -> latestGossip.roleLeader(r))(collection.breakOut)) receiver match { case Some(ref) ⇒ ref ! state case None ⇒ publish(state) @@ -275,6 +324,7 @@ private[cluster] final class ClusterDomainEventPublisher extends Actor with Acto } } diffLeader(oldGossip, newGossip) foreach publish + diffRolesLeader(oldGossip, newGossip) foreach publish // publish internal SeenState for testing purposes diffSeen(oldGossip, newGossip) foreach publish } diff --git a/akka-cluster/src/main/scala/akka/cluster/ClusterReadView.scala b/akka-cluster/src/main/scala/akka/cluster/ClusterReadView.scala index 807fe85e52..39eaa63bfd 100644 --- a/akka-cluster/src/main/scala/akka/cluster/ClusterReadView.scala +++ b/akka-cluster/src/main/scala/akka/cluster/ClusterReadView.scala @@ -57,7 +57,10 @@ private[akka] class ClusterReadView(cluster: Cluster) extends Closeable { // replace current member with new member (might have different status, only address is used in equals) state = state.copy(members = state.members - event.member + event.member, unreachable = state.unreachable - event.member) - case LeaderChanged(leader) ⇒ state = state.copy(leader = leader) + case LeaderChanged(leader) ⇒ + state = state.copy(leader = leader) + case RoleLeaderChanged(role, leader) ⇒ + state = state.copy(roleLeaderMap = state.roleLeaderMap + (role -> leader)) case s: CurrentClusterState ⇒ state = s case CurrentInternalStats(stats) ⇒ _latestStats = stats case ClusterMetricsChanged(nodes) ⇒ _clusterMetrics = nodes @@ -68,7 +71,7 @@ private[akka] class ClusterReadView(cluster: Cluster) extends Closeable { def self: Member = { state.members.find(_.address == selfAddress).orElse(state.unreachable.find(_.address == selfAddress)). - getOrElse(Member(selfAddress, MemberStatus.Removed)) + getOrElse(Member(selfAddress, MemberStatus.Removed, cluster.selfRoles)) } /** diff --git a/akka-cluster/src/main/scala/akka/cluster/ClusterSettings.scala b/akka-cluster/src/main/scala/akka/cluster/ClusterSettings.scala index 27bde94a7f..64d02ca03d 100644 --- a/akka-cluster/src/main/scala/akka/cluster/ClusterSettings.scala +++ b/akka-cluster/src/main/scala/akka/cluster/ClusterSettings.scala @@ -5,6 +5,7 @@ package akka.cluster import scala.collection.immutable import com.typesafe.config.Config +import com.typesafe.config.ConfigObject import scala.concurrent.duration.Duration import java.util.concurrent.TimeUnit.MILLISECONDS import akka.ConfigurationException @@ -50,9 +51,16 @@ class ClusterSettings(val config: Config, val systemName: String) { final val PublishStatsInterval: FiniteDuration = Duration(cc.getMilliseconds("publish-stats-interval"), MILLISECONDS) final val AutoJoin: Boolean = cc.getBoolean("auto-join") final val AutoDown: Boolean = cc.getBoolean("auto-down") + final val Roles: Set[String] = immutableSeq(cc.getStringList("roles")).toSet final val MinNrOfMembers: Int = { cc.getInt("min-nr-of-members") } requiring (_ > 0, "min-nr-of-members must be > 0") + final val MinNrOfMembersOfRole: Map[String, Int] = { + import scala.collection.JavaConverters._ + cc.getConfig("role").root.asScala.collect { + case (key, value: ConfigObject) ⇒ (key -> value.toConfig.getInt("min-nr-of-members")) + }.toMap + } final val JmxEnabled: Boolean = cc.getBoolean("jmx.enabled") final val UseDispatcher: String = cc.getString("use-dispatcher") match { case "" ⇒ Dispatchers.DefaultDispatcherId diff --git a/akka-cluster/src/main/scala/akka/cluster/Gossip.scala b/akka-cluster/src/main/scala/akka/cluster/Gossip.scala index 2d3ba52570..a5eac87bfb 100644 --- a/akka-cluster/src/main/scala/akka/cluster/Gossip.scala +++ b/akka-cluster/src/main/scala/akka/cluster/Gossip.scala @@ -185,12 +185,18 @@ private[cluster] case class Gossip( def isLeader(address: Address): Boolean = leader == Some(address) - def leader: Option[Address] = { - if (members.isEmpty) None - else members.find(m ⇒ m.status != Joining && m.status != Exiting && m.status != Down). - orElse(Some(members.min(Member.leaderStatusOrdering))).map(_.address) + def leader: Option[Address] = leaderOf(members) + + def roleLeader(role: String): Option[Address] = leaderOf(members.filter(_.hasRole(role))) + + private def leaderOf(mbrs: immutable.SortedSet[Member]): Option[Address] = { + if (mbrs.isEmpty) None + else mbrs.find(m ⇒ m.status != Joining && m.status != Exiting && m.status != Down). + orElse(Some(mbrs.min(Member.leaderStatusOrdering))).map(_.address) } + def allRoles: Set[String] = members.flatMap(_.roles) + def isSingletonCluster: Boolean = members.size == 1 /** @@ -201,7 +207,7 @@ private[cluster] case class Gossip( def member(address: Address): Member = { members.find(_.address == address).orElse(overview.unreachable.find(_.address == address)). - getOrElse(Member(address, Removed)) + getOrElse(Member(address, Removed, Set.empty)) } override def toString = diff --git a/akka-cluster/src/main/scala/akka/cluster/Member.scala b/akka-cluster/src/main/scala/akka/cluster/Member.scala index db3b126571..8da1d7141c 100644 --- a/akka-cluster/src/main/scala/akka/cluster/Member.scala +++ b/akka-cluster/src/main/scala/akka/cluster/Member.scala @@ -12,15 +12,26 @@ import akka.actor.Address import MemberStatus._ /** - * Represents the address and the current status of a cluster member node. + * Represents the address, current status, and roles of a cluster member node. * - * Note: `hashCode` and `equals` are solely based on the underlying `Address`, not its `MemberStatus`. + * Note: `hashCode` and `equals` are solely based on the underlying `Address`, not its `MemberStatus` + * and roles. */ -class Member(val address: Address, val status: MemberStatus) extends ClusterMessage { +case class Member(val address: Address, val status: MemberStatus, roles: Set[String]) extends ClusterMessage { override def hashCode = address.## - override def equals(other: Any) = Member.unapply(this) == Member.unapply(other) + override def equals(other: Any) = other match { + case m: Member ⇒ address == m.address + case _ ⇒ false + } override def toString = "Member(address = %s, status = %s)" format (address, status) - def copy(address: Address = this.address, status: MemberStatus = this.status): Member = new Member(address, status) + + def hasRole(role: String): Boolean = roles.contains(role) + + /** + * Java API + */ + def getRoles: java.util.Set[String] = + scala.collection.JavaConverters.setAsJavaSetConverter(roles).asJava } /** @@ -65,13 +76,6 @@ object Member { def compare(a: Member, b: Member): Int = addressOrdering.compare(a.address, b.address) } - def apply(address: Address, status: MemberStatus): Member = new Member(address, status) - - def unapply(other: Any) = other match { - case m: Member ⇒ Some(m.address) - case _ ⇒ None - } - def pickHighestPriority(a: Set[Member], b: Set[Member]): Set[Member] = { // group all members by Address => Seq[Member] val groupedByAddress = (a.toSeq ++ b.toSeq).groupBy(_.address) diff --git a/akka-cluster/src/main/scala/akka/cluster/routing/ClusterRouterConfig.scala b/akka-cluster/src/main/scala/akka/cluster/routing/ClusterRouterConfig.scala index 1eb8b7a712..12ac7b7206 100644 --- a/akka-cluster/src/main/scala/akka/cluster/routing/ClusterRouterConfig.scala +++ b/akka-cluster/src/main/scala/akka/cluster/routing/ClusterRouterConfig.scala @@ -74,26 +74,31 @@ object ClusterRouterSettings { /** * Settings for create and deploy of the routees */ - def apply(totalInstances: Int, maxInstancesPerNode: Int, allowLocalRoutees: Boolean): ClusterRouterSettings = - new ClusterRouterSettings(totalInstances, maxInstancesPerNode, allowLocalRoutees) + def apply(totalInstances: Int, maxInstancesPerNode: Int, allowLocalRoutees: Boolean, useRole: Option[String]): ClusterRouterSettings = + new ClusterRouterSettings(totalInstances, maxInstancesPerNode, routeesPath = "", allowLocalRoutees, useRole) /** * Settings for remote deployment of the routees, allowed to use routees on own node */ - def apply(totalInstances: Int, maxInstancesPerNode: Int): ClusterRouterSettings = - apply(totalInstances, maxInstancesPerNode, allowLocalRoutees = true) + def apply(totalInstances: Int, maxInstancesPerNode: Int, useRole: Option[String]): ClusterRouterSettings = + apply(totalInstances, maxInstancesPerNode, allowLocalRoutees = true, useRole) /** * Settings for lookup of the routees */ - def apply(totalInstances: Int, routeesPath: String, allowLocalRoutees: Boolean): ClusterRouterSettings = - new ClusterRouterSettings(totalInstances, routeesPath, allowLocalRoutees) + def apply(totalInstances: Int, routeesPath: String, allowLocalRoutees: Boolean, useRole: Option[String]): ClusterRouterSettings = + new ClusterRouterSettings(totalInstances, maxInstancesPerNode = 1, routeesPath, allowLocalRoutees, useRole) /** * Settings for lookup of the routees, allowed to use routees on own node */ - def apply(totalInstances: Int, routeesPath: String): ClusterRouterSettings = - apply(totalInstances, routeesPath, allowLocalRoutees = true) + def apply(totalInstances: Int, routeesPath: String, useRole: Option[String]): ClusterRouterSettings = + apply(totalInstances, routeesPath, allowLocalRoutees = true, useRole) + + def useRoleOption(role: String): Option[String] = role match { + case null | "" ⇒ None + case _ ⇒ Some(role) + } } /** @@ -106,19 +111,22 @@ case class ClusterRouterSettings private[akka] ( totalInstances: Int, maxInstancesPerNode: Int, routeesPath: String, - allowLocalRoutees: Boolean) { + allowLocalRoutees: Boolean, + useRole: Option[String]) { /** * Java API: Settings for create and deploy of the routees */ - def this(totalInstances: Int, maxInstancesPerNode: Int, allowLocalRoutees: Boolean) = - this(totalInstances, maxInstancesPerNode, routeesPath = "", allowLocalRoutees) + def this(totalInstances: Int, maxInstancesPerNode: Int, allowLocalRoutees: Boolean, useRole: String) = + this(totalInstances, maxInstancesPerNode, routeesPath = "", allowLocalRoutees, + ClusterRouterSettings.useRoleOption(useRole)) /** * Java API: Settings for lookup of the routees */ - def this(totalInstances: Int, routeesPath: String, allowLocalRoutees: Boolean) = - this(totalInstances, maxInstancesPerNode = 1, routeesPath, allowLocalRoutees) + def this(totalInstances: Int, routeesPath: String, allowLocalRoutees: Boolean, useRole: String) = + this(totalInstances, maxInstancesPerNode = 1, routeesPath, allowLocalRoutees, + ClusterRouterSettings.useRoleOption(useRole)) if (totalInstances <= 0) throw new IllegalArgumentException("totalInstances of cluster router must be > 0") if (maxInstancesPerNode <= 0) throw new IllegalArgumentException("maxInstancesPerNode of cluster router must be > 0") @@ -220,7 +228,7 @@ private[akka] class ClusterRouteeProvider( private[routing] def availableNodes: immutable.SortedSet[Address] = { import Member.addressOrdering val currentNodes = nodes - if (currentNodes.isEmpty && settings.allowLocalRoutees) + if (currentNodes.isEmpty && settings.allowLocalRoutees && satisfiesRole(cluster.selfRoles)) //use my own node, cluster information not updated yet immutable.SortedSet(cluster.selfAddress) else @@ -236,7 +244,14 @@ private[akka] class ClusterRouteeProvider( } private[routing] def isAvailable(m: Member): Boolean = - m.status == MemberStatus.Up && (settings.allowLocalRoutees || m.address != cluster.selfAddress) + m.status == MemberStatus.Up && + satisfiesRole(m.roles) && + (settings.allowLocalRoutees || m.address != cluster.selfAddress) + + private def satisfiesRole(memberRoles: Set[String]): Boolean = settings.useRole match { + case None ⇒ true + case Some(r) ⇒ memberRoles.contains(r) + } } diff --git a/akka-cluster/src/multi-jvm/scala/akka/cluster/MinMembersBeforeUpSpec.scala b/akka-cluster/src/multi-jvm/scala/akka/cluster/MinMembersBeforeUpSpec.scala index 33ab5a7d4e..a91d672326 100644 --- a/akka-cluster/src/multi-jvm/scala/akka/cluster/MinMembersBeforeUpSpec.scala +++ b/akka-cluster/src/multi-jvm/scala/akka/cluster/MinMembersBeforeUpSpec.scala @@ -7,6 +7,7 @@ import com.typesafe.config.ConfigFactory import org.scalatest.BeforeAndAfter import scala.collection.immutable.SortedSet import scala.concurrent.duration._ +import akka.remote.testconductor.RoleName import akka.remote.testkit.MultiNodeConfig import akka.remote.testkit.MultiNodeSpec import akka.testkit._ @@ -25,64 +26,110 @@ object MinMembersBeforeUpMultiJvmSpec extends MultiNodeConfig { withFallback(MultiNodeClusterSpec.clusterConfigWithFailureDetectorPuppet)) } +object MinMembersOfRoleBeforeUpMultiJvmSpec extends MultiNodeConfig { + val first = role("first") + val second = role("second") + val third = role("third") + + commonConfig(debugConfig(on = false).withFallback(ConfigFactory.parseString( + "akka.cluster.role.backend.min-nr-of-members = 2")). + withFallback(MultiNodeClusterSpec.clusterConfigWithFailureDetectorPuppet)) + + nodeConfig(first)( + ConfigFactory.parseString("akka.cluster.roles =[frontend]")) + + nodeConfig(second, third)( + ConfigFactory.parseString("akka.cluster.roles =[backend]")) +} + class MinMembersBeforeUpMultiJvmNode1 extends MinMembersBeforeUpSpec class MinMembersBeforeUpMultiJvmNode2 extends MinMembersBeforeUpSpec class MinMembersBeforeUpMultiJvmNode3 extends MinMembersBeforeUpSpec -abstract class MinMembersBeforeUpSpec - extends MultiNodeSpec(MinMembersBeforeUpMultiJvmSpec) - with MultiNodeClusterSpec { +class MinMembersOfRoleBeforeUpMultiJvmNode1 extends MinMembersOfRoleBeforeUpSpec +class MinMembersOfRoleBeforeUpMultiJvmNode2 extends MinMembersOfRoleBeforeUpSpec +class MinMembersOfRoleBeforeUpMultiJvmNode3 extends MinMembersOfRoleBeforeUpSpec - import MinMembersBeforeUpMultiJvmSpec._ - import ClusterEvent._ +abstract class MinMembersBeforeUpSpec extends MinMembersBeforeUpBase(MinMembersBeforeUpMultiJvmSpec) { + + override def first: RoleName = MinMembersBeforeUpMultiJvmSpec.first + override def second: RoleName = MinMembersBeforeUpMultiJvmSpec.second + override def third: RoleName = MinMembersBeforeUpMultiJvmSpec.third "Cluster leader" must { "wait with moving members to UP until minimum number of members have joined" taggedAs LongRunningTest in { - - val onUpLatch = TestLatch(1) - cluster.registerOnMemberUp(onUpLatch.countDown()) - - runOn(first) { - cluster join myself - awaitCond { - val result = clusterView.status == Joining - clusterView.refreshCurrentState() - result - } - } - enterBarrier("first-started") - - onUpLatch.isOpen must be(false) - - runOn(second) { - cluster.join(first) - } - runOn(first, second) { - val expectedAddresses = Set(first, second) map address - awaitCond { - val result = clusterView.members.map(_.address) == expectedAddresses - clusterView.refreshCurrentState() - result - } - clusterView.members.map(_.status) must be(Set(Joining)) - // and it should not change - 1 to 5 foreach { _ ⇒ - Thread.sleep(1000) - clusterView.members.map(_.address) must be(expectedAddresses) - clusterView.members.map(_.status) must be(Set(Joining)) - } - } - enterBarrier("second-joined") - - runOn(third) { - cluster.join(first) - } - awaitClusterUp(first, second, third) - - onUpLatch.await - - enterBarrier("after-1") + testWaitMovingMembersToUp() } - } } + +abstract class MinMembersOfRoleBeforeUpSpec extends MinMembersBeforeUpBase(MinMembersOfRoleBeforeUpMultiJvmSpec) { + + override def first: RoleName = MinMembersOfRoleBeforeUpMultiJvmSpec.first + override def second: RoleName = MinMembersOfRoleBeforeUpMultiJvmSpec.second + override def third: RoleName = MinMembersOfRoleBeforeUpMultiJvmSpec.third + + "Cluster leader" must { + "wait with moving members to UP until minimum number of members with specific role have joined" taggedAs LongRunningTest in { + testWaitMovingMembersToUp() + } + } +} + +abstract class MinMembersBeforeUpBase(multiNodeConfig: MultiNodeConfig) + extends MultiNodeSpec(multiNodeConfig) + with MultiNodeClusterSpec { + + import ClusterEvent._ + + def first: RoleName + def second: RoleName + def third: RoleName + + def testWaitMovingMembersToUp(): Unit = { + val onUpLatch = TestLatch(1) + cluster.registerOnMemberUp(onUpLatch.countDown()) + + runOn(first) { + cluster join myself + awaitCond { + val result = clusterView.status == Joining + clusterView.refreshCurrentState() + result + } + } + enterBarrier("first-started") + + onUpLatch.isOpen must be(false) + + runOn(second) { + cluster.join(first) + } + runOn(first, second) { + val expectedAddresses = Set(first, second) map address + awaitCond { + val result = clusterView.members.map(_.address) == expectedAddresses + clusterView.refreshCurrentState() + result + } + clusterView.members.map(_.status) must be(Set(Joining)) + // and it should not change + 1 to 5 foreach { _ ⇒ + Thread.sleep(1000) + clusterView.members.map(_.address) must be(expectedAddresses) + clusterView.members.map(_.status) must be(Set(Joining)) + } + } + enterBarrier("second-joined") + + runOn(third) { + cluster.join(first) + } + awaitClusterUp(first, second, third) + + onUpLatch.await + + enterBarrier("after-1") + } + +} diff --git a/akka-cluster/src/multi-jvm/scala/akka/cluster/TransitionSpec.scala b/akka-cluster/src/multi-jvm/scala/akka/cluster/TransitionSpec.scala index 9df1268b93..4aa2a963c4 100644 --- a/akka-cluster/src/multi-jvm/scala/akka/cluster/TransitionSpec.scala +++ b/akka-cluster/src/multi-jvm/scala/akka/cluster/TransitionSpec.scala @@ -230,7 +230,7 @@ abstract class TransitionSpec runOn(third) { markNodeAsUnavailable(second) reapUnreachable() - awaitCond(clusterView.unreachableMembers.contains(Member(second, Up))) + awaitCond(clusterView.unreachableMembers.contains(Member(second, Up, Set.empty))) awaitCond(seenLatestGossip == Set(third)) } @@ -239,7 +239,7 @@ abstract class TransitionSpec third gossipTo first runOn(first, third) { - awaitCond(clusterView.unreachableMembers.contains(Member(second, Up))) + awaitCond(clusterView.unreachableMembers.contains(Member(second, Up, Set.empty))) } runOn(first) { @@ -251,7 +251,7 @@ abstract class TransitionSpec first gossipTo third runOn(first, third) { - awaitCond(clusterView.unreachableMembers.contains(Member(second, Down))) + awaitCond(clusterView.unreachableMembers.contains(Member(second, Down, Set.empty))) awaitMemberStatus(second, Down) awaitCond(seenLatestGossip == Set(first, third)) } diff --git a/akka-cluster/src/multi-jvm/scala/akka/cluster/routing/AdaptiveLoadBalancingRouterSpec.scala b/akka-cluster/src/multi-jvm/scala/akka/cluster/routing/AdaptiveLoadBalancingRouterSpec.scala index c98413c21a..70477d03b7 100644 --- a/akka-cluster/src/multi-jvm/scala/akka/cluster/routing/AdaptiveLoadBalancingRouterSpec.scala +++ b/akka-cluster/src/multi-jvm/scala/akka/cluster/routing/AdaptiveLoadBalancingRouterSpec.scala @@ -115,7 +115,7 @@ abstract class AdaptiveLoadBalancingRouterSpec extends MultiNodeSpec(AdaptiveLoa def startRouter(name: String): ActorRef = { val router = system.actorOf(Props[Routee].withRouter(ClusterRouterConfig( local = AdaptiveLoadBalancingRouter(HeapMetricsSelector), - settings = ClusterRouterSettings(totalInstances = 10, maxInstancesPerNode = 1))), name) + settings = ClusterRouterSettings(totalInstances = 10, maxInstancesPerNode = 1, useRole = None))), name) awaitCond { // it may take some time until router receives cluster member events currentRoutees(router).size == roles.size diff --git a/akka-cluster/src/multi-jvm/scala/akka/cluster/routing/ClusterConsistentHashingRouterSpec.scala b/akka-cluster/src/multi-jvm/scala/akka/cluster/routing/ClusterConsistentHashingRouterSpec.scala index 7f6cdba39a..b72f793228 100644 --- a/akka-cluster/src/multi-jvm/scala/akka/cluster/routing/ClusterConsistentHashingRouterSpec.scala +++ b/akka-cluster/src/multi-jvm/scala/akka/cluster/routing/ClusterConsistentHashingRouterSpec.scala @@ -124,7 +124,7 @@ abstract class ClusterConsistentHashingRouterSpec extends MultiNodeSpec(ClusterC "deploy programatically defined routees to the member nodes in the cluster" taggedAs LongRunningTest in { runOn(first) { val router2 = system.actorOf(Props[Echo].withRouter(ClusterRouterConfig(local = ConsistentHashingRouter(), - settings = ClusterRouterSettings(totalInstances = 10, maxInstancesPerNode = 2))), "router2") + settings = ClusterRouterSettings(totalInstances = 10, maxInstancesPerNode = 2, useRole = None))), "router2") awaitCond { // it may take some time until router receives cluster member events currentRoutees(router2).size == 6 @@ -157,7 +157,7 @@ abstract class ClusterConsistentHashingRouterSpec extends MultiNodeSpec(ClusterC val router4 = system.actorOf(Props[Echo].withRouter(ClusterRouterConfig( local = ConsistentHashingRouter(hashMapping = hashMapping), - settings = ClusterRouterSettings(totalInstances = 10, maxInstancesPerNode = 1))), "router4") + settings = ClusterRouterSettings(totalInstances = 10, maxInstancesPerNode = 1, useRole = None))), "router4") assertHashMapping(router4) } diff --git a/akka-cluster/src/multi-jvm/scala/akka/cluster/routing/ClusterRoundRobinRoutedActorSpec.scala b/akka-cluster/src/multi-jvm/scala/akka/cluster/routing/ClusterRoundRobinRoutedActorSpec.scala index 0caf029de2..430df7ab50 100644 --- a/akka-cluster/src/multi-jvm/scala/akka/cluster/routing/ClusterRoundRobinRoutedActorSpec.scala +++ b/akka-cluster/src/multi-jvm/scala/akka/cluster/routing/ClusterRoundRobinRoutedActorSpec.scala @@ -71,10 +71,21 @@ object ClusterRoundRobinRoutedActorMultiJvmSpec extends MultiNodeConfig { routees-path = "/user/myservice" } } + /router5 { + router = round-robin + nr-of-instances = 10 + cluster { + enabled = on + use-role = a + } + } } """)). withFallback(MultiNodeClusterSpec.clusterConfig)) + nodeConfig(first, second)(ConfigFactory.parseString("""akka.cluster.roles =["a", "c"]""")) + nodeConfig(third)(ConfigFactory.parseString("""akka.cluster.roles =["b", "c"]""")) + } class ClusterRoundRobinRoutedActorMultiJvmNode1 extends ClusterRoundRobinRoutedActorSpec @@ -89,9 +100,10 @@ abstract class ClusterRoundRobinRoutedActorSpec extends MultiNodeSpec(ClusterRou lazy val router1 = system.actorOf(Props[SomeActor].withRouter(RoundRobinRouter()), "router1") lazy val router2 = system.actorOf(Props[SomeActor].withRouter(ClusterRouterConfig(RoundRobinRouter(), - ClusterRouterSettings(totalInstances = 3, maxInstancesPerNode = 1))), "router2") + ClusterRouterSettings(totalInstances = 3, maxInstancesPerNode = 1, useRole = None))), "router2") lazy val router3 = system.actorOf(Props[SomeActor].withRouter(RoundRobinRouter()), "router3") lazy val router4 = system.actorOf(Props[SomeActor].withRouter(RoundRobinRouter()), "router4") + lazy val router5 = system.actorOf(Props[SomeActor].withRouter(RoundRobinRouter()), "router5") def receiveReplies(routeeType: RouteeType, expectedReplies: Int): Map[Address, Int] = { val zero = Map.empty[Address, Int] ++ roles.map(address(_) -> 0) @@ -240,6 +252,28 @@ abstract class ClusterRoundRobinRoutedActorSpec extends MultiNodeSpec(ClusterRou enterBarrier("after-6") } + "deploy routees to specified node role" taggedAs LongRunningTest in { + + runOn(first) { + awaitCond(currentRoutees(router5).size == 2) + + val iterationCount = 10 + for (i ← 0 until iterationCount) { + router5 ! "hit" + } + + val replies = receiveReplies(DeployRoutee, iterationCount) + + replies(first) must be > (0) + replies(second) must be > (0) + replies(third) must be(0) + replies(fourth) must be(0) + replies.values.sum must be(iterationCount) + } + + enterBarrier("after-7") + } + "deploy programatically defined routees to the member nodes in the cluster" taggedAs LongRunningTest in { runOn(first) { @@ -263,7 +297,7 @@ abstract class ClusterRoundRobinRoutedActorSpec extends MultiNodeSpec(ClusterRou replies.values.sum must be(iterationCount) } - enterBarrier("after-7") + enterBarrier("after-8") } "deploy programatically defined routees to other node when a node becomes down" taggedAs LongRunningTest in { @@ -292,7 +326,7 @@ abstract class ClusterRoundRobinRoutedActorSpec extends MultiNodeSpec(ClusterRou replies.values.sum must be(iterationCount) } - enterBarrier("after-8") + enterBarrier("after-9") } } diff --git a/akka-cluster/src/test/scala/akka/cluster/ClusterConfigSpec.scala b/akka-cluster/src/test/scala/akka/cluster/ClusterConfigSpec.scala index 9cdb47aeb2..7e268660a3 100644 --- a/akka-cluster/src/test/scala/akka/cluster/ClusterConfigSpec.scala +++ b/akka-cluster/src/test/scala/akka/cluster/ClusterConfigSpec.scala @@ -39,6 +39,8 @@ class ClusterConfigSpec extends AkkaSpec { AutoJoin must be(true) AutoDown must be(false) MinNrOfMembers must be(1) + MinNrOfMembersOfRole must be === Map.empty + Roles must be === Set.empty JmxEnabled must be(true) UseDispatcher must be(Dispatchers.DefaultDispatcherId) GossipDifferentViewProbability must be(0.8 plusOrMinus 0.0001) diff --git a/akka-cluster/src/test/scala/akka/cluster/ClusterDeployerSpec.scala b/akka-cluster/src/test/scala/akka/cluster/ClusterDeployerSpec.scala index 7aa7b81047..3c40f1df6e 100644 --- a/akka-cluster/src/test/scala/akka/cluster/ClusterDeployerSpec.scala +++ b/akka-cluster/src/test/scala/akka/cluster/ClusterDeployerSpec.scala @@ -53,7 +53,7 @@ class ClusterDeployerSpec extends AkkaSpec(ClusterDeployerSpec.deployerConf) { service, deployment.get.config, ClusterRouterConfig(RoundRobinRouter(20), ClusterRouterSettings( - totalInstances = 20, maxInstancesPerNode = 3, allowLocalRoutees = false)), + totalInstances = 20, maxInstancesPerNode = 3, allowLocalRoutees = false, useRole = None)), ClusterScope))) } @@ -67,7 +67,7 @@ class ClusterDeployerSpec extends AkkaSpec(ClusterDeployerSpec.deployerConf) { service, deployment.get.config, ClusterRouterConfig(RoundRobinRouter(20), ClusterRouterSettings( - totalInstances = 20, routeesPath = "/user/myservice", allowLocalRoutees = false)), + totalInstances = 20, routeesPath = "/user/myservice", allowLocalRoutees = false, useRole = None)), ClusterScope))) } diff --git a/akka-cluster/src/test/scala/akka/cluster/ClusterDomainEventPublisherSpec.scala b/akka-cluster/src/test/scala/akka/cluster/ClusterDomainEventPublisherSpec.scala index 3d3ce0ac3e..f7f6c7789b 100644 --- a/akka-cluster/src/test/scala/akka/cluster/ClusterDomainEventPublisherSpec.scala +++ b/akka-cluster/src/test/scala/akka/cluster/ClusterDomainEventPublisherSpec.scala @@ -24,23 +24,24 @@ class ClusterDomainEventPublisherSpec extends AkkaSpec with BeforeAndAfterEach with ImplicitSender { var publisher: ActorRef = _ - val aUp = Member(Address("akka.tcp", "sys", "a", 2552), Up) + val aUp = Member(Address("akka.tcp", "sys", "a", 2552), Up, Set.empty) val aLeaving = aUp.copy(status = Leaving) val aExiting = aUp.copy(status = Exiting) val aRemoved = aUp.copy(status = Removed) - val bUp = Member(Address("akka.tcp", "sys", "b", 2552), Up) + val bUp = Member(Address("akka.tcp", "sys", "b", 2552), Up, Set.empty) val bRemoved = bUp.copy(status = Removed) - val cJoining = Member(Address("akka.tcp", "sys", "c", 2552), Joining) + val cJoining = Member(Address("akka.tcp", "sys", "c", 2552), Joining, Set("GRP")) val cUp = cJoining.copy(status = Up) val cRemoved = cUp.copy(status = Removed) - val dUp = Member(Address("akka.tcp", "sys", "a", 2551), Up) + val a51Up = Member(Address("akka.tcp", "sys", "a", 2551), Up, Set.empty) + val dUp = Member(Address("akka.tcp", "sys", "d", 2552), Up, Set("GRP")) val g0 = Gossip(members = SortedSet(aUp)).seen(aUp.address) val g1 = Gossip(members = SortedSet(aUp, bUp, cJoining)).seen(aUp.address).seen(bUp.address).seen(cJoining.address) val g2 = Gossip(members = SortedSet(aUp, bUp, cUp)).seen(aUp.address) val g3 = g2.seen(bUp.address).seen(cUp.address) - val g4 = Gossip(members = SortedSet(dUp, aUp, bUp, cUp)).seen(aUp.address) - val g5 = Gossip(members = SortedSet(dUp, aUp, bUp, cUp)).seen(aUp.address).seen(bUp.address).seen(cUp.address).seen(dUp.address) + val g4 = Gossip(members = SortedSet(a51Up, aUp, bUp, cUp)).seen(aUp.address) + val g5 = Gossip(members = SortedSet(a51Up, aUp, bUp, cUp)).seen(aUp.address).seen(bUp.address).seen(cUp.address).seen(a51Up.address) val g6 = Gossip(members = SortedSet(aLeaving, bUp, cUp)).seen(aUp.address) val g7 = Gossip(members = SortedSet(aExiting, bUp, cUp)).seen(aUp.address) @@ -69,10 +70,10 @@ class ClusterDomainEventPublisherSpec extends AkkaSpec "publish leader changed" in { publisher ! PublishChanges(g4) - memberSubscriber.expectMsg(MemberUp(dUp)) + memberSubscriber.expectMsg(MemberUp(a51Up)) memberSubscriber.expectMsg(MemberUp(bUp)) memberSubscriber.expectMsg(MemberUp(cUp)) - memberSubscriber.expectMsg(LeaderChanged(Some(dUp.address))) + memberSubscriber.expectMsg(LeaderChanged(Some(a51Up.address))) memberSubscriber.expectNoMsg(1 second) } @@ -96,15 +97,25 @@ class ClusterDomainEventPublisherSpec extends AkkaSpec "not publish leader changed when same leader" in { publisher ! PublishChanges(g4) - memberSubscriber.expectMsg(MemberUp(dUp)) + memberSubscriber.expectMsg(MemberUp(a51Up)) memberSubscriber.expectMsg(MemberUp(bUp)) memberSubscriber.expectMsg(MemberUp(cUp)) - memberSubscriber.expectMsg(LeaderChanged(Some(dUp.address))) + memberSubscriber.expectMsg(LeaderChanged(Some(a51Up.address))) publisher ! PublishChanges(g5) memberSubscriber.expectNoMsg(1 second) } + "publish role leader changed" in { + val subscriber = TestProbe() + publisher ! Subscribe(subscriber.ref, classOf[RoleLeaderChanged]) + subscriber.expectMsgType[CurrentClusterState] + publisher ! PublishChanges(Gossip(members = SortedSet(cJoining, dUp))) + subscriber.expectMsg(RoleLeaderChanged("GRP", Some(dUp.address))) + publisher ! PublishChanges(Gossip(members = SortedSet(cUp, dUp))) + subscriber.expectMsg(RoleLeaderChanged("GRP", Some(cUp.address))) + } + "send CurrentClusterState when subscribe" in { val subscriber = TestProbe() publisher ! Subscribe(subscriber.ref, classOf[ClusterDomainEvent]) @@ -132,6 +143,7 @@ class ClusterDomainEventPublisherSpec extends AkkaSpec publisher ! PublishChanges(g3) subscriber.expectMsg(MemberUp(bUp)) subscriber.expectMsg(MemberUp(cUp)) + subscriber.expectMsg(RoleLeaderChanged("GRP", Some(cUp.address))) subscriber.expectMsgType[SeenChanged] publisher ! PublishStart diff --git a/akka-cluster/src/test/scala/akka/cluster/ClusterDomainEventSpec.scala b/akka-cluster/src/test/scala/akka/cluster/ClusterDomainEventSpec.scala index 21e20d2b6f..f624012307 100644 --- a/akka-cluster/src/test/scala/akka/cluster/ClusterDomainEventSpec.scala +++ b/akka-cluster/src/test/scala/akka/cluster/ClusterDomainEventSpec.scala @@ -15,19 +15,25 @@ class ClusterDomainEventSpec extends WordSpec with MustMatchers { import MemberStatus._ import ClusterEvent._ - val a1 = Member(Address("akka.tcp", "sys", "a", 2552), Up) - val a2 = Member(Address("akka.tcp", "sys", "a", 2552), Joining) - val a3 = Member(Address("akka.tcp", "sys", "a", 2552), Removed) - val b1 = Member(Address("akka.tcp", "sys", "b", 2552), Up) - val b2 = Member(Address("akka.tcp", "sys", "b", 2552), Removed) - val b3 = Member(Address("akka.tcp", "sys", "b", 2552), Down) - val c1 = Member(Address("akka.tcp", "sys", "c", 2552), Leaving) - val c2 = Member(Address("akka.tcp", "sys", "c", 2552), Up) - val d1 = Member(Address("akka.tcp", "sys", "d", 2552), Leaving) - val d2 = Member(Address("akka.tcp", "sys", "d", 2552), Removed) - val e1 = Member(Address("akka.tcp", "sys", "e", 2552), Joining) - val e2 = Member(Address("akka.tcp", "sys", "e", 2552), Up) - val e3 = Member(Address("akka.tcp", "sys", "e", 2552), Down) + val aRoles = Set("AA", "AB") + val aJoining = Member(Address("akka.tcp", "sys", "a", 2552), Joining, aRoles) + val aUp = Member(Address("akka.tcp", "sys", "a", 2552), Up, aRoles) + val aRemoved = Member(Address("akka.tcp", "sys", "a", 2552), Removed, aRoles) + val bRoles = Set("AB", "BB") + val bUp = Member(Address("akka.tcp", "sys", "b", 2552), Up, bRoles) + val bDown = Member(Address("akka.tcp", "sys", "b", 2552), Down, bRoles) + val bRemoved = Member(Address("akka.tcp", "sys", "b", 2552), Removed, bRoles) + val cRoles = Set.empty[String] + val cUp = Member(Address("akka.tcp", "sys", "c", 2552), Up, cRoles) + val cLeaving = Member(Address("akka.tcp", "sys", "c", 2552), Leaving, cRoles) + val dRoles = Set("DD", "DE") + val dLeaving = Member(Address("akka.tcp", "sys", "d", 2552), Leaving, dRoles) + val dExiting = Member(Address("akka.tcp", "sys", "d", 2552), Exiting, dRoles) + val dRemoved = Member(Address("akka.tcp", "sys", "d", 2552), Removed, dRoles) + val eRoles = Set("EE", "DE") + val eJoining = Member(Address("akka.tcp", "sys", "e", 2552), Joining, eRoles) + val eUp = Member(Address("akka.tcp", "sys", "e", 2552), Up, eRoles) + val eDown = Member(Address("akka.tcp", "sys", "e", 2552), Down, eRoles) def converge(gossip: Gossip): (Gossip, Set[Address]) = ((gossip, Set.empty[Address]) /: gossip.members) { (gs, m) ⇒ (gs._1.seen(m.address), gs._2 + m.address) } @@ -35,66 +41,83 @@ class ClusterDomainEventSpec extends WordSpec with MustMatchers { "Domain events" must { "be empty for the same gossip" in { - val g1 = Gossip(members = SortedSet(a1)) + val g1 = Gossip(members = SortedSet(aUp)) diffUnreachable(g1, g1) must be(Seq.empty) } "be produced for new members" in { - val (g1, _) = converge(Gossip(members = SortedSet(a1))) - val (g2, s2) = converge(Gossip(members = SortedSet(a1, b1, e1))) + val (g1, _) = converge(Gossip(members = SortedSet(aUp))) + val (g2, s2) = converge(Gossip(members = SortedSet(aUp, bUp, eJoining))) - diffMemberEvents(g1, g2) must be(Seq(MemberUp(b1))) + diffMemberEvents(g1, g2) must be(Seq(MemberUp(bUp))) diffUnreachable(g1, g2) must be(Seq.empty) diffSeen(g1, g2) must be(Seq(SeenChanged(convergence = true, seenBy = s2))) } "be produced for changed status of members" in { - val (g1, _) = converge(Gossip(members = SortedSet(a2, b1, c2))) - val (g2, s2) = converge(Gossip(members = SortedSet(a1, b1, c1, e1))) + val (g1, _) = converge(Gossip(members = SortedSet(aJoining, bUp, cUp))) + val (g2, s2) = converge(Gossip(members = SortedSet(aUp, bUp, cLeaving, eJoining))) - diffMemberEvents(g1, g2) must be(Seq(MemberUp(a1))) + diffMemberEvents(g1, g2) must be(Seq(MemberUp(aUp))) diffUnreachable(g1, g2) must be(Seq.empty) diffSeen(g1, g2) must be(Seq(SeenChanged(convergence = true, seenBy = s2))) } "be produced for members in unreachable" in { - val g1 = Gossip(members = SortedSet(a1, b1), overview = GossipOverview(unreachable = Set(c2, e2))) - val g2 = Gossip(members = SortedSet(a1), overview = GossipOverview(unreachable = Set(c2, b3, e3))) + val g1 = Gossip(members = SortedSet(aUp, bUp), overview = GossipOverview(unreachable = Set(cUp, eUp))) + val g2 = Gossip(members = SortedSet(aUp), overview = GossipOverview(unreachable = Set(cUp, bDown, eDown))) - diffUnreachable(g1, g2) must be(Seq(UnreachableMember(b3))) + diffUnreachable(g1, g2) must be(Seq(UnreachableMember(bDown))) diffSeen(g1, g2) must be(Seq.empty) } "be produced for removed members" in { - val (g1, _) = converge(Gossip(members = SortedSet(a1, d1))) - val (g2, s2) = converge(Gossip(members = SortedSet(a1))) + val (g1, _) = converge(Gossip(members = SortedSet(aUp, dLeaving))) + val (g2, s2) = converge(Gossip(members = SortedSet(aUp))) - diffMemberEvents(g1, g2) must be(Seq(MemberRemoved(d2))) + diffMemberEvents(g1, g2) must be(Seq(MemberRemoved(dRemoved))) diffUnreachable(g1, g2) must be(Seq.empty) diffSeen(g1, g2) must be(Seq(SeenChanged(convergence = true, seenBy = s2))) } "be produced for convergence changes" in { - val g1 = Gossip(members = SortedSet(a1, b1, e1)).seen(a1.address).seen(b1.address).seen(e1.address) - val g2 = Gossip(members = SortedSet(a1, b1, e1)).seen(a1.address).seen(b1.address) + val g1 = Gossip(members = SortedSet(aUp, bUp, eJoining)).seen(aUp.address).seen(bUp.address).seen(eJoining.address) + val g2 = Gossip(members = SortedSet(aUp, bUp, eJoining)).seen(aUp.address).seen(bUp.address) diffMemberEvents(g1, g2) must be(Seq.empty) diffUnreachable(g1, g2) must be(Seq.empty) - diffSeen(g1, g2) must be(Seq(SeenChanged(convergence = false, seenBy = Set(a1.address, b1.address)))) + diffSeen(g1, g2) must be(Seq(SeenChanged(convergence = false, seenBy = Set(aUp.address, bUp.address)))) diffMemberEvents(g2, g1) must be(Seq.empty) diffUnreachable(g2, g1) must be(Seq.empty) - diffSeen(g2, g1) must be(Seq(SeenChanged(convergence = true, seenBy = Set(a1.address, b1.address, e1.address)))) + diffSeen(g2, g1) must be(Seq(SeenChanged(convergence = true, seenBy = Set(aUp.address, bUp.address, eJoining.address)))) } "be produced for leader changes" in { - val (g1, _) = converge(Gossip(members = SortedSet(a1, b1, e1))) - val (g2, s2) = converge(Gossip(members = SortedSet(b1, e1))) + val (g1, _) = converge(Gossip(members = SortedSet(aUp, bUp, eJoining))) + val (g2, s2) = converge(Gossip(members = SortedSet(bUp, eJoining))) - diffMemberEvents(g1, g2) must be(Seq(MemberRemoved(a3))) + diffMemberEvents(g1, g2) must be(Seq(MemberRemoved(aRemoved))) diffUnreachable(g1, g2) must be(Seq.empty) diffSeen(g1, g2) must be(Seq(SeenChanged(convergence = true, seenBy = s2))) - diffLeader(g1, g2) must be(Seq(LeaderChanged(Some(b1.address)))) + diffLeader(g1, g2) must be(Seq(LeaderChanged(Some(bUp.address)))) + } + + "be produced for role leader changes" in { + val g0 = Gossip.empty + val g1 = Gossip(members = SortedSet(aUp, bUp, cUp, dLeaving, eJoining)) + val g2 = Gossip(members = SortedSet(bUp, cUp, dExiting, eJoining)) + diffRolesLeader(g0, g1) must be( + Set(RoleLeaderChanged("AA", Some(aUp.address)), + RoleLeaderChanged("AB", Some(aUp.address)), + RoleLeaderChanged("BB", Some(bUp.address)), + RoleLeaderChanged("DD", Some(dLeaving.address)), + RoleLeaderChanged("DE", Some(dLeaving.address)), + RoleLeaderChanged("EE", Some(eUp.address)))) + diffRolesLeader(g1, g2) must be( + Set(RoleLeaderChanged("AA", None), + RoleLeaderChanged("AB", Some(bUp.address)), + RoleLeaderChanged("DE", Some(eJoining.address)))) } } } diff --git a/akka-cluster/src/test/scala/akka/cluster/GossipSpec.scala b/akka-cluster/src/test/scala/akka/cluster/GossipSpec.scala index 9b677141d3..1e646c7bed 100644 --- a/akka-cluster/src/test/scala/akka/cluster/GossipSpec.scala +++ b/akka-cluster/src/test/scala/akka/cluster/GossipSpec.scala @@ -14,18 +14,18 @@ class GossipSpec extends WordSpec with MustMatchers { import MemberStatus._ - val a1 = Member(Address("akka.tcp", "sys", "a", 2552), Up) - val a2 = Member(Address("akka.tcp", "sys", "a", 2552), Joining) - val b1 = Member(Address("akka.tcp", "sys", "b", 2552), Up) - val b2 = Member(Address("akka.tcp", "sys", "b", 2552), Removed) - val c1 = Member(Address("akka.tcp", "sys", "c", 2552), Leaving) - val c2 = Member(Address("akka.tcp", "sys", "c", 2552), Up) - val c3 = Member(Address("akka.tcp", "sys", "c", 2552), Exiting) - val d1 = Member(Address("akka.tcp", "sys", "d", 2552), Leaving) - val d2 = Member(Address("akka.tcp", "sys", "d", 2552), Removed) - val e1 = Member(Address("akka.tcp", "sys", "e", 2552), Joining) - val e2 = Member(Address("akka.tcp", "sys", "e", 2552), Up) - val e3 = Member(Address("akka.tcp", "sys", "e", 2552), Down) + val a1 = Member(Address("akka.tcp", "sys", "a", 2552), Up, Set.empty) + val a2 = a1.copy(status = Joining) + val b1 = Member(Address("akka.tcp", "sys", "b", 2552), Up, Set.empty) + val b2 = b1.copy(status = Removed) + val c1 = Member(Address("akka.tcp", "sys", "c", 2552), Leaving, Set.empty) + val c2 = c1.copy(status = Up) + val c3 = c1.copy(status = Exiting) + val d1 = Member(Address("akka.tcp", "sys", "d", 2552), Leaving, Set.empty) + val d2 = d1.copy(status = Removed) + val e1 = Member(Address("akka.tcp", "sys", "e", 2552), Joining, Set.empty) + val e2 = e1.copy(status = Up) + val e3 = e1.copy(status = Down) "A Gossip" must { diff --git a/akka-cluster/src/test/scala/akka/cluster/MemberOrderingSpec.scala b/akka-cluster/src/test/scala/akka/cluster/MemberOrderingSpec.scala index 3e19ab1e0c..c8537b2bb0 100644 --- a/akka-cluster/src/test/scala/akka/cluster/MemberOrderingSpec.scala +++ b/akka-cluster/src/test/scala/akka/cluster/MemberOrderingSpec.scala @@ -17,29 +17,31 @@ class MemberOrderingSpec extends WordSpec with MustMatchers { import Member.addressOrdering import MemberStatus._ + def m(address: Address, status: MemberStatus): Member = Member(address, status, Set.empty) + "An Ordering[Member]" must { "order members by host:port" in { val members = SortedSet.empty[Member] + - Member(AddressFromURIString("akka://sys@darkstar:1112"), Up) + - Member(AddressFromURIString("akka://sys@darkstar:1113"), Joining) + - Member(AddressFromURIString("akka://sys@darkstar:1111"), Up) + m(AddressFromURIString("akka://sys@darkstar:1112"), Up) + + m(AddressFromURIString("akka://sys@darkstar:1113"), Joining) + + m(AddressFromURIString("akka://sys@darkstar:1111"), Up) val seq = members.toSeq seq.size must equal(3) - seq(0) must equal(Member(AddressFromURIString("akka://sys@darkstar:1111"), Up)) - seq(1) must equal(Member(AddressFromURIString("akka://sys@darkstar:1112"), Up)) - seq(2) must equal(Member(AddressFromURIString("akka://sys@darkstar:1113"), Joining)) + seq(0) must equal(m(AddressFromURIString("akka://sys@darkstar:1111"), Up)) + seq(1) must equal(m(AddressFromURIString("akka://sys@darkstar:1112"), Up)) + seq(2) must equal(m(AddressFromURIString("akka://sys@darkstar:1113"), Joining)) } "be sorted by address correctly" in { import Member.ordering // sorting should be done on host and port, only - val m1 = Member(Address("akka.tcp", "sys1", "host1", 9000), MemberStatus.Up) - val m2 = Member(Address("akka.tcp", "sys1", "host1", 10000), MemberStatus.Up) - val m3 = Member(Address("cluster", "sys2", "host2", 8000), MemberStatus.Up) - val m4 = Member(Address("cluster", "sys2", "host2", 9000), MemberStatus.Up) - val m5 = Member(Address("cluster", "sys1", "host2", 10000), MemberStatus.Up) + val m1 = m(Address("akka.tcp", "sys1", "host1", 9000), Up) + val m2 = m(Address("akka.tcp", "sys1", "host1", 10000), Up) + val m3 = m(Address("cluster", "sys2", "host2", 8000), Up) + val m4 = m(Address("cluster", "sys2", "host2", 9000), Up) + val m5 = m(Address("cluster", "sys1", "host2", 10000), Up) val expected = IndexedSeq(m1, m2, m3, m4, m5) val shuffled = Random.shuffle(expected) @@ -49,9 +51,9 @@ class MemberOrderingSpec extends WordSpec with MustMatchers { "have stable equals and hashCode" in { val address = Address("akka.tcp", "sys1", "host1", 9000) - val m1 = Member(address, MemberStatus.Joining) - val m2 = Member(address, MemberStatus.Up) - val m3 = Member(address.copy(port = Some(10000)), MemberStatus.Up) + val m1 = m(address, Joining) + val m2 = m(address, Up) + val m3 = m(address.copy(port = Some(10000)), Up) m1 must be(m2) m1.hashCode must be(m2.hashCode) @@ -64,9 +66,9 @@ class MemberOrderingSpec extends WordSpec with MustMatchers { val address1 = Address("akka.tcp", "sys1", "host1", 9001) val address2 = address1.copy(port = Some(9002)) - val x = Member(address1, Exiting) - val y = Member(address1, Removed) - val z = Member(address2, Up) + val x = m(address1, Exiting) + val y = m(address1, Removed) + val z = m(address2, Up) Member.ordering.compare(x, y) must be(0) Member.ordering.compare(x, z) must be(Member.ordering.compare(y, z)) } @@ -76,11 +78,11 @@ class MemberOrderingSpec extends WordSpec with MustMatchers { val address2 = address1.copy(port = Some(9002)) val address3 = address1.copy(port = Some(9003)) - (SortedSet(Member(address1, MemberStatus.Joining)) - Member(address1, MemberStatus.Up)) must be(SortedSet.empty[Member]) - (SortedSet(Member(address1, MemberStatus.Exiting)) - Member(address1, MemberStatus.Removed)) must be(SortedSet.empty[Member]) - (SortedSet(Member(address1, MemberStatus.Up)) - Member(address1, MemberStatus.Exiting)) must be(SortedSet.empty[Member]) - (SortedSet(Member(address2, Up), Member(address3, Joining), Member(address1, MemberStatus.Exiting)) - Member(address1, MemberStatus.Removed)) must be( - SortedSet(Member(address2, Up), Member(address3, Joining))) + (SortedSet(m(address1, Joining)) - m(address1, Up)) must be(SortedSet.empty[Member]) + (SortedSet(m(address1, Exiting)) - m(address1, Removed)) must be(SortedSet.empty[Member]) + (SortedSet(m(address1, Up)) - m(address1, Exiting)) must be(SortedSet.empty[Member]) + (SortedSet(m(address2, Up), m(address3, Joining), m(address1, Exiting)) - m(address1, Removed)) must be( + SortedSet(m(address2, Up), m(address3, Joining))) } } @@ -136,14 +138,14 @@ class MemberOrderingSpec extends WordSpec with MustMatchers { "order members with status Joining, Exiting and Down last" in { val address = Address("akka.tcp", "sys1", "host1", 5000) - val m1 = Member(address, MemberStatus.Joining) - val m2 = Member(address.copy(port = Some(7000)), MemberStatus.Joining) - val m3 = Member(address.copy(port = Some(3000)), MemberStatus.Exiting) - val m4 = Member(address.copy(port = Some(6000)), MemberStatus.Exiting) - val m5 = Member(address.copy(port = Some(2000)), MemberStatus.Down) - val m6 = Member(address.copy(port = Some(4000)), MemberStatus.Down) - val m7 = Member(address.copy(port = Some(8000)), MemberStatus.Up) - val m8 = Member(address.copy(port = Some(9000)), MemberStatus.Up) + val m1 = m(address, Joining) + val m2 = m(address.copy(port = Some(7000)), Joining) + val m3 = m(address.copy(port = Some(3000)), Exiting) + val m4 = m(address.copy(port = Some(6000)), Exiting) + val m5 = m(address.copy(port = Some(2000)), Down) + val m6 = m(address.copy(port = Some(4000)), Down) + val m7 = m(address.copy(port = Some(8000)), Up) + val m8 = m(address.copy(port = Some(9000)), Up) val expected = IndexedSeq(m7, m8, m1, m2, m3, m4, m5, m6) val shuffled = Random.shuffle(expected) shuffled.sorted(Member.leaderStatusOrdering) must be(expected) diff --git a/akka-contrib/docs/cluster-singleton.rst b/akka-contrib/docs/cluster-singleton.rst index 47a372115b..5205a5546a 100644 --- a/akka-contrib/docs/cluster-singleton.rst +++ b/akka-contrib/docs/cluster-singleton.rst @@ -19,12 +19,13 @@ such as single-point of bottleneck. Single-point of failure is also a relevant c but for some cases this feature takes care of that by making sure that another singleton instance will eventually be started. -The cluster singleton pattern is implemented by ``akka.contrib.pattern.ClusterSingletonManager``, -which is an actor that is supposed to be started on all nodes in the cluster. -The actual singleton actor is started by the ``ClusterSingletonManager`` on the -leader node of the cluster by creating a child actor from supplied ``Props``. -``ClusterSingletonManager`` makes sure that at most one singleton instance is -running at any point in time. +The cluster singleton pattern is implemented by ``akka.contrib.pattern.ClusterSingletonManager``. +It manages singleton actor instance among all cluster nodes or a group of nodes tagged with +a specific role. ``ClusterSingletonManager`` is an actor that is supposed to be started on +all nodes, or all nodes with specified role, in the cluster. The actual singleton actor is +started by the ``ClusterSingletonManager`` on the leader node by creating a child actor from +supplied ``Props``. ``ClusterSingletonManager`` makes sure that at most one singleton instance +is running at any point in time. The singleton actor is always running on the leader member, which is nothing more than the address currently sorted first in the member ring. This can change when adding @@ -39,9 +40,9 @@ not be a graceful hand-over, but more than one active singletons is prevented by reasonable means. Some corner cases are eventually resolved by configurable timeouts. You access the singleton actor with ``actorFor`` using the names you have specified when -creating the ClusterSingletonManager. You can subscribe to cluster ``LeaderChanged`` events -to keep track of which node it is supposed to be running on. Alternatively the singleton -actor may broadcast its existence when it is started. +creating the ClusterSingletonManager. You can subscribe to cluster ``LeaderChanged`` or +``RoleLeaderChanged`` events to keep track of which node it is supposed to be running on. +Alternatively the singleton actor may broadcast its existence when it is started. An Example ---------- @@ -57,7 +58,12 @@ supply the ``Props`` of the singleton actor, in this case the JMS queue consumer .. includecode:: @contribSrc@/src/multi-jvm/scala/akka/contrib/pattern/ClusterSingletonManagerSpec.scala#create-singleton-manager +Here we limit the singleton to nodes tagged with the ``"worker"`` role, but all nodes, independent of +role, can be used by specifying ``None`` as ``role`` parameter. + The corresponding Java API for the ``singeltonProps`` function is ``akka.contrib.pattern.ClusterSingletonPropsFactory``. +The Java API constructor takes a plain String for the role parameter and ``null`` means that all nodes, independent of +role, are used. Here we use an application specific ``terminationMessage`` to be able to close the resources before actually stopping the singleton actor. Note that ``PoisonPill`` is a @@ -72,12 +78,15 @@ This message will be sent over to the ``ClusterSingletonManager`` at the new lea will be passed to the ``singletonProps`` factory when creating the new singleton instance. With the names given above the path of singleton actor can be constructed by subscribing to -``LeaderChanged`` cluster event and the actor reference is then looked up using ``actorFor``: +``RoleLeaderChanged`` cluster event and the actor reference is then looked up using ``actorFor``: -.. includecode:: @contribSrc@/src/multi-jvm/scala/akka/contrib/pattern/ClusterSingletonManagerSpec.scala#singleton-proxy +.. includecode:: @contribSrc@/src/multi-jvm/scala/akka/contrib/pattern/ClusterSingletonManagerSpec.scala#singleton-proxy2 + +Subscribe to ``LeaderChanged`` instead of ``RoleLeaderChanged`` if you don't limit the singleton to +the group of members tagged with a specific role. Note that the hand-over might still be in progress and the singleton actor might not be started yet -when you receive the ``LeaderChanged`` event. +when you receive the ``LeaderChanged`` / ``RoleLeaderChanged`` event. To test scenarios where the cluster leader node is removed or shut down you can use :ref:`multi-node-testing` and utilize the fact that the leader is supposed to be the first member when sorted by member address. diff --git a/akka-contrib/src/main/scala/akka/contrib/pattern/ClusterSingletonManager.scala b/akka-contrib/src/main/scala/akka/contrib/pattern/ClusterSingletonManager.scala index be5dfa4717..e31e84db8c 100644 --- a/akka-contrib/src/main/scala/akka/contrib/pattern/ClusterSingletonManager.scala +++ b/akka-contrib/src/main/scala/akka/contrib/pattern/ClusterSingletonManager.scala @@ -90,6 +90,11 @@ object ClusterSingletonManager { val TakeOverRetryTimer = "take-over-retry" val CleanupTimer = "cleanup" + def roleOption(role: String): Option[String] = role match { + case null | "" ⇒ None + case _ ⇒ Some(role) + } + object LeaderChangedBuffer { /** * Request to deliver one more event. @@ -110,7 +115,7 @@ object ClusterSingletonManager { * `GetNext` request is allowed. Incoming events are buffered and delivered * upon `GetNext` request. */ - class LeaderChangedBuffer extends Actor { + class LeaderChangedBuffer(role: Option[String]) extends Actor { import LeaderChangedBuffer._ import context.dispatcher @@ -119,14 +124,23 @@ object ClusterSingletonManager { var memberCount = 0 // subscribe to LeaderChanged, re-subscribe when restart - override def preStart(): Unit = cluster.subscribe(self, classOf[LeaderChanged]) + override def preStart(): Unit = role match { + case None ⇒ cluster.subscribe(self, classOf[LeaderChanged]) + case Some(_) ⇒ cluster.subscribe(self, classOf[RoleLeaderChanged]) + } override def postStop(): Unit = cluster.unsubscribe(self) def receive = { case state: CurrentClusterState ⇒ - changes :+= InitialLeaderState(state.leader, state.members.size) + val initial = role match { + case None ⇒ InitialLeaderState(state.leader, state.members.size) + case Some(r) ⇒ InitialLeaderState(state.roleLeader(r), state.members.count(_.hasRole(r))) + } + changes :+= initial case event: LeaderChanged ⇒ changes :+= event + case RoleLeaderChanged(r, leader) ⇒ + if (role.orNull == r) changes :+= LeaderChanged(leader) case GetNext if changes.isEmpty ⇒ context.become(deliverNext, discardOld = false) case GetNext ⇒ @@ -138,11 +152,20 @@ object ClusterSingletonManager { // the buffer was empty when GetNext was received, deliver next event immediately def deliverNext: Actor.Receive = { case state: CurrentClusterState ⇒ - context.parent ! InitialLeaderState(state.leader, state.members.size) + val initial = role match { + case None ⇒ InitialLeaderState(state.leader, state.members.size) + case Some(r) ⇒ InitialLeaderState(state.roleLeader(r), state.members.count(_.hasRole(r))) + } + context.parent ! initial context.unbecome() case event: LeaderChanged ⇒ context.parent ! event context.unbecome() + case RoleLeaderChanged(r, leader) ⇒ + if (role.orNull == r) { + context.parent ! LeaderChanged(leader) + context.unbecome() + } } } @@ -176,11 +199,13 @@ trait ClusterSingletonPropsFactory extends Serializable { class ClusterSingletonManagerIsStuck(message: String) extends AkkaException(message, null) /** - * Manages a cluster wide singleton actor instance, i.e. - * at most one singleton instance is running at any point in time. - * The ClusterSingletonManager is supposed to be started on all - * nodes in the cluster with `actorOf`. The actual singleton is - * started on the leader node of the cluster by creating a child + * Manages singleton actor instance among all cluster nodes or a group + * of nodes tagged with a specific role. At most one singleton instance + * is running at any point in time. + * + * The ClusterSingletonManager is supposed to be started on all nodes, + * or all nodes with specified role, in the cluster with `actorOf`. + * The actual singleton is started on the leader node by creating a child * actor from the supplied `singletonProps`. * * The singleton actor is always running on the leader member, which is @@ -206,7 +231,8 @@ class ClusterSingletonManagerIsStuck(message: String) extends AkkaException(mess * * You access the singleton actor with `actorFor` using the names you have * specified when creating the ClusterSingletonManager. You can subscribe to - * [[akka.cluster.ClusterEvent.LeaderChanged]] to keep track of which node + * [[akka.cluster.ClusterEvent.LeaderChanged]] or + * [[akka.cluster.ClusterEvent.RoleLeaderChanged]] to keep track of which node * it is supposed to be running on. Alternatively the singleton actor may * broadcast its existence when it is started. * @@ -232,6 +258,10 @@ class ClusterSingletonManagerIsStuck(message: String) extends AkkaException(mess * Note that [[akka.actor.PoisonPill]] is a perfectly fine * `terminationMessage` if you only need to stop the actor. * + * '''''role''''' Singleton among the nodes tagged with specified role. + * If the role is not specified it's a singleton among all nodes in + * the cluster. + * * '''''maxHandOverRetries''''' When a node is becoming leader it sends * hand-over request to previous leader. This is retried with the * `retryInterval` until the previous leader confirms that the hand @@ -262,6 +292,7 @@ class ClusterSingletonManager( singletonProps: Option[Any] ⇒ Props, singletonName: String, terminationMessage: Any, + role: Option[String], maxHandOverRetries: Int = 20, maxTakeOverRetries: Int = 15, retryInterval: FiniteDuration = 1.second, @@ -278,13 +309,14 @@ class ClusterSingletonManager( def this( singletonName: String, terminationMessage: Any, + role: String, maxHandOverRetries: Int, maxTakeOverRetries: Int, retryInterval: FiniteDuration, loggingEnabled: Boolean, singletonPropsFactory: ClusterSingletonPropsFactory) = this(handOverData ⇒ singletonPropsFactory.create(handOverData.orNull), singletonName, terminationMessage, - maxHandOverRetries, maxTakeOverRetries, retryInterval) + ClusterSingletonManager.Internal.roleOption(role), maxHandOverRetries, maxTakeOverRetries, retryInterval) /** * Java API constructor with default values. @@ -292,8 +324,10 @@ class ClusterSingletonManager( def this( singletonName: String, terminationMessage: Any, + role: String, singletonPropsFactory: ClusterSingletonPropsFactory) = - this(handOverData ⇒ singletonPropsFactory.create(handOverData.orNull), singletonName, terminationMessage) + this(handOverData ⇒ singletonPropsFactory.create(handOverData.orNull), singletonName, terminationMessage, + ClusterSingletonManager.Internal.roleOption(role)) import ClusterSingletonManager._ import ClusterSingletonManager.Internal._ @@ -301,6 +335,12 @@ class ClusterSingletonManager( val cluster = Cluster(context.system) val selfAddressOption = Some(cluster.selfAddress) + + role match { + case None ⇒ + case Some(r) ⇒ require(cluster.selfRoles.contains(r), s"This cluster member [${cluster.selfAddress}] doesn't have the role [$role]") + } + // started when when self member is Up var leaderChangedBuffer: ActorRef = _ // Previous GetNext request delivered event and new GetNext is to be sent @@ -357,7 +397,7 @@ class ClusterSingletonManager( when(Start) { case Event(StartLeaderChangedBuffer, _) ⇒ - leaderChangedBuffer = context.actorOf(Props[LeaderChangedBuffer].withDispatcher(context.props.dispatcher)) + leaderChangedBuffer = context.actorOf(Props(new LeaderChangedBuffer(role)).withDispatcher(context.props.dispatcher)) getNextLeaderChanged() stay diff --git a/akka-contrib/src/multi-jvm/scala/akka/contrib/pattern/ClusterSingletonManagerChaosSpec.scala b/akka-contrib/src/multi-jvm/scala/akka/contrib/pattern/ClusterSingletonManagerChaosSpec.scala index 6255910ae4..3d0cd3c186 100644 --- a/akka-contrib/src/multi-jvm/scala/akka/contrib/pattern/ClusterSingletonManagerChaosSpec.scala +++ b/akka-contrib/src/multi-jvm/scala/akka/contrib/pattern/ClusterSingletonManagerChaosSpec.scala @@ -90,7 +90,8 @@ class ClusterSingletonManagerChaosSpec extends MultiNodeSpec(ClusterSingletonMan system.actorOf(Props(new ClusterSingletonManager( singletonProps = handOverData ⇒ Props(new Echo(testActor)), singletonName = "echo", - terminationMessage = PoisonPill)), + terminationMessage = PoisonPill, + role = None)), name = "singleton") } diff --git a/akka-contrib/src/multi-jvm/scala/akka/contrib/pattern/ClusterSingletonManagerSpec.scala b/akka-contrib/src/multi-jvm/scala/akka/contrib/pattern/ClusterSingletonManagerSpec.scala index 835a78290b..f1ae011c61 100644 --- a/akka-contrib/src/multi-jvm/scala/akka/contrib/pattern/ClusterSingletonManagerSpec.scala +++ b/akka-contrib/src/multi-jvm/scala/akka/contrib/pattern/ClusterSingletonManagerSpec.scala @@ -27,6 +27,7 @@ import akka.actor.Terminated object ClusterSingletonManagerSpec extends MultiNodeConfig { val controller = role("controller") + val observer = role("observer") val first = role("first") val second = role("second") val third = role("third") @@ -42,6 +43,9 @@ object ClusterSingletonManagerSpec extends MultiNodeConfig { akka.cluster.auto-down = on """)) + nodeConfig(first, second, third, fourth, fifth, sixth)( + ConfigFactory.parseString("akka.cluster.roles =[worker]")) + object PointToPointChannel { case object RegisterConsumer case object UnregisterConsumer @@ -162,6 +166,30 @@ object ClusterSingletonManagerSpec extends MultiNodeConfig { } //#singleton-proxy + // documentation of how to keep track of the role leader address in user land + //#singleton-proxy2 + class ConsumerProxy2 extends Actor { + // subscribe to RoleLeaderChanged, re-subscribe when restart + override def preStart(): Unit = + Cluster(context.system).subscribe(self, classOf[RoleLeaderChanged]) + override def postStop(): Unit = + Cluster(context.system).unsubscribe(self) + + val role = "worker" + var leaderAddress: Option[Address] = None + + def receive = { + case state: CurrentClusterState ⇒ leaderAddress = state.roleLeader(role) + case RoleLeaderChanged(r, leader) ⇒ if (r == role) leaderAddress = leader + case other ⇒ consumer foreach { _ forward other } + } + + def consumer: Option[ActorRef] = + leaderAddress map (a ⇒ context.actorFor(RootActorPath(a) / + "user" / "singleton" / "consumer")) + } + //#singleton-proxy2 + } class ClusterSingletonManagerMultiJvmNode1 extends ClusterSingletonManagerSpec @@ -171,6 +199,7 @@ class ClusterSingletonManagerMultiJvmNode4 extends ClusterSingletonManagerSpec class ClusterSingletonManagerMultiJvmNode5 extends ClusterSingletonManagerSpec class ClusterSingletonManagerMultiJvmNode6 extends ClusterSingletonManagerSpec class ClusterSingletonManagerMultiJvmNode7 extends ClusterSingletonManagerSpec +class ClusterSingletonManagerMultiJvmNode8 extends ClusterSingletonManagerSpec class ClusterSingletonManagerSpec extends MultiNodeSpec(ClusterSingletonManagerSpec) with STMultiNodeSpec with ImplicitSender { import ClusterSingletonManagerSpec._ @@ -181,13 +210,13 @@ class ClusterSingletonManagerSpec extends MultiNodeSpec(ClusterSingletonManagerS //#sort-cluster-roles // Sort the roles in the order used by the cluster. - lazy val sortedClusterRoles: immutable.IndexedSeq[RoleName] = { + lazy val sortedWorkerNodes: immutable.IndexedSeq[RoleName] = { implicit val clusterOrdering: Ordering[RoleName] = new Ordering[RoleName] { import Member.addressOrdering def compare(x: RoleName, y: RoleName) = addressOrdering.compare(node(x).address, node(y).address) } - roles.filterNot(_ == controller).toVector.sorted + roles.filterNot(r ⇒ r == controller || r == observer).toVector.sorted } //#sort-cluster-roles @@ -196,7 +225,7 @@ class ClusterSingletonManagerSpec extends MultiNodeSpec(ClusterSingletonManagerS def join(from: RoleName, to: RoleName): Unit = { runOn(from) { Cluster(system) join node(to).address - createSingleton() + if (Cluster(system).selfRoles.contains("worker")) createSingleton() } } @@ -206,7 +235,8 @@ class ClusterSingletonManagerSpec extends MultiNodeSpec(ClusterSingletonManagerS singletonProps = handOverData ⇒ Props(new Consumer(handOverData, queue, testActor)), singletonName = "consumer", - terminationMessage = End)), + terminationMessage = End, + role = Some("worker"))), name = "singleton") //#create-singleton-manager } @@ -231,7 +261,7 @@ class ClusterSingletonManagerSpec extends MultiNodeSpec(ClusterSingletonManagerS runOn(leader) { expectMsg(msg) } - runOn(sortedClusterRoles.filterNot(_ == leader): _*) { + runOn(sortedWorkerNodes.filterNot(_ == leader): _*) { expectNoMsg(1 second) } enterBarrier(leader.name + "-verified") @@ -251,7 +281,7 @@ class ClusterSingletonManagerSpec extends MultiNodeSpec(ClusterSingletonManagerS "A ClusterSingletonManager" must { "startup in single member cluster" in within(10 seconds) { - log.info("Sorted cluster nodes [{}]", sortedClusterRoles.map(node(_).address).mkString(", ")) + log.info("Sorted cluster nodes [{}]", sortedWorkerNodes.map(node(_).address).mkString(", ")) runOn(controller) { // watch that it is not terminated, which would indicate misbehaviour @@ -259,44 +289,48 @@ class ClusterSingletonManagerSpec extends MultiNodeSpec(ClusterSingletonManagerS } enterBarrier("queue-started") - join(sortedClusterRoles.last, sortedClusterRoles.last) - verify(sortedClusterRoles.last, msg = 1, expectedCurrent = 0) + join(sortedWorkerNodes.last, sortedWorkerNodes.last) + verify(sortedWorkerNodes.last, msg = 1, expectedCurrent = 0) + + // join the observer node as well, which should not influence since it doesn't have the "worker" role + join(observer, sortedWorkerNodes.last) + enterBarrier("after-1") } "hand over when new leader joins to 1 node cluster" in within(15 seconds) { - val newLeaderRole = sortedClusterRoles(4) - join(newLeaderRole, sortedClusterRoles.last) + val newLeaderRole = sortedWorkerNodes(4) + join(newLeaderRole, sortedWorkerNodes.last) verify(newLeaderRole, msg = 2, expectedCurrent = 1) } "hand over when new leader joins to 2 nodes cluster" in within(15 seconds) { - val newLeaderRole = sortedClusterRoles(3) - join(newLeaderRole, sortedClusterRoles.last) + val newLeaderRole = sortedWorkerNodes(3) + join(newLeaderRole, sortedWorkerNodes.last) verify(newLeaderRole, msg = 3, expectedCurrent = 2) } "hand over when new leader joins to 3 nodes cluster" in within(15 seconds) { - val newLeaderRole = sortedClusterRoles(2) - join(newLeaderRole, sortedClusterRoles.last) + val newLeaderRole = sortedWorkerNodes(2) + join(newLeaderRole, sortedWorkerNodes.last) verify(newLeaderRole, msg = 4, expectedCurrent = 3) } "hand over when new leader joins to 4 nodes cluster" in within(15 seconds) { - val newLeaderRole = sortedClusterRoles(1) - join(newLeaderRole, sortedClusterRoles.last) + val newLeaderRole = sortedWorkerNodes(1) + join(newLeaderRole, sortedWorkerNodes.last) verify(newLeaderRole, msg = 5, expectedCurrent = 4) } "hand over when new leader joins to 5 nodes cluster" in within(15 seconds) { - val newLeaderRole = sortedClusterRoles(0) - join(newLeaderRole, sortedClusterRoles.last) + val newLeaderRole = sortedWorkerNodes(0) + join(newLeaderRole, sortedWorkerNodes.last) verify(newLeaderRole, msg = 6, expectedCurrent = 5) } "hand over when leader leaves in 6 nodes cluster " in within(30 seconds) { //#test-leave - val leaveRole = sortedClusterRoles(0) - val newLeaderRole = sortedClusterRoles(1) + val leaveRole = sortedWorkerNodes(0) + val newLeaderRole = sortedWorkerNodes(1) runOn(leaveRole) { Cluster(system) leave node(leaveRole).address @@ -320,18 +354,18 @@ class ClusterSingletonManagerSpec extends MultiNodeSpec(ClusterSingletonManagerS system.eventStream.publish(Mute(EventFilter.error(pattern = ".*Association failed.*"))) enterBarrier("logs-muted") - crash(sortedClusterRoles(1)) - verify(sortedClusterRoles(2), msg = 8, expectedCurrent = 0) + crash(sortedWorkerNodes(1)) + verify(sortedWorkerNodes(2), msg = 8, expectedCurrent = 0) } "take over when two leaders crash in 3 nodes cluster" in within(60 seconds) { - crash(sortedClusterRoles(2), sortedClusterRoles(3)) - verify(sortedClusterRoles(4), msg = 9, expectedCurrent = 0) + crash(sortedWorkerNodes(2), sortedWorkerNodes(3)) + verify(sortedWorkerNodes(4), msg = 9, expectedCurrent = 0) } "take over when leader crashes in 2 nodes cluster" in within(60 seconds) { - crash(sortedClusterRoles(4)) - verify(sortedClusterRoles(5), msg = 10, expectedCurrent = 0) + crash(sortedWorkerNodes(4)) + verify(sortedWorkerNodes(5), msg = 10, expectedCurrent = 0) } } diff --git a/akka-docs/rst/cluster/cluster-usage-java.rst b/akka-docs/rst/cluster/cluster-usage-java.rst index 38220d1224..a6c1542529 100644 --- a/akka-docs/rst/cluster/cluster-usage-java.rst +++ b/akka-docs/rst/cluster/cluster-usage-java.rst @@ -85,7 +85,7 @@ In the log output you see that the cluster node has been started and changed sta 2552 corresponds to the port of the second seed-nodes element in the configuration. In the log output you see that the cluster node has been started and joins the other seed node -and becomes a member of the cluster. It's status changed to 'Up'. +and becomes a member of the cluster. Its status changed to 'Up'. Switch over to the first terminal window and see in the log output that the member joined. @@ -237,8 +237,17 @@ frontend nodes and 3 backend nodes:: mvn exec:java \ -Dexec.mainClass="sample.cluster.transformation.japi.TransformationFrontendMain" +Node Roles +^^^^^^^^^^ -.. note:: The above example should probably be designed as two separate, frontend/backend, clusters, when there is a `cluster client for decoupling clusters `_. +Not all nodes of a cluster need to perform the same function: there might be one sub-set which runs the web front-end, +one which runs the data access layer and one for the number-crunching. Deployment of actors—for example by cluster-aware +routers—can take node roles into account to achieve this distribution of responsibilities. + +The roles of a node is defined in the configuration property named ``akka.cluster.roles`` +and it is typically defined in the start script as a system property or environment variable. + +The roles of the nodes is part of the membership information in ``MemberEvent`` that you can subscribe to. How To Startup when Cluster Size Reached ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -251,6 +260,11 @@ before the leader changes member status of 'Joining' members to 'Up'. .. includecode:: ../../../akka-samples/akka-sample-cluster/src/main/resources/factorial.conf#min-nr-of-members +In a similar way you can define required number of members of a certain role +before the leader changes member status of 'Joining' members to 'Up'. + +.. includecode:: ../../../akka-samples/akka-sample-cluster/src/main/resources/factorial.conf#role-min-nr-of-members + You can start the actors in a ``registerOnMemberUp`` callback, which will be invoked when the current member status is changed tp 'Up', i.e. the cluster has at least the defined number of members. @@ -265,10 +279,10 @@ Cluster Singleton Pattern For some use cases it is convenient and sometimes also mandatory to ensure that you have exactly one actor of a certain type running somewhere in the cluster. -This can be implemented by subscribing to ``LeaderChanged`` events, but there are -several corner cases to consider. Therefore, this specific use case is made easily -accessible by the :ref:`cluster-singleton` in the contrib module. You can use it as is, -or adjust to fit your specific needs. +This can be implemented by subscribing to ``LeaderChanged`` or ``RoleLeaderChanged`` +events, but there are several corner cases to consider. Therefore, this specific use +case is made easily accessible by the :ref:`cluster-singleton` in the contrib module. +You can use it as is, or adjust to fit your specific needs. Failure Detector ^^^^^^^^^^^^^^^^ @@ -307,7 +321,7 @@ previous heartbeat. Phi is calculated from the mean and standard deviation of historical inter arrival times. The previous chart is an example for standard deviation of 200 ms. If the heartbeats arrive with less deviation the curve becomes steeper, -i.e. it's possible to determine failure more quickly. The curve looks like this for +i.e. it is possible to determine failure more quickly. The curve looks like this for a standard deviation of 100 ms. .. image:: images/phi2.png @@ -345,7 +359,9 @@ are already running, the configuration for a router looks like this: .. includecode:: ../../../akka-samples/akka-sample-cluster/src/multi-jvm/scala/sample/cluster/stats/StatsSampleSpec.scala#router-lookup-config -It's the relative actor path defined in ``routees-path`` that identify what actor to lookup. +It is the relative actor path defined in ``routees-path`` that identify what actor to lookup. +It is possible to limit the lookup of routees to member nodes tagged with a certain role by +specifying ``use-role``. ``nr-of-instances`` defines total number of routees in the cluster, but there will not be more than one per node. Setting ``nr-of-instances`` to a high value will result in new routees @@ -361,6 +377,9 @@ the configuration for a router looks like this: .. includecode:: ../../../akka-samples/akka-sample-cluster/src/multi-jvm/scala/sample/cluster/stats/StatsSampleSingleMasterSpec.scala#router-deploy-config +It is possible to limit the deployment of routees to member nodes tagged with a certain role by +specifying ``use-role``. + ``nr-of-instances`` defines total number of routees in the cluster, but the number of routees per node, ``max-nr-of-instances-per-node``, will not be exceeded. Setting ``nr-of-instances`` to a high value will result in creating and deploying additional routees when new nodes join @@ -373,8 +392,8 @@ The same type of router could also have been defined in code: See :ref:`cluster_configuration_java` section for further descriptions of the settings. -Router Example with Remote Deployed Routees -------------------------------------------- +Router Example with Lookup of Routees +------------------------------------- Let's take a look at how to use cluster aware routers. @@ -411,7 +430,7 @@ or with create and deploy of routees. Remember, routees are the workers in this We start with the router setup with lookup of routees. All nodes start ``StatsService`` and ``StatsWorker`` actors and the router is configured with ``routees-path``: -.. includecode:: ../../../akka-samples/akka-sample-cluster/src/main/java/sample/cluster/stats/japi/StatsSampleMain.java#start-router-lookup +.. includecode:: ../../../akka-samples/akka-sample-cluster/src/main/resources/application.conf#config-router-lookup This means that user requests can be sent to ``StatsService`` on any node and it will use ``StatsWorker`` on all nodes. There can only be one worker per node, but that worker could easily @@ -424,22 +443,22 @@ Run it by starting nodes in different terminal windows. For example, starting 3 service nodes and 1 client:: mvn exec:java \ - -Dexec.mainClass="run-main sample.cluster.stats.japi.StatsSampleMain" \ + -Dexec.mainClass="sample.cluster.stats.japi.StatsSampleMain" \ -Dexec.args="2551" mvn exec:java \ - -Dexec.mainClass="run-main sample.cluster.stats.japi.StatsSampleMain" \ + -Dexec.mainClass="sample.cluster.stats.japi.StatsSampleMain" \ -Dexec.args="2552" mvn exec:java \ - -Dexec.mainClass="run-main sample.cluster.stats.japi.StatsSampleMain" + -Dexec.mainClass="sample.cluster.stats.japi.StatsSampleMain" mvn exec:java \ - -Dexec.mainClass="run-main sample.cluster.stats.japi.StatsSampleMain" + -Dexec.mainClass="sample.cluster.stats.japi.StatsSampleMain" -Router Example with Lookup of Routees -------------------------------------- +Router Example with Remote Deployed Routees +------------------------------------------- The above setup is nice for this example, but we will also take a look at how to use a single master node that creates and deploys workers. To keep track of a single @@ -460,7 +479,7 @@ sorted first in the member ring, i.e. it can change when new nodes join or when All nodes start ``StatsFacade`` and the ``ClusterSingletonManager``. The router is now configured like this: -.. includecode:: ../../../akka-samples/akka-sample-cluster/src/main/java/sample/cluster/stats/japi/StatsSampleOneMasterMain.java#start-router-deploy +.. includecode:: ../../../akka-samples/akka-sample-cluster/src/main/resources/application.conf#config-router-deploy This example is included in ``akka-samples/akka-sample-cluster`` and you can try it by copying the `source <@github@/akka-samples/akka-sample-cluster>`_ to your @@ -539,11 +558,11 @@ The frontend that receives user jobs and delegates to the backends via the route .. includecode:: ../../../akka-samples/akka-sample-cluster/src/main/java/sample/cluster/factorial/japi/FactorialFrontend.java#frontend -As you can see, the router is defined in the same way as other routers, and in this case it's configured as follows: +As you can see, the router is defined in the same way as other routers, and in this case it is configured as follows: .. includecode:: ../../../akka-samples/akka-sample-cluster/src/main/resources/application.conf#adaptive-router -It's only router type ``adaptive`` and the ``metrics-selector`` that is specific to this router, other things work +It is only router type ``adaptive`` and the ``metrics-selector`` that is specific to this router, other things work in the same way as other routers. The same type of router could also have been defined in code: @@ -559,18 +578,18 @@ Run it by starting nodes in different terminal windows. For example, starting 3 one frontend:: mvn exec:java \ - -Dexec.mainClass="sample.cluster.factorial.FactorialBackendMain" \ + -Dexec.mainClass="sample.cluster.factorial.japi.FactorialBackendMain" \ -Dexec.args="2551" mvn exec:java \ - -Dexec.mainClass="sample.cluster.factorial.FactorialBackendMain" \ + -Dexec.mainClass="sample.cluster.factorial.japi.FactorialBackendMain" \ -Dexec.args="2552" mvn exec:java \ - -Dexec.mainClass="sample.cluster.factorial.FactorialBackendMain" + -Dexec.mainClass="sample.cluster.factorial.japi.FactorialBackendMain" mvn exec:java \ - -Dexec.mainClass="sample.cluster.factorial.FactorialFrontendMain" + -Dexec.mainClass="sample.cluster.factorial.japi.FactorialFrontendMain" Press ctrl-c in the terminal window of the frontend to stop the factorial calculations. @@ -578,7 +597,7 @@ Press ctrl-c in the terminal window of the frontend to stop the factorial calcul Subscribe to Metrics Events --------------------------- -It's possible to subscribe to the metrics events directly to implement other functionality. +It is possible to subscribe to the metrics events directly to implement other functionality. .. includecode:: ../../../akka-samples/akka-sample-cluster/src/main/java/sample/cluster/factorial/japi/MetricsListener.java#metrics-listener diff --git a/akka-docs/rst/cluster/cluster-usage-scala.rst b/akka-docs/rst/cluster/cluster-usage-scala.rst index e029e57f31..804234a892 100644 --- a/akka-docs/rst/cluster/cluster-usage-scala.rst +++ b/akka-docs/rst/cluster/cluster-usage-scala.rst @@ -63,7 +63,7 @@ In the log output you see that the cluster node has been started and changed sta 2552 corresponds to the port of the second seed-nodes element in the configuration. In the log output you see that the cluster node has been started and joins the other seed node -and becomes a member of the cluster. It's status changed to 'Up'. +and becomes a member of the cluster. Its status changed to 'Up'. Switch over to the first terminal window and see in the log output that the member joined. @@ -210,8 +210,17 @@ frontend nodes and 3 backend nodes:: run-main sample.cluster.transformation.TransformationFrontend +Node Roles +^^^^^^^^^^ -.. note:: The above example should probably be designed as two separate, frontend/backend, clusters, when there is a `cluster client for decoupling clusters `_. +Not all nodes of a cluster need to perform the same function: there might be one sub-set which runs the web front-end, +one which runs the data access layer and one for the number-crunching. Deployment of actors—for example by cluster-aware +routers—can take node roles into account to achieve this distribution of responsibilities. + +The roles of a node is defined in the configuration property named ``akka.cluster.roles`` +and it is typically defined in the start script as a system property or environment variable. + +The roles of the nodes is part of the membership information in ``MemberEvent`` that you can subscribe to. How To Startup when Cluster Size Reached ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -224,6 +233,11 @@ before the leader changes member status of 'Joining' members to 'Up'. .. includecode:: ../../../akka-samples/akka-sample-cluster/src/main/resources/factorial.conf#min-nr-of-members +In a similar way you can define required number of members of a certain role +before the leader changes member status of 'Joining' members to 'Up'. + +.. includecode:: ../../../akka-samples/akka-sample-cluster/src/main/resources/factorial.conf#role-min-nr-of-members + You can start the actors in a ``registerOnMemberUp`` callback, which will be invoked when the current member status is changed tp 'Up', i.e. the cluster has at least the defined number of members. @@ -238,10 +252,10 @@ Cluster Singleton Pattern For some use cases it is convenient and sometimes also mandatory to ensure that you have exactly one actor of a certain type running somewhere in the cluster. -This can be implemented by subscribing to ``LeaderChanged`` events, but there are -several corner cases to consider. Therefore, this specific use case is made easily -accessible by the :ref:`cluster-singleton` in the contrib module. You can use it as is, -or adjust to fit your specific needs. +This can be implemented by subscribing to ``LeaderChanged`` or ``RoleLeaderChanged`` +events, but there are several corner cases to consider. Therefore, this specific use +case is made easily accessible by the :ref:`cluster-singleton` in the contrib module. +You can use it as is, or adjust to fit your specific needs. Failure Detector ^^^^^^^^^^^^^^^^ @@ -280,7 +294,7 @@ previous heartbeat. Phi is calculated from the mean and standard deviation of historical inter arrival times. The previous chart is an example for standard deviation of 200 ms. If the heartbeats arrive with less deviation the curve becomes steeper, -i.e. it's possible to determine failure more quickly. The curve looks like this for +i.e. it is possible to determine failure more quickly. The curve looks like this for a standard deviation of 100 ms. .. image:: images/phi2.png @@ -321,7 +335,9 @@ are already running, the configuration for a router looks like this: .. includecode:: ../../../akka-samples/akka-sample-cluster/src/multi-jvm/scala/sample/cluster/stats/StatsSampleSpec.scala#router-lookup-config -It's the relative actor path defined in ``routees-path`` that identify what actor to lookup. +It is the relative actor path defined in ``routees-path`` that identify what actor to lookup. +It is possible to limit the lookup of routees to member nodes tagged with a certain role by +specifying ``use-role``. ``nr-of-instances`` defines total number of routees in the cluster, but there will not be more than one per node. Setting ``nr-of-instances`` to a high value will result in new routees @@ -336,6 +352,8 @@ the configuration for a router looks like this: .. includecode:: ../../../akka-samples/akka-sample-cluster/src/multi-jvm/scala/sample/cluster/stats/StatsSampleSingleMasterSpec.scala#router-deploy-config +It is possible to limit the deployment of routees to member nodes tagged with a certain role by +specifying ``use-role``. ``nr-of-instances`` defines total number of routees in the cluster, but the number of routees per node, ``max-nr-of-instances-per-node``, will not be exceeded. Setting ``nr-of-instances`` @@ -349,8 +367,8 @@ The same type of router could also have been defined in code: See :ref:`cluster_configuration_scala` section for further descriptions of the settings. -Router Example with Remote Deployed Routees -------------------------------------------- +Router Example with Lookup of Routees +------------------------------------- Let's take a look at how to use cluster aware routers. @@ -385,7 +403,7 @@ or with create and deploy of routees. Remember, routees are the workers in this We start with the router setup with lookup of routees. All nodes start ``StatsService`` and ``StatsWorker`` actors and the router is configured with ``routees-path``: -.. includecode:: ../../../akka-samples/akka-sample-cluster/src/main/scala/sample/cluster/stats/StatsSample.scala#start-router-lookup +.. includecode:: ../../../akka-samples/akka-sample-cluster/src/main/resources/application.conf#config-router-lookup This means that user requests can be sent to ``StatsService`` on any node and it will use ``StatsWorker`` on all nodes. There can only be one worker per node, but that worker could easily @@ -407,8 +425,8 @@ service nodes and 1 client:: run-main sample.cluster.stats.StatsSample -Router Example with Lookup of Routees -------------------------------------- +Router Example with Remote Deployed Routees +------------------------------------------- The above setup is nice for this example, but we will also take a look at how to use a single master node that creates and deploys workers. To keep track of a single @@ -429,7 +447,7 @@ sorted first in the member ring, i.e. it can change when new nodes join or when All nodes start ``StatsFacade`` and the ``ClusterSingletonManager``. The router is now configured like this: -.. includecode:: ../../../akka-samples/akka-sample-cluster/src/main/scala/sample/cluster/stats/StatsSample.scala#start-router-deploy +.. includecode:: ../../../akka-samples/akka-sample-cluster/src/main/resources/application.conf#config-router-deploy This example is included in ``akka-samples/akka-sample-cluster`` @@ -495,11 +513,11 @@ The frontend that receives user jobs and delegates to the backends via the route .. includecode:: ../../../akka-samples/akka-sample-cluster/src/main/scala/sample/cluster/factorial/FactorialSample.scala#frontend -As you can see, the router is defined in the same way as other routers, and in this case it's configured as follows: +As you can see, the router is defined in the same way as other routers, and in this case it is configured as follows: .. includecode:: ../../../akka-samples/akka-sample-cluster/src/main/resources/application.conf#adaptive-router -It's only router type ``adaptive`` and the ``metrics-selector`` that is specific to this router, other things work +It is only router type ``adaptive`` and the ``metrics-selector`` that is specific to this router, other things work in the same way as other routers. The same type of router could also have been defined in code: @@ -528,7 +546,7 @@ Press ctrl-c in the terminal window of the frontend to stop the factorial calcul Subscribe to Metrics Events --------------------------- -It's possible to subscribe to the metrics events directly to implement other functionality. +It is possible to subscribe to the metrics events directly to implement other functionality. .. includecode:: ../../../akka-samples/akka-sample-cluster/src/main/scala/sample/cluster/factorial/FactorialSample.scala#metrics-listener @@ -560,7 +578,7 @@ implemented differently, but often they are the same and extend an abstract test .. 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 +and so on. It is 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. diff --git a/akka-samples/akka-sample-cluster/src/main/java/sample/cluster/factorial/japi/FactorialBackendMain.java b/akka-samples/akka-sample-cluster/src/main/java/sample/cluster/factorial/japi/FactorialBackendMain.java index 14bf75091a..7041fd9428 100644 --- a/akka-samples/akka-sample-cluster/src/main/java/sample/cluster/factorial/japi/FactorialBackendMain.java +++ b/akka-samples/akka-sample-cluster/src/main/java/sample/cluster/factorial/japi/FactorialBackendMain.java @@ -1,5 +1,6 @@ package sample.cluster.factorial.japi; +import com.typesafe.config.Config; import com.typesafe.config.ConfigFactory; import akka.actor.ActorSystem; import akka.actor.Props; @@ -7,12 +8,15 @@ import akka.actor.Props; public class FactorialBackendMain { public static void main(String[] args) throws Exception { - // Override the configuration of the port - // when specified as program argument - if (args.length > 0) - System.setProperty("akka.remote.netty.tcp.port", args[0]); + // Override the configuration of the port when specified as program argument + final Config config = + (args.length > 0 ? + ConfigFactory.parseString(String.format("akka.remote.netty.tcp.port=%s", args[0])) : + ConfigFactory.empty()). + withFallback(ConfigFactory.parseString("akka.cluster.roles = [backend]")). + withFallback(ConfigFactory.load("factorial")); - ActorSystem system = ActorSystem.create("ClusterSystem", ConfigFactory.load("factorial")); + ActorSystem system = ActorSystem.create("ClusterSystem", config); system.actorOf(new Props(FactorialBackend.class), "factorialBackend"); diff --git a/akka-samples/akka-sample-cluster/src/main/java/sample/cluster/factorial/japi/FactorialFrontend.java b/akka-samples/akka-sample-cluster/src/main/java/sample/cluster/factorial/japi/FactorialFrontend.java index 13af688739..0ee5fc0624 100644 --- a/akka-samples/akka-sample-cluster/src/main/java/sample/cluster/factorial/japi/FactorialFrontend.java +++ b/akka-samples/akka-sample-cluster/src/main/java/sample/cluster/factorial/japi/FactorialFrontend.java @@ -62,29 +62,31 @@ public class FactorialFrontend extends UntypedActor { abstract class FactorialFrontend2 extends UntypedActor { //#router-lookup-in-code int totalInstances = 100; - String routeesPath = "/user/statsWorker"; + String routeesPath = "/user/factorialBackend"; boolean allowLocalRoutees = true; + String useRole = "backend"; ActorRef backend = getContext().actorOf( new Props(FactorialBackend.class).withRouter(new ClusterRouterConfig( - new AdaptiveLoadBalancingRouter(HeapMetricsSelector.getInstance(), 0), + new AdaptiveLoadBalancingRouter(HeapMetricsSelector.getInstance(), 0), new ClusterRouterSettings( - totalInstances, routeesPath, allowLocalRoutees))), + totalInstances, routeesPath, allowLocalRoutees, useRole))), "factorialBackendRouter2"); //#router-lookup-in-code } //not used, only for documentation -abstract class StatsService3 extends UntypedActor { +abstract class FactorialFrontend3 extends UntypedActor { //#router-deploy-in-code int totalInstances = 100; int maxInstancesPerNode = 3; boolean allowLocalRoutees = false; + String useRole = "backend"; ActorRef backend = getContext().actorOf( new Props(FactorialBackend.class).withRouter(new ClusterRouterConfig( new AdaptiveLoadBalancingRouter( - SystemLoadAverageMetricsSelector.getInstance(), 0), + SystemLoadAverageMetricsSelector.getInstance(), 0), new ClusterRouterSettings( - totalInstances, maxInstancesPerNode, allowLocalRoutees))), + totalInstances, maxInstancesPerNode, allowLocalRoutees, useRole))), "factorialBackendRouter3"); //#router-deploy-in-code } \ No newline at end of file diff --git a/akka-samples/akka-sample-cluster/src/main/java/sample/cluster/factorial/japi/FactorialFrontendMain.java b/akka-samples/akka-sample-cluster/src/main/java/sample/cluster/factorial/japi/FactorialFrontendMain.java index e22ad6c2cb..c92c2a3eff 100644 --- a/akka-samples/akka-sample-cluster/src/main/java/sample/cluster/factorial/japi/FactorialFrontendMain.java +++ b/akka-samples/akka-sample-cluster/src/main/java/sample/cluster/factorial/japi/FactorialFrontendMain.java @@ -1,5 +1,6 @@ package sample.cluster.factorial.japi; +import com.typesafe.config.Config; import com.typesafe.config.ConfigFactory; import akka.actor.ActorSystem; import akka.actor.Props; @@ -12,8 +13,11 @@ public class FactorialFrontendMain { public static void main(String[] args) throws Exception { final int upToN = (args.length == 0 ? 200 : Integer.valueOf(args[0])); - final ActorSystem system = ActorSystem.create("ClusterSystem", ConfigFactory.load("factorial")); - system.log().info("Factorials will start when 3 members in the cluster."); + final Config config = ConfigFactory.parseString("akka.cluster.roles = [frontend]"). + withFallback(ConfigFactory.load("factorial")); + + final ActorSystem system = ActorSystem.create("ClusterSystem", config); + system.log().info("Factorials will start when 2 backend members in the cluster."); //#registerOnUp Cluster.get(system).registerOnMemberUp(new Runnable() { @Override diff --git a/akka-samples/akka-sample-cluster/src/main/java/sample/cluster/stats/japi/StatsFacade.java b/akka-samples/akka-sample-cluster/src/main/java/sample/cluster/stats/japi/StatsFacade.java index 9b311835d3..bd80401cf9 100644 --- a/akka-samples/akka-sample-cluster/src/main/java/sample/cluster/stats/japi/StatsFacade.java +++ b/akka-samples/akka-sample-cluster/src/main/java/sample/cluster/stats/japi/StatsFacade.java @@ -9,7 +9,7 @@ import akka.actor.UntypedActor; import akka.dispatch.Recover; import akka.cluster.Cluster; import akka.cluster.ClusterEvent.CurrentClusterState; -import akka.cluster.ClusterEvent.LeaderChanged; +import akka.cluster.ClusterEvent.RoleLeaderChanged; import akka.event.Logging; import akka.event.LoggingAdapter; import akka.util.Timeout; @@ -25,10 +25,10 @@ public class StatsFacade extends UntypedActor { Address currentMaster = null; - //subscribe to cluster changes, MemberEvent + //subscribe to cluster changes, RoleLeaderChanged @Override public void preStart() { - cluster.subscribe(getSelf(), LeaderChanged.class); + cluster.subscribe(getSelf(), RoleLeaderChanged.class); } //re-subscribe when restart @@ -57,11 +57,12 @@ public class StatsFacade extends UntypedActor { } else if (message instanceof CurrentClusterState) { CurrentClusterState state = (CurrentClusterState) message; - currentMaster = state.getLeader(); + currentMaster = state.getRoleLeader("compute"); - } else if (message instanceof LeaderChanged) { - LeaderChanged leaderChanged = (LeaderChanged) message; - currentMaster = leaderChanged.getLeader(); + } else if (message instanceof RoleLeaderChanged) { + RoleLeaderChanged leaderChanged = (RoleLeaderChanged) message; + if (leaderChanged.role().equals("compute")) + currentMaster = leaderChanged.getLeader(); } else { unhandled(message); diff --git a/akka-samples/akka-sample-cluster/src/main/java/sample/cluster/stats/japi/StatsSampleClient.java b/akka-samples/akka-sample-cluster/src/main/java/sample/cluster/stats/japi/StatsSampleClient.java index 2a89390677..d0147007af 100644 --- a/akka-samples/akka-sample-cluster/src/main/java/sample/cluster/stats/japi/StatsSampleClient.java +++ b/akka-samples/akka-sample-cluster/src/main/java/sample/cluster/stats/japi/StatsSampleClient.java @@ -77,14 +77,15 @@ public class StatsSampleClient extends UntypedActor { CurrentClusterState state = (CurrentClusterState) message; nodes.clear(); for (Member member : state.getMembers()) { - if (member.status().equals(MemberStatus.up())) { + if (member.hasRole("compute") && member.status().equals(MemberStatus.up())) { nodes.add(member.address()); } } } else if (message instanceof MemberUp) { MemberUp mUp = (MemberUp) message; - nodes.add(mUp.member().address()); + if (mUp.member().hasRole("compute")) + nodes.add(mUp.member().address()); } else if (message instanceof MemberEvent) { MemberEvent other = (MemberEvent) message; diff --git a/akka-samples/akka-sample-cluster/src/main/java/sample/cluster/stats/japi/StatsSampleClientMain.java b/akka-samples/akka-sample-cluster/src/main/java/sample/cluster/stats/japi/StatsSampleClientMain.java index 191a8f264c..7ce373231b 100644 --- a/akka-samples/akka-sample-cluster/src/main/java/sample/cluster/stats/japi/StatsSampleClientMain.java +++ b/akka-samples/akka-sample-cluster/src/main/java/sample/cluster/stats/japi/StatsSampleClientMain.java @@ -8,6 +8,7 @@ import akka.actor.UntypedActorFactory; public class StatsSampleClientMain { public static void main(String[] args) throws Exception { + // note that client is not a compute node, role not defined ActorSystem system = ActorSystem.create("ClusterSystem"); system.actorOf(new Props(new UntypedActorFactory() { @Override diff --git a/akka-samples/akka-sample-cluster/src/main/java/sample/cluster/stats/japi/StatsSampleMain.java b/akka-samples/akka-sample-cluster/src/main/java/sample/cluster/stats/japi/StatsSampleMain.java index 9ca02ad644..e8c808f8c4 100644 --- a/akka-samples/akka-sample-cluster/src/main/java/sample/cluster/stats/japi/StatsSampleMain.java +++ b/akka-samples/akka-sample-cluster/src/main/java/sample/cluster/stats/japi/StatsSampleMain.java @@ -1,37 +1,25 @@ package sample.cluster.stats.japi; +import com.typesafe.config.Config; +import com.typesafe.config.ConfigFactory; import akka.actor.ActorSystem; import akka.actor.Props; -import com.typesafe.config.ConfigFactory; - public class StatsSampleMain { public static void main(String[] args) throws Exception { - // Override the configuration of the port - // when specified as program argument - if (args.length > 0) - System.setProperty("akka.remote.netty.tcp.port", args[0]); + // Override the configuration of the port when specified as program argument + final Config config = + (args.length > 0 ? + ConfigFactory.parseString(String.format("akka.remote.netty.tcp.port=%s", args[0])) : + ConfigFactory.empty()). + withFallback(ConfigFactory.parseString("akka.cluster.roles = [compute]")). + withFallback(ConfigFactory.load()); - //#start-router-lookup - ActorSystem system = ActorSystem.create("ClusterSystem", - ConfigFactory.parseString( - "akka.actor.deployment { \n" + - " /statsService/workerRouter { \n" + - " router = consistent-hashing \n" + - " nr-of-instances = 100 \n" + - " cluster { \n" + - " enabled = on \n" + - " routees-path = \"/user/statsWorker\" \n" + - " allow-local-routees = on \n" + - " } \n" + - " } \n" + - "} \n") - .withFallback(ConfigFactory.load())); + ActorSystem system = ActorSystem.create("ClusterSystem", config); system.actorOf(new Props(StatsWorker.class), "statsWorker"); system.actorOf(new Props(StatsService.class), "statsService"); - //#start-router-lookup } } diff --git a/akka-samples/akka-sample-cluster/src/main/java/sample/cluster/stats/japi/StatsSampleOneMasterClientMain.java b/akka-samples/akka-sample-cluster/src/main/java/sample/cluster/stats/japi/StatsSampleOneMasterClientMain.java index 3c0101fbb6..942ff37d62 100644 --- a/akka-samples/akka-sample-cluster/src/main/java/sample/cluster/stats/japi/StatsSampleOneMasterClientMain.java +++ b/akka-samples/akka-sample-cluster/src/main/java/sample/cluster/stats/japi/StatsSampleOneMasterClientMain.java @@ -11,22 +11,8 @@ import akka.contrib.pattern.ClusterSingletonPropsFactory; public class StatsSampleOneMasterClientMain { public static void main(String[] args) throws Exception { + // note that client is not a compute node, role not defined ActorSystem system = ActorSystem.create("ClusterSystem"); - - // the client is also part of the cluster - system.actorOf(new Props(new UntypedActorFactory() { - @Override - public ClusterSingletonManager create() { - return new ClusterSingletonManager("statsService", PoisonPill.getInstance(), - new ClusterSingletonPropsFactory() { - @Override - public Props create(Object handOverData) { - return new Props(StatsService.class); - } - }); - } - }), "singleton"); - system.actorOf(new Props(new UntypedActorFactory() { @Override public UntypedActor create() { diff --git a/akka-samples/akka-sample-cluster/src/main/java/sample/cluster/stats/japi/StatsSampleOneMasterMain.java b/akka-samples/akka-sample-cluster/src/main/java/sample/cluster/stats/japi/StatsSampleOneMasterMain.java index aeed8b133f..be4e8789a6 100644 --- a/akka-samples/akka-sample-cluster/src/main/java/sample/cluster/stats/japi/StatsSampleOneMasterMain.java +++ b/akka-samples/akka-sample-cluster/src/main/java/sample/cluster/stats/japi/StatsSampleOneMasterMain.java @@ -1,5 +1,7 @@ package sample.cluster.stats.japi; +import com.typesafe.config.Config; +import com.typesafe.config.ConfigFactory; import akka.actor.ActorSystem; import akka.actor.PoisonPill; import akka.actor.Props; @@ -7,38 +9,25 @@ import akka.actor.UntypedActorFactory; import akka.contrib.pattern.ClusterSingletonManager; import akka.contrib.pattern.ClusterSingletonPropsFactory; -import com.typesafe.config.ConfigFactory; - public class StatsSampleOneMasterMain { public static void main(String[] args) throws Exception { - // Override the configuration of the port - // when specified as program argument - if (args.length > 0) - System.setProperty("akka.remote.netty.tcp.port", args[0]); + // Override the configuration of the port when specified as program argument + final Config config = + (args.length > 0 ? + ConfigFactory.parseString(String.format("akka.remote.netty.tcp.port=%s", args[0])) : + ConfigFactory.empty()). + withFallback(ConfigFactory.parseString("akka.cluster.roles = [compute]")). + withFallback(ConfigFactory.load()); - //#start-router-deploy - ActorSystem system = ActorSystem.create("ClusterSystem", - ConfigFactory.parseString( - "akka.actor.deployment { \n" + - " /singleton/statsService/workerRouter { \n" + - " router = consistent-hashing \n" + - " nr-of-instances = 100 \n" + - " cluster { \n" + - " enabled = on \n" + - " max-nr-of-instances-per-node = 3 \n" + - " allow-local-routees = off \n" + - " } \n" + - " } \n" + - "} \n") - .withFallback(ConfigFactory.load())); - //#start-router-deploy + ActorSystem system = ActorSystem.create("ClusterSystem", config); //#create-singleton-manager system.actorOf(new Props(new UntypedActorFactory() { @Override public ClusterSingletonManager create() { return new ClusterSingletonManager("statsService", PoisonPill.getInstance(), + "compute", new ClusterSingletonPropsFactory() { @Override public Props create(Object handOverData) { diff --git a/akka-samples/akka-sample-cluster/src/main/java/sample/cluster/stats/japi/StatsService.java b/akka-samples/akka-sample-cluster/src/main/java/sample/cluster/stats/japi/StatsService.java index 6a3c2ca44d..81342c7d13 100644 --- a/akka-samples/akka-sample-cluster/src/main/java/sample/cluster/stats/japi/StatsService.java +++ b/akka-samples/akka-sample-cluster/src/main/java/sample/cluster/stats/japi/StatsService.java @@ -60,10 +60,11 @@ abstract class StatsService2 extends UntypedActor { int totalInstances = 100; String routeesPath = "/user/statsWorker"; boolean allowLocalRoutees = true; + String useRole = "compute"; ActorRef workerRouter = getContext().actorOf( new Props(StatsWorker.class).withRouter(new ClusterRouterConfig( new ConsistentHashingRouter(0), new ClusterRouterSettings( - totalInstances, routeesPath, allowLocalRoutees))), + totalInstances, routeesPath, allowLocalRoutees, useRole))), "workerRouter2"); //#router-lookup-in-code } @@ -74,10 +75,11 @@ abstract class StatsService3 extends UntypedActor { int totalInstances = 100; int maxInstancesPerNode = 3; boolean allowLocalRoutees = false; + String useRole = "compute"; ActorRef workerRouter = getContext().actorOf( new Props(StatsWorker.class).withRouter(new ClusterRouterConfig( new ConsistentHashingRouter(0), new ClusterRouterSettings( - totalInstances, maxInstancesPerNode, allowLocalRoutees))), + totalInstances, maxInstancesPerNode, allowLocalRoutees, useRole))), "workerRouter3"); //#router-deploy-in-code } diff --git a/akka-samples/akka-sample-cluster/src/main/java/sample/cluster/transformation/japi/TransformationBackend.java b/akka-samples/akka-sample-cluster/src/main/java/sample/cluster/transformation/japi/TransformationBackend.java index 9214874cbf..786ae874b7 100644 --- a/akka-samples/akka-sample-cluster/src/main/java/sample/cluster/transformation/japi/TransformationBackend.java +++ b/akka-samples/akka-sample-cluster/src/main/java/sample/cluster/transformation/japi/TransformationBackend.java @@ -54,10 +54,9 @@ public class TransformationBackend extends UntypedActor { } } - //try to register to all nodes, even though there - // might not be any frontend on all nodes void register(Member member) { - getContext().actorFor(member.address() + "/user/frontend").tell( + if (member.hasRole("frontend")) + getContext().actorFor(member.address() + "/user/frontend").tell( BACKEND_REGISTRATION, getSelf()); } } diff --git a/akka-samples/akka-sample-cluster/src/main/resources/application.conf b/akka-samples/akka-sample-cluster/src/main/resources/application.conf index b67eeac829..4c94ac6284 100644 --- a/akka-samples/akka-sample-cluster/src/main/resources/application.conf +++ b/akka-samples/akka-sample-cluster/src/main/resources/application.conf @@ -21,6 +21,36 @@ akka { } # //#cluster +# //#config-router-lookup +akka.actor.deployment { + /statsService/workerRouter { + router = consistent-hashing + nr-of-instances = 100 + cluster { + enabled = on + routees-path = "/user/statsWorker" + allow-local-routees = on + use-role = compute + } + } +} +# //#config-router-lookup + +# //#config-router-deploy +akka.actor.deployment { + /singleton/statsService/workerRouter { + router = consistent-hashing + nr-of-instances = 100 + cluster { + enabled = on + max-nr-of-instances-per-node = 3 + allow-local-routees = off + use-role = compute + } + } +} +# //#config-router-deploy + # //#adaptive-router akka.actor.deployment { /factorialFrontend/factorialBackendRouter = { @@ -33,6 +63,7 @@ akka.actor.deployment { cluster { enabled = on routees-path = "/user/factorialBackend" + use-role = backend allow-local-routees = off } } diff --git a/akka-samples/akka-sample-cluster/src/main/resources/factorial.conf b/akka-samples/akka-sample-cluster/src/main/resources/factorial.conf index 17e82db15d..e0c79671b6 100644 --- a/akka-samples/akka-sample-cluster/src/main/resources/factorial.conf +++ b/akka-samples/akka-sample-cluster/src/main/resources/factorial.conf @@ -2,4 +2,11 @@ include "application" # //#min-nr-of-members akka.cluster.min-nr-of-members = 3 -# //#min-nr-of-members \ No newline at end of file +# //#min-nr-of-members + +# //#role-min-nr-of-members +akka.cluster.role { + frontend.min-nr-of-members = 1 + backend.min-nr-of-members = 2 +} +# //#role-min-nr-of-members \ No newline at end of file diff --git a/akka-samples/akka-sample-cluster/src/main/scala/sample/cluster/factorial/FactorialSample.scala b/akka-samples/akka-sample-cluster/src/main/scala/sample/cluster/factorial/FactorialSample.scala index 15e5bfa21f..cc6c58fcfe 100644 --- a/akka-samples/akka-sample-cluster/src/main/scala/sample/cluster/factorial/FactorialSample.scala +++ b/akka-samples/akka-sample-cluster/src/main/scala/sample/cluster/factorial/FactorialSample.scala @@ -22,8 +22,11 @@ object FactorialFrontend { def main(args: Array[String]): Unit = { val upToN = if (args.isEmpty) 200 else args(0).toInt - val system = ActorSystem("ClusterSystem", ConfigFactory.load("factorial")) - system.log.info("Factorials will start when 3 members in the cluster.") + val config = ConfigFactory.parseString("akka.cluster.roles = [frontend]"). + withFallback(ConfigFactory.load("factorial")) + + val system = ActorSystem("ClusterSystem", config) + system.log.info("Factorials will start when 2 backend members in the cluster.") //#registerOnUp Cluster(system) registerOnMemberUp { system.actorOf(Props(new FactorialFrontend(upToN, repeat = true)), @@ -58,11 +61,14 @@ class FactorialFrontend(upToN: Int, repeat: Boolean) extends Actor with ActorLog object FactorialBackend { def main(args: Array[String]): Unit = { - // Override the configuration of the port - // when specified as program argument - if (args.nonEmpty) System.setProperty("akka.remote.netty.tcp.port", args(0)) + // Override the configuration of the port when specified as program argument + val config = + (if (args.nonEmpty) ConfigFactory.parseString(s"akka.remote.netty.tcp.port=${args(0)}") + else ConfigFactory.empty).withFallback( + ConfigFactory.parseString("akka.cluster.roles = [backend]")). + withFallback(ConfigFactory.load("factorial")) - val system = ActorSystem("ClusterSystem", ConfigFactory.load("factorial")) + val system = ActorSystem("ClusterSystem", config) system.actorOf(Props[FactorialBackend], name = "factorialBackend") system.actorOf(Props[MetricsListener], name = "metricsListener") @@ -143,8 +149,8 @@ abstract class FactorialFrontend2 extends Actor { val backend = context.actorOf(Props[FactorialBackend].withRouter( ClusterRouterConfig(AdaptiveLoadBalancingRouter(HeapMetricsSelector), ClusterRouterSettings( - totalInstances = 100, routeesPath = "/user/statsWorker", - allowLocalRoutees = true))), + totalInstances = 100, routeesPath = "/user/factorialBackend", + allowLocalRoutees = true, useRole = Some("backend")))), name = "factorialBackendRouter2") //#router-lookup-in-code } @@ -161,7 +167,7 @@ abstract class FactorialFrontend3 extends Actor { ClusterRouterConfig(AdaptiveLoadBalancingRouter( SystemLoadAverageMetricsSelector), ClusterRouterSettings( totalInstances = 100, maxInstancesPerNode = 3, - allowLocalRoutees = false))), + allowLocalRoutees = false, useRole = Some("backend")))), name = "factorialBackendRouter3") //#router-deploy-in-code } \ No newline at end of file diff --git a/akka-samples/akka-sample-cluster/src/main/scala/sample/cluster/stats/StatsSample.scala b/akka-samples/akka-sample-cluster/src/main/scala/sample/cluster/stats/StatsSample.scala index 6f157a77d6..41fc1723df 100644 --- a/akka-samples/akka-sample-cluster/src/main/scala/sample/cluster/stats/StatsSample.scala +++ b/akka-samples/akka-sample-cluster/src/main/scala/sample/cluster/stats/StatsSample.scala @@ -95,9 +95,9 @@ class StatsFacade extends Actor with ActorLogging { var currentMaster: Option[Address] = None - // subscribe to cluster changes, LeaderChanged + // subscribe to cluster changes, RoleLeaderChanged // re-subscribe when restart - override def preStart(): Unit = cluster.subscribe(self, classOf[LeaderChanged]) + override def preStart(): Unit = cluster.subscribe(self, classOf[RoleLeaderChanged]) override def postStop(): Unit = cluster.unsubscribe(self) def receive = { @@ -112,8 +112,11 @@ class StatsFacade extends Actor with ActorLogging { case _ ⇒ JobFailed("Service unavailable, try again later") } pipeTo sender } - case state: CurrentClusterState ⇒ currentMaster = state.leader - case LeaderChanged(leader) ⇒ currentMaster = leader + case state: CurrentClusterState ⇒ + currentMaster = state.roleLeader("compute") + case RoleLeaderChanged(role, leader) ⇒ + if (role == "compute") + currentMaster = leader } } @@ -121,58 +124,36 @@ class StatsFacade extends Actor with ActorLogging { object StatsSample { def main(args: Array[String]): Unit = { - // Override the configuration of the port - // when specified as program argument - if (args.nonEmpty) System.setProperty("akka.remote.netty.tcp.port", args(0)) + // Override the configuration of the port when specified as program argument + val config = + (if (args.nonEmpty) ConfigFactory.parseString(s"akka.remote.netty.tcp.port=${args(0)}") + else ConfigFactory.empty).withFallback( + ConfigFactory.parseString("akka.cluster.roles = [compute]")). + withFallback(ConfigFactory.load()) - //#start-router-lookup - val system = ActorSystem("ClusterSystem", ConfigFactory.parseString(""" - akka.actor.deployment { - /statsService/workerRouter { - router = consistent-hashing - nr-of-instances = 100 - cluster { - enabled = on - routees-path = "/user/statsWorker" - allow-local-routees = on - } - } - } - """).withFallback(ConfigFactory.load())) + val system = ActorSystem("ClusterSystem", config) system.actorOf(Props[StatsWorker], name = "statsWorker") system.actorOf(Props[StatsService], name = "statsService") - //#start-router-lookup - } } object StatsSampleOneMaster { def main(args: Array[String]): Unit = { - // Override the configuration of the port - // when specified as program argument - if (args.nonEmpty) System.setProperty("akka.remote.netty.tcp.port", args(0)) + // Override the configuration of the port when specified as program argument + val config = + (if (args.nonEmpty) ConfigFactory.parseString(s"akka.remote.netty.tcp.port=${args(0)}") + else ConfigFactory.empty).withFallback( + ConfigFactory.parseString("akka.cluster.roles = [compute]")). + withFallback(ConfigFactory.load()) - //#start-router-deploy - val system = ActorSystem("ClusterSystem", ConfigFactory.parseString(""" - akka.actor.deployment { - /singleton/statsService/workerRouter { - router = consistent-hashing - nr-of-instances = 100 - cluster { - enabled = on - max-nr-of-instances-per-node = 3 - allow-local-routees = off - } - } - } - """).withFallback(ConfigFactory.load())) - //#start-router-deploy + val system = ActorSystem("ClusterSystem", config) //#create-singleton-manager system.actorOf(Props(new ClusterSingletonManager( singletonProps = _ ⇒ Props[StatsService], singletonName = "statsService", - terminationMessage = PoisonPill)), name = "singleton") + terminationMessage = PoisonPill, role = Some("compute"))), + name = "singleton") //#create-singleton-manager system.actorOf(Props[StatsFacade], name = "statsFacade") } @@ -180,6 +161,7 @@ object StatsSampleOneMaster { object StatsSampleClient { def main(args: Array[String]): Unit = { + // note that client is not a compute node, role not defined val system = ActorSystem("ClusterSystem") system.actorOf(Props(new StatsSampleClient("/user/statsService")), "client") } @@ -187,13 +169,8 @@ object StatsSampleClient { object StatsSampleOneMasterClient { def main(args: Array[String]): Unit = { + // note that client is not a compute node, role not defined val system = ActorSystem("ClusterSystem") - - // the client is also part of the cluster - system.actorOf(Props(new ClusterSingletonManager( - singletonProps = _ ⇒ Props[StatsService], singletonName = "statsService", - terminationMessage = PoisonPill)), name = "singleton") - system.actorOf(Props(new StatsSampleClient("/user/statsFacade")), "client") } } @@ -230,10 +207,12 @@ class StatsSampleClient(servicePath: String) extends Actor { case failed: JobFailed ⇒ println(failed) case state: CurrentClusterState ⇒ - nodes = state.members.collect { case m if m.status == MemberStatus.Up ⇒ m.address } - case MemberUp(m) ⇒ nodes += m.address - case other: MemberEvent ⇒ nodes -= other.member.address - case UnreachableMember(m) ⇒ nodes -= m.address + nodes = state.members.collect { + case m if m.hasRole("compute") && m.status == MemberStatus.Up ⇒ m.address + } + case MemberUp(m) if m.hasRole("compute") ⇒ nodes += m.address + case other: MemberEvent ⇒ nodes -= other.member.address + case UnreachableMember(m) ⇒ nodes -= m.address } } @@ -248,7 +227,7 @@ abstract class StatsService2 extends Actor { val workerRouter = context.actorOf(Props[StatsWorker].withRouter( ClusterRouterConfig(ConsistentHashingRouter(), ClusterRouterSettings( totalInstances = 100, routeesPath = "/user/statsWorker", - allowLocalRoutees = true))), + allowLocalRoutees = true, useRole = Some("compute")))), name = "workerRouter2") //#router-lookup-in-code } @@ -263,7 +242,7 @@ abstract class StatsService3 extends Actor { val workerRouter = context.actorOf(Props[StatsWorker].withRouter( ClusterRouterConfig(ConsistentHashingRouter(), ClusterRouterSettings( totalInstances = 100, maxInstancesPerNode = 3, - allowLocalRoutees = false))), + allowLocalRoutees = false, useRole = None))), name = "workerRouter3") //#router-deploy-in-code } diff --git a/akka-samples/akka-sample-cluster/src/main/scala/sample/cluster/transformation/TransformationSample.scala b/akka-samples/akka-sample-cluster/src/main/scala/sample/cluster/transformation/TransformationSample.scala index e0947e04e7..01d309b2cd 100644 --- a/akka-samples/akka-sample-cluster/src/main/scala/sample/cluster/transformation/TransformationSample.scala +++ b/akka-samples/akka-sample-cluster/src/main/scala/sample/cluster/transformation/TransformationSample.scala @@ -3,7 +3,6 @@ package sample.cluster.transformation //#imports import language.postfixOps import scala.concurrent.duration._ - import akka.actor.Actor import akka.actor.ActorRef import akka.actor.ActorSystem @@ -17,6 +16,7 @@ import akka.cluster.Member import akka.cluster.MemberStatus import akka.pattern.ask import akka.util.Timeout +import com.typesafe.config.ConfigFactory //#imports //#messages @@ -28,11 +28,14 @@ case object BackendRegistration object TransformationFrontend { def main(args: Array[String]): Unit = { - // Override the configuration of the port - // when specified as program argument - if (args.nonEmpty) System.setProperty("akka.remote.netty.tcp.port", args(0)) + // Override the configuration of the port when specified as program argument + val config = + (if (args.nonEmpty) ConfigFactory.parseString(s"akka.remote.netty.tcp.port=${args(0)}") + else ConfigFactory.empty).withFallback( + ConfigFactory.parseString("akka.cluster.roles = [frontend]")). + withFallback(ConfigFactory.load()) - val system = ActorSystem("ClusterSystem") + val system = ActorSystem("ClusterSystem", config) val frontend = system.actorOf(Props[TransformationFrontend], name = "frontend") import system.dispatcher @@ -41,7 +44,7 @@ object TransformationFrontend { (frontend ? TransformationJob("hello-" + n)) onSuccess { case result ⇒ println(result) } - // wait a while until next request, + // wait a while until next request, // to avoid flooding the console with output Thread.sleep(2000) } @@ -75,11 +78,14 @@ class TransformationFrontend extends Actor { object TransformationBackend { def main(args: Array[String]): Unit = { - // Override the configuration of the port - // when specified as program argument - if (args.nonEmpty) System.setProperty("akka.remote.netty.tcp.port", args(0)) + // Override the configuration of the port when specified as program argument + val config = + (if (args.nonEmpty) ConfigFactory.parseString(s"akka.remote.netty.tcp.port=${args(0)}") + else ConfigFactory.empty).withFallback( + ConfigFactory.parseString("akka.cluster.roles = [backend]")). + withFallback(ConfigFactory.load()) - val system = ActorSystem("ClusterSystem") + val system = ActorSystem("ClusterSystem", config) system.actorOf(Props[TransformationBackend], name = "backend") } } @@ -101,10 +107,9 @@ class TransformationBackend extends Actor { case MemberUp(m) ⇒ register(m) } - // try to register to all nodes, even though there - // might not be any frontend on all nodes def register(member: Member): Unit = - context.actorFor(RootActorPath(member.address) / "user" / "frontend") ! - BackendRegistration + if (member.hasRole("frontend")) + context.actorFor(RootActorPath(member.address) / "user" / "frontend") ! + BackendRegistration } //#backend \ No newline at end of file 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 aaa87c9d43..4dda541937 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 @@ -31,6 +31,7 @@ object StatsSampleSingleMasterSpecConfig extends MultiNodeConfig { akka.loglevel = INFO akka.actor.provider = "akka.cluster.ClusterActorRefProvider" akka.remote.log-remote-lifecycle-events = off + akka.cluster.roles = [compute] akka.cluster.auto-join = off # don't use sigar for tests, native lib not in path akka.cluster.metrics.collector-class = akka.cluster.JmxMetricsCollector @@ -43,6 +44,7 @@ object StatsSampleSingleMasterSpecConfig extends MultiNodeConfig { enabled = on max-nr-of-instances-per-node = 3 allow-local-routees = off + use-role = compute } } } @@ -75,15 +77,15 @@ abstract class StatsSampleSingleMasterSpec extends MultiNodeSpec(StatsSampleSing Cluster(system) join node(first).address expectMsgAllOf( - MemberUp(Member(node(first).address, MemberStatus.Up)), - MemberUp(Member(node(second).address, MemberStatus.Up)), - MemberUp(Member(node(third).address, MemberStatus.Up))) + MemberUp(Member(node(first).address, MemberStatus.Up, Set.empty)), + MemberUp(Member(node(second).address, MemberStatus.Up, Set.empty)), + MemberUp(Member(node(third).address, MemberStatus.Up, Set.empty))) Cluster(system).unsubscribe(testActor) system.actorOf(Props(new ClusterSingletonManager( singletonProps = _ ⇒ Props[StatsService], singletonName = "statsService", - terminationMessage = PoisonPill)), name = "singleton") + terminationMessage = PoisonPill, role = Some("compute"))), name = "singleton") system.actorOf(Props[StatsFacade], "statsFacade") 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 7d9fbda51b..57aaf1f3b3 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 @@ -26,6 +26,7 @@ object StatsSampleSpecConfig extends MultiNodeConfig { commonConfig(ConfigFactory.parseString(""" akka.actor.provider = "akka.cluster.ClusterActorRefProvider" akka.remote.log-remote-lifecycle-events = off + akka.cluster.roles = [compute] akka.cluster.auto-join = off # don't use sigar for tests, native lib not in path akka.cluster.metrics.collector-class = akka.cluster.JmxMetricsCollector @@ -38,6 +39,7 @@ object StatsSampleSpecConfig extends MultiNodeConfig { enabled = on routees-path = "/user/statsWorker" allow-local-routees = on + use-role = compute } } } @@ -96,9 +98,9 @@ abstract class StatsSampleSpec extends MultiNodeSpec(StatsSampleSpecConfig) system.actorOf(Props[StatsService], "statsService") expectMsgAllOf( - MemberUp(Member(firstAddress, MemberStatus.Up)), - MemberUp(Member(secondAddress, MemberStatus.Up)), - MemberUp(Member(thirdAddress, MemberStatus.Up))) + MemberUp(Member(firstAddress, MemberStatus.Up, Set.empty)), + MemberUp(Member(secondAddress, MemberStatus.Up, Set.empty)), + MemberUp(Member(thirdAddress, MemberStatus.Up, Set.empty))) Cluster(system).unsubscribe(testActor) diff --git a/akka-samples/akka-sample-cluster/src/multi-jvm/scala/sample/cluster/stats/japi/StatsSampleJapiSpec.scala b/akka-samples/akka-sample-cluster/src/multi-jvm/scala/sample/cluster/stats/japi/StatsSampleJapiSpec.scala index 4583dac90e..8cd068123b 100644 --- a/akka-samples/akka-sample-cluster/src/multi-jvm/scala/sample/cluster/stats/japi/StatsSampleJapiSpec.scala +++ b/akka-samples/akka-sample-cluster/src/multi-jvm/scala/sample/cluster/stats/japi/StatsSampleJapiSpec.scala @@ -30,6 +30,7 @@ object StatsSampleJapiSpecConfig extends MultiNodeConfig { commonConfig(ConfigFactory.parseString(""" akka.actor.provider = "akka.cluster.ClusterActorRefProvider" akka.remote.log-remote-lifecycle-events = off + akka.cluster.roles = [compute] akka.cluster.auto-join = off # don't use sigar for tests, native lib not in path akka.cluster.metrics.collector-class = akka.cluster.JmxMetricsCollector @@ -41,6 +42,7 @@ object StatsSampleJapiSpecConfig extends MultiNodeConfig { enabled = on routees-path = "/user/statsWorker" allow-local-routees = on + use-role = compute } } } @@ -81,9 +83,9 @@ abstract class StatsSampleJapiSpec extends MultiNodeSpec(StatsSampleJapiSpecConf system.actorOf(Props[StatsService], "statsService") expectMsgAllOf( - MemberUp(Member(firstAddress, MemberStatus.Up)), - MemberUp(Member(secondAddress, MemberStatus.Up)), - MemberUp(Member(thirdAddress, MemberStatus.Up))) + MemberUp(Member(firstAddress, MemberStatus.Up, Set.empty)), + MemberUp(Member(secondAddress, MemberStatus.Up, Set.empty)), + MemberUp(Member(thirdAddress, MemberStatus.Up, Set.empty))) Cluster(system).unsubscribe(testActor) diff --git a/akka-samples/akka-sample-cluster/src/multi-jvm/scala/sample/cluster/stats/japi/StatsSampleSingleMasterJapiSpec.scala b/akka-samples/akka-sample-cluster/src/multi-jvm/scala/sample/cluster/stats/japi/StatsSampleSingleMasterJapiSpec.scala index 3393db1a5f..ce6304f3ba 100644 --- a/akka-samples/akka-sample-cluster/src/multi-jvm/scala/sample/cluster/stats/japi/StatsSampleSingleMasterJapiSpec.scala +++ b/akka-samples/akka-sample-cluster/src/multi-jvm/scala/sample/cluster/stats/japi/StatsSampleSingleMasterJapiSpec.scala @@ -33,6 +33,7 @@ object StatsSampleSingleMasterJapiSpecConfig extends MultiNodeConfig { akka.loglevel = INFO akka.actor.provider = "akka.cluster.ClusterActorRefProvider" akka.remote.log-remote-lifecycle-events = off + akka.cluster.roles = [compute] akka.cluster.auto-join = off # don't use sigar for tests, native lib not in path akka.cluster.metrics.collector-class = akka.cluster.JmxMetricsCollector @@ -44,6 +45,7 @@ object StatsSampleSingleMasterJapiSpecConfig extends MultiNodeConfig { enabled = on max-nr-of-instances-per-node = 3 allow-local-routees = off + use-role = compute } } } @@ -75,15 +77,16 @@ abstract class StatsSampleSingleMasterJapiSpec extends MultiNodeSpec(StatsSample Cluster(system) join node(first).address expectMsgAllOf( - MemberUp(Member(node(first).address, MemberStatus.Up)), - MemberUp(Member(node(second).address, MemberStatus.Up)), - MemberUp(Member(node(third).address, MemberStatus.Up))) + MemberUp(Member(node(first).address, MemberStatus.Up, Set.empty)), + MemberUp(Member(node(second).address, MemberStatus.Up, Set.empty)), + MemberUp(Member(node(third).address, MemberStatus.Up, Set.empty))) Cluster(system).unsubscribe(testActor) system.actorOf(Props(new ClusterSingletonManager( singletonName = "statsService", terminationMessage = PoisonPill, + role = null, singletonPropsFactory = new ClusterSingletonPropsFactory { def create(handOverData: Any) = Props[StatsService] })), name = "singleton") 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 0f18b0ce5b..ef7730d08e 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 @@ -33,6 +33,11 @@ object TransformationSampleSpecConfig extends MultiNodeConfig { akka.cluster.metrics.collector-class = akka.cluster.JmxMetricsCollector """)) + nodeConfig(frontend1, frontend2)( + ConfigFactory.parseString("akka.cluster.roles =[frontend]")) + + nodeConfig(backend1, backend2, backend3)( + ConfigFactory.parseString("akka.cluster.roles =[backend]")) } // need one concrete test class per node diff --git a/akka-samples/akka-sample-cluster/src/multi-jvm/scala/sample/cluster/transformation/japi/TransformationSampleJapiSpec.scala b/akka-samples/akka-sample-cluster/src/multi-jvm/scala/sample/cluster/transformation/japi/TransformationSampleJapiSpec.scala index 2fbba499ea..c781ffe809 100644 --- a/akka-samples/akka-sample-cluster/src/multi-jvm/scala/sample/cluster/transformation/japi/TransformationSampleJapiSpec.scala +++ b/akka-samples/akka-sample-cluster/src/multi-jvm/scala/sample/cluster/transformation/japi/TransformationSampleJapiSpec.scala @@ -34,6 +34,12 @@ object TransformationSampleJapiSpecConfig extends MultiNodeConfig { akka.cluster.metrics.collector-class = akka.cluster.JmxMetricsCollector """)) + nodeConfig(frontend1, frontend2)( + ConfigFactory.parseString("akka.cluster.roles =[frontend]")) + + nodeConfig(backend1, backend2, backend3)( + ConfigFactory.parseString("akka.cluster.roles =[backend]")) + } // need one concrete test class per node From 9584bb38bd743ba0bd1f613c91dcb4cbe21bebb1 Mon Sep 17 00:00:00 2001 From: Roland Kuhn Date: Mon, 18 Mar 2013 18:41:27 +0100 Subject: [PATCH 27/47] update to SBT 0.12.2 --- project/build.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project/build.properties b/project/build.properties index 4474a03e1a..66ad72ce2e 100644 --- a/project/build.properties +++ b/project/build.properties @@ -1 +1 @@ -sbt.version=0.12.1 +sbt.version=0.12.2 From 08d2dec78517dbbfca9f3ca1dcb15e70bd354201 Mon Sep 17 00:00:00 2001 From: Patrik Nordwall Date: Tue, 19 Mar 2013 17:36:36 +0100 Subject: [PATCH 28/47] Shutdown/cleanup cluster extension if actor init fails, see #3162 * Stop ClusterDaemon if init of core actor fails. * Activate jmx-enabled setting * Adjust the err msg of InvalidActorNameException to match conventions --- .../actor/dungeon/ChildrenContainer.scala | 4 ++-- .../src/main/scala/akka/cluster/Cluster.scala | 23 ++++++++++++++----- 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/akka-actor/src/main/scala/akka/actor/dungeon/ChildrenContainer.scala b/akka-actor/src/main/scala/akka/actor/dungeon/ChildrenContainer.scala index 13ab3b4c8d..1dbd117904 100644 --- a/akka-actor/src/main/scala/akka/actor/dungeon/ChildrenContainer.scala +++ b/akka-actor/src/main/scala/akka/actor/dungeon/ChildrenContainer.scala @@ -131,7 +131,7 @@ private[akka] object ChildrenContainer { override def reserve(name: String): ChildrenContainer = if (c contains name) - throw new InvalidActorNameException("actor name " + name + " is not unique!") + throw new InvalidActorNameException(s"actor name [$name] is not unique!") else new NormalChildrenContainer(c.updated(name, ChildNameReserved)) override def unreserve(name: String): ChildrenContainer = c.get(name) match { @@ -193,7 +193,7 @@ private[akka] object ChildrenContainer { case Termination ⇒ throw new IllegalStateException("cannot reserve actor name '" + name + "': terminating") case _ ⇒ if (c contains name) - throw new InvalidActorNameException("actor name " + name + " is not unique!") + throw new InvalidActorNameException(s"actor name [$name] is not unique!") else copy(c = c.updated(name, ChildNameReserved)) } diff --git a/akka-cluster/src/main/scala/akka/cluster/Cluster.scala b/akka-cluster/src/main/scala/akka/cluster/Cluster.scala index 9221d81295..090ba4e15e 100644 --- a/akka-cluster/src/main/scala/akka/cluster/Cluster.scala +++ b/akka-cluster/src/main/scala/akka/cluster/Cluster.scala @@ -29,6 +29,7 @@ import akka.remote.FailureDetector import com.typesafe.config.Config import akka.event.LoggingAdapter import java.util.concurrent.ThreadFactory +import scala.util.control.NonFatal /** * Cluster Extension Id and factory for creating Cluster extension. @@ -86,6 +87,8 @@ class Cluster(val system: ExtendedActorSystem) extends Extension { private val _isTerminated = new AtomicBoolean(false) private val log = Logging(system, "Cluster") + // ClusterJmx is initialized as the last thing in the constructor + private var clusterJmx: Option[ClusterJmx] = None log.info("Cluster Node [{}] - is starting up...", selfAddress) @@ -157,7 +160,14 @@ class Cluster(val system: ExtendedActorSystem) extends Extension { */ private[cluster] val clusterCore: ActorRef = { implicit val timeout = system.settings.CreationTimeout - Await.result((clusterDaemons ? InternalClusterAction.GetClusterCoreRef).mapTo[ActorRef], timeout.duration) + try { + Await.result((clusterDaemons ? InternalClusterAction.GetClusterCoreRef).mapTo[ActorRef], timeout.duration) + } catch { + case NonFatal(e) ⇒ + log.error(e, "Failed to startup Cluster") + shutdown() + throw e + } } @volatile @@ -170,11 +180,12 @@ class Cluster(val system: ExtendedActorSystem) extends Extension { system.registerOnTermination(shutdown()) - private val clusterJmx: Option[ClusterJmx] = { - val jmx = new ClusterJmx(this, log) - jmx.createMBean() - Some(jmx) - } + if (JmxEnabled) + clusterJmx = { + val jmx = new ClusterJmx(this, log) + jmx.createMBean() + Some(jmx) + } log.info("Cluster Node [{}] - has started up successfully", selfAddress) From f18575e25166c86b3549d1b7a60b0ea99fafbfbf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Antonsson?= Date: Wed, 20 Mar 2013 10:18:24 +0100 Subject: [PATCH 29/47] Fixes and additions to the ThrottlerTransportAdapter. See #2930 --- .../akka/remote/testconductor/Player.scala | 2 +- .../transport/ThrottlerTransportAdapter.scala | 38 +++++++++--- .../ThrottlerTransportAdapterSpec.scala | 62 ++++++++++++++++--- 3 files changed, 86 insertions(+), 16 deletions(-) diff --git a/akka-multi-node-testkit/src/main/scala/akka/remote/testconductor/Player.scala b/akka-multi-node-testkit/src/main/scala/akka/remote/testconductor/Player.scala index 0ed0130c1e..384d8cad38 100644 --- a/akka-multi-node-testkit/src/main/scala/akka/remote/testconductor/Player.scala +++ b/akka-multi-node-testkit/src/main/scala/akka/remote/testconductor/Player.scala @@ -227,7 +227,7 @@ private[akka] class ClientFSM(name: RoleName, controllerAddr: InetSocketAddress) val cmdFuture = TestConductor().transport.managementCommand(SetThrottle(t.target, t.direction, mode)) cmdFuture onSuccess { - case b: Boolean ⇒ self ! ToServer(Done) + case true ⇒ self ! ToServer(Done) case _ ⇒ throw new RuntimeException("Throttle was requested from the TestConductor, but no transport " + "adapters available that support throttling. Specify `testTransport(on = true)` in your MultiNodeConfig") } diff --git a/akka-remote/src/main/scala/akka/remote/transport/ThrottlerTransportAdapter.scala b/akka-remote/src/main/scala/akka/remote/transport/ThrottlerTransportAdapter.scala index c7a0d7991f..2726d71191 100644 --- a/akka-remote/src/main/scala/akka/remote/transport/ThrottlerTransportAdapter.scala +++ b/akka-remote/src/main/scala/akka/remote/transport/ThrottlerTransportAdapter.scala @@ -142,6 +142,20 @@ object ThrottlerTransportAdapter { */ def getInstance = this } + + /** + * Management Command to force dissocation of an address. + */ + @SerialVersionUID(1L) + case class ForceDissociate(address: Address) + + @SerialVersionUID(1L) + case object ForceDissociateAck { + /** + * Java API: get the singleton instance + */ + def getInstance = this + } } class ThrottlerTransportAdapter(_wrappedTransport: Transport, _system: ExtendedActorSystem) @@ -155,11 +169,15 @@ class ThrottlerTransportAdapter(_wrappedTransport: Transport, _system: ExtendedA Props(new ThrottlerManager(wt)) } - override def managementCommand(cmd: Any): Future[Boolean] = cmd match { - case s: SetThrottle ⇒ - import ActorTransportAdapter.AskTimeout - manager ? s map { case SetThrottleAck ⇒ true } - case _ ⇒ wrappedTransport.managementCommand(cmd) + override def managementCommand(cmd: Any): Future[Boolean] = { + import ActorTransportAdapter.AskTimeout + cmd match { + case s: SetThrottle ⇒ + manager ? s map { case SetThrottleAck ⇒ true } + case f: ForceDissociate ⇒ + manager ? f map { case ForceDissociateAck ⇒ true } + case _ ⇒ wrappedTransport.managementCommand(cmd) + } } } @@ -212,8 +230,14 @@ private[transport] class ThrottlerManager(wrappedTransport: Transport) extends A case (`naked`, handle) ⇒ setMode(handle, mode, direction) case _ ⇒ ok } - Future.sequence(allAcks).map(_ ⇒ SetThrottleAck) pipeTo sender + case ForceDissociate(address) ⇒ + val naked = nakedAddress(address) + handleTable.foreach { + case (`naked`, handle) ⇒ handle.disassociate() + case _ ⇒ + } + sender ! ForceDissociateAck case Checkin(origin, handle) ⇒ val naked: Address = nakedAddress(origin) @@ -247,7 +271,7 @@ private[transport] class ThrottlerManager(wrappedTransport: Transport) extends A import ActorTransportAdapter.AskTimeout if (direction.includes(Direction.Send)) handle.outboundThrottleMode.set(mode) - if (direction.includes(Direction.Receive)) + if (direction.includes(Direction.Receive) && !handle.throttlerActor.isTerminated) (handle.throttlerActor ? mode).mapTo[SetThrottleAck.type] else Future.successful(SetThrottleAck) diff --git a/akka-remote/src/test/scala/akka/remote/transport/ThrottlerTransportAdapterSpec.scala b/akka-remote/src/test/scala/akka/remote/transport/ThrottlerTransportAdapterSpec.scala index f7f364d1be..9f31b5a271 100644 --- a/akka-remote/src/test/scala/akka/remote/transport/ThrottlerTransportAdapterSpec.scala +++ b/akka-remote/src/test/scala/akka/remote/transport/ThrottlerTransportAdapterSpec.scala @@ -6,7 +6,7 @@ import akka.testkit.{ TimingTest, DefaultTimeout, ImplicitSender, AkkaSpec } import ThrottlerTransportAdapterSpec._ import scala.concurrent.duration._ import scala.concurrent.Await -import akka.remote.transport.ThrottlerTransportAdapter.{ Direction, TokenBucket, SetThrottle } +import akka.remote.transport.ThrottlerTransportAdapter._ import akka.remote.RemoteActorRefProvider import akka.testkit.TestEvent import akka.testkit.EventFilter @@ -18,6 +18,7 @@ object ThrottlerTransportAdapterSpec { #loglevel = DEBUG actor.provider = "akka.remote.RemoteActorRefProvider" + remote.netty.tcp.hostname = "localhost" remote.retry-latch-closed-for = 0 s remote.log-remote-lifecycle-events = on @@ -29,13 +30,14 @@ object ThrottlerTransportAdapterSpec { class Echo extends Actor { override def receive = { case "ping" ⇒ sender ! "pong" + case x ⇒ sender ! x } } val PingPacketSize = 148 - val MessageCount = 100 + val MessageCount = 30 val BytesPerSecond = 500 - val TotalTime = (MessageCount * PingPacketSize) / BytesPerSecond + val TotalTime: Long = (MessageCount * PingPacketSize) / BytesPerSecond class ThrottlingTester(remote: ActorRef, controller: ActorRef) extends Actor { var messageCount = MessageCount @@ -66,17 +68,61 @@ class ThrottlerTransportAdapterSpec extends AkkaSpec(configA) with ImplicitSende val rootB = RootActorPath(systemB.asInstanceOf[ExtendedActorSystem].provider.getDefaultAddress) val here = system.actorFor(rootB / "user" / "echo") + def throttle(direction: Direction, mode: ThrottleMode): Boolean = { + val rootBAddress = Address("akka", "systemB", "localhost", rootB.address.port.get) + val transport = system.asInstanceOf[ExtendedActorSystem].provider.asInstanceOf[RemoteActorRefProvider].transport + Await.result(transport.managementCommand(SetThrottle(rootBAddress, direction, mode)), 3.seconds) + } + + def dissociate(): Boolean = { + val rootBAddress = Address("akka", "systemB", "localhost", rootB.address.port.get) + val transport = system.asInstanceOf[ExtendedActorSystem].provider.asInstanceOf[RemoteActorRefProvider].transport + Await.result(transport.managementCommand(ForceDissociate(rootBAddress)), 3.seconds) + } + "ThrottlerTransportAdapter" must { "maintain average message rate" taggedAs TimingTest in { - Await.result( - system.asInstanceOf[ExtendedActorSystem].provider.asInstanceOf[RemoteActorRefProvider].transport - .managementCommand(SetThrottle(Address("akka", "systemB", "localhost", rootB.address.port.get), Direction.Send, TokenBucket(200, 500, 0, 0))), 3.seconds) + throttle(Direction.Send, TokenBucket(200, 500, 0, 0)) must be(true) val tester = system.actorOf(Props(new ThrottlingTester(here, self))) ! "start" - expectMsgPF((TotalTime + 3).seconds) { - case time: Long ⇒ log.warning("Total time of transmission: " + NANOSECONDS.toSeconds(time)) + val time = NANOSECONDS.toSeconds(expectMsgType[Long]((TotalTime + 3).seconds)) + log.warning("Total time of transmission: " + time) + time must be > (TotalTime - 3) + throttle(Direction.Send, Unthrottled) must be(true) + } + + "must survive blackholing" taggedAs TimingTest in { + here ! "Blackhole 1" + expectMsg("Blackhole 1") + + throttle(Direction.Both, Blackhole) must be(true) + + here ! "Blackhole 2" + expectNoMsg(1.seconds) + dissociate() must be(true) + expectNoMsg(1.seconds) + + throttle(Direction.Both, Unthrottled) must be(true) + + // after we remove the Blackhole we can't be certain of the state + // of the connection, repeat until success + here ! "Blackhole 3" + awaitCond({ + if (receiveOne(Duration.Zero) == "Blackhole 3") + true + else { + here ! "Blackhole 3" + false + } + }, 5.seconds) + + here ! "Cleanup" + fishForMessage(5.seconds) { + case "Cleanup" ⇒ true + case "Blackhole 3" ⇒ false } } + } override def beforeTermination() { From e18281803e97fe68dcf02c4dc71da6258206e797 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Antonsson?= Date: Wed, 20 Mar 2013 11:21:09 +0100 Subject: [PATCH 30/47] Read instead of write to get connection reset by peer. See #3128 --- akka-actor-tests/src/test/scala/akka/io/TcpConnectionSpec.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/akka-actor-tests/src/test/scala/akka/io/TcpConnectionSpec.scala b/akka-actor-tests/src/test/scala/akka/io/TcpConnectionSpec.scala index ad276a9015..fceb271ab4 100644 --- a/akka-actor-tests/src/test/scala/akka/io/TcpConnectionSpec.scala +++ b/akka-actor-tests/src/test/scala/akka/io/TcpConnectionSpec.scala @@ -44,7 +44,7 @@ class TcpConnectionSpec extends AkkaSpec("akka.io.tcp.register-timeout = 500ms") val clientSocketOnServer = acceptServerSideConnection(serverSocket) clientSocketOnServer.socket.setSoLinger(true, 0) clientSocketOnServer.close() - clientSocket.write(ByteBuffer.allocate(1)) + clientSocket.read(ByteBuffer.allocate(1)) null } catch { case NonFatal(e) ⇒ e.getMessage From 5827a27b94de7fdcff0aaecda2bb21db798cd06c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Antonsson?= Date: Wed, 20 Mar 2013 10:32:18 +0100 Subject: [PATCH 31/47] Make joining to the same node multiple times work, and reenable blackhole test. See #2930 --- .../scala/akka/cluster/ClusterDaemon.scala | 26 ++++++++++++------- .../akka/cluster/MultiNodeClusterSpec.scala | 21 +++++++++++++++ .../UnreachableNodeRejoinsClusterSpec.scala | 19 +++++++------- .../transport/ThrottlerTransportAdapter.scala | 12 ++++----- .../ThrottlerTransportAdapterSpec.scala | 6 ++--- 5 files changed, 57 insertions(+), 27 deletions(-) diff --git a/akka-cluster/src/main/scala/akka/cluster/ClusterDaemon.scala b/akka-cluster/src/main/scala/akka/cluster/ClusterDaemon.scala index 64854f243b..5e03085e3a 100644 --- a/akka-cluster/src/main/scala/akka/cluster/ClusterDaemon.scala +++ b/akka-cluster/src/main/scala/akka/cluster/ClusterDaemon.scala @@ -239,6 +239,8 @@ private[cluster] final class ClusterCoreDaemon(publisher: ActorRef) extends Acto var seedNodeProcess: Option[ActorRef] = None + var tryingToJoinWith: Option[Address] = None + /** * Looks up and returns the remote cluster command connection for the specific address. */ @@ -341,7 +343,6 @@ private[cluster] final class ClusterCoreDaemon(publisher: ActorRef) extends Acto log.warning("Trying to join member with wrong ActorSystem name, but was ignored, expected [{}] but was [{}]", selfAddress.system, address.system) else if (!latestGossip.members.exists(_.address == address)) { - // to support manual join when joining to seed nodes is stuck (no seed nodes available) val snd = sender seedNodeProcess match { @@ -355,15 +356,18 @@ private[cluster] final class ClusterCoreDaemon(publisher: ActorRef) extends Acto case None ⇒ // no seedNodeProcess in progress } - // wipe our state since a node that joins a cluster must be empty - latestGossip = Gossip.empty - // wipe the failure detector since we are starting fresh and shouldn't care about the past - failureDetector.reset() - // wipe the publisher since we are starting fresh - publisher ! PublishStart - - publish(latestGossip) + // only wipe the state if we're not in the process of joining this address + if (tryingToJoinWith.forall(_ != address)) { + tryingToJoinWith = Some(address) + // wipe our state since a node that joins a cluster must be empty + latestGossip = Gossip.empty + // wipe the failure detector since we are starting fresh and shouldn't care about the past + failureDetector.reset() + // wipe the publisher since we are starting fresh + publisher ! PublishStart + publish(latestGossip) + } context.become(initialized) if (address == selfAddress) joining(address, cluster.selfRoles) @@ -517,6 +521,10 @@ private[cluster] final class ClusterCoreDaemon(publisher: ActorRef) extends Acto } else if (localGossip.overview.isNonDownUnreachable(from)) { log.debug("Ignoring received gossip from unreachable [{}] ", from) } else { + // if we're in the remote gossip and not Removed, then we're not joining + if (tryingToJoinWith.nonEmpty && remoteGossip.member(selfAddress).status != Removed) + tryingToJoinWith = None + val comparison = remoteGossip.version tryCompareTo localGossip.version val conflict = comparison.isEmpty diff --git a/akka-cluster/src/multi-jvm/scala/akka/cluster/MultiNodeClusterSpec.scala b/akka-cluster/src/multi-jvm/scala/akka/cluster/MultiNodeClusterSpec.scala index f5d07b8724..52135b47d9 100644 --- a/akka-cluster/src/multi-jvm/scala/akka/cluster/MultiNodeClusterSpec.scala +++ b/akka-cluster/src/multi-jvm/scala/akka/cluster/MultiNodeClusterSpec.scala @@ -182,6 +182,27 @@ trait MultiNodeClusterSpec extends Suite with STMultiNodeSpec with WatchedByCoro enterBarrier(roles.map(_.name).mkString("-") + "-joined") } + /** + * Join the specific node within the given period by sending repeated join + * requests at periodic intervals until we succeed. + */ + def joinWithin(joinNode: RoleName, max: Duration = remaining, interval: Duration = 1.second): Unit = { + def memberInState(member: Address, status: Seq[MemberStatus]): Boolean = + clusterView.members.exists { m ⇒ (m.address == member) && status.contains(m.status) } + + cluster join joinNode + awaitCond({ + clusterView.refreshCurrentState() + if (memberInState(joinNode, List(MemberStatus.up)) && + memberInState(myself, List(MemberStatus.Joining, MemberStatus.Up))) + true + else { + cluster join joinNode + false + } + }, max, interval) + } + /** * Assert that the member addresses match the expected addresses in the * sort order used by the cluster. diff --git a/akka-cluster/src/multi-jvm/scala/akka/cluster/UnreachableNodeRejoinsClusterSpec.scala b/akka-cluster/src/multi-jvm/scala/akka/cluster/UnreachableNodeRejoinsClusterSpec.scala index 9f076c1d78..fe1061e8a8 100644 --- a/akka-cluster/src/multi-jvm/scala/akka/cluster/UnreachableNodeRejoinsClusterSpec.scala +++ b/akka-cluster/src/multi-jvm/scala/akka/cluster/UnreachableNodeRejoinsClusterSpec.scala @@ -25,10 +25,14 @@ case class UnreachableNodeRejoinsClusterMultiNodeConfig(failureDetectorPuppet: B commonConfig(ConfigFactory.parseString( """ + # this setting is here to limit the number of retries and failures while the + # node is being blackholed + akka.remote.failure-detector.retry-gate-closed-for = 500 ms + akka.remote.log-remote-lifecycle-events = off akka.cluster.publish-stats-interval = 0s akka.loglevel = INFO - """).withFallback(debugConfig(on = false).withFallback(MultiNodeClusterSpec.clusterConfig))) + """).withFallback(debugConfig(on = false).withFallback(MultiNodeClusterSpec.clusterConfig(failureDetectorPuppet)))) testTransport(on = true) } @@ -74,8 +78,7 @@ abstract class UnreachableNodeRejoinsClusterSpec(multiNodeConfig: UnreachableNod endBarrier } - // FIXME ignored due to ticket #2930 - timeout changing throttler mode - "mark a node as UNREACHABLE when we pull the network" taggedAs LongRunningTest ignore { + "mark a node as UNREACHABLE when we pull the network" taggedAs LongRunningTest in { // let them send at least one heartbeat to each other after the gossip convergence // because for new joining nodes we remove them from the failure detector when // receive gossip @@ -125,8 +128,7 @@ abstract class UnreachableNodeRejoinsClusterSpec(multiNodeConfig: UnreachableNod endBarrier } - // FIXME ignored due to ticket #2930 - timeout changing throttler mode - "mark the node as DOWN" taggedAs LongRunningTest ignore { + "mark the node as DOWN" taggedAs LongRunningTest in { runOn(master) { cluster down victim } @@ -135,13 +137,12 @@ abstract class UnreachableNodeRejoinsClusterSpec(multiNodeConfig: UnreachableNod awaitMembersUp(roles.size - 1, Set(victim)) // eventually removed awaitCond(clusterView.unreachableMembers.isEmpty, 15 seconds) - } + } endBarrier } - // FIXME ignored due to ticket #2930 - timeout changing throttler mode - "allow node to REJOIN when the network is plugged back in" taggedAs LongRunningTest ignore { + "allow node to REJOIN when the network is plugged back in" taggedAs LongRunningTest in { runOn(first) { // put the network back in allBut(victim).foreach { roleName ⇒ @@ -152,7 +153,7 @@ abstract class UnreachableNodeRejoinsClusterSpec(multiNodeConfig: UnreachableNod enterBarrier("plug_in_victim") runOn(victim) { - cluster join master + joinWithin(master, 10.seconds) } awaitMembersUp(roles.size) diff --git a/akka-remote/src/main/scala/akka/remote/transport/ThrottlerTransportAdapter.scala b/akka-remote/src/main/scala/akka/remote/transport/ThrottlerTransportAdapter.scala index 2726d71191..55dfc152da 100644 --- a/akka-remote/src/main/scala/akka/remote/transport/ThrottlerTransportAdapter.scala +++ b/akka-remote/src/main/scala/akka/remote/transport/ThrottlerTransportAdapter.scala @@ -147,10 +147,10 @@ object ThrottlerTransportAdapter { * Management Command to force dissocation of an address. */ @SerialVersionUID(1L) - case class ForceDissociate(address: Address) + case class ForceDisassociate(address: Address) @SerialVersionUID(1L) - case object ForceDissociateAck { + case object ForceDisassociateAck { /** * Java API: get the singleton instance */ @@ -174,8 +174,8 @@ class ThrottlerTransportAdapter(_wrappedTransport: Transport, _system: ExtendedA cmd match { case s: SetThrottle ⇒ manager ? s map { case SetThrottleAck ⇒ true } - case f: ForceDissociate ⇒ - manager ? f map { case ForceDissociateAck ⇒ true } + case f: ForceDisassociate ⇒ + manager ? f map { case ForceDisassociateAck ⇒ true } case _ ⇒ wrappedTransport.managementCommand(cmd) } } @@ -231,13 +231,13 @@ private[transport] class ThrottlerManager(wrappedTransport: Transport) extends A case _ ⇒ ok } Future.sequence(allAcks).map(_ ⇒ SetThrottleAck) pipeTo sender - case ForceDissociate(address) ⇒ + case ForceDisassociate(address) ⇒ val naked = nakedAddress(address) handleTable.foreach { case (`naked`, handle) ⇒ handle.disassociate() case _ ⇒ } - sender ! ForceDissociateAck + sender ! ForceDisassociateAck case Checkin(origin, handle) ⇒ val naked: Address = nakedAddress(origin) diff --git a/akka-remote/src/test/scala/akka/remote/transport/ThrottlerTransportAdapterSpec.scala b/akka-remote/src/test/scala/akka/remote/transport/ThrottlerTransportAdapterSpec.scala index 9f31b5a271..e2f935f109 100644 --- a/akka-remote/src/test/scala/akka/remote/transport/ThrottlerTransportAdapterSpec.scala +++ b/akka-remote/src/test/scala/akka/remote/transport/ThrottlerTransportAdapterSpec.scala @@ -74,10 +74,10 @@ class ThrottlerTransportAdapterSpec extends AkkaSpec(configA) with ImplicitSende Await.result(transport.managementCommand(SetThrottle(rootBAddress, direction, mode)), 3.seconds) } - def dissociate(): Boolean = { + def disassociate(): Boolean = { val rootBAddress = Address("akka", "systemB", "localhost", rootB.address.port.get) val transport = system.asInstanceOf[ExtendedActorSystem].provider.asInstanceOf[RemoteActorRefProvider].transport - Await.result(transport.managementCommand(ForceDissociate(rootBAddress)), 3.seconds) + Await.result(transport.managementCommand(ForceDisassociate(rootBAddress)), 3.seconds) } "ThrottlerTransportAdapter" must { @@ -99,7 +99,7 @@ class ThrottlerTransportAdapterSpec extends AkkaSpec(configA) with ImplicitSende here ! "Blackhole 2" expectNoMsg(1.seconds) - dissociate() must be(true) + disassociate() must be(true) expectNoMsg(1.seconds) throttle(Direction.Both, Unthrottled) must be(true) From b6ae5df920c7cae5d264dd6e916d2e5ebd92af23 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Antonsson?= Date: Wed, 20 Mar 2013 13:29:19 +0100 Subject: [PATCH 32/47] Wait for the second member to become Up. See #3140 --- .../src/multi-jvm/scala/akka/cluster/TransitionSpec.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/akka-cluster/src/multi-jvm/scala/akka/cluster/TransitionSpec.scala b/akka-cluster/src/multi-jvm/scala/akka/cluster/TransitionSpec.scala index 4aa2a963c4..dd379382e8 100644 --- a/akka-cluster/src/multi-jvm/scala/akka/cluster/TransitionSpec.scala +++ b/akka-cluster/src/multi-jvm/scala/akka/cluster/TransitionSpec.scala @@ -140,7 +140,7 @@ abstract class TransitionSpec runOn(first) { leaderActions() awaitMemberStatus(first, Up) - awaitMemberStatus(second, Joining) + awaitMemberStatus(second, Up) } enterBarrier("leader-actions-2") From 268b8f21858edef8f7868fdf8cf2200550c51683 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Antonsson?= Date: Thu, 21 Mar 2013 10:07:48 +0100 Subject: [PATCH 33/47] Configure RemotingSpec to not create 500 threads. See #3134 --- .../test/scala/akka/remote/RemotingSpec.scala | 53 ++++++++++++------- 1 file changed, 35 insertions(+), 18 deletions(-) diff --git a/akka-remote/src/test/scala/akka/remote/RemotingSpec.scala b/akka-remote/src/test/scala/akka/remote/RemotingSpec.scala index 7ae8c33df1..013330f2b2 100644 --- a/akka-remote/src/test/scala/akka/remote/RemotingSpec.scala +++ b/akka-remote/src/test/scala/akka/remote/RemotingSpec.scala @@ -50,35 +50,50 @@ object RemotingSpec { enabled-algorithms = [TLS_RSA_WITH_AES_128_CBC_SHA] } + common-netty-settings { + port = 0 + hostname = "localhost" + server-socket-worker-pool.pool-size-max = 2 + client-socket-worker-pool.pool-size-max = 2 + } + akka { actor.provider = "akka.remote.RemoteActorRefProvider" - remote.transport = "akka.remote.Remoting" - remote.retry-latch-closed-for = 1 s - remote.log-remote-lifecycle-events = on + remote { + transport = "akka.remote.Remoting" - remote.enabled-transports = [ - "akka.remote.test", - "akka.remote.netty.tcp", - "akka.remote.netty.udp", - "akka.remote.netty.ssl" - ] + retry-latch-closed-for = 1 s + log-remote-lifecycle-events = on - remote.netty.tcp.port = 0 - remote.netty.tcp.hostname = "localhost" - remote.netty.udp.port = 0 - remote.netty.udp.hostname = "localhost" - remote.netty.ssl.port = 0 - remote.netty.ssl.hostname = "localhost" - remote.netty.ssl.security = ${common-ssl-settings} + enabled-transports = [ + "akka.remote.test", + "akka.remote.netty.tcp", + "akka.remote.netty.udp", + "akka.remote.netty.ssl" + ] - remote.test { + writer-dispatcher { + executor = "fork-join-executor" + fork-join-executor { + parallelism-min = 2 + parallelism-max = 2 + } + } + + netty.tcp = ${common-netty-settings} + netty.udp = ${common-netty-settings} + netty.ssl = ${common-netty-settings} + netty.ssl.security = ${common-ssl-settings} + + test { transport-class = "akka.remote.transport.TestTransport" applied-adapters = [] registry-key = aX33k0jWKg local-address = "test://RemotingSpec@localhost:12345" maximum-payload-bytes = 32000 bytes scheme-identifier = test + } } actor.deployment { @@ -157,7 +172,9 @@ class RemotingSpec extends AkkaSpec(RemotingSpec.cfg) with ImplicitSender with D } "not be exhausted by sending to broken connections" in { - val moreSystems = Vector.fill(10)(ActorSystem(other.name, other.settings.config)) + val tcpOnlyConfig = ConfigFactory.parseString("""akka.remote.enabled-transports = ["akka.remote.netty.tcp"]"""). + withFallback(other.settings.config) + val moreSystems = Vector.fill(5)(ActorSystem(other.name, tcpOnlyConfig)) moreSystems foreach (_.actorOf(Props[Echo2], name = "echo")) val moreRefs = moreSystems map (sys ⇒ system.actorFor(RootActorPath(addr(sys, "tcp")) / "user" / "echo")) val aliveEcho = system.actorFor(RootActorPath(addr(other, "tcp")) / "user" / "echo") From ea8ec24d4d50c549fd58e8043fca125a853ce2b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Antonsson?= Date: Thu, 21 Mar 2013 12:38:20 +0100 Subject: [PATCH 34/47] Changed ThrottlerManager setMode to use watch to ensure completion of future. See #2930 --- .../transport/ThrottlerTransportAdapter.scala | 28 +++++++++++++++---- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/akka-remote/src/main/scala/akka/remote/transport/ThrottlerTransportAdapter.scala b/akka-remote/src/main/scala/akka/remote/transport/ThrottlerTransportAdapter.scala index 55dfc152da..f6c7c59e29 100644 --- a/akka-remote/src/main/scala/akka/remote/transport/ThrottlerTransportAdapter.scala +++ b/akka-remote/src/main/scala/akka/remote/transport/ThrottlerTransportAdapter.scala @@ -4,15 +4,14 @@ package akka.remote.transport import akka.actor._ -import akka.pattern.ask -import akka.pattern.pipe +import akka.pattern.{ PromiseActorRef, ask, pipe } import akka.remote.transport.ActorTransportAdapter.AssociateUnderlying import akka.remote.transport.AkkaPduCodec.Associate import akka.remote.transport.AssociationHandle.{ ActorHandleEventListener, Disassociated, InboundPayload, HandleEventListener } import akka.remote.transport.ThrottlerManager.Checkin import akka.remote.transport.ThrottlerTransportAdapter._ import akka.remote.transport.Transport._ -import akka.util.ByteString +import akka.util.{ Timeout, ByteString } import java.util.concurrent.TimeUnit import java.util.concurrent.atomic.AtomicReference import scala.annotation.tailrec @@ -23,6 +22,7 @@ import scala.math.min import scala.util.{ Success, Failure } import scala.util.control.NonFatal import scala.concurrent.duration._ +import akka.dispatch.{ Unwatch, Watch } class ThrottlerProvider extends TransportAdapterProvider { @@ -271,8 +271,8 @@ private[transport] class ThrottlerManager(wrappedTransport: Transport) extends A import ActorTransportAdapter.AskTimeout if (direction.includes(Direction.Send)) handle.outboundThrottleMode.set(mode) - if (direction.includes(Direction.Receive) && !handle.throttlerActor.isTerminated) - (handle.throttlerActor ? mode).mapTo[SetThrottleAck.type] + if (direction.includes(Direction.Receive)) + askWithDeathCompletion(handle.throttlerActor, mode, SetThrottleAck).mapTo[SetThrottleAck.type] else Future.successful(SetThrottleAck) } @@ -284,6 +284,24 @@ private[transport] class ThrottlerManager(wrappedTransport: Transport) extends A ThrottlerHandle(originalHandle, throttlerActor) } + private def askWithDeathCompletion(target: ActorRef, question: Any, answer: Any)(implicit timeout: Timeout): Future[Any] = { + if (target.isTerminated) Future successful answer + else { + val internalTarget = target.asInstanceOf[InternalActorRef] + val promiseActorRef = PromiseActorRef(context.system.asInstanceOf[ExtendedActorSystem].provider, timeout) + internalTarget.sendSystemMessage(Watch(target, promiseActorRef)) + val future = promiseActorRef.result.future + future onComplete { // remember to unwatch if termination didn't complete + case Success(Terminated(`target`)) ⇒ () + case _ ⇒ internalTarget.sendSystemMessage(Unwatch(target, promiseActorRef)) + } + target.tell(question, promiseActorRef) + future map { + case Terminated(`target`) ⇒ answer + case x ⇒ x + } + } + } } /** From b738487dc8d6685d5e7d937b5c28bab31fc49a40 Mon Sep 17 00:00:00 2001 From: Patrik Nordwall Date: Wed, 13 Mar 2013 16:01:57 +0100 Subject: [PATCH 35/47] Add UID to RemoteActorRef, see #3072 * Sending to a previous incarnation of an actor shall fail, to make remote actors work the same way as local ones (in the sense that after Terminated() the ref is not working anymore) * Changed equality of ActorRef to take the uid into account * Parse uid fragment in RelativeActorPath and ActorPathExtractor * Handle uid in getChild and in RemoteSystemDaemon * Use toSerializationFormat and toSerializationFormatWithAddress in serialization * Replaced var uid in ActorCell and ChildRestartStats with constructor parameters (path) * Create the uid in one single place, in makeChild in parent * Handle ActorRef with and without uid in DeathWatch * Optimize ActorPath.toString and friends * Update documentation and migration guide --- .../test/scala/akka/actor/ActorDSLSpec.scala | 4 +- .../scala/akka/actor/ActorLookupSpec.scala | 25 +++ .../test/scala/akka/actor/ActorPathSpec.scala | 46 +++++ .../akka/actor/RelativeActorPathSpec.scala | 3 + .../akka/actor/SupervisorHierarchySpec.scala | 52 +++--- .../scala/akka/actor/SupervisorSpec.scala | 8 +- .../akka/serialization/SerializeSpec.scala | 4 +- .../src/main/scala/akka/actor/ActorCell.scala | 37 ++-- .../src/main/scala/akka/actor/ActorPath.scala | 159 +++++++++++++++--- .../src/main/scala/akka/actor/ActorRef.scala | 52 ++++-- .../scala/akka/actor/ActorRefProvider.scala | 2 +- .../src/main/scala/akka/actor/Address.scala | 12 +- .../main/scala/akka/actor/FaultHandling.scala | 2 +- .../akka/actor/RepointableActorRef.scala | 19 +-- .../scala/akka/actor/dungeon/Children.scala | 3 +- .../scala/akka/actor/dungeon/DeathWatch.scala | 31 +++- .../scala/akka/actor/dungeon/Dispatch.scala | 6 +- .../akka/actor/dungeon/FaultHandling.scala | 2 +- .../akka/dispatch/AbstractDispatcher.scala | 8 +- .../src/main/scala/akka/event/Logging.scala | 2 +- .../akka/pattern/GracefulStopSupport.scala | 8 +- .../src/main/scala/akka/routing/Routing.scala | 8 +- .../src/main/scala/akka/util/Crypt.scala | 2 +- .../src/main/scala/akka/util/Helpers.scala | 4 +- .../scala/akka/cluster/StressSpec.scala | 27 +-- .../akka/contrib/pattern/ReliableProxy.scala | 3 +- akka-docs/rst/general/addressing.rst | 55 +++++- .../SerializationDocTestBase.java | 4 +- akka-docs/rst/java/serialization.rst | 10 +- .../project/migration-guide-2.1.x-2.2.x.rst | 40 +++++ .../serialization/SerializationDocSpec.scala | 8 +- akka-docs/rst/scala/serialization.rst | 11 +- .../akka/remote/RemoteActorRefProvider.scala | 23 ++- .../main/scala/akka/remote/RemoteDaemon.scala | 20 ++- .../serialization/ProtobufSerializer.scala | 4 +- .../akka/remote/transport/AkkaPduCodec.scala | 2 +- .../test/scala/akka/remote/RemotingSpec.scala | 87 +++++++--- 37 files changed, 607 insertions(+), 186 deletions(-) create mode 100644 akka-actor-tests/src/test/scala/akka/actor/ActorPathSpec.scala diff --git a/akka-actor-tests/src/test/scala/akka/actor/ActorDSLSpec.scala b/akka-actor-tests/src/test/scala/akka/actor/ActorDSLSpec.scala index bdefe3e7b7..2d7b7bc13d 100644 --- a/akka-actor-tests/src/test/scala/akka/actor/ActorDSLSpec.scala +++ b/akka-actor-tests/src/test/scala/akka/actor/ActorDSLSpec.scala @@ -208,14 +208,14 @@ class ActorDSLSpec extends AkkaSpec { // here we pass in the ActorRefFactory explicitly as an example val a = actor(system, "fred")(new Act { val b = actor("barney")(new Act { - whenStarting { context.parent ! ("hello from " + self) } + whenStarting { context.parent ! ("hello from " + self.path) } }) become { case x ⇒ testActor ! x } }) //#nested-actor - expectMsg("hello from Actor[akka://ActorDSLSpec/user/fred/barney]") + expectMsg("hello from akka://ActorDSLSpec/user/fred/barney") lastSender must be(a) } diff --git a/akka-actor-tests/src/test/scala/akka/actor/ActorLookupSpec.scala b/akka-actor-tests/src/test/scala/akka/actor/ActorLookupSpec.scala index 02b69f83d8..6ac5aa8361 100644 --- a/akka-actor-tests/src/test/scala/akka/actor/ActorLookupSpec.scala +++ b/akka-actor-tests/src/test/scala/akka/actor/ActorLookupSpec.scala @@ -71,11 +71,33 @@ class ActorLookupSpec extends AkkaSpec with DefaultTimeout { } "find actors by looking up their string representation" in { + // this is only true for local actor references system.actorFor(c1.path.toString) must be === c1 system.actorFor(c2.path.toString) must be === c2 system.actorFor(c21.path.toString) must be === c21 } + "take actor incarnation into account when comparing actor references" in { + val name = "abcdefg" + val a1 = system.actorOf(p, name) + watch(a1) + a1 ! PoisonPill + expectMsgType[Terminated].actor must be === a1 + + // not equal because it's terminated + system.actorFor(a1.path.toString) must not be (a1) + + val a2 = system.actorOf(p, name) + a2.path must be(a1.path) + a2.path.toString must be(a1.path.toString) + a2 must not be (a1) + a2.toString must not be (a1.toString) + + watch(a2) + a2 ! PoisonPill + expectMsgType[Terminated].actor must be === a2 + } + "find actors by looking up their root-anchored relative path" in { system.actorFor(c1.path.elements.mkString("/", "/", "")) must be === c1 system.actorFor(c2.path.elements.mkString("/", "/", "")) must be === c2 @@ -163,6 +185,9 @@ class ActorLookupSpec extends AkkaSpec with DefaultTimeout { "find actors by looking up their string representation" in { def check(looker: ActorRef, pathOf: ActorRef, result: ActorRef) { Await.result(looker ? LookupString(pathOf.path.toString), timeout.duration) must be === result + // with uid + Await.result(looker ? LookupString(pathOf.path.toSerializationFormat), timeout.duration) must be === result + // with trailing / Await.result(looker ? LookupString(pathOf.path.toString + "/"), timeout.duration) must be === result } for { diff --git a/akka-actor-tests/src/test/scala/akka/actor/ActorPathSpec.scala b/akka-actor-tests/src/test/scala/akka/actor/ActorPathSpec.scala new file mode 100644 index 0000000000..ef29bb2603 --- /dev/null +++ b/akka-actor-tests/src/test/scala/akka/actor/ActorPathSpec.scala @@ -0,0 +1,46 @@ +/** + * Copyright (C) 2009-2013 Typesafe Inc. + */ +package akka.actor + +import org.scalatest.WordSpec +import org.scalatest.matchers.MustMatchers + +class ActorPathSpec extends WordSpec with MustMatchers { + + "ActorPath" must { + + "create correct toString" in { + val a = Address("akka.tcp", "mysys") + RootActorPath(a).toString must be("akka.tcp://mysys/") + (RootActorPath(a) / "user").toString must be("akka.tcp://mysys/user") + (RootActorPath(a) / "user" / "foo").toString must be("akka.tcp://mysys/user/foo") + (RootActorPath(a) / "user" / "foo" / "bar").toString must be("akka.tcp://mysys/user/foo/bar") + } + + "create correct toStringWithAddress" in { + val local = Address("akka.tcp", "mysys") + val a = local.copy(host = Some("aaa"), port = Some(2552)) + val b = a.copy(host = Some("bb")) + val c = a.copy(host = Some("cccc")) + val root = RootActorPath(local) + root.toStringWithAddress(a) must be("akka.tcp://mysys@aaa:2552/") + (root / "user").toStringWithAddress(a) must be("akka.tcp://mysys@aaa:2552/user") + (root / "user" / "foo").toStringWithAddress(a) must be("akka.tcp://mysys@aaa:2552/user/foo") + + // root.toStringWithAddress(b) must be("akka.tcp://mysys@bb:2552/") + (root / "user").toStringWithAddress(b) must be("akka.tcp://mysys@bb:2552/user") + (root / "user" / "foo").toStringWithAddress(b) must be("akka.tcp://mysys@bb:2552/user/foo") + + root.toStringWithAddress(c) must be("akka.tcp://mysys@cccc:2552/") + (root / "user").toStringWithAddress(c) must be("akka.tcp://mysys@cccc:2552/user") + (root / "user" / "foo").toStringWithAddress(c) must be("akka.tcp://mysys@cccc:2552/user/foo") + + val rootA = RootActorPath(a) + rootA.toStringWithAddress(b) must be("akka.tcp://mysys@aaa:2552/") + (rootA / "user").toStringWithAddress(b) must be("akka.tcp://mysys@aaa:2552/user") + (rootA / "user" / "foo").toStringWithAddress(b) must be("akka.tcp://mysys@aaa:2552/user/foo") + + } + } +} diff --git a/akka-actor-tests/src/test/scala/akka/actor/RelativeActorPathSpec.scala b/akka-actor-tests/src/test/scala/akka/actor/RelativeActorPathSpec.scala index fd076463c7..5cf80eac88 100644 --- a/akka-actor-tests/src/test/scala/akka/actor/RelativeActorPathSpec.scala +++ b/akka-actor-tests/src/test/scala/akka/actor/RelativeActorPathSpec.scala @@ -23,5 +23,8 @@ class RelativeActorPathSpec extends WordSpec with MustMatchers { val name = URLEncoder.encode("akka://ClusterSystem@127.0.0.1:2552", "UTF-8") elements(name) must be(List(name)) } + "match path with uid fragment" in { + elements("foo/bar/baz#1234") must be(List("foo", "bar", "baz#1234")) + } } } diff --git a/akka-actor-tests/src/test/scala/akka/actor/SupervisorHierarchySpec.scala b/akka-actor-tests/src/test/scala/akka/actor/SupervisorHierarchySpec.scala index a36af3d895..8e91abb570 100644 --- a/akka-actor-tests/src/test/scala/akka/actor/SupervisorHierarchySpec.scala +++ b/akka-actor-tests/src/test/scala/akka/actor/SupervisorHierarchySpec.scala @@ -54,7 +54,7 @@ object SupervisorHierarchySpec { } case class Ready(ref: ActorRef) - case class Died(ref: ActorRef) + case class Died(path: ActorPath) case object Abort case object PingOfDeath case object PongOfDeath @@ -112,17 +112,17 @@ object SupervisorHierarchySpec { * upon Restart or would have to be managed by the highest supervisor (which * is undesirable). */ - case class HierarchyState(log: Vector[Event], kids: Map[ActorRef, Int], failConstr: Failure) - val stateCache = new ConcurrentHashMap[ActorRef, HierarchyState]() + case class HierarchyState(log: Vector[Event], kids: Map[ActorPath, Int], failConstr: Failure) + val stateCache = new ConcurrentHashMap[ActorPath, HierarchyState]() class Hierarchy(size: Int, breadth: Int, listener: ActorRef, myLevel: Int) extends Actor { var log = Vector.empty[Event] - stateCache.get(self) match { + stateCache.get(self.path) match { case hs @ HierarchyState(l: Vector[Event], _, f: Failure) if f.failConstr > 0 ⇒ val log = l :+ Event("Failed in constructor", identityHashCode(this)) - stateCache.put(self, hs.copy(log = log, failConstr = f.copy(failConstr = f.failConstr - 1))) + stateCache.put(self.path, hs.copy(log = log, failConstr = f.copy(failConstr = f.failConstr - 1))) throw f case _ ⇒ } @@ -149,7 +149,7 @@ object SupervisorHierarchySpec { log :+= Event("started", identityHashCode(this)) listener ! Ready(self) val s = size - 1 // subtract myself - val kidInfo: Map[ActorRef, Int] = + val kidInfo: Map[ActorPath, Int] = if (s > 0) { val kids = Random.nextInt(Math.min(breadth, s)) + 1 val sizes = s / kids @@ -158,10 +158,10 @@ object SupervisorHierarchySpec { (1 to kids).map { (id) ⇒ val kidSize = if (rest > 0) { rest -= 1; sizes + 1 } else sizes val props = propsTemplate.withCreator(new Hierarchy(kidSize, breadth, listener, myLevel + 1)) - (context.watch(context.actorOf(props, id.toString)), kidSize) + (context.watch(context.actorOf(props, id.toString)).path, kidSize) }(collection.breakOut) } else Map() - stateCache.put(self, HierarchyState(log, kidInfo, null)) + stateCache.put(self.path, HierarchyState(log, kidInfo, null)) } var preRestartCalled = false @@ -178,12 +178,12 @@ object SupervisorHierarchySpec { context.unwatch(child) context.stop(child) } - stateCache.put(self, stateCache.get(self).copy(log = log)) + stateCache.put(self.path, stateCache.get(self.path).copy(log = log)) if (f.failPre > 0) { f.failPre -= 1 throw f } - case _ ⇒ stateCache.put(self, stateCache.get(self).copy(log = log)) + case _ ⇒ stateCache.put(self.path, stateCache.get(self.path).copy(log = log)) } } } @@ -217,14 +217,14 @@ object SupervisorHierarchySpec { }) override def postRestart(cause: Throwable) { - val state = stateCache.get(self) + val state = stateCache.get(self.path) log = state.log log :+= Event("restarted " + suspendCount + " " + cause, identityHashCode(this)) state.kids foreach { - case (child, kidSize) ⇒ - val name = child.path.name - if (context.actorFor(name).isTerminated) { - listener ! Died(child) + case (childPath, kidSize) ⇒ + val name = childPath.name + if (context.child(name).isEmpty) { + listener ! Died(childPath) val props = Props(new Hierarchy(kidSize, breadth, listener, myLevel + 1)).withDispatcher("hierarchy") context.watch(context.actorOf(props, name)) } @@ -243,7 +243,7 @@ object SupervisorHierarchySpec { if (failed || suspended) { listener ! ErrorLog("not resumed (" + failed + ", " + suspended + ")", log) } else { - stateCache.put(self, HierarchyState(log, Map(), null)) + stateCache.put(self.path, HierarchyState(log, Map(), null)) } } @@ -270,7 +270,7 @@ object SupervisorHierarchySpec { val handler: Receive = { case f: Failure ⇒ setFlags(f.directive) - stateCache.put(self, stateCache.get(self).copy(failConstr = f.copy())) + stateCache.put(self.path, stateCache.get(self.path).copy(failConstr = f.copy())) throw f case "ping" ⇒ { Thread.sleep((Random.nextFloat * 1.03).toLong); sender ! "pong" } case Dump(0) ⇒ abort("dump") @@ -281,9 +281,9 @@ object SupervisorHierarchySpec { * (if the unwatch() came too late), so just ignore in this case. */ val name = ref.path.name - if (pongsToGo == 0 && context.actorFor(name).isTerminated) { - listener ! Died(ref) - val kids = stateCache.get(self).kids(ref) + if (pongsToGo == 0 && context.child(name).isEmpty) { + listener ! Died(ref.path) + val kids = stateCache.get(self.path).kids(ref.path) val props = Props(new Hierarchy(kids, breadth, listener, myLevel + 1)).withDispatcher("hierarchy") context.watch(context.actorOf(props, name)) } else { @@ -469,8 +469,8 @@ object SupervisorHierarchySpec { case x if x > 0.03 ⇒ 1 case _ ⇒ 2 } - private def bury(ref: ActorRef): Unit = { - val deadGuy = ref.path.elements + private def bury(path: ActorPath): Unit = { + val deadGuy = path.elements val deadGuySize = deadGuy.size val isChild = (other: ActorRef) ⇒ other.path.elements.take(deadGuySize) == deadGuy idleChildren = idleChildren filterNot isChild @@ -499,8 +499,8 @@ object SupervisorHierarchySpec { else context.system.scheduler.scheduleOnce(workSchedule, self, Work)(context.dispatcher) stay using (x - 1) case Event(Work, _) ⇒ if (pingChildren.isEmpty) goto(LastPing) else goto(Finishing) - case Event(Died(ref), _) ⇒ - bury(ref) + case Event(Died(path), _) ⇒ + bury(path) stay case Event("pong", _) ⇒ pingChildren -= sender @@ -631,7 +631,7 @@ object SupervisorHierarchySpec { case l: LocalActorRef ⇒ l.underlying.actor match { case h: Hierarchy ⇒ errors :+= target -> ErrorLog("forced", h.log) - case _ ⇒ errors :+= target -> ErrorLog("fetched", stateCache.get(target).log) + case _ ⇒ errors :+= target -> ErrorLog("fetched", stateCache.get(target.path).log) } if (depth > 0) { l.underlying.children foreach (getErrors(_, depth - 1)) @@ -644,7 +644,7 @@ object SupervisorHierarchySpec { case l: LocalActorRef ⇒ l.underlying.actor match { case h: Hierarchy ⇒ errors :+= target -> ErrorLog("forced", h.log) - case _ ⇒ errors :+= target -> ErrorLog("fetched", stateCache.get(target).log) + case _ ⇒ errors :+= target -> ErrorLog("fetched", stateCache.get(target.path).log) } if (target != hierarchy) getErrorsUp(l.getParent) } diff --git a/akka-actor-tests/src/test/scala/akka/actor/SupervisorSpec.scala b/akka-actor-tests/src/test/scala/akka/actor/SupervisorSpec.scala index 10576187d2..6a0bc9abbb 100644 --- a/akka-actor-tests/src/test/scala/akka/actor/SupervisorSpec.scala +++ b/akka-actor-tests/src/test/scala/akka/actor/SupervisorSpec.scala @@ -393,10 +393,10 @@ class SupervisorSpec extends AkkaSpec with BeforeAndAfterEach with ImplicitSende override def postRestart(reason: Throwable): Unit = testActor ! "parent restarted" def receive = { - case t @ Terminated(`child`) ⇒ testActor ! "child terminated" - case l: TestLatch ⇒ child ! l - case "test" ⇒ sender ! "green" - case "testchild" ⇒ child forward "test" + case Terminated(a) if a.path == child.path ⇒ testActor ! "child terminated" // FIXME case t @ Terminated(`child`) ticket #3156 + case l: TestLatch ⇒ child ! l + case "test" ⇒ sender ! "green" + case "testchild" ⇒ child forward "test" } })) diff --git a/akka-actor-tests/src/test/scala/akka/serialization/SerializeSpec.scala b/akka-actor-tests/src/test/scala/akka/serialization/SerializeSpec.scala index 8e1ccd1bdc..14076463f5 100644 --- a/akka-actor-tests/src/test/scala/akka/serialization/SerializeSpec.scala +++ b/akka-actor-tests/src/test/scala/akka/serialization/SerializeSpec.scala @@ -332,7 +332,7 @@ class SerializationCompatibilitySpec extends AkkaSpec(SerializationTests.mostlyR String.valueOf(encodeHex(ser.serialize(obj, obj.getClass).get)) must be(asExpected) "be preserved for the Create SystemMessage" in { - verify(Create(1234), "aced00057372000c7363616c612e5475706c6532bc7daadf46211a990200024c00025f317400124c6a6176612f6c616e672f4f626a6563743b4c00025f3271007e0001787073720014616b6b612e64697370617463682e437265617465bcdf9f7f2675038d0200014900037569647870000004d27671007e0003") + verify(Create(), "aced00057372000c7363616c612e5475706c6532bc7daadf46211a990200024c00025f317400124c6a6176612f6c616e672f4f626a6563743b4c00025f3271007e0001787073720014616b6b612e64697370617463682e437265617465000000000000000302000078707671007e0003") } "be preserved for the Recreate SystemMessage" in { verify(Recreate(null), "aced00057372000c7363616c612e5475706c6532bc7daadf46211a990200024c00025f317400124c6a6176612f6c616e672f4f626a6563743b4c00025f3271007e0001787073720016616b6b612e64697370617463682e52656372656174650987c65c8d378a800200014c000563617573657400154c6a6176612f6c616e672f5468726f7761626c653b7870707671007e0003") @@ -347,7 +347,7 @@ class SerializationCompatibilitySpec extends AkkaSpec(SerializationTests.mostlyR verify(Terminate(), "aced00057372000c7363616c612e5475706c6532bc7daadf46211a990200024c00025f317400124c6a6176612f6c616e672f4f626a6563743b4c00025f3271007e0001787073720017616b6b612e64697370617463682e5465726d696e61746509d66ca68318700f02000078707671007e0003") } "be preserved for the Supervise SystemMessage" in { - verify(Supervise(FakeActorRef("child"), true, 2468), "aced00057372000c7363616c612e5475706c6532bc7daadf46211a990200024c00025f317400124c6a6176612f6c616e672f4f626a6563743b4c00025f3271007e0001787073720017616b6b612e64697370617463682e5375706572766973652d0b363f56ab5feb0200035a00056173796e634900037569644c00056368696c647400154c616b6b612f6163746f722f4163746f725265663b787001000009a47372001f616b6b612e73657269616c697a6174696f6e2e46616b654163746f7252656600000000000000010200014c00046e616d657400124c6a6176612f6c616e672f537472696e673b7872001b616b6b612e6163746f722e496e7465726e616c4163746f725265660d0aa2ca1e82097602000078720013616b6b612e6163746f722e4163746f72526566c3585dde655f469402000078707400056368696c647671007e0003") + verify(Supervise(FakeActorRef("child"), true), "aced00057372000c7363616c612e5475706c6532bc7daadf46211a990200024c00025f317400124c6a6176612f6c616e672f4f626a6563743b4c00025f3271007e0001787073720017616b6b612e64697370617463682e53757065727669736500000000000000030200025a00056173796e634c00056368696c647400154c616b6b612f6163746f722f4163746f725265663b7870017372001f616b6b612e73657269616c697a6174696f6e2e46616b654163746f7252656600000000000000010200014c00046e616d657400124c6a6176612f6c616e672f537472696e673b7872001b616b6b612e6163746f722e496e7465726e616c4163746f725265660d0aa2ca1e82097602000078720013616b6b612e6163746f722e4163746f72526566c3585dde655f469402000078707400056368696c647671007e0003") } "be preserved for the ChildTerminated SystemMessage" in { verify(ChildTerminated(FakeActorRef("child")), "aced00057372000c7363616c612e5475706c6532bc7daadf46211a990200024c00025f317400124c6a6176612f6c616e672f4f626a6563743b4c00025f3271007e000178707372001d616b6b612e64697370617463682e4368696c645465726d696e617465644c84222437ed5db40200014c00056368696c647400154c616b6b612f6163746f722f4163746f725265663b78707372001f616b6b612e73657269616c697a6174696f6e2e46616b654163746f7252656600000000000000010200014c00046e616d657400124c6a6176612f6c616e672f537472696e673b7872001b616b6b612e6163746f722e496e7465726e616c4163746f725265660d0aa2ca1e82097602000078720013616b6b612e6163746f722e4163746f72526566c3585dde655f469402000078707400056368696c647671007e0003") diff --git a/akka-actor/src/main/scala/akka/actor/ActorCell.scala b/akka-actor/src/main/scala/akka/actor/ActorCell.scala index e570718149..6b1d42a529 100644 --- a/akka-actor/src/main/scala/akka/actor/ActorCell.scala +++ b/akka-actor/src/main/scala/akka/actor/ActorCell.scala @@ -16,6 +16,7 @@ import akka.event.Logging.{ LogEvent, Debug, Error } import akka.japi.Procedure import akka.dispatch.NullMessage import scala.concurrent.ExecutionContext +import scala.concurrent.forkjoin.ThreadLocalRandom /** * The actor context - the view of the actor cell from the actor. @@ -304,8 +305,26 @@ private[akka] object ActorCell { final val emptyBehaviorStack: List[Actor.Receive] = Nil final val emptyActorRefSet: Set[ActorRef] = immutable.TreeSet.empty + final val emptyActorRefMap: Map[ActorPath, ActorRef] = immutable.TreeMap.empty final val terminatedProps: Props = Props(() ⇒ throw new IllegalActorStateException("This Actor has been terminated")) + + final val undefinedUid = 0 + + @tailrec final def newUid(): Int = { + // Note that this uid is also used as hashCode in ActorRef, so be careful + // to not break hashing if you change the way uid is generated + val uid = ThreadLocalRandom.current.nextInt() + if (uid == undefinedUid) newUid + else uid + } + + final def splitNameAndUid(name: String): (String, Int) = { + val i = name.indexOf('#') + if (i < 0) (name, undefinedUid) + else (name.substring(0, i), Integer.valueOf(name.substring(i + 1))) + } + } //ACTORCELL IS 64bytes and should stay that way unless very good reason not to (machine sympathy, cache line fit) @@ -337,7 +356,7 @@ private[akka] class ActorCell( protected final def lookupRoot = self final def provider = system.provider - protected var uid: Int = 0 + protected def uid: Int = self.path.uid private[this] var _actor: Actor = _ def actor: Actor = _actor protected def actor_=(a: Actor): Unit = _actor = a @@ -361,7 +380,7 @@ private[akka] class ActorCell( var todo = message.next try { message match { - case Create(uid) ⇒ create(uid) + case Create() ⇒ create() case Watch(watchee, watcher) ⇒ addWatcher(watchee, watcher) case Unwatch(watchee, watcher) ⇒ remWatcher(watchee, watcher) case Recreate(cause) ⇒ @@ -379,10 +398,10 @@ private[akka] class ActorCell( case null ⇒ faultResume(inRespToFailure) case w: WaitingForChildren ⇒ w.enqueue(message) } - case Terminate() ⇒ terminate() - case Supervise(child, async, uid) ⇒ supervise(child, async, uid) - case ChildTerminated(child) ⇒ todo = handleChildTerminated(child) - case NoMessage ⇒ // only here to suppress warning + case Terminate() ⇒ terminate() + case Supervise(child, async) ⇒ supervise(child, async) + case ChildTerminated(child) ⇒ todo = handleChildTerminated(child) + case NoMessage ⇒ // only here to suppress warning } } catch handleNonFatalOrInterruptedException { e ⇒ handleInvokeFailure(Nil, e) @@ -473,7 +492,7 @@ private[akka] class ActorCell( } } - protected def create(uid: Int): Unit = { + protected def create(): Unit = { def clearOutActorIfNonNull(): Unit = { if (actor != null) { clearActorFields(actor) @@ -481,7 +500,6 @@ private[akka] class ActorCell( } } try { - this.uid = uid val created = newActor() actor = created created.preStart() @@ -505,12 +523,11 @@ private[akka] class ActorCell( } } - private def supervise(child: ActorRef, async: Boolean, uid: Int): Unit = + private def supervise(child: ActorRef, async: Boolean): Unit = if (!isTerminating) { // Supervise is the first thing we get from a new child, so store away the UID for later use in handleFailure() initChild(child) match { case Some(crs) ⇒ - crs.uid = uid handleSupervise(child, async) if (system.settings.DebugLifecycle) publish(Debug(self.path.toString, clazz(actor), "now supervising " + child)) case None ⇒ publish(Error(self.path.toString, clazz(actor), "received Supervise from unregistered child " + child + ", this will not end well")) diff --git a/akka-actor/src/main/scala/akka/actor/ActorPath.scala b/akka-actor/src/main/scala/akka/actor/ActorPath.scala index 4b87568cb0..20800e2785 100644 --- a/akka-actor/src/main/scala/akka/actor/ActorPath.scala +++ b/akka-actor/src/main/scala/akka/actor/ActorPath.scala @@ -6,6 +6,7 @@ import scala.annotation.tailrec import scala.collection.immutable import akka.japi.Util.immutableSeq import java.net.MalformedURLException +import java.lang.{ StringBuilder ⇒ JStringBuilder } object ActorPath { /** @@ -35,6 +36,13 @@ object ActorPath { * as possible, which owing to the bottom-up recursive nature of ActorPath * is sorted by path elements FROM RIGHT TO LEFT, where RootActorPath > * ChildActorPath in case the number of elements is different. + * + * Two actor paths are compared equal when they have the same name and parent + * elements, including the root address information. That does not necessarily + * mean that they point to the same incarnation of the actor if the actor is + * re-created with the same path. In other words, in contrast to how actor + * references are compared the unique id of the actor is not taken into account + * when comparing actor paths. */ @SerialVersionUID(1L) sealed trait ActorPath extends Comparable[ActorPath] with Serializable { @@ -96,6 +104,37 @@ sealed trait ActorPath extends Comparable[ActorPath] with Serializable { * information. */ def toStringWithAddress(address: Address): String + + /** + * Generate full String representation including the + * uid for the actor cell instance as URI fragment. + * This representation should be used as serialized + * representation instead of `toString`. + */ + def toSerializationFormat: String + + /** + * Generate full String representation including the uid for the actor cell + * instance as URI fragment, replacing the Address in the RootActor Path + * with the given one unless this path’s address includes host and port + * information. This representation should be used as serialized + * representation instead of `toStringWithAddress`. + */ + def toSerializationFormatWithAddress(address: Address): String + + /** + * INTERNAL API + * Unique identifier of the actor. Used for distinguishing + * different incarnations of actors with same path (name elements). + */ + private[akka] def uid: Int + + /** + * INTERNAL API + * Creates a new ActorPath with same elements but with the specified `uid`. + */ + private[akka] def withUid(uid: Int): ActorPath + } /** @@ -109,29 +148,55 @@ final case class RootActorPath(address: Address, name: String = "/") extends Act override def root: RootActorPath = this - override def /(child: String): ActorPath = new ChildActorPath(this, child) + override def /(child: String): ActorPath = { + val (childName, uid) = ActorCell.splitNameAndUid(child) + new ChildActorPath(this, childName, uid) + } override def elements: immutable.Iterable[String] = ActorPath.emptyActorPath override val toString: String = address + name + override val toSerializationFormat: String = toString + override def toStringWithAddress(addr: Address): String = if (address.host.isDefined) address + name else addr + name + override def toSerializationFormatWithAddress(addr: Address): String = toStringWithAddress(addr) + override def compareTo(other: ActorPath): Int = other match { case r: RootActorPath ⇒ toString compareTo r.toString // FIXME make this cheaper by comparing address and name in isolation case c: ChildActorPath ⇒ 1 } + + /** + * INTERNAL API + */ + private[akka] def uid: Int = ActorCell.undefinedUid + + /** + * INTERNAL API + */ + override private[akka] def withUid(uid: Int): ActorPath = + if (uid == ActorCell.undefinedUid) this + else throw new IllegalStateException("RootActorPath must not have uid") + } @SerialVersionUID(1L) -final class ChildActorPath(val parent: ActorPath, val name: String) extends ActorPath { +final class ChildActorPath private[akka] (val parent: ActorPath, val name: String, override private[akka] val uid: Int) extends ActorPath { if (name.indexOf('/') != -1) throw new IllegalArgumentException("/ is a path separator and is not legal in ActorPath names: [%s]" format name) + if (name.indexOf('#') != -1) throw new IllegalArgumentException("# is a fragment separator and is not legal in ActorPath names: [%s]" format name) + + def this(parent: ActorPath, name: String) = this(parent, name, ActorCell.undefinedUid) override def address: Address = root.address - override def /(child: String): ActorPath = new ChildActorPath(this, child) + override def /(child: String): ActorPath = { + val (childName, uid) = ActorCell.splitNameAndUid(child) + new ChildActorPath(this, childName, uid) + } override def elements: immutable.Iterable[String] = { @tailrec @@ -151,28 +216,82 @@ final class ChildActorPath(val parent: ActorPath, val name: String) extends Acto rec(this) } - // TODO research whether this should be cached somehow (might be fast enough, but creates GC pressure) - /* - * idea: add one field which holds the total length (because that is known) - * so that only one String needs to be allocated before traversal; this is - * cheaper than any cache + /** + * INTERNAL API */ - override def toString = { - @tailrec - def rec(p: ActorPath, s: StringBuilder): StringBuilder = p match { - case r: RootActorPath ⇒ s.insert(0, r.toString) - case _ ⇒ rec(p.parent, s.insert(0, '/').insert(0, p.name)) - } - rec(parent, new StringBuilder(32).append(name)).toString + override private[akka] def withUid(uid: Int): ActorPath = + if (uid == this.uid) this + else new ChildActorPath(parent, name, uid) + + override def toString: String = { + val length = toStringLength + buildToString(new JStringBuilder(length), length, 0, _.toString).toString } - override def toStringWithAddress(addr: Address) = { + override def toSerializationFormat: String = { + val length = toStringLength + val sb = buildToString(new JStringBuilder(length + 12), length, 0, _.toString) + appendUidFragment(sb).toString + } + + private def toStringLength: Int = toStringOffset + name.length + + private val toStringOffset: Int = parent match { + case r: RootActorPath ⇒ r.address.toString.length + r.name.length + case c: ChildActorPath ⇒ c.toStringLength + 1 + } + + override def toStringWithAddress(addr: Address): String = { + val diff = addressStringLengthDiff(addr) + val length = toStringLength + diff + buildToString(new JStringBuilder(length), length, diff, _.toStringWithAddress(addr)).toString + } + + override def toSerializationFormatWithAddress(addr: Address): String = { + val diff = addressStringLengthDiff(addr) + val length = toStringLength + diff + val sb = buildToString(new JStringBuilder(length + 12), length, diff, _.toStringWithAddress(addr)) + appendUidFragment(sb).toString + } + + private def addressStringLengthDiff(addr: Address): Int = { + val r = root + if (r.address.host.isDefined) 0 + else (addr.toString.length - r.address.toString.length) + } + + /** + * Optimized toString construction. Used by `toString`, `toSerializationFormat`, + * and friends `WithAddress` + * @param sb builder that will be modified (and same instance is returned) + * @param length pre-calculated length of the to be constructed String, not + * necessarily same as sb.capacity because more things may be appended to the + * sb afterwards + * @param diff difference in offset for each child element, due to different address + * @param rootString function to construct the root element string + */ + private def buildToString(sb: JStringBuilder, length: Int, diff: Int, rootString: RootActorPath ⇒ String): JStringBuilder = { @tailrec - def rec(p: ActorPath, s: StringBuilder): StringBuilder = p match { - case r: RootActorPath ⇒ s.insert(0, r.toStringWithAddress(addr)) - case _ ⇒ rec(p.parent, s.insert(0, '/').insert(0, p.name)) + def rec(p: ActorPath): JStringBuilder = p match { + case r: RootActorPath ⇒ + val rootStr = rootString(r) + sb.replace(0, rootStr.length, rootStr) + case c: ChildActorPath ⇒ + val start = c.toStringOffset + diff + val end = start + c.name.length + sb.replace(start, end, c.name) + if (c ne this) + sb.replace(end, end + 1, "/") + rec(c.parent) } - rec(parent, new StringBuilder(32).append(name)).toString + + sb.setLength(length) + rec(this) + } + + private def appendUidFragment(sb: JStringBuilder): JStringBuilder = { + if (uid == ActorCell.undefinedUid) sb + else sb.append("#").append(uid) } override def equals(other: Any): Boolean = { diff --git a/akka-actor/src/main/scala/akka/actor/ActorRef.scala b/akka-actor/src/main/scala/akka/actor/ActorRef.scala index 99aaaa69fa..ff42918ebe 100644 --- a/akka-actor/src/main/scala/akka/actor/ActorRef.scala +++ b/akka-actor/src/main/scala/akka/actor/ActorRef.scala @@ -12,7 +12,6 @@ import akka.event.EventStream import scala.annotation.tailrec import java.util.concurrent.ConcurrentHashMap import akka.event.LoggingAdapter -import scala.concurrent.forkjoin.ThreadLocalRandom import scala.collection.JavaConverters /** @@ -73,6 +72,17 @@ import scala.collection.JavaConverters * * ActorRef does not have a method for terminating the actor it points to, use * [[akka.actor.ActorRefFactory]]`.stop(child)` for this purpose. + * + * Two actor references are compared equal when they have the same path and point to + * the same actor incarnation. A reference pointing to a terminated actor doesn't compare + * equal to a reference pointing to another (re-created) actor with the same path. + * Actor references acquired with `actorFor` do not always include the full information + * about the underlying actor identity and therefore such references do not always compare + * equal to references acquired with `actorOf`, `sender`, or `context.self`. + * + * If you need to keep track of actor references in a collection and do not care + * about the exact actor incarnation you can use the ``ActorPath`` as key because + * the unique id of the actor is not taken into account when comparing actor paths. */ abstract class ActorRef extends java.lang.Comparable[ActorRef] with Serializable { scalaRef: InternalActorRef ⇒ @@ -83,9 +93,13 @@ abstract class ActorRef extends java.lang.Comparable[ActorRef] with Serializable def path: ActorPath /** - * Comparison only takes address into account. + * Comparison takes path and the unique id of the actor cell into account. */ - final def compareTo(other: ActorRef) = this.path compareTo other.path + final def compareTo(other: ActorRef) = { + val x = this.path compareTo other.path + if (x == 0) this.path.uid compareTo other.path.uid + else x + } /** * Sends the specified message to the sender, i.e. fire-and-forget semantics. @@ -122,15 +136,22 @@ abstract class ActorRef extends java.lang.Comparable[ActorRef] with Serializable */ def isTerminated: Boolean - // FIXME RK check if we should scramble the bits or whether they can stay the same - final override def hashCode: Int = path.hashCode + final override def hashCode: Int = { + if (path.uid == ActorCell.undefinedUid) path.hashCode + else path.uid + } + /** + * Equals takes path and the unique id of the actor cell into account. + */ final override def equals(that: Any): Boolean = that match { - case other: ActorRef ⇒ path == other.path + case other: ActorRef ⇒ path.uid == other.path.uid && path == other.path case _ ⇒ false } - override def toString = "Actor[%s]".format(path) + override def toString: String = + if (path.uid == ActorCell.undefinedUid) s"Actor[${path}]" + else s"Actor[${path}#${path.uid}]" } /** @@ -270,7 +291,7 @@ private[akka] class LocalActorRef private[akka] ( * object from another thread as soon as we run init. */ private val actorCell: ActorCell = newActorCell(_system, this, _props, _supervisor) - actorCell.init(ThreadLocalRandom.current.nextInt(), sendSupervise = true) + actorCell.init(sendSupervise = true) protected def newActorCell(system: ActorSystemImpl, ref: InternalActorRef, props: Props, supervisor: InternalActorRef): ActorCell = new ActorCell(system, ref, props, supervisor) @@ -316,11 +337,14 @@ private[akka] class LocalActorRef private[akka] ( * Method for looking up a single child beneath this actor. Override in order * to inject “synthetic” actor paths like “/temp”. */ - protected def getSingleChild(name: String): InternalActorRef = - actorCell.getChildByName(name) match { - case Some(crs: ChildRestartStats) ⇒ crs.child.asInstanceOf[InternalActorRef] - case _ ⇒ Nobody + protected def getSingleChild(name: String): InternalActorRef = { + val (childName, uid) = ActorCell.splitNameAndUid(name) + actorCell.getChildByName(childName) match { + case Some(crs: ChildRestartStats) if uid == ActorCell.undefinedUid || uid == crs.uid ⇒ + crs.child.asInstanceOf[InternalActorRef] + case _ ⇒ Nobody } + } override def getChild(names: Iterator[String]): InternalActorRef = { /* @@ -384,8 +408,8 @@ private[akka] case class SerializedActorRef private (path: String) { private[akka] object SerializedActorRef { def apply(path: ActorPath): SerializedActorRef = { Serialization.currentTransportAddress.value match { - case null ⇒ new SerializedActorRef(path.toString) - case addr ⇒ new SerializedActorRef(path.toStringWithAddress(addr)) + case null ⇒ new SerializedActorRef(path.toSerializationFormat) + case addr ⇒ new SerializedActorRef(path.toSerializationFormatWithAddress(addr)) } } } diff --git a/akka-actor/src/main/scala/akka/actor/ActorRefProvider.scala b/akka-actor/src/main/scala/akka/actor/ActorRefProvider.scala index cb262495fe..4c6b13d9b6 100644 --- a/akka-actor/src/main/scala/akka/actor/ActorRefProvider.scala +++ b/akka-actor/src/main/scala/akka/actor/ActorRefProvider.scala @@ -396,7 +396,7 @@ class LocalActorRefProvider private[akka] ( override def sendSystemMessage(message: SystemMessage): Unit = stopped ifOff { message match { - case Supervise(_, _, _) ⇒ // TODO register child in some map to keep track of it and enable shutdown after all dead + case Supervise(_, _) ⇒ // TODO register child in some map to keep track of it and enable shutdown after all dead case ChildTerminated(_) ⇒ stop() case _ ⇒ log.error(this + " received unexpected system message [" + message + "]") } diff --git a/akka-actor/src/main/scala/akka/actor/Address.scala b/akka-actor/src/main/scala/akka/actor/Address.scala index 72f0a0c38a..8bde7ef8cc 100644 --- a/akka-actor/src/main/scala/akka/actor/Address.scala +++ b/akka-actor/src/main/scala/akka/actor/Address.scala @@ -47,7 +47,7 @@ final case class Address private (protocol: String, system: String, host: Option */ @transient override lazy val toString: String = { - val sb = (new StringBuilder(protocol)).append("://").append(system) + val sb = (new java.lang.StringBuilder(protocol)).append("://").append(system) if (host.isDefined) sb.append('@').append(host.get) if (port.isDefined) sb.append(':').append(port.get) @@ -76,12 +76,14 @@ object Address { } private[akka] trait PathUtils { - protected def split(s: String): List[String] = { + protected def split(s: String, fragment: String): List[String] = { @tailrec def rec(pos: Int, acc: List[String]): List[String] = { val from = s.lastIndexOf('/', pos - 1) val sub = s.substring(from + 1, pos) - val l = sub :: acc + val l = + if ((fragment ne null) && acc.isEmpty) sub + "#" + fragment :: acc + else sub :: acc if (from == -1) l else rec(from, l) } rec(s.length, Nil) @@ -93,7 +95,7 @@ object RelativeActorPath extends PathUtils { try { val uri = new URI(addr) if (uri.isAbsolute) None - else Some(split(uri.getRawPath)) + else Some(split(uri.getRawPath, uri.getRawFragment)) } catch { case _: URISyntaxException ⇒ None } @@ -142,7 +144,7 @@ object ActorPathExtractor extends PathUtils { val uri = new URI(addr) uri.getRawPath match { case null ⇒ None - case path ⇒ AddressFromURIString.unapply(uri).map((_, split(path).drop(1))) + case path ⇒ AddressFromURIString.unapply(uri).map((_, split(path, uri.getRawFragment).drop(1))) } } catch { case _: URISyntaxException ⇒ None diff --git a/akka-actor/src/main/scala/akka/actor/FaultHandling.scala b/akka-actor/src/main/scala/akka/actor/FaultHandling.scala index 171290b4d1..4612bff4e5 100644 --- a/akka-actor/src/main/scala/akka/actor/FaultHandling.scala +++ b/akka-actor/src/main/scala/akka/actor/FaultHandling.scala @@ -32,7 +32,7 @@ private[akka] case object ChildNameReserved extends ChildStats case class ChildRestartStats(child: ActorRef, var maxNrOfRetriesCount: Int = 0, var restartTimeWindowStartNanos: Long = 0L) extends ChildStats { - var uid: Int = 0 + def uid: Int = child.path.uid //FIXME How about making ChildRestartStats immutable and then move these methods into the actual supervisor strategies? def requestRestartPermission(retriesWindow: (Option[Int], Option[Int])): Boolean = diff --git a/akka-actor/src/main/scala/akka/actor/RepointableActorRef.scala b/akka-actor/src/main/scala/akka/actor/RepointableActorRef.scala index 5b265a6055..8a1a6a6a03 100644 --- a/akka-actor/src/main/scala/akka/actor/RepointableActorRef.scala +++ b/akka-actor/src/main/scala/akka/actor/RepointableActorRef.scala @@ -10,7 +10,6 @@ import java.util.concurrent.TimeUnit import java.util.concurrent.locks.ReentrantLock import scala.annotation.tailrec -import scala.concurrent.forkjoin.ThreadLocalRandom import akka.actor.dungeon.ChildrenContainer import akka.event.Logging.Warning @@ -73,10 +72,9 @@ private[akka] class RepointableActorRef( def initialize(async: Boolean): this.type = underlying match { case null ⇒ - val uid = ThreadLocalRandom.current.nextInt() - swapCell(new UnstartedCell(system, this, props, supervisor, uid)) + swapCell(new UnstartedCell(system, this, props, supervisor)) swapLookup(underlying) - supervisor.sendSystemMessage(Supervise(this, async, uid)) + supervisor.sendSystemMessage(Supervise(this, async)) if (!async) point() this case other ⇒ throw new IllegalStateException("initialize called more than once!") @@ -112,7 +110,7 @@ private[akka] class RepointableActorRef( * unstarted cell. The cell must be fully functional. */ def newCell(old: UnstartedCell): Cell = - new ActorCell(system, this, props, supervisor).init(old.uid, sendSupervise = false) + new ActorCell(system, this, props, supervisor).init(sendSupervise = false) def start(): Unit = () @@ -144,9 +142,11 @@ private[akka] class RepointableActorRef( case ".." ⇒ getParent.getChild(name) case "" ⇒ getChild(name) case other ⇒ - lookup.getChildByName(other) match { - case Some(crs: ChildRestartStats) ⇒ crs.child.asInstanceOf[InternalActorRef].getChild(name) - case _ ⇒ Nobody + val (childName, uid) = ActorCell.splitNameAndUid(other) + lookup.getChildByName(childName) match { + case Some(crs: ChildRestartStats) if uid == ActorCell.undefinedUid || uid == crs.uid ⇒ + crs.child.asInstanceOf[InternalActorRef].getChild(name) + case _ ⇒ Nobody } } } else this @@ -162,8 +162,7 @@ private[akka] class RepointableActorRef( private[akka] class UnstartedCell(val systemImpl: ActorSystemImpl, val self: RepointableActorRef, val props: Props, - val supervisor: InternalActorRef, - val uid: Int) extends Cell { + val supervisor: InternalActorRef) extends Cell { /* * This lock protects all accesses to this cell’s queues. It also ensures diff --git a/akka-actor/src/main/scala/akka/actor/dungeon/Children.scala b/akka-actor/src/main/scala/akka/actor/dungeon/Children.scala index 9c64d79a64..c419f1a14f 100644 --- a/akka-actor/src/main/scala/akka/actor/dungeon/Children.scala +++ b/akka-actor/src/main/scala/akka/actor/dungeon/Children.scala @@ -182,7 +182,8 @@ private[akka] trait Children { this: ActorCell ⇒ // this name will either be unreserved or overwritten with a real child below val actor = try { - cell.provider.actorOf(cell.systemImpl, props, cell.self, cell.self.path / name, + val childPath = (cell.self.path / name).withUid(ActorCell.newUid()) + cell.provider.actorOf(cell.systemImpl, props, cell.self, childPath, systemService = systemService, deploy = None, lookupDeploy = true, async = async) } catch { case e: InterruptedException ⇒ diff --git a/akka-actor/src/main/scala/akka/actor/dungeon/DeathWatch.scala b/akka-actor/src/main/scala/akka/actor/dungeon/DeathWatch.scala index a0fd5f1632..cff5665ad3 100644 --- a/akka-actor/src/main/scala/akka/actor/dungeon/DeathWatch.scala +++ b/akka-actor/src/main/scala/akka/actor/dungeon/DeathWatch.scala @@ -8,6 +8,7 @@ import akka.actor.{ Terminated, InternalActorRef, ActorRef, ActorRefScope, Actor import akka.dispatch.{ ChildTerminated, Watch, Unwatch } import akka.event.Logging.{ Warning, Error, Debug } import scala.util.control.NonFatal +import akka.actor.MinimalActorRef private[akka] trait DeathWatch { this: ActorCell ⇒ @@ -16,7 +17,7 @@ private[akka] trait DeathWatch { this: ActorCell ⇒ override final def watch(subject: ActorRef): ActorRef = subject match { case a: InternalActorRef ⇒ - if (a != self && !watching.contains(a)) { + if (a != self && !watchingContains(a)) { maintainAddressTerminatedSubscription(a) { a.sendSystemMessage(Watch(a, self)) // ➡➡➡ NEVER SEND THE SAME SYSTEM MESSAGE OBJECT TO TWO ACTORS ⬅⬅⬅ watching += a @@ -27,10 +28,10 @@ private[akka] trait DeathWatch { this: ActorCell ⇒ override final def unwatch(subject: ActorRef): ActorRef = subject match { case a: InternalActorRef ⇒ - if (a != self && watching.contains(a)) { + if (a != self && watchingContains(a)) { maintainAddressTerminatedSubscription(a) { a.sendSystemMessage(Unwatch(a, self)) // ➡➡➡ NEVER SEND THE SAME SYSTEM MESSAGE OBJECT TO TWO ACTORS ⬅⬅⬅ - watching -= a + removeWatching(a) } } a @@ -41,13 +42,28 @@ private[akka] trait DeathWatch { this: ActorCell ⇒ * it will be propagated to user's receive. */ protected def watchedActorTerminated(t: Terminated): Unit = - if (watching.contains(t.actor)) { + if (watchingContains(t.actor)) { maintainAddressTerminatedSubscription(t.actor) { - watching -= t.actor + removeWatching(t.actor) } receiveMessage(t) } + // TODO this should be removed and be replaced with `watching.contains(subject)` + // when all actor references have uid, i.e. actorFor is removed + private def watchingContains(subject: ActorRef): Boolean = { + watching.contains(subject) || (subject.path.uid != ActorCell.undefinedUid && + watching.contains(new UndefinedUidActorRef(subject))) + } + + // TODO this should be removed and be replaced with `watching -= subject` + // when all actor references have uid, i.e. actorFor is removed + private def removeWatching(subject: ActorRef): Unit = { + watching -= subject + if (subject.path.uid != ActorCell.undefinedUid) + watching -= new UndefinedUidActorRef(subject) + } + protected def tellWatchersWeDied(actor: Actor): Unit = { if (!watchedBy.isEmpty) { val terminated = Terminated(self)(existenceConfirmed = true, addressTerminated = false) @@ -168,3 +184,8 @@ private[akka] trait DeathWatch { this: ActorCell ⇒ private def subscribeAddressTerminated(): Unit = system.eventStream.subscribe(self, classOf[AddressTerminated]) } + +private[akka] class UndefinedUidActorRef(ref: ActorRef) extends MinimalActorRef { + override val path = ref.path.withUid(ActorCell.undefinedUid) + override def provider = throw new UnsupportedOperationException("UndefinedUidActorRef does not provide") +} \ No newline at end of file diff --git a/akka-actor/src/main/scala/akka/actor/dungeon/Dispatch.scala b/akka-actor/src/main/scala/akka/actor/dungeon/Dispatch.scala index a931164d43..075ff49a0b 100644 --- a/akka-actor/src/main/scala/akka/actor/dungeon/Dispatch.scala +++ b/akka-actor/src/main/scala/akka/actor/dungeon/Dispatch.scala @@ -40,7 +40,7 @@ private[akka] trait Dispatch { this: ActorCell ⇒ * reasonably different from the previous UID of a possible actor with the same path, * which can be achieved by using ThreadLocalRandom.current.nextInt(). */ - final def init(uid: Int, sendSupervise: Boolean): this.type = { + final def init(sendSupervise: Boolean): this.type = { /* * Create the mailbox and enqueue the Create() message to ensure that * this is processed before anything else. @@ -49,11 +49,11 @@ private[akka] trait Dispatch { this: ActorCell ⇒ mailbox.setActor(this) // ➡➡➡ NEVER SEND THE SAME SYSTEM MESSAGE OBJECT TO TWO ACTORS ⬅⬅⬅ - mailbox.systemEnqueue(self, Create(uid)) + mailbox.systemEnqueue(self, Create()) if (sendSupervise) { // ➡➡➡ NEVER SEND THE SAME SYSTEM MESSAGE OBJECT TO TWO ACTORS ⬅⬅⬅ - parent.sendSystemMessage(akka.dispatch.Supervise(self, async = false, uid)) + parent.sendSystemMessage(akka.dispatch.Supervise(self, async = false)) parent ! NullMessage // read ScalaDoc of NullMessage to see why } this diff --git a/akka-actor/src/main/scala/akka/actor/dungeon/FaultHandling.scala b/akka-actor/src/main/scala/akka/actor/dungeon/FaultHandling.scala index 7764d966e0..ac5d1a48e0 100644 --- a/akka-actor/src/main/scala/akka/actor/dungeon/FaultHandling.scala +++ b/akka-actor/src/main/scala/akka/actor/dungeon/FaultHandling.scala @@ -134,7 +134,7 @@ private[akka] trait FaultHandling { this: ActorCell ⇒ private def finishCreate(): Unit = { try resumeNonRecursive() finally clearFailed() - create(uid) + create() } protected def terminate() { diff --git a/akka-actor/src/main/scala/akka/dispatch/AbstractDispatcher.scala b/akka-actor/src/main/scala/akka/dispatch/AbstractDispatcher.scala index 3ec4e24e90..b0e97f2f0c 100644 --- a/akka-actor/src/main/scala/akka/dispatch/AbstractDispatcher.scala +++ b/akka-actor/src/main/scala/akka/dispatch/AbstractDispatcher.scala @@ -82,8 +82,8 @@ private[akka] sealed trait SystemMessage extends PossiblyHarmful with Serializab /** * INTERNAL API */ -@SerialVersionUID(-4836972106317757555L) -private[akka] case class Create(uid: Int) extends SystemMessage // send to self from Dispatcher.register +@SerialVersionUID(3L) +private[akka] case class Create() extends SystemMessage // send to self from Dispatcher.register /** * INTERNAL API */ @@ -107,8 +107,8 @@ private[akka] case class Terminate() extends SystemMessage // sent to self from /** * INTERNAL API */ -@SerialVersionUID(3245747602115485675L) -private[akka] case class Supervise(child: ActorRef, async: Boolean, uid: Int) extends SystemMessage // sent to supervisor ActorRef from ActorCell.start +@SerialVersionUID(3L) +private[akka] case class Supervise(child: ActorRef, async: Boolean) extends SystemMessage // sent to supervisor ActorRef from ActorCell.start /** * INTERNAL API */ diff --git a/akka-actor/src/main/scala/akka/event/Logging.scala b/akka-actor/src/main/scala/akka/event/Logging.scala index 53504c5d2c..266958bf83 100644 --- a/akka-actor/src/main/scala/akka/event/Logging.scala +++ b/akka-actor/src/main/scala/akka/event/Logging.scala @@ -872,7 +872,7 @@ trait LoggingAdapter { } def format(t: String, arg: Any*): String = { - val sb = new StringBuilder(64) + val sb = new java.lang.StringBuilder(64) var p = 0 var rest = t while (p < arg.length) { diff --git a/akka-actor/src/main/scala/akka/pattern/GracefulStopSupport.scala b/akka-actor/src/main/scala/akka/pattern/GracefulStopSupport.scala index dba1cd6461..84980e4ee0 100644 --- a/akka-actor/src/main/scala/akka/pattern/GracefulStopSupport.scala +++ b/akka-actor/src/main/scala/akka/pattern/GracefulStopSupport.scala @@ -55,13 +55,13 @@ trait GracefulStopSupport { internalTarget.sendSystemMessage(Watch(target, ref)) val f = ref.result.future f onComplete { // Just making sure we're not leaking here - case Success(Terminated(`target`)) ⇒ () - case _ ⇒ internalTarget.sendSystemMessage(Unwatch(target, ref)) + case Success(Terminated(a)) if a.path == target.path ⇒ () + case _ ⇒ internalTarget.sendSystemMessage(Unwatch(target, ref)) } target ! stopMessage f map { - case Terminated(`target`) ⇒ true - case _ ⇒ false + case Terminated(a) if a.path == target.path ⇒ true + case _ ⇒ false } case s ⇒ throw new IllegalArgumentException("Unknown ActorSystem implementation: '" + s + "'") } diff --git a/akka-actor/src/main/scala/akka/routing/Routing.scala b/akka-actor/src/main/scala/akka/routing/Routing.scala index f09168ed73..c880496077 100644 --- a/akka-actor/src/main/scala/akka/routing/Routing.scala +++ b/akka-actor/src/main/scala/akka/routing/Routing.scala @@ -35,7 +35,7 @@ private[akka] class RoutedActorRef(_system: ActorSystemImpl, _props: Props, _sup " is invalid - you can not use a 'BalancingDispatcher' as a Router's dispatcher, you can however use it for the routees.") } else _props.routerConfig.verifyConfig() - override def newCell(old: UnstartedCell): Cell = new RoutedActorCell(system, this, props, supervisor).init(old.uid, sendSupervise = false) + override def newCell(old: UnstartedCell): Cell = new RoutedActorCell(system, this, props, supervisor).init(sendSupervise = false) } @@ -76,7 +76,7 @@ private[akka] class RoutedActorCell(_system: ActorSystemImpl, _ref: InternalActo def applyRoute(sender: ActorRef, message: Any): immutable.Iterable[Destination] = message match { case _: AutoReceivedMessage ⇒ Destination(sender, self) :: Nil - case CurrentRoutees ⇒ sender ! RouterRoutees(_routees); Nil + case CurrentRoutees ⇒ { sender ! RouterRoutees(_routees); Nil } case msg if route.isDefinedAt(sender, msg) ⇒ route(sender, message) case _ ⇒ Nil } @@ -94,13 +94,13 @@ private[akka] class RoutedActorCell(_system: ActorSystemImpl, _ref: InternalActo } /** - * Adds the routees to existing routees. + * Removes the abandoned routees from existing routees. * Removes death watch of the routees. Doesn't stop the routees. * Not thread safe, but intended to be called from protected points, such as * `Resizer.resize` */ private[akka] def removeRoutees(abandonedRoutees: immutable.Iterable[ActorRef]): Unit = { - _routees = abandonedRoutees.foldLeft(_routees) { (xs, x) ⇒ unwatch(x); xs.filterNot(_ == x) } + _routees = abandonedRoutees.foldLeft(_routees) { (xs, x) ⇒ unwatch(x); xs.filterNot(_.path == x.path) } } /** diff --git a/akka-actor/src/main/scala/akka/util/Crypt.scala b/akka-actor/src/main/scala/akka/util/Crypt.scala index 86b98a2cfd..b398ae98ea 100644 --- a/akka-actor/src/main/scala/akka/util/Crypt.scala +++ b/akka-actor/src/main/scala/akka/util/Crypt.scala @@ -32,7 +32,7 @@ object Crypt { } def hexify(bytes: Array[Byte]): String = { - val builder = new StringBuilder(bytes.length * 2) + val builder = new java.lang.StringBuilder(bytes.length * 2) bytes.foreach { byte ⇒ builder.append(hex.charAt((byte & 0xF0) >> 4)).append(hex.charAt(byte & 0xF)) } builder.toString } diff --git a/akka-actor/src/main/scala/akka/util/Helpers.scala b/akka-actor/src/main/scala/akka/util/Helpers.scala index c0963b30fa..f62724e065 100644 --- a/akka-actor/src/main/scala/akka/util/Helpers.scala +++ b/akka-actor/src/main/scala/akka/util/Helpers.scala @@ -40,8 +40,8 @@ object Helpers { final val base64chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+~" @tailrec - def base64(l: Long, sb: StringBuilder = new StringBuilder("$")): String = { - sb += base64chars.charAt(l.toInt & 63) + def base64(l: Long, sb: java.lang.StringBuilder = new java.lang.StringBuilder("$")): String = { + sb append base64chars.charAt(l.toInt & 63) val next = l >>> 6 if (next == 0) sb.toString else base64(next, sb) diff --git a/akka-cluster/src/multi-jvm/scala/akka/cluster/StressSpec.scala b/akka-cluster/src/multi-jvm/scala/akka/cluster/StressSpec.scala index ec851b1594..fdf33cb391 100644 --- a/akka-cluster/src/multi-jvm/scala/akka/cluster/StressSpec.scala +++ b/akka-cluster/src/multi-jvm/scala/akka/cluster/StressSpec.scala @@ -254,7 +254,7 @@ object StressMultiJvmSpec extends MultiNodeConfig { def maxDuration = results.map(_.duration).max - def totalClusterStats = results.foldLeft(ClusterStats()){_ :+ _.clusterStats} + def totalClusterStats = results.foldLeft(ClusterStats()) { _ :+ _.clusterStats } def formatMetrics: String = { import akka.cluster.Member.addressOrdering @@ -302,7 +302,7 @@ object StressMultiJvmSpec extends MultiNodeConfig { s"${monitor}\t${subject}\t${phi.count}\t${phi.countAboveOne}\t${phi.max.form}" def formatStats: String = - (clusterStatsObservedByNode map { case (monitor, stats) => s"${monitor}\t${stats}" }). + (clusterStatsObservedByNode map { case (monitor, stats) ⇒ s"${monitor}\t${stats}" }). mkString("ClusterStats\n", "\n", "") } @@ -403,7 +403,7 @@ object StressMultiJvmSpec extends MultiNodeConfig { def receive = { case StatsTick ⇒ val res = StatsResult(cluster.selfAddress, cluster.readView.latestStats :- startStats) - reportTo foreach { _ ! res } + reportTo foreach { _ ! res } case ReportTo(ref) ⇒ reportTo = ref case Reset ⇒ @@ -553,7 +553,9 @@ object StressMultiJvmSpec extends MultiNodeConfig { * Used for remote death watch testing */ class Watchee extends Actor { - def receive = Actor.emptyBehavior + def receive = { + case Ping ⇒ sender ! Pong + } } /** @@ -621,6 +623,9 @@ object StressMultiJvmSpec extends MultiNodeConfig { case class ChildrenCount(numberOfChildren: Int, numberOfChildRestarts: Int) case object Reset + case object Ping + case object Pong + } class StressMultiJvmNode1 extends StressSpec @@ -700,7 +705,7 @@ abstract class StressSpec runOn(roles.head) { val r = clusterResultAggregator watch(r) - expectMsgPF(remaining) { case Terminated(`r`) ⇒ true } + expectMsgPF(remaining) { case Terminated(a) if a.path == r.path ⇒ true } } enterBarrier("cluster-result-done-" + step) } @@ -773,7 +778,9 @@ abstract class StressSpec } enterBarrier("watchee-created-" + step) runOn(roles.head) { - watch(system.actorFor(node(removeRole) / "user" / "watchee")) + system.actorFor(node(removeRole) / "user" / "watchee") ! Ping + expectMsg(Pong) + watch(lastSender) } enterBarrier("watch-estabilished-" + step) @@ -790,9 +797,9 @@ abstract class StressSpec } runOn(roles.head) { - val expectedRef = system.actorFor(RootActorPath(removeAddress) / "user" / "watchee") + val expectedPath = RootActorPath(removeAddress) / "user" / "watchee" expectMsgPF(remaining) { - case Terminated(`expectedRef`) ⇒ true + case Terminated(a) if a.path == expectedPath ⇒ true } } enterBarrier("watch-verified-" + step) @@ -939,7 +946,7 @@ abstract class StressSpec workResult.jobsPerSecond.form, workResult.retryCount, workResult.sendCount) watch(m) - expectMsgPF(remaining) { case Terminated(`m`) ⇒ true } + expectMsgPF(remaining) { case Terminated(a) if a.path == m.path ⇒ true } workResult } @@ -947,7 +954,7 @@ abstract class StressSpec within(duration + 10.seconds) { val rounds = (duration.toMillis / oneIteration.toMillis).max(1).toInt val supervisor = system.actorOf(Props[Supervisor], "supervisor") - for (count <- 0 until rounds) { + for (count ← 0 until rounds) { createResultAggregator(title, expectedResults = nbrUsedRoles, includeInHistory = false) reportResult { diff --git a/akka-contrib/src/main/scala/akka/contrib/pattern/ReliableProxy.scala b/akka-contrib/src/main/scala/akka/contrib/pattern/ReliableProxy.scala index b3e24e2361..f34b73d589 100644 --- a/akka-contrib/src/main/scala/akka/contrib/pattern/ReliableProxy.scala +++ b/akka-contrib/src/main/scala/akka/contrib/pattern/ReliableProxy.scala @@ -26,7 +26,8 @@ object ReliableProxy { } else { log.debug("received msg of {} from {} with wrong serial", msg.asInstanceOf[AnyRef].getClass, snd) } - case Terminated(`target`) ⇒ context stop self + //TODO use exact match of target when all actor references have uid, i.e. actorFor has been removed + case Terminated(a) if a.path == target.path ⇒ context stop self } } diff --git a/akka-docs/rst/general/addressing.rst b/akka-docs/rst/general/addressing.rst index dfd7ea3946..12d1d81417 100644 --- a/akka-docs/rst/general/addressing.rst +++ b/akka-docs/rst/general/addressing.rst @@ -91,6 +91,26 @@ followed by the concatenation of the path elements, from root guardian to the designated actor; the path elements are the names of the traversed actors and are separated by slashes. +What is the Difference Between Actor Reference and Path? +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +An actor reference designates a single actor and the life-cycle of the reference +matches that actor’s life-cycle; an actor path represents a name which may or +may not be inhabited by an actor and the path itself does not have a life-cycle, +it never becomes invalid. You can create an actor path without creating an actor, +but you cannot create an actor reference without creating corresponding actor. + +.. note:: + + That definition does not hold for ``actorFor``, which is one of the reasons why + ``actorFor`` is deprecated in favor of ``actorSelection``. + +You can create an actor, terminate it, and then create a new actor with the same +actor path. The newly created actor is a new incarnation of the actor. It is not +the same actor. An actor reference to the old incarnation is not valid for the new +incarnation. Messages sent to the old actor reference will not be delivered +to the new incarnation even though they have the same path. + Actor Path Anchors ^^^^^^^^^^^^^^^^^^ @@ -180,9 +200,9 @@ the whole lifetime of the actor. In the case of a local actor reference, the named actor needs to exist before the lookup, or else the acquired reference will be an :class:`EmptyLocalActorRef`. This will be true even if an actor with that exact path is created after acquiring the actor reference. For remote actor -references the behaviour is different and sending messages to such a reference -will under the hood look up the actor by path on the remote system for every -message send. +references acquired with `actorFor` the behaviour is different and sending messages +to such a reference will under the hood look up the actor by path on the remote +system for every message send. Absolute vs. Relative Paths ``````````````````````````` @@ -246,18 +266,39 @@ Summary: ``actorOf`` vs. ``actorFor`` - ``actorFor`` only ever looks up an existing actor, i.e. does not create one. +Actor Reference and Path Equality +--------------------------------- + +Equality of ``ActorRef`` match the intention that an ``ActorRef`` corresponds to +the target actor incarnation. Two actor references are compared equal when they have +the same path and point to the same actor incarnation. A reference pointing to a +terminated actor does not compare equal to a reference pointing to another (re-created) +actor with the same path. Note that a restart of an actor caused by a failure still +means that it is the same actor incarnation, i.e. a restart is not visible for the +consumer of the ``ActorRef``. + +Remote actor references acquired with ``actorFor`` do not include the full +information about the underlying actor identity and therefore such references +do not compare equal to references acquired with ``actorOf``, ``sender``, +or ``context.self``. Because of this ``actorFor`` is deprecated in favor of +``actorSelection``. + +If you need to keep track of actor references in a collection and do not care about +the exact actor incarnation you can use the ``ActorPath`` as key, because the identifier +of the target actor is not taken into account when comparing actor paths. + Reusing Actor Paths ------------------- -When an actor is terminated, its path will point to the dead letter mailbox, +When an actor is terminated, its reference will point to the dead letter mailbox, DeathWatch will publish its final transition and in general it is not expected to come back to life again (since the actor life cycle does not allow this). While it is possible to create an actor at a later time with an identical path—simply due to it being impossible to enforce the opposite without keeping the set of all actors ever created available—this is not good practice: remote -actor references which “died” suddenly start to work again, but without any -guarantee of ordering between this transition and any other event, hence the -new inhabitant of the path may receive messages which were destined for the +actor references acquired with ``actorFor`` which “died” suddenly start to work +again, but without any guarantee of ordering between this transition and any +other event, hence the new inhabitant of the path may receive messages which were destined for the previous tenant. It may be the right thing to do in very specific circumstances, but make sure diff --git a/akka-docs/rst/java/code/docs/serialization/SerializationDocTestBase.java b/akka-docs/rst/java/code/docs/serialization/SerializationDocTestBase.java index bd4f246d18..3eac0502d2 100644 --- a/akka-docs/rst/java/code/docs/serialization/SerializationDocTestBase.java +++ b/akka-docs/rst/java/code/docs/serialization/SerializationDocTestBase.java @@ -67,8 +67,8 @@ public class SerializationDocTestBase { // within a piece of code that sets it, // so either you need to supply your own, // or simply use the local path. - if (transportAddress == null) identifier = theActorRef.path().toString(); - else identifier = theActorRef.path().toStringWithAddress(transportAddress); + if (transportAddress == null) identifier = theActorRef.path().toSerializationFormat(); + else identifier = theActorRef.path().toSerializationFormatWithAddress(transportAddress); // Then just serialize the identifier however you like diff --git a/akka-docs/rst/java/serialization.rst b/akka-docs/rst/java/serialization.rst index 4668597c4f..7d72b6ef43 100644 --- a/akka-docs/rst/java/serialization.rst +++ b/akka-docs/rst/java/serialization.rst @@ -120,9 +120,17 @@ you might want to know how to serialize and deserialize them properly, here's th .. note:: - ``ActorPath.toStringWithAddress`` only differs from ``toString`` if the + ``ActorPath.toSerializationFormatWithAddress`` differs from ``toString`` if the address does not already have ``host`` and ``port`` components, i.e. it only inserts address information for local addresses. + + ``toSerializationFormatWithAddress`` also adds the unique id of the actor, which will + change when the actor is stopped and then created again with the same name. + Sending messages to a reference pointing the old actor will not be delivered + to the new actor. If you do not want this behavior, e.g. in case of long term + storage of the reference, you can use ``toStringWithAddress``, which does not + include the unique id. + This assumes that serialization happens in the context of sending a message through the remote transport. There are other uses of serialization, though, diff --git a/akka-docs/rst/project/migration-guide-2.1.x-2.2.x.rst b/akka-docs/rst/project/migration-guide-2.1.x-2.2.x.rst index f414095a79..fef4462db8 100644 --- a/akka-docs/rst/project/migration-guide-2.1.x-2.2.x.rst +++ b/akka-docs/rst/project/migration-guide-2.1.x-2.2.x.rst @@ -152,4 +152,44 @@ available via the ``inbound`` boolean field of the event. New configuration settings are also available, see the remoting documentation for more detail: :ref:`remoting-scala` +ActorRef equality and sending to remote actors +============================================== +Sending messages to an ``ActorRef`` must have the same semantics no matter if the target actor is located +on a remote host or in the same ``ActorSystem`` in the same JVM. This was not always the case. For example +when the target actor is terminated and created again under the same path. Sending to local references +of the previous incarnation of the actor will not be delivered to the new incarnation, but that was the case +for remote references. The reason was that the target actor was looked up by its path on every message +delivery and the path didn't distinguish between the two incarnations of the actor. This has been fixed, and +sending messages to remote references that points to a terminated actor will not be delivered to a new +actor with the same path. + +Equality of ``ActorRef`` has been changed to match the intention that an ``ActorRef`` corresponds to the target +actor instance. Two actor references are compared equal when they have the same path and point to the same +actor incarnation. A reference pointing to a terminated actor does not compare equal to a reference pointing +to another (re-created) actor with the same path. Note that a restart of an actor caused by a failure still +means that it's the same actor incarnation, i.e. a restart is not visible for the consumer of the ``ActorRef``. + +Equality in 2.1 was only based on the path of the ``ActorRef``. If you need to keep track of actor references +in a collection and do not care about the exact actor incarnation you can use the ``ActorPath`` as key, because +the identifier of the target actor is not taken into account when comparing actor paths. + +Remote actor references acquired with ``actorFor`` do not include the full information about the underlying actor +identity and therefore such references do not compare equal to references acquired with ``actorOf``, +``sender``, or ``context.self``. Because of this ``actorFor`` is deprecated, as explained in +:ref:`migration_2.2_actorSelection`. + +Note that when a parent actor is restarted its children are by default stopped and re-created, i.e. the child +after the restart will be a different incarnation than the child before the restart. This has always been the +case, but in some situations you might not have noticed, e.g. when comparing such actor references or sending +messages to remote deployed children of a restarted parent. + +This may also have implications if you compare the ``ActorRef`` received in a ``Terminated`` message +with an expected ``ActorRef``. + +.. _migration_2.2_actorSelection: + +Use ``actorSelection`` instead of ``actorFor`` +============================================== + +FIXME: ticket #3074 \ No newline at end of file diff --git a/akka-docs/rst/scala/code/docs/serialization/SerializationDocSpec.scala b/akka-docs/rst/scala/code/docs/serialization/SerializationDocSpec.scala index 33d7ea09b7..0dacdeff3b 100644 --- a/akka-docs/rst/scala/code/docs/serialization/SerializationDocSpec.scala +++ b/akka-docs/rst/scala/code/docs/serialization/SerializationDocSpec.scala @@ -171,8 +171,8 @@ package docs.serialization { // so either you need to supply your own, // or simply use the local path. val identifier: String = Serialization.currentTransportAddress.value match { - case null ⇒ theActorRef.path.toString - case address ⇒ theActorRef.path.toStringWithAddress(address) + case null ⇒ theActorRef.path.toSerializationFormat + case address ⇒ theActorRef.path.toSerializationFormatWithAddress(address) } // Then just serialize the identifier however you like @@ -192,7 +192,7 @@ package docs.serialization { } def serializeTo(ref: ActorRef, remote: Address): String = - ref.path.toStringWithAddress(ExternalAddress(theActorSystem).addressFor(remote)) + ref.path.toSerializationFormatWithAddress(ExternalAddress(theActorSystem).addressFor(remote)) //#external-address } @@ -207,7 +207,7 @@ package docs.serialization { } def serializeAkkaDefault(ref: ActorRef): String = - ref.path.toStringWithAddress(ExternalAddress(theActorSystem).addressForAkka) + ref.path.toSerializationFormatWithAddress(ExternalAddress(theActorSystem).addressForAkka) //#external-address-default } } diff --git a/akka-docs/rst/scala/serialization.rst b/akka-docs/rst/scala/serialization.rst index 70a02faecd..1e59226b7d 100644 --- a/akka-docs/rst/scala/serialization.rst +++ b/akka-docs/rst/scala/serialization.rst @@ -109,9 +109,16 @@ you might want to know how to serialize and deserialize them properly, here's th .. note:: - ``ActorPath.toStringWithAddress`` only differs from ``toString`` if the + ``ActorPath.toSerializationFormatWithAddress`` differs from ``toString`` if the address does not already have ``host`` and ``port`` components, i.e. it only - inserts address information for local addresses. + inserts address information for local addresses. + + ``toSerializationFormatWithAddress`` also adds the unique id of the actor, which will + change when the actor is stopped and then created again with the same name. + Sending messages to a reference pointing the old actor will not be delivered + to the new actor. If you don't want this behavior, e.g. in case of long term + storage of the reference, you can use ``toStringWithAddress``, which doesn't + include the unique id. This assumes that serialization happens in the context of sending a message through the remote transport. There are other uses of serialization, though, diff --git a/akka-remote/src/main/scala/akka/remote/RemoteActorRefProvider.scala b/akka-remote/src/main/scala/akka/remote/RemoteActorRefProvider.scala index 2066aacc58..c23e3c66b2 100644 --- a/akka-remote/src/main/scala/akka/remote/RemoteActorRefProvider.scala +++ b/akka-remote/src/main/scala/akka/remote/RemoteActorRefProvider.scala @@ -241,7 +241,8 @@ private[akka] class RemoteActorRefProvider( } else { try { val localAddress = transport.localAddressForRemote(addr) - val rpath = RootActorPath(addr) / "remote" / localAddress.protocol / localAddress.hostPort / path.elements + val rpath = (RootActorPath(addr) / "remote" / localAddress.protocol / localAddress.hostPort / path.elements). + withUid(path.uid) new RemoteActorRef(transport, localAddress, rpath, supervisor, Some(props), Some(d)) } catch { case NonFatal(e) ⇒ @@ -280,15 +281,19 @@ private[akka] class RemoteActorRefProvider( * Called in deserialization of incoming remote messages. In this case the correct local address is known, therefore * this method is faster than the actorFor above. */ - def actorForWithLocalAddress(ref: InternalActorRef, path: String, localAddress: Address): InternalActorRef = path match { - case ActorPathExtractor(address, elems) ⇒ - if (hasAddress(address)) actorFor(rootGuardian, elems) - else new RemoteActorRef(transport, localAddress, - new RootActorPath(address) / elems, Nobody, props = None, deploy = None) - case _ ⇒ local.actorFor(ref, path) + def actorForWithLocalAddress(ref: InternalActorRef, path: String, localAddress: Address): InternalActorRef = { + path match { + case ActorPathExtractor(address, elems) ⇒ + if (hasAddress(address)) actorFor(rootGuardian, elems) + else new RemoteActorRef(transport, localAddress, + new RootActorPath(address) / elems, Nobody, props = None, deploy = None) + case _ ⇒ + local.actorFor(ref, path) + } } - def actorFor(ref: InternalActorRef, path: Iterable[String]): InternalActorRef = local.actorFor(ref, path) + def actorFor(ref: InternalActorRef, path: Iterable[String]): InternalActorRef = + local.actorFor(ref, path) /** * Using (checking out) actor on a specific node. @@ -297,7 +302,7 @@ private[akka] class RemoteActorRefProvider( log.debug("[{}] Instantiating Remote Actor [{}]", rootPath, path) // we don’t wait for the ACK, because the remote end will process this command before any other message to the new actor - actorFor(RootActorPath(path.address) / "remote") ! DaemonMsgCreate(props, deploy, path.toString, supervisor) + actorFor(RootActorPath(path.address) / "remote") ! DaemonMsgCreate(props, deploy, path.toSerializationFormat, supervisor) } def getExternalAddressFor(addr: Address): Option[Address] = { diff --git a/akka-remote/src/main/scala/akka/remote/RemoteDaemon.scala b/akka-remote/src/main/scala/akka/remote/RemoteDaemon.scala index 45160e718f..6f8b994d54 100644 --- a/akka-remote/src/main/scala/akka/remote/RemoteDaemon.scala +++ b/akka-remote/src/main/scala/akka/remote/RemoteDaemon.scala @@ -12,6 +12,7 @@ import akka.dispatch.Watch import akka.actor.ActorRefWithCell import akka.actor.ActorRefScope import akka.util.Switch +import akka.actor.RootActorPath /** * INTERNAL API @@ -54,11 +55,14 @@ private[akka] class RemoteSystemDaemon( @tailrec def rec(s: String, n: Int): (InternalActorRef, Int) = { - getChild(s) match { + import akka.actor.ActorCell._ + val (childName, uid) = splitNameAndUid(s) + getChild(childName) match { case null ⇒ val last = s.lastIndexOf('/') if (last == -1) (Nobody, n) else rec(s.substring(0, last), n + 1) + case ref if uid != undefinedUid && uid != ref.path.uid ⇒ (Nobody, n) case ref ⇒ (ref, n) } } @@ -82,15 +86,21 @@ private[akka] class RemoteSystemDaemon( // TODO RK currently the extracted “address” is just ignored, is that okay? // TODO RK canonicalize path so as not to duplicate it always #1446 val subpath = elems.drop(1) - val path = this.path / subpath + val p = this.path / subpath + val childName = { + val s = subpath.mkString("/") + val i = s.indexOf('#') + if (i < 0) s + else s.substring(0, i) + } val isTerminating = !terminating.whileOff { val actor = system.provider.actorOf(system, props, supervisor.asInstanceOf[InternalActorRef], - path, systemService = false, Some(deploy), lookupDeploy = true, async = false) - addChild(subpath.mkString("/"), actor) + p, systemService = false, Some(deploy), lookupDeploy = true, async = false) + addChild(childName, actor) actor.sendSystemMessage(Watch(actor, this)) actor.start() } - if (isTerminating) log.error("Skipping [{}] to RemoteSystemDaemon on [{}] while terminating", message, path.address) + if (isTerminating) log.error("Skipping [{}] to RemoteSystemDaemon on [{}] while terminating", message, p.address) case _ ⇒ log.debug("remote path does not match path from message [{}]", message) } diff --git a/akka-remote/src/main/scala/akka/remote/serialization/ProtobufSerializer.scala b/akka-remote/src/main/scala/akka/remote/serialization/ProtobufSerializer.scala index 42f2978db6..056439c23e 100644 --- a/akka-remote/src/main/scala/akka/remote/serialization/ProtobufSerializer.scala +++ b/akka-remote/src/main/scala/akka/remote/serialization/ProtobufSerializer.scala @@ -19,8 +19,8 @@ object ProtobufSerializer { */ def serializeActorRef(ref: ActorRef): ActorRefProtocol = { val identifier: String = Serialization.currentTransportAddress.value match { - case null ⇒ ref.path.toString - case address ⇒ ref.path.toStringWithAddress(address) + case null ⇒ ref.path.toSerializationFormat + case address ⇒ ref.path.toSerializationFormatWithAddress(address) } ActorRefProtocol.newBuilder.setPath(identifier).build } diff --git a/akka-remote/src/main/scala/akka/remote/transport/AkkaPduCodec.scala b/akka-remote/src/main/scala/akka/remote/transport/AkkaPduCodec.scala index a64d011404..58bcea77d8 100644 --- a/akka-remote/src/main/scala/akka/remote/transport/AkkaPduCodec.scala +++ b/akka-remote/src/main/scala/akka/remote/transport/AkkaPduCodec.scala @@ -182,7 +182,7 @@ private[remote] object AkkaPduProtobufCodec extends AkkaPduCodec { private def serializeActorRef(defaultAddress: Address, ref: ActorRef): ActorRefProtocol = { ActorRefProtocol.newBuilder.setPath( - if (ref.path.address.host.isDefined) ref.path.toString else ref.path.toStringWithAddress(defaultAddress)).build() + if (ref.path.address.host.isDefined) ref.path.toSerializationFormat else ref.path.toSerializationFormatWithAddress(defaultAddress)).build() } private def serializeAddress(address: Address): Option[AddressProtocol] = { diff --git a/akka-remote/src/test/scala/akka/remote/RemotingSpec.scala b/akka-remote/src/test/scala/akka/remote/RemotingSpec.scala index 013330f2b2..98b4cd455c 100644 --- a/akka-remote/src/test/scala/akka/remote/RemotingSpec.scala +++ b/akka-remote/src/test/scala/akka/remote/RemotingSpec.scala @@ -119,14 +119,14 @@ class RemotingSpec extends AkkaSpec(RemotingSpec.cfg) with ImplicitSender with D test.local-address = "test://remote-sys@localhost:12346" } """).withFallback(system.settings.config).resolve() - val other = ActorSystem("remote-sys", conf) + val otherSystem = ActorSystem("remote-sys", conf) for ( (name, proto) ← Seq( "/gonk" -> "tcp", "/zagzag" -> "udp", "/roghtaar" -> "ssl.tcp") - ) deploy(system, Deploy(name, scope = RemoteScope(addr(other, proto)))) + ) deploy(system, Deploy(name, scope = RemoteScope(addr(otherSystem, proto)))) def addr(sys: ActorSystem, proto: String) = sys.asInstanceOf[ExtendedActorSystem].provider.getExternalAddressFor(Address(s"akka.$proto", "", "", 0)).get @@ -135,12 +135,12 @@ class RemotingSpec extends AkkaSpec(RemotingSpec.cfg) with ImplicitSender with D sys.asInstanceOf[ExtendedActorSystem].provider.asInstanceOf[RemoteActorRefProvider].deployer.deploy(d) } - val remote = other.actorOf(Props[Echo2], "echo") + val remote = otherSystem.actorOf(Props[Echo2], "echo") val here = system.actorFor("akka.test://remote-sys@localhost:12346/user/echo") override def afterTermination() { - other.shutdown() + otherSystem.shutdown() AssociationRegistry.clear() } @@ -168,16 +168,16 @@ class RemotingSpec extends AkkaSpec(RemotingSpec.cfg) with ImplicitSender with D "send dead letters on remote if actor does not exist" in { EventFilter.warning(pattern = "dead.*buh", occurrences = 1).intercept { system.actorFor("akka.test://remote-sys@localhost:12346/does/not/exist") ! "buh" - }(other) + }(otherSystem) } "not be exhausted by sending to broken connections" in { val tcpOnlyConfig = ConfigFactory.parseString("""akka.remote.enabled-transports = ["akka.remote.netty.tcp"]"""). - withFallback(other.settings.config) - val moreSystems = Vector.fill(5)(ActorSystem(other.name, tcpOnlyConfig)) + withFallback(otherSystem.settings.config) + val moreSystems = Vector.fill(5)(ActorSystem(otherSystem.name, tcpOnlyConfig)) moreSystems foreach (_.actorOf(Props[Echo2], name = "echo")) val moreRefs = moreSystems map (sys ⇒ system.actorFor(RootActorPath(addr(sys, "tcp")) / "user" / "echo")) - val aliveEcho = system.actorFor(RootActorPath(addr(other, "tcp")) / "user" / "echo") + val aliveEcho = system.actorFor(RootActorPath(addr(otherSystem, "tcp")) / "user" / "echo") val n = 100 // first everything is up and running @@ -223,6 +223,30 @@ class RemotingSpec extends AkkaSpec(RemotingSpec.cfg) with ImplicitSender with D expectMsg("postStop") } + "not send to remote re-created actor with same name" in { + val echo = otherSystem.actorOf(Props[Echo1], "otherEcho1") + echo ! 71 + expectMsg(71) + echo ! PoisonPill + expectMsg("postStop") + echo ! 72 + expectNoMsg(1.second) + + val echo2 = otherSystem.actorOf(Props[Echo1], "otherEcho1") + echo2 ! 73 + expectMsg(73) + // msg to old ActorRef (different uid) should not get through + echo2.path.uid must not be (echo.path.uid) + echo ! 74 + expectNoMsg(1.second) + + otherSystem.actorFor("/user/otherEcho1") ! 75 + expectMsg(75) + + system.actorFor("akka.test://remote-sys@localhost:12346/user/otherEcho1") ! 76 + expectMsg(76) + } + "look-up actors across node boundaries" in { val l = system.actorOf(Props(new Actor { def receive = { @@ -230,20 +254,41 @@ class RemotingSpec extends AkkaSpec(RemotingSpec.cfg) with ImplicitSender with D case s: String ⇒ sender ! context.actorFor(s) } }), "looker") + // child is configured to be deployed on remote-sys (otherSystem) l ! (Props[Echo1], "child") - val r = expectMsgType[ActorRef] - r ! (Props[Echo1], "grandchild") - val remref = expectMsgType[ActorRef] - remref.asInstanceOf[ActorRefScope].isLocal must be(true) + val child = expectMsgType[ActorRef] + // grandchild is configured to be deployed on RemotingSpec (system) + child ! (Props[Echo1], "grandchild") + val grandchild = expectMsgType[ActorRef] + grandchild.asInstanceOf[ActorRefScope].isLocal must be(true) + grandchild ! 43 + expectMsg(43) val myref = system.actorFor(system / "looker" / "child" / "grandchild") myref.isInstanceOf[RemoteActorRef] must be(true) - myref ! 43 - expectMsg(43) - lastSender must be theSameInstanceAs remref - r.asInstanceOf[RemoteActorRef].getParent must be(l) - system.actorFor("/user/looker/child") must be theSameInstanceAs r + myref ! 44 + expectMsg(44) + lastSender must be(grandchild) + lastSender must be theSameInstanceAs grandchild + child.asInstanceOf[RemoteActorRef].getParent must be(l) + system.actorFor("/user/looker/child") must be theSameInstanceAs child Await.result(l ? "child/..", timeout.duration).asInstanceOf[AnyRef] must be theSameInstanceAs l Await.result(system.actorFor(system / "looker" / "child") ? "..", timeout.duration).asInstanceOf[AnyRef] must be theSameInstanceAs l + + watch(child) + child ! PoisonPill + expectMsg("postStop") + expectMsgType[Terminated].actor must be === child + l ! (Props[Echo1], "child") + val child2 = expectMsgType[ActorRef] + child2 ! 45 + expectMsg(45) + // msg to old ActorRef (different uid) should not get through + child2.path.uid must not be (child.path.uid) + child ! 46 + expectNoMsg(1.second) + system.actorFor(system / "looker" / "child") ! 47 + expectMsg(47) + } "not fail ask across node boundaries" in { @@ -255,7 +300,7 @@ class RemotingSpec extends AkkaSpec(RemotingSpec.cfg) with ImplicitSender with D "be able to use multiple transports and use the appropriate one (TCP)" in { val r = system.actorOf(Props[Echo1], "gonk") r.path.toString must be === - s"akka.tcp://remote-sys@localhost:${port(other, "tcp")}/remote/akka.tcp/RemotingSpec@localhost:${port(system, "tcp")}/user/gonk" + s"akka.tcp://remote-sys@localhost:${port(otherSystem, "tcp")}/remote/akka.tcp/RemotingSpec@localhost:${port(system, "tcp")}/user/gonk" r ! 42 expectMsg(42) EventFilter[Exception]("crash", occurrences = 1).intercept { @@ -271,7 +316,7 @@ class RemotingSpec extends AkkaSpec(RemotingSpec.cfg) with ImplicitSender with D "be able to use multiple transports and use the appropriate one (UDP)" in { val r = system.actorOf(Props[Echo1], "zagzag") r.path.toString must be === - s"akka.udp://remote-sys@localhost:${port(other, "udp")}/remote/akka.udp/RemotingSpec@localhost:${port(system, "udp")}/user/zagzag" + s"akka.udp://remote-sys@localhost:${port(otherSystem, "udp")}/remote/akka.udp/RemotingSpec@localhost:${port(system, "udp")}/user/zagzag" r ! 42 expectMsg(10.seconds, 42) EventFilter[Exception]("crash", occurrences = 1).intercept { @@ -287,7 +332,7 @@ class RemotingSpec extends AkkaSpec(RemotingSpec.cfg) with ImplicitSender with D "be able to use multiple transports and use the appropriate one (SSL)" in { val r = system.actorOf(Props[Echo1], "roghtaar") r.path.toString must be === - s"akka.ssl.tcp://remote-sys@localhost:${port(other, "ssl.tcp")}/remote/akka.ssl.tcp/RemotingSpec@localhost:${port(system, "ssl.tcp")}/user/roghtaar" + s"akka.ssl.tcp://remote-sys@localhost:${port(otherSystem, "ssl.tcp")}/remote/akka.ssl.tcp/RemotingSpec@localhost:${port(system, "ssl.tcp")}/user/roghtaar" r ! 42 expectMsg(10.seconds, 42) EventFilter[Exception]("crash", occurrences = 1).intercept { @@ -305,7 +350,7 @@ class RemotingSpec extends AkkaSpec(RemotingSpec.cfg) with ImplicitSender with D override def beforeTermination() { system.eventStream.publish(TestEvent.Mute( EventFilter.warning(pattern = "received dead letter.*(InboundPayload|Disassociate)"))) - other.eventStream.publish(TestEvent.Mute( + otherSystem.eventStream.publish(TestEvent.Mute( EventFilter[EndpointException](), EventFilter.error(start = "AssociationError"), EventFilter.warning(pattern = "received dead letter.*(InboundPayload|Disassociate|HandleListener)"))) From f8c3717ca1e2ba47b157b7db8aae27a8b3a32416 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Endre=20S=C3=A1ndor=20Varga?= Date: Tue, 5 Mar 2013 16:19:54 +0100 Subject: [PATCH 36/47] Changed Failed to be a SystemMessage - Moved system messages to their own package. - All queueing operations are now hidden behind a SystemMessageList value class - Introduced dual SystemMessageList types to encode the ordering in the type. - Protects against accidentally missed reverse calls or accidentally reversed lists - Makes ordering expectations by fields/parameters explicit - Fixed serialization tests - Fixes to logging in HierarchyStressSpec --- .../scala/akka/actor/DeathWatchSpec.scala | 12 +- .../akka/actor/SupervisorHierarchySpec.scala | 10 +- .../akka/actor/dispatch/ActorModelSpec.scala | 4 +- .../sysmsg/SystemMessageListSpec.scala | 116 ++++++++ .../akka/serialization/SerializeSpec.scala | 25 +- .../akka/util/internal/HashedWheelTimer.java | 2 +- .../src/main/scala/akka/actor/Actor.scala | 6 - .../src/main/scala/akka/actor/ActorCell.scala | 110 +++++--- .../src/main/scala/akka/actor/ActorRef.scala | 1 + .../scala/akka/actor/ActorRefProvider.scala | 17 +- .../main/scala/akka/actor/ActorSystem.scala | 3 +- .../akka/actor/RepointableActorRef.scala | 1 + .../actor/dungeon/ChildrenContainer.scala | 8 +- .../scala/akka/actor/dungeon/DeathWatch.scala | 4 +- .../scala/akka/actor/dungeon/Dispatch.scala | 5 +- .../akka/actor/dungeon/FaultHandling.scala | 60 ++-- .../akka/dispatch/AbstractDispatcher.scala | 90 +----- .../akka/dispatch/BalancingDispatcher.scala | 11 +- .../main/scala/akka/dispatch/Dispatcher.scala | 1 + .../main/scala/akka/dispatch/Mailbox.scala | 79 +++--- .../akka/dispatch/sysmsg/SystemMessage.scala | 257 ++++++++++++++++++ .../main/scala/akka/pattern/AskSupport.scala | 2 +- .../akka/pattern/GracefulStopSupport.scala | 2 +- .../cluster/ClusterActorRefProvider.scala | 2 +- .../general/message-delivery-guarantees.rst | 18 ++ akka-docs/rst/general/supervision.rst | 9 + akka-docs/rst/java/untyped-actors.rst | 8 + akka-docs/rst/scala/actors.rst | 7 + .../src/main/scala/akka/remote/Endpoint.scala | 2 +- .../akka/remote/RemoteActorRefProvider.scala | 2 +- .../main/scala/akka/remote/RemoteDaemon.scala | 2 +- .../scala/akka/remote/RemoteTransport.scala | 5 +- .../testkit/CallingThreadDispatcher.scala | 3 +- .../akka/testkit/TestEventListener.scala | 2 +- 34 files changed, 636 insertions(+), 250 deletions(-) create mode 100644 akka-actor-tests/src/test/scala/akka/dispatch/sysmsg/SystemMessageListSpec.scala create mode 100644 akka-actor/src/main/scala/akka/dispatch/sysmsg/SystemMessage.scala diff --git a/akka-actor-tests/src/test/scala/akka/actor/DeathWatchSpec.scala b/akka-actor-tests/src/test/scala/akka/actor/DeathWatchSpec.scala index 54d30d4b65..5fa63fc0f8 100644 --- a/akka-actor-tests/src/test/scala/akka/actor/DeathWatchSpec.scala +++ b/akka-actor-tests/src/test/scala/akka/actor/DeathWatchSpec.scala @@ -5,11 +5,11 @@ package akka.actor import language.postfixOps +import akka.dispatch.sysmsg.Failed +import akka.pattern.ask import akka.testkit._ import scala.concurrent.duration._ -import java.util.concurrent.atomic._ import scala.concurrent.Await -import akka.pattern.ask @org.junit.runner.RunWith(classOf[org.scalatest.junit.JUnitRunner]) class LocalDeathWatchSpec extends AkkaSpec with ImplicitSender with DefaultTimeout with DeathWatchSpec @@ -129,7 +129,7 @@ trait DeathWatchSpec { this: AkkaSpec with ImplicitSender with DefaultTimeout case class FF(fail: Failed) val strategy = new OneForOneStrategy(maxNrOfRetries = 0)(SupervisorStrategy.makeDecider(List(classOf[Exception]))) { override def handleFailure(context: ActorContext, child: ActorRef, cause: Throwable, stats: ChildRestartStats, children: Iterable[ChildRestartStats]) = { - testActor.tell(FF(Failed(cause, 0)), child) + testActor.tell(FF(Failed(child, cause, 0)), child) super.handleFailure(context, child, cause, stats, children) } } @@ -145,9 +145,9 @@ trait DeathWatchSpec { this: AkkaSpec with ImplicitSender with DefaultTimeout failed ! Kill val result = receiveWhile(3 seconds, messages = 3) { - case FF(Failed(_: ActorKilledException, _)) if lastSender eq failed ⇒ 1 - case FF(Failed(DeathPactException(`failed`), _)) if lastSender eq brother ⇒ 2 - case WrappedTerminated(Terminated(`brother`)) ⇒ 3 + case FF(Failed(_, _: ActorKilledException, _)) if lastSender eq failed ⇒ 1 + case FF(Failed(_, DeathPactException(`failed`), _)) if lastSender eq brother ⇒ 2 + case WrappedTerminated(Terminated(`brother`)) ⇒ 3 } testActor.isTerminated must not be true result must be(Seq(1, 2, 3)) diff --git a/akka-actor-tests/src/test/scala/akka/actor/SupervisorHierarchySpec.scala b/akka-actor-tests/src/test/scala/akka/actor/SupervisorHierarchySpec.scala index 8e91abb570..b94936ce84 100644 --- a/akka-actor-tests/src/test/scala/akka/actor/SupervisorHierarchySpec.scala +++ b/akka-actor-tests/src/test/scala/akka/actor/SupervisorHierarchySpec.scala @@ -242,6 +242,8 @@ object SupervisorHierarchySpec { override def postStop { if (failed || suspended) { listener ! ErrorLog("not resumed (" + failed + ", " + suspended + ")", log) + val state = stateCache.get(self) + stateCache.put(self.path, state.copy(log = log)) } else { stateCache.put(self.path, HierarchyState(log, Map(), null)) } @@ -249,7 +251,7 @@ object SupervisorHierarchySpec { def check(msg: Any): Boolean = { suspended = false - log :+= Event(msg, identityHashCode(this)) + log :+= Event(msg, identityHashCode(Hierarchy.this)) if (failed) { abort("processing message while failed") failed = false @@ -287,13 +289,15 @@ object SupervisorHierarchySpec { val props = Props(new Hierarchy(kids, breadth, listener, myLevel + 1)).withDispatcher("hierarchy") context.watch(context.actorOf(props, name)) } else { - log :+= Event(sender + " terminated while pongOfDeath", identityHashCode(this)) + // WARNING: The Terminated that is logged by this is logged by check() above, too. It is not + // an indication of duplicate Terminate messages + log :+= Event(sender + " terminated while pongOfDeath", identityHashCode(Hierarchy.this)) } case Abort ⇒ abort("terminating") case PingOfDeath ⇒ if (size > 1) { pongsToGo = context.children.size - log :+= Event("sending " + pongsToGo + " pingOfDeath", identityHashCode(this)) + log :+= Event("sending " + pongsToGo + " pingOfDeath", identityHashCode(Hierarchy.this)) context.children foreach (_ ! PingOfDeath) } else { context stop self diff --git a/akka-actor-tests/src/test/scala/akka/actor/dispatch/ActorModelSpec.scala b/akka-actor-tests/src/test/scala/akka/actor/dispatch/ActorModelSpec.scala index d2cc572a69..fcaf47d49a 100644 --- a/akka-actor-tests/src/test/scala/akka/actor/dispatch/ActorModelSpec.scala +++ b/akka-actor-tests/src/test/scala/akka/actor/dispatch/ActorModelSpec.scala @@ -16,6 +16,7 @@ import org.scalatest.junit.JUnitRunner import com.typesafe.config.Config import akka.actor._ +import akka.dispatch.sysmsg._ import akka.dispatch._ import akka.event.Logging.Error import akka.pattern.ask @@ -390,7 +391,8 @@ abstract class ActorModelSpec(config: String) extends AkkaSpec(config) with Defa def compare(l: AnyRef, r: AnyRef) = (l, r) match { case (ll: ActorCell, rr: ActorCell) ⇒ ll.self.path compareTo rr.self.path } } foreach { case cell: ActorCell ⇒ - System.err.println(" - " + cell.self.path + " " + cell.isTerminated + " " + cell.mailbox.status + " " + cell.mailbox.numberOfMessages + " " + SystemMessage.size(cell.mailbox.systemDrain(null))) + System.err.println(" - " + cell.self.path + " " + cell.isTerminated + " " + cell.mailbox.status + " " + + cell.mailbox.numberOfMessages + " " + cell.mailbox.systemDrain(SystemMessageList.LNil).size) } System.err.println("Mailbox: " + mq.numberOfMessages + " " + mq.hasMessages) diff --git a/akka-actor-tests/src/test/scala/akka/dispatch/sysmsg/SystemMessageListSpec.scala b/akka-actor-tests/src/test/scala/akka/dispatch/sysmsg/SystemMessageListSpec.scala new file mode 100644 index 0000000000..a7059cb748 --- /dev/null +++ b/akka-actor-tests/src/test/scala/akka/dispatch/sysmsg/SystemMessageListSpec.scala @@ -0,0 +1,116 @@ +/** + * Copyright (C) 2009-2013 Typesafe Inc. + */ +package akka.dispatch.sysmsg + +import akka.testkit.AkkaSpec + +class SystemMessageListSpec extends AkkaSpec { + import SystemMessageList.LNil + import SystemMessageList.ENil + + "The SystemMessageList value class" must { + + "handle empty lists correctly" in { + LNil.head must be === null + LNil.isEmpty must be(true) + (LNil.reverse == ENil) must be(true) + } + + "able to append messages" in { + val create0 = Create(0) + val create1 = Create(1) + val create2 = Create(2) + ((create0 :: LNil).head eq create0) must be(true) + ((create1 :: create0 :: LNil).head eq create1) must be(true) + ((create2 :: create1 :: create0 :: LNil).head eq create2) must be(true) + + (create2.next eq create1) must be(true) + (create1.next eq create0) must be(true) + (create0.next eq null) must be(true) + } + + "able to deconstruct head and tail" in { + val create0 = Create(0) + val create1 = Create(1) + val create2 = Create(2) + val list = create2 :: create1 :: create0 :: LNil + + (list.head eq create2) must be(true) + (list.tail.head eq create1) must be(true) + (list.tail.tail.head eq create0) must be(true) + (list.tail.tail.tail.head eq null) must be(true) + } + + "properly report size and emptyness" in { + val create0 = Create(0) + val create1 = Create(1) + val create2 = Create(2) + val list = create2 :: create1 :: create0 :: LNil + + list.size must be === 3 + list.isEmpty must be(false) + + list.tail.size must be === 2 + list.tail.isEmpty must be(false) + + list.tail.tail.size must be === 1 + list.tail.tail.isEmpty must be(false) + + list.tail.tail.tail.size must be === 0 + list.tail.tail.tail.isEmpty must be(true) + + } + + "properly reverse contents" in { + val create0 = Create(0) + val create1 = Create(1) + val create2 = Create(2) + val list = create2 :: create1 :: create0 :: LNil + val listRev: EarliestFirstSystemMessageList = list.reverse + + listRev.isEmpty must be(false) + listRev.size must be === 3 + + (listRev.head eq create0) must be(true) + (listRev.tail.head eq create1) must be(true) + (listRev.tail.tail.head eq create2) must be(true) + (listRev.tail.tail.tail.head eq null) must be(true) + + (create0.next eq create1) must be(true) + (create1.next eq create2) must be(true) + (create2.next eq null) must be(true) + } + + } + + "EarliestFirstSystemMessageList" must { + + "properly prepend reversed message lists to the front" in { + val create0 = Create(0) + val create1 = Create(1) + val create2 = Create(2) + val create3 = Create(3) + val create4 = Create(4) + val create5 = Create(5) + + val fwdList = create3 :: create4 :: create5 :: ENil + val revList = create2 :: create1 :: create0 :: LNil + + val list = revList reverse_::: fwdList + + (list.head eq create0) must be(true) + (list.tail.head eq create1) must be(true) + (list.tail.tail.head eq create2) must be(true) + (list.tail.tail.tail.head eq create3) must be(true) + (list.tail.tail.tail.tail.head eq create4) must be(true) + (list.tail.tail.tail.tail.tail.head eq create5) must be(true) + (list.tail.tail.tail.tail.tail.tail.head eq null) must be(true) + + (LNil reverse_::: ENil) == ENil must be(true) + ((create0 :: LNil reverse_::: ENil).head eq create0) must be(true) + ((LNil reverse_::: create0 :: ENil).head eq create0) must be(true) + } + + } +} diff --git a/akka-actor-tests/src/test/scala/akka/serialization/SerializeSpec.scala b/akka-actor-tests/src/test/scala/akka/serialization/SerializeSpec.scala index 14076463f5..26c551e8be 100644 --- a/akka-actor-tests/src/test/scala/akka/serialization/SerializeSpec.scala +++ b/akka-actor-tests/src/test/scala/akka/serialization/SerializeSpec.scala @@ -8,7 +8,7 @@ import language.postfixOps import akka.testkit.{ AkkaSpec, EventFilter } import akka.actor._ -import akka.dispatch._ +import akka.dispatch.sysmsg._ import java.io._ import scala.concurrent.Await import akka.util.Timeout @@ -109,7 +109,7 @@ object SerializationTests { } serialization-bindings { - "akka.dispatch.SystemMessage" = test + "akka.dispatch.sysmsg.SystemMessage" = test } } } @@ -125,6 +125,7 @@ object SerializationTests { classOf[ChildTerminated], classOf[Watch], classOf[Unwatch], + classOf[Failed], NoMessage.getClass) } @@ -335,31 +336,35 @@ class SerializationCompatibilitySpec extends AkkaSpec(SerializationTests.mostlyR verify(Create(), "aced00057372000c7363616c612e5475706c6532bc7daadf46211a990200024c00025f317400124c6a6176612f6c616e672f4f626a6563743b4c00025f3271007e0001787073720014616b6b612e64697370617463682e437265617465000000000000000302000078707671007e0003") } "be preserved for the Recreate SystemMessage" in { - verify(Recreate(null), "aced00057372000c7363616c612e5475706c6532bc7daadf46211a990200024c00025f317400124c6a6176612f6c616e672f4f626a6563743b4c00025f3271007e0001787073720016616b6b612e64697370617463682e52656372656174650987c65c8d378a800200014c000563617573657400154c6a6176612f6c616e672f5468726f7761626c653b7870707671007e0003") + verify(Recreate(null), "aced00057372000c7363616c612e5475706c6532bc7daadf46211a990200024c00025f317400124c6a6176612f6c616e672f4f626a6563743b4c00025f3271007e000178707372001d616b6b612e64697370617463682e7379736d73672e52656372656174650987c65c8d378a800200014c000563617573657400154c6a6176612f6c616e672f5468726f7761626c653b7870707671007e0003") } "be preserved for the Suspend SystemMessage" in { - verify(Suspend(), "aced00057372000c7363616c612e5475706c6532bc7daadf46211a990200024c00025f317400124c6a6176612f6c616e672f4f626a6563743b4c00025f3271007e0001787073720015616b6b612e64697370617463682e53757370656e6464e531d5d134b59902000078707671007e0003") + verify(Suspend(), "aced00057372000c7363616c612e5475706c6532bc7daadf46211a990200024c00025f317400124c6a6176612f6c616e672f4f626a6563743b4c00025f3271007e000178707372001c616b6b612e64697370617463682e7379736d73672e53757370656e6464e531d5d134b59902000078707671007e0003") } "be preserved for the Resume SystemMessage" in { - verify(Resume(null), "aced00057372000c7363616c612e5475706c6532bc7daadf46211a990200024c00025f317400124c6a6176612f6c616e672f4f626a6563743b4c00025f3271007e0001787073720014616b6b612e64697370617463682e526573756d65dc5e646d445fcb010200014c000f63617573656442794661696c7572657400154c6a6176612f6c616e672f5468726f7761626c653b7870707671007e0003") + verify(Resume(null), "aced00057372000c7363616c612e5475706c6532bc7daadf46211a990200024c00025f317400124c6a6176612f6c616e672f4f626a6563743b4c00025f3271007e000178707372001b616b6b612e64697370617463682e7379736d73672e526573756d65dc5e646d445fcb010200014c000f63617573656442794661696c7572657400154c6a6176612f6c616e672f5468726f7761626c653b7870707671007e0003") } "be preserved for the Terminate SystemMessage" in { - verify(Terminate(), "aced00057372000c7363616c612e5475706c6532bc7daadf46211a990200024c00025f317400124c6a6176612f6c616e672f4f626a6563743b4c00025f3271007e0001787073720017616b6b612e64697370617463682e5465726d696e61746509d66ca68318700f02000078707671007e0003") + verify(Terminate(), "aced00057372000c7363616c612e5475706c6532bc7daadf46211a990200024c00025f317400124c6a6176612f6c616e672f4f626a6563743b4c00025f3271007e000178707372001e616b6b612e64697370617463682e7379736d73672e5465726d696e61746509d66ca68318700f02000078707671007e0003") } "be preserved for the Supervise SystemMessage" in { verify(Supervise(FakeActorRef("child"), true), "aced00057372000c7363616c612e5475706c6532bc7daadf46211a990200024c00025f317400124c6a6176612f6c616e672f4f626a6563743b4c00025f3271007e0001787073720017616b6b612e64697370617463682e53757065727669736500000000000000030200025a00056173796e634c00056368696c647400154c616b6b612f6163746f722f4163746f725265663b7870017372001f616b6b612e73657269616c697a6174696f6e2e46616b654163746f7252656600000000000000010200014c00046e616d657400124c6a6176612f6c616e672f537472696e673b7872001b616b6b612e6163746f722e496e7465726e616c4163746f725265660d0aa2ca1e82097602000078720013616b6b612e6163746f722e4163746f72526566c3585dde655f469402000078707400056368696c647671007e0003") } "be preserved for the ChildTerminated SystemMessage" in { - verify(ChildTerminated(FakeActorRef("child")), "aced00057372000c7363616c612e5475706c6532bc7daadf46211a990200024c00025f317400124c6a6176612f6c616e672f4f626a6563743b4c00025f3271007e000178707372001d616b6b612e64697370617463682e4368696c645465726d696e617465644c84222437ed5db40200014c00056368696c647400154c616b6b612f6163746f722f4163746f725265663b78707372001f616b6b612e73657269616c697a6174696f6e2e46616b654163746f7252656600000000000000010200014c00046e616d657400124c6a6176612f6c616e672f537472696e673b7872001b616b6b612e6163746f722e496e7465726e616c4163746f725265660d0aa2ca1e82097602000078720013616b6b612e6163746f722e4163746f72526566c3585dde655f469402000078707400056368696c647671007e0003") + verify(ChildTerminated(FakeActorRef("child")), "aced00057372000c7363616c612e5475706c6532bc7daadf46211a990200024c00025f317400124c6a6176612f6c616e672f4f626a6563743b4c00025f3271007e0001787073720024616b6b612e64697370617463682e7379736d73672e4368696c645465726d696e617465644c84222437ed5db40200014c00056368696c647400154c616b6b612f6163746f722f4163746f725265663b78707372001f616b6b612e73657269616c697a6174696f6e2e46616b654163746f7252656600000000000000010200014c00046e616d657400124c6a6176612f6c616e672f537472696e673b7872001b616b6b612e6163746f722e496e7465726e616c4163746f72526566db6eaed9e69a356302000078720013616b6b612e6163746f722e4163746f72526566c3585dde655f469402000078707400056368696c647671007e0003") } "be preserved for the Watch SystemMessage" in { - verify(Watch(FakeActorRef("watchee"), FakeActorRef("watcher")), "aced00057372000c7363616c612e5475706c6532bc7daadf46211a990200024c00025f317400124c6a6176612f6c616e672f4f626a6563743b4c00025f3271007e0001787073720013616b6b612e64697370617463682e57617463682e1e65bc74394fc40200024c0007776174636865657400154c616b6b612f6163746f722f4163746f725265663b4c00077761746368657271007e000478707372001f616b6b612e73657269616c697a6174696f6e2e46616b654163746f7252656600000000000000010200014c00046e616d657400124c6a6176612f6c616e672f537472696e673b7872001b616b6b612e6163746f722e496e7465726e616c4163746f725265660d0aa2ca1e82097602000078720013616b6b612e6163746f722e4163746f72526566c3585dde655f46940200007870740007776174636865657371007e0006740007776174636865727671007e0003") + verify(Watch(FakeActorRef("watchee"), FakeActorRef("watcher")), "aced00057372000c7363616c612e5475706c6532bc7daadf46211a990200024c00025f317400124c6a6176612f6c616e672f4f626a6563743b4c00025f3271007e000178707372001a616b6b612e64697370617463682e7379736d73672e57617463682e1e65bc74394fc40200024c0007776174636865657400154c616b6b612f6163746f722f4163746f725265663b4c00077761746368657271007e000478707372001f616b6b612e73657269616c697a6174696f6e2e46616b654163746f7252656600000000000000010200014c00046e616d657400124c6a6176612f6c616e672f537472696e673b7872001b616b6b612e6163746f722e496e7465726e616c4163746f72526566db6eaed9e69a356302000078720013616b6b612e6163746f722e4163746f72526566c3585dde655f46940200007870740007776174636865657371007e0006740007776174636865727671007e0003") } "be preserved for the Unwatch SystemMessage" in { - verify(Unwatch(FakeActorRef("watchee"), FakeActorRef("watcher")), "aced00057372000c7363616c612e5475706c6532bc7daadf46211a990200024c00025f317400124c6a6176612f6c616e672f4f626a6563743b4c00025f3271007e0001787073720015616b6b612e64697370617463682e556e776174636858501f7ee63dc2100200024c0007776174636865657400154c616b6b612f6163746f722f4163746f725265663b4c00077761746368657271007e000478707372001f616b6b612e73657269616c697a6174696f6e2e46616b654163746f7252656600000000000000010200014c00046e616d657400124c6a6176612f6c616e672f537472696e673b7872001b616b6b612e6163746f722e496e7465726e616c4163746f725265660d0aa2ca1e82097602000078720013616b6b612e6163746f722e4163746f72526566c3585dde655f46940200007870740007776174636865657371007e0006740007776174636865727671007e0003") + verify(Unwatch(FakeActorRef("watchee"), FakeActorRef("watcher")), "aced00057372000c7363616c612e5475706c6532bc7daadf46211a990200024c00025f317400124c6a6176612f6c616e672f4f626a6563743b4c00025f3271007e000178707372001c616b6b612e64697370617463682e7379736d73672e556e776174636858501f7ee63dc2100200024c0007776174636865657400154c616b6b612f6163746f722f4163746f725265663b4c00077761746368657271007e000478707372001f616b6b612e73657269616c697a6174696f6e2e46616b654163746f7252656600000000000000010200014c00046e616d657400124c6a6176612f6c616e672f537472696e673b7872001b616b6b612e6163746f722e496e7465726e616c4163746f72526566db6eaed9e69a356302000078720013616b6b612e6163746f722e4163746f72526566c3585dde655f46940200007870740007776174636865657371007e0006740007776174636865727671007e0003") } "be preserved for the NoMessage SystemMessage" in { - verify(NoMessage, "aced00057372000c7363616c612e5475706c6532bc7daadf46211a990200024c00025f317400124c6a6176612f6c616e672f4f626a6563743b4c00025f3271007e0001787073720018616b6b612e64697370617463682e4e6f4d65737361676524b401a3610ccb70dd02000078707671007e0003") + verify(NoMessage, "aced00057372000c7363616c612e5475706c6532bc7daadf46211a990200024c00025f317400124c6a6176612f6c616e672f4f626a6563743b4c00025f3271007e000178707372001f616b6b612e64697370617463682e7379736d73672e4e6f4d65737361676524b401a3610ccb70dd02000078707671007e0003") + } + "be preserved for the Failed SystemMessage" in { + // Using null as the cause to avoid a large serialized message + verify(Failed(FakeActorRef("child"), cause = null, uid = 0), "aced00057372000c7363616c612e5475706c6532bc7daadf46211a990200024c00025f317400124c6a6176612f6c616e672f4f626a6563743b4c00025f3271007e000178707372001b616b6b612e64697370617463682e7379736d73672e4661696c656400000000000000030200034900037569644c000563617573657400154c6a6176612f6c616e672f5468726f7761626c653b4c00056368696c647400154c616b6b612f6163746f722f4163746f725265663b787000000000707372001f616b6b612e73657269616c697a6174696f6e2e46616b654163746f7252656600000000000000010200014c00046e616d657400124c6a6176612f6c616e672f537472696e673b7872001b616b6b612e6163746f722e496e7465726e616c4163746f72526566db6eaed9e69a356302000078720013616b6b612e6163746f722e4163746f72526566c3585dde655f469402000078707400056368696c647671007e0003") } } } diff --git a/akka-actor/src/main/java/akka/util/internal/HashedWheelTimer.java b/akka-actor/src/main/java/akka/util/internal/HashedWheelTimer.java index c0c38887a3..0c1765cde9 100644 --- a/akka-actor/src/main/java/akka/util/internal/HashedWheelTimer.java +++ b/akka-actor/src/main/java/akka/util/internal/HashedWheelTimer.java @@ -26,7 +26,7 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock; -import akka.dispatch.SystemMessage; +import akka.dispatch.sysmsg.SystemMessage; import akka.util.Helpers; import scala.concurrent.duration.Duration; import scala.concurrent.duration.FiniteDuration; diff --git a/akka-actor/src/main/scala/akka/actor/Actor.scala b/akka-actor/src/main/scala/akka/actor/Actor.scala index acd93b21f9..2659c10a19 100644 --- a/akka-actor/src/main/scala/akka/actor/Actor.scala +++ b/akka-actor/src/main/scala/akka/actor/Actor.scala @@ -28,12 +28,6 @@ trait PossiblyHarmful */ trait NoSerializationVerificationNeeded -/** - * INTERNAL API - */ -@SerialVersionUID(2L) -private[akka] case class Failed(cause: Throwable, uid: Int) extends AutoReceivedMessage with PossiblyHarmful - abstract class PoisonPill extends AutoReceivedMessage with PossiblyHarmful /** diff --git a/akka-actor/src/main/scala/akka/actor/ActorCell.scala b/akka-actor/src/main/scala/akka/actor/ActorCell.scala index 6b1d42a529..649db7cd02 100644 --- a/akka-actor/src/main/scala/akka/actor/ActorCell.scala +++ b/akka-actor/src/main/scala/akka/actor/ActorCell.scala @@ -4,9 +4,17 @@ package akka.actor +import akka.actor.dungeon.ChildrenContainer +import akka.dispatch.Envelope +import akka.dispatch.NullMessage +import akka.dispatch.sysmsg._ +import akka.event.Logging.Debug +import akka.event.Logging.{ LogEvent, Error } +import akka.japi.Procedure import java.io.{ ObjectOutputStream, NotSerializableException } -import scala.annotation.tailrec +import scala.annotation.{ switch, tailrec } import scala.collection.immutable +import scala.concurrent.ExecutionContext import scala.concurrent.duration.Duration import scala.util.control.NonFatal import akka.actor.dungeon.ChildrenContainer @@ -16,7 +24,6 @@ import akka.event.Logging.{ LogEvent, Debug, Error } import akka.japi.Procedure import akka.dispatch.NullMessage import scala.concurrent.ExecutionContext -import scala.concurrent.forkjoin.ThreadLocalRandom /** * The actor context - the view of the actor cell from the actor. @@ -325,6 +332,9 @@ private[akka] object ActorCell { else (name.substring(0, i), Integer.valueOf(name.substring(i + 1))) } + final val DefaultState = 0 + final val SuspendedState = 1 + final val SuspendedWaitForChildrenState = 2 } //ACTORCELL IS 64bytes and should stay that way unless very good reason not to (machine sympathy, cache line fit) @@ -362,12 +372,24 @@ private[akka] class ActorCell( protected def actor_=(a: Actor): Unit = _actor = a var currentMessage: Envelope = _ private var behaviorStack: List[Actor.Receive] = emptyBehaviorStack + private[this] var sysmsgStash: LatestFirstSystemMessageList = SystemMessageList.LNil + + protected def stash(msg: SystemMessage): Unit = { + assert(msg.unlinked) + sysmsgStash ::= msg + } + + private def unstashAll(): LatestFirstSystemMessageList = { + val unstashed = sysmsgStash + sysmsgStash = SystemMessageList.LNil + unstashed + } /* * MESSAGE PROCESSING */ //Memory consistency is handled by the Mailbox (reading mailbox status then processing messages, then writing mailbox status - @tailrec final def systemInvoke(message: SystemMessage): Unit = { + final def systemInvoke(message: SystemMessage): Unit = { /* * When recreate/suspend/resume are received while restarting (i.e. between * preRestart and postRestart, waiting for children to terminate), these @@ -377,36 +399,61 @@ private[akka] class ActorCell( * types (hence the overwrite further down). Mailbox sets message.next=null * before systemInvoke, so this will only be non-null during such a replay. */ - var todo = message.next - try { - message match { - case Create() ⇒ create() - case Watch(watchee, watcher) ⇒ addWatcher(watchee, watcher) - case Unwatch(watchee, watcher) ⇒ remWatcher(watchee, watcher) - case Recreate(cause) ⇒ - waitingForChildrenOrNull match { - case null ⇒ faultRecreate(cause) - case w: WaitingForChildren ⇒ w.enqueue(message) - } - case Suspend() ⇒ - waitingForChildrenOrNull match { - case null ⇒ faultSuspend() - case w: WaitingForChildren ⇒ w.enqueue(message) - } - case Resume(inRespToFailure) ⇒ - waitingForChildrenOrNull match { - case null ⇒ faultResume(inRespToFailure) - case w: WaitingForChildren ⇒ w.enqueue(message) - } - case Terminate() ⇒ terminate() - case Supervise(child, async) ⇒ supervise(child, async) - case ChildTerminated(child) ⇒ todo = handleChildTerminated(child) - case NoMessage ⇒ // only here to suppress warning + + def calculateState: Int = + if (waitingForChildrenOrNull ne null) SuspendedWaitForChildrenState + else if (mailbox.isSuspended) SuspendedState + else DefaultState + + @tailrec def sendAllToDeadLetters(messages: EarliestFirstSystemMessageList): Unit = + if (messages.nonEmpty) { + val tail = messages.tail + val msg = messages.head + msg.unlink() + provider.deadLetters ! msg + sendAllToDeadLetters(tail) } - } catch handleNonFatalOrInterruptedException { e ⇒ - handleInvokeFailure(Nil, e) + + def shouldStash(m: SystemMessage, state: Int): Boolean = + (state: @switch) match { + case DefaultState ⇒ false + case SuspendedState ⇒ m.isInstanceOf[StashWhenFailed] + case SuspendedWaitForChildrenState ⇒ m.isInstanceOf[StashWhenWaitingForChildren] + } + + @tailrec + def invokeAll(messages: EarliestFirstSystemMessageList, currentState: Int): Unit = { + val rest = messages.tail + val message = messages.head + message.unlink() + try { + message match { + case message: SystemMessage if shouldStash(message, currentState) ⇒ stash(message) + case f: Failed ⇒ handleFailure(f) + case Create() ⇒ create(uid) + case Watch(watchee, watcher) ⇒ addWatcher(watchee, watcher) + case Unwatch(watchee, watcher) ⇒ remWatcher(watchee, watcher) + case Recreate(cause) ⇒ faultRecreate(cause) + case Suspend() ⇒ faultSuspend() + case Resume(inRespToFailure) ⇒ faultResume(inRespToFailure) + case Terminate() ⇒ terminate() + case Supervise(child, async) ⇒ supervise(child, async, uid) + case ChildTerminated(child) ⇒ handleChildTerminated(child) + case NoMessage ⇒ // only here to suppress warning + } + } catch handleNonFatalOrInterruptedException { e ⇒ + handleInvokeFailure(Nil, e) + } + val newState = calculateState + // As each state accepts a strict subset of another state, it is enough to unstash if we "walk up" the state + // chain + val todo = if (newState < currentState) unstashAll() reverse_::: rest else rest + + if (isTerminated) sendAllToDeadLetters(todo) + else if (todo.nonEmpty) invokeAll(todo, newState) } - if (todo != null) systemInvoke(todo) + + invokeAll(new EarliestFirstSystemMessageList(message), calculateState) } //Memory consistency is handled by the Mailbox (reading mailbox status then processing messages, then writing mailbox status @@ -430,7 +477,6 @@ private[akka] class ActorCell( publish(Debug(self.path.toString, clazz(actor), "received AutoReceiveMessage " + msg)) msg.message match { - case Failed(cause, uid) ⇒ handleFailure(sender, cause, uid) case t: Terminated ⇒ watchedActorTerminated(t) case AddressTerminated(address) ⇒ addressTerminated(address) case Kill ⇒ throw new ActorKilledException("Kill") diff --git a/akka-actor/src/main/scala/akka/actor/ActorRef.scala b/akka-actor/src/main/scala/akka/actor/ActorRef.scala index ff42918ebe..fe0f66f5f5 100644 --- a/akka-actor/src/main/scala/akka/actor/ActorRef.scala +++ b/akka-actor/src/main/scala/akka/actor/ActorRef.scala @@ -5,6 +5,7 @@ package akka.actor import akka.dispatch._ +import akka.dispatch.sysmsg._ import akka.util._ import java.lang.{ UnsupportedOperationException, IllegalStateException } import akka.serialization.{ Serialization, JavaSerializer } diff --git a/akka-actor/src/main/scala/akka/actor/ActorRefProvider.scala b/akka-actor/src/main/scala/akka/actor/ActorRefProvider.scala index 4c6b13d9b6..4a01b87f95 100644 --- a/akka-actor/src/main/scala/akka/actor/ActorRefProvider.scala +++ b/akka-actor/src/main/scala/akka/actor/ActorRefProvider.scala @@ -4,7 +4,8 @@ package akka.actor -import akka.dispatch._ +import akka.dispatch.sysmsg._ +import akka.dispatch.NullMessage import akka.routing._ import akka.event._ import akka.util.{ Switch, Helpers } @@ -388,17 +389,17 @@ class LocalActorRefProvider private[akka] ( override def isTerminated: Boolean = stopped.isOn override def !(message: Any)(implicit sender: ActorRef = Actor.noSender): Unit = stopped.ifOff(message match { - case null ⇒ throw new InvalidMessageException("Message is null") - case Failed(ex, _) if sender ne null ⇒ { causeOfTermination = Some(ex); sender.asInstanceOf[InternalActorRef].stop() } - case NullMessage ⇒ // do nothing - case _ ⇒ log.error(this + " received unexpected message [" + message + "]") + case null ⇒ throw new InvalidMessageException("Message is null") + case NullMessage ⇒ // do nothing + case _ ⇒ log.error(this + " received unexpected message [" + message + "]") }) override def sendSystemMessage(message: SystemMessage): Unit = stopped ifOff { message match { - case Supervise(_, _) ⇒ // TODO register child in some map to keep track of it and enable shutdown after all dead - case ChildTerminated(_) ⇒ stop() - case _ ⇒ log.error(this + " received unexpected system message [" + message + "]") + case Failed(child, ex, _) ⇒ { causeOfTermination = Some(ex); child.asInstanceOf[InternalActorRef].stop() } + case Supervise(_, _) ⇒ // TODO register child in some map to keep track of it and enable shutdown after all dead + case ChildTerminated(_) ⇒ stop() + case _ ⇒ log.error(this + " received unexpected system message [" + message + "]") } } } diff --git a/akka-actor/src/main/scala/akka/actor/ActorSystem.scala b/akka-actor/src/main/scala/akka/actor/ActorSystem.scala index 6a39dd513b..3b5408b5f9 100644 --- a/akka-actor/src/main/scala/akka/actor/ActorSystem.scala +++ b/akka-actor/src/main/scala/akka/actor/ActorSystem.scala @@ -10,6 +10,7 @@ import java.util.concurrent.TimeUnit.MILLISECONDS import com.typesafe.config.{ Config, ConfigFactory } import akka.event._ import akka.dispatch._ +import akka.dispatch.sysmsg.{ SystemMessageList, EarliestFirstSystemMessageList, LatestFirstSystemMessageList, SystemMessage } import akka.japi.Util.immutableSeq import akka.actor.dungeon.ChildrenContainer import akka.util._ @@ -559,7 +560,7 @@ private[akka] class ActorSystemImpl(val name: String, applicationConfig: Config, becomeClosed() def systemEnqueue(receiver: ActorRef, handle: SystemMessage): Unit = deadLetters ! DeadLetter(handle, receiver, receiver) - def systemDrain(newContents: SystemMessage): SystemMessage = null + def systemDrain(newContents: LatestFirstSystemMessageList): EarliestFirstSystemMessageList = SystemMessageList.ENil def hasSystemMessages = false } diff --git a/akka-actor/src/main/scala/akka/actor/RepointableActorRef.scala b/akka-actor/src/main/scala/akka/actor/RepointableActorRef.scala index 8a1a6a6a03..123a576462 100644 --- a/akka-actor/src/main/scala/akka/actor/RepointableActorRef.scala +++ b/akka-actor/src/main/scala/akka/actor/RepointableActorRef.scala @@ -15,6 +15,7 @@ import akka.actor.dungeon.ChildrenContainer import akka.event.Logging.Warning import akka.util.Unsafe import akka.dispatch._ +import akka.dispatch.sysmsg._ import util.Try /** diff --git a/akka-actor/src/main/scala/akka/actor/dungeon/ChildrenContainer.scala b/akka-actor/src/main/scala/akka/actor/dungeon/ChildrenContainer.scala index 1dbd117904..2a60fc5fca 100644 --- a/akka-actor/src/main/scala/akka/actor/dungeon/ChildrenContainer.scala +++ b/akka-actor/src/main/scala/akka/actor/dungeon/ChildrenContainer.scala @@ -7,7 +7,7 @@ package akka.actor.dungeon import scala.collection.immutable import akka.actor.{ InvalidActorNameException, ChildStats, ChildRestartStats, ChildNameReserved, ActorRef } -import akka.dispatch.SystemMessage +import akka.dispatch.sysmsg.{ EarliestFirstSystemMessageList, SystemMessageList, LatestFirstSystemMessageList, SystemMessage } import akka.util.Collections.{ EmptyImmutableSeq, PartialImmutableValuesIterable } /** @@ -62,11 +62,7 @@ private[akka] object ChildrenContainer { override final def valuesIterator = stats.valuesIterator } - trait WaitingForChildren { - private var todo: SystemMessage = null - def enqueue(message: SystemMessage) = { message.next = todo; todo = message } - def dequeueAll(): SystemMessage = { val ret = SystemMessage.reverse(todo); todo = null; ret } - } + trait WaitingForChildren trait EmptyChildrenContainer extends ChildrenContainer { val emptyStats = immutable.TreeMap.empty[String, ChildStats] diff --git a/akka-actor/src/main/scala/akka/actor/dungeon/DeathWatch.scala b/akka-actor/src/main/scala/akka/actor/dungeon/DeathWatch.scala index cff5665ad3..6dbebb806b 100644 --- a/akka-actor/src/main/scala/akka/actor/dungeon/DeathWatch.scala +++ b/akka-actor/src/main/scala/akka/actor/dungeon/DeathWatch.scala @@ -5,7 +5,7 @@ package akka.actor.dungeon import akka.actor.{ Terminated, InternalActorRef, ActorRef, ActorRefScope, ActorCell, Actor, Address, AddressTerminated } -import akka.dispatch.{ ChildTerminated, Watch, Unwatch } +import akka.dispatch.sysmsg.{ ChildTerminated, Watch, Unwatch } import akka.event.Logging.{ Warning, Error, Debug } import scala.util.control.NonFatal import akka.actor.MinimalActorRef @@ -188,4 +188,4 @@ private[akka] trait DeathWatch { this: ActorCell ⇒ private[akka] class UndefinedUidActorRef(ref: ActorRef) extends MinimalActorRef { override val path = ref.path.withUid(ActorCell.undefinedUid) override def provider = throw new UnsupportedOperationException("UndefinedUidActorRef does not provide") -} \ No newline at end of file +} diff --git a/akka-actor/src/main/scala/akka/actor/dungeon/Dispatch.scala b/akka-actor/src/main/scala/akka/actor/dungeon/Dispatch.scala index 075ff49a0b..4235330b7e 100644 --- a/akka-actor/src/main/scala/akka/actor/dungeon/Dispatch.scala +++ b/akka-actor/src/main/scala/akka/actor/dungeon/Dispatch.scala @@ -5,7 +5,8 @@ package akka.actor.dungeon import scala.annotation.tailrec -import akka.dispatch.{ Terminate, SystemMessage, Suspend, Resume, Recreate, MessageDispatcher, Mailbox, Envelope, Create } +import akka.dispatch.{ MessageDispatcher, Mailbox, Envelope } +import akka.dispatch.sysmsg._ import akka.event.Logging.Error import akka.util.Unsafe import akka.dispatch.NullMessage @@ -53,7 +54,7 @@ private[akka] trait Dispatch { this: ActorCell ⇒ if (sendSupervise) { // ➡➡➡ NEVER SEND THE SAME SYSTEM MESSAGE OBJECT TO TWO ACTORS ⬅⬅⬅ - parent.sendSystemMessage(akka.dispatch.Supervise(self, async = false)) + parent.sendSystemMessage(akka.dispatch.sysmsg.Supervise(self, async = false, uid)) parent ! NullMessage // read ScalaDoc of NullMessage to see why } this diff --git a/akka-actor/src/main/scala/akka/actor/dungeon/FaultHandling.scala b/akka-actor/src/main/scala/akka/actor/dungeon/FaultHandling.scala index ac5d1a48e0..5badb8c180 100644 --- a/akka-actor/src/main/scala/akka/actor/dungeon/FaultHandling.scala +++ b/akka-actor/src/main/scala/akka/actor/dungeon/FaultHandling.scala @@ -4,20 +4,19 @@ package akka.actor.dungeon -import scala.annotation.tailrec -import akka.actor.{ PreRestartException, PostRestartException, InternalActorRef, Failed, ActorRef, ActorInterruptedException, ActorCell, Actor } -import akka.dispatch._ -import akka.event.Logging.{ Warning, Error, Debug } -import scala.util.control.NonFatal -import akka.event.Logging -import scala.collection.immutable -import akka.dispatch.ChildTerminated -import akka.actor.PreRestartException -import akka.actor.Failed import akka.actor.PostRestartException +import akka.actor.PreRestartException +import akka.actor.{ InternalActorRef, ActorRef, ActorInterruptedException, ActorCell, Actor } +import akka.dispatch._ +import akka.dispatch.sysmsg.ChildTerminated +import akka.dispatch.sysmsg._ +import akka.event.Logging import akka.event.Logging.Debug +import akka.event.Logging.Error +import scala.collection.immutable import scala.concurrent.duration.Duration import scala.util.control.Exception._ +import scala.util.control.NonFatal private[akka] trait FaultHandling { this: ActorCell ⇒ @@ -134,7 +133,10 @@ private[akka] trait FaultHandling { this: ActorCell ⇒ private def finishCreate(): Unit = { try resumeNonRecursive() finally clearFailed() - create() + try create() + catch handleNonFatalOrInterruptedException { e ⇒ + handleInvokeFailure(Nil, e) + } } protected def terminate() { @@ -169,14 +171,18 @@ private[akka] trait FaultHandling { this: ActorCell ⇒ suspendNonRecursive() // suspend children val skip: Set[ActorRef] = currentMessage match { - case Envelope(Failed(_, _), child) ⇒ { setFailed(child); Set(child) } - case _ ⇒ { setFailed(self); Set.empty } + case Envelope(Failed(_, _, _), child) ⇒ setFailed(child); Set(child) + case _ ⇒ setFailed(self); Set.empty } suspendChildren(exceptFor = skip ++ childrenNotToSuspend) t match { // tell supervisor - case _: InterruptedException ⇒ parent.tell(Failed(new ActorInterruptedException(t), uid), self) - case _ ⇒ parent.tell(Failed(t, uid), self) + case _: InterruptedException ⇒ + // ➡➡➡ NEVER SEND THE SAME SYSTEM MESSAGE OBJECT TO TWO ACTORS ⬅⬅⬅ + parent.sendSystemMessage(Failed(self, new ActorInterruptedException(t), uid)) + case _ ⇒ + // ➡➡➡ NEVER SEND THE SAME SYSTEM MESSAGE OBJECT TO TWO ACTORS ⬅⬅⬅ + parent.sendSystemMessage(Failed(self, t, uid)) } } catch handleNonFatalOrInterruptedException { e ⇒ publish(Error(e, self.path.toString, clazz(actor), @@ -237,23 +243,25 @@ private[akka] trait FaultHandling { this: ActorCell ⇒ } } - final protected def handleFailure(child: ActorRef, cause: Throwable, uid: Int): Unit = - getChildByRef(child) match { + final protected def handleFailure(f: Failed): Unit = { + currentMessage = Envelope(f, f.child, system) + getChildByRef(f.child) match { /* * only act upon the failure, if it comes from a currently known child; * the UID protects against reception of a Failed from a child which was * killed in preRestart and re-created in postRestart */ - case Some(stats) if stats.uid == uid ⇒ - if (!actor.supervisorStrategy.handleFailure(this, child, cause, stats, getAllChildStats)) throw cause + case Some(stats) if stats.uid == f.uid ⇒ + if (!actor.supervisorStrategy.handleFailure(this, f.child, f.cause, stats, getAllChildStats)) throw f.cause case Some(stats) ⇒ publish(Debug(self.path.toString, clazz(actor), - "dropping Failed(" + cause + ") from old child " + child + " (uid=" + stats.uid + " != " + uid + ")")) + "dropping Failed(" + f.cause + ") from old child " + f.child + " (uid=" + stats.uid + " != " + f.uid + ")")) case None ⇒ - publish(Debug(self.path.toString, clazz(actor), "dropping Failed(" + cause + ") from unknown child " + child)) + publish(Debug(self.path.toString, clazz(actor), "dropping Failed(" + f.cause + ") from unknown child " + f.child)) } + } - final protected def handleChildTerminated(child: ActorRef): SystemMessage = { + final protected def handleChildTerminated(child: ActorRef): Unit = { val status = removeChildAndGetStateChange(child) /* * if this fails, we do nothing in case of terminating/restarting state, @@ -272,10 +280,10 @@ private[akka] trait FaultHandling { this: ActorCell ⇒ * then we are continuing the previously suspended recreate/create/terminate action */ status match { - case Some(c @ ChildrenContainer.Recreation(cause)) ⇒ { finishRecreate(cause, actor); c.dequeueAll() } - case Some(c @ ChildrenContainer.Creation()) ⇒ { finishCreate(); c.dequeueAll() } - case Some(ChildrenContainer.Termination) ⇒ { finishTerminate(); null } - case _ ⇒ null + case Some(c @ ChildrenContainer.Recreation(cause)) ⇒ finishRecreate(cause, actor) + case Some(c @ ChildrenContainer.Creation()) ⇒ finishCreate() + case Some(ChildrenContainer.Termination) ⇒ finishTerminate() + case _ ⇒ } } diff --git a/akka-actor/src/main/scala/akka/dispatch/AbstractDispatcher.scala b/akka-actor/src/main/scala/akka/dispatch/AbstractDispatcher.scala index b0e97f2f0c..282a66e796 100644 --- a/akka-actor/src/main/scala/akka/dispatch/AbstractDispatcher.scala +++ b/akka-actor/src/main/scala/akka/dispatch/AbstractDispatcher.scala @@ -7,6 +7,7 @@ package akka.dispatch import java.util.concurrent._ import akka.event.Logging.{ Error, LogEventException } import akka.actor._ +import akka.dispatch.sysmsg._ import akka.event.EventStream import com.typesafe.config.Config import akka.util.{ Unsafe, Index } @@ -41,95 +42,6 @@ object Envelope { */ case object NullMessage extends AutoReceivedMessage -/** - * INTERNAL API - */ -private[akka] object SystemMessage { - @tailrec - final def size(list: SystemMessage, acc: Int = 0): Int = { - if (list eq null) acc else size(list.next, acc + 1) - } - - @tailrec - final def reverse(list: SystemMessage, acc: SystemMessage = null): SystemMessage = { - if (list eq null) acc else { - val next = list.next - list.next = acc - reverse(next, list) - } - } -} - -/** - * System messages are handled specially: they form their own queue within - * each actor’s mailbox. This queue is encoded in the messages themselves to - * avoid extra allocations and overhead. The next pointer is a normal var, and - * it does not need to be volatile because in the enqueuing method its update - * is immediately succeeded by a volatile write and all reads happen after the - * volatile read in the dequeuing thread. Afterwards, the obtained list of - * system messages is handled in a single thread only and not ever passed around, - * hence no further synchronization is needed. - * - * INTERNAL API - * - * ➡➡➡ NEVER SEND THE SAME SYSTEM MESSAGE OBJECT TO TWO ACTORS ⬅⬅⬅ - */ -private[akka] sealed trait SystemMessage extends PossiblyHarmful with Serializable { - @transient - var next: SystemMessage = _ -} - -/** - * INTERNAL API - */ -@SerialVersionUID(3L) -private[akka] case class Create() extends SystemMessage // send to self from Dispatcher.register -/** - * INTERNAL API - */ -@SerialVersionUID(686735569005808256L) -private[akka] case class Recreate(cause: Throwable) extends SystemMessage // sent to self from ActorCell.restart -/** - * INTERNAL API - */ -@SerialVersionUID(7270271967867221401L) -private[akka] case class Suspend() extends SystemMessage // sent to self from ActorCell.suspend -/** - * INTERNAL API - */ -@SerialVersionUID(-2567504317093262591L) -private[akka] case class Resume(causedByFailure: Throwable) extends SystemMessage // sent to self from ActorCell.resume -/** - * INTERNAL API - */ -@SerialVersionUID(708873453777219599L) -private[akka] case class Terminate() extends SystemMessage // sent to self from ActorCell.stop -/** - * INTERNAL API - */ -@SerialVersionUID(3L) -private[akka] case class Supervise(child: ActorRef, async: Boolean) extends SystemMessage // sent to supervisor ActorRef from ActorCell.start -/** - * INTERNAL API - */ -@SerialVersionUID(5513569382760799668L) -private[akka] case class ChildTerminated(child: ActorRef) extends SystemMessage // sent to supervisor from ActorCell.doTerminate -/** - * INTERNAL API - */ -@SerialVersionUID(3323205435124174788L) -private[akka] case class Watch(watchee: ActorRef, watcher: ActorRef) extends SystemMessage // sent to establish a DeathWatch -/** - * INTERNAL API - */ -@SerialVersionUID(6363620903363658256L) -private[akka] case class Unwatch(watchee: ActorRef, watcher: ActorRef) extends SystemMessage // sent to tear down a DeathWatch -/** - * INTERNAL API - */ -@SerialVersionUID(-5475916034683997987L) -private[akka] case object NoMessage extends SystemMessage // switched into the mailbox to signal termination - final case class TaskInvocation(eventStream: EventStream, runnable: Runnable, cleanup: () ⇒ Unit) extends Batchable { final override def isBatchable: Boolean = runnable match { case b: Batchable ⇒ b.isBatchable diff --git a/akka-actor/src/main/scala/akka/dispatch/BalancingDispatcher.scala b/akka-actor/src/main/scala/akka/dispatch/BalancingDispatcher.scala index 841a359b87..3942762e09 100644 --- a/akka-actor/src/main/scala/akka/dispatch/BalancingDispatcher.scala +++ b/akka-actor/src/main/scala/akka/dispatch/BalancingDispatcher.scala @@ -5,6 +5,7 @@ package akka.dispatch import akka.actor.{ ActorCell, ActorRef } +import akka.dispatch.sysmsg._ import scala.annotation.tailrec import scala.concurrent.duration.Duration import akka.util.Helpers @@ -56,13 +57,13 @@ class BalancingDispatcher( override def cleanUp(): Unit = { val dlq = system.deadLetterMailbox //Don't call the original implementation of this since it scraps all messages, and we don't want to do that - var message = systemDrain(NoMessage) - while (message ne null) { + var messages = systemDrain(new LatestFirstSystemMessageList(NoMessage)) + while (messages.nonEmpty) { // message must be “virgin” before being able to systemEnqueue again - val next = message.next - message.next = null + val message = messages.head + messages = messages.tail + message.unlink() dlq.systemEnqueue(system.deadLetters, message) - message = next } } } diff --git a/akka-actor/src/main/scala/akka/dispatch/Dispatcher.scala b/akka-actor/src/main/scala/akka/dispatch/Dispatcher.scala index cc101e6311..74c765f401 100644 --- a/akka-actor/src/main/scala/akka/dispatch/Dispatcher.scala +++ b/akka-actor/src/main/scala/akka/dispatch/Dispatcher.scala @@ -7,6 +7,7 @@ package akka.dispatch import akka.event.Logging.Error import akka.actor.ActorCell import akka.event.Logging +import akka.dispatch.sysmsg.SystemMessage import java.util.concurrent.atomic.AtomicReference import java.util.concurrent.{ ExecutorService, RejectedExecutionException } import scala.concurrent.forkjoin.ForkJoinPool diff --git a/akka-actor/src/main/scala/akka/dispatch/Mailbox.scala b/akka-actor/src/main/scala/akka/dispatch/Mailbox.scala index e055c4b327..2d4efe6464 100644 --- a/akka-actor/src/main/scala/akka/dispatch/Mailbox.scala +++ b/akka-actor/src/main/scala/akka/dispatch/Mailbox.scala @@ -6,6 +6,7 @@ package akka.dispatch import java.util.{ Comparator, PriorityQueue, Queue, Deque } import java.util.concurrent._ import akka.AkkaException +import akka.dispatch.sysmsg._ import akka.actor.{ ActorCell, ActorRef, Cell, ActorSystem, InternalActorRef, DeadLetter } import akka.util.{ Unsafe, BoundedBlockingQueue } import akka.event.Logging.Error @@ -14,6 +15,9 @@ import scala.annotation.tailrec import scala.util.control.NonFatal import com.typesafe.config.Config import scala.concurrent.duration.FiniteDuration +import akka.actor.DeadLetter +import akka.dispatch.BoundedMailbox +import akka.dispatch.BoundedDequeBasedMailbox /** * INTERNAL API @@ -196,11 +200,16 @@ private[akka] abstract class Mailbox(val messageQueue: MessageQueue) /* * AtomicReferenceFieldUpdater for system queue. */ - protected final def systemQueueGet: SystemMessage = - Unsafe.instance.getObjectVolatile(this, AbstractMailbox.systemMessageOffset).asInstanceOf[SystemMessage] + protected final def systemQueueGet: LatestFirstSystemMessageList = + // Note: contrary how it looks, there is no allocation here, as SystemMessageList is a value class and as such + // it just exists as a typed view during compile-time. The actual return type is still SystemMessage. + new LatestFirstSystemMessageList(Unsafe.instance.getObjectVolatile(this, AbstractMailbox.systemMessageOffset).asInstanceOf[SystemMessage]) - protected final def systemQueuePut(_old: SystemMessage, _new: SystemMessage): Boolean = - Unsafe.instance.compareAndSwapObject(this, AbstractMailbox.systemMessageOffset, _old, _new) + protected final def systemQueuePut(_old: LatestFirstSystemMessageList, _new: LatestFirstSystemMessageList): Boolean = + // Note: calling .head is not actually existing on the bytecode level as the parameters _old and _new + // are SystemMessage instances hidden during compile time behind the SystemMessageList value class. + // Without calling .head the parameters would be boxed in SystemMessageList wrapper. + Unsafe.instance.compareAndSwapObject(this, AbstractMailbox.systemMessageOffset, _old.head, _new.head) final def canBeScheduledForExecution(hasMessageHint: Boolean, hasSystemMessageHint: Boolean): Boolean = status match { case Open | Scheduled ⇒ hasMessageHint || hasSystemMessageHint || hasSystemMessages || hasMessages @@ -248,28 +257,28 @@ private[akka] abstract class Mailbox(val messageQueue: MessageQueue) */ final def processAllSystemMessages() { var interruption: Throwable = null - var nextMessage = systemDrain(null) - while ((nextMessage ne null) && !isClosed) { - val msg = nextMessage - nextMessage = nextMessage.next - msg.next = null + var messageList = systemDrain(SystemMessageList.LNil) + while ((messageList.nonEmpty) && !isClosed) { + val msg = messageList.head + messageList = messageList.tail + msg.unlink() if (debug) println(actor.self + " processing system message " + msg + " with " + actor.childrenRefs) // we know here that systemInvoke ensures that only "fatal" exceptions get rethrown actor systemInvoke msg if (Thread.interrupted()) interruption = new InterruptedException("Interrupted while processing system messages") // don’t ever execute normal message when system message present! - if ((nextMessage eq null) && !isClosed) nextMessage = systemDrain(null) + if ((messageList.isEmpty) && !isClosed) messageList = systemDrain(SystemMessageList.LNil) } /* * if we closed the mailbox, we must dump the remaining system messages * to deadLetters (this is essential for DeathWatch) */ val dlm = actor.systemImpl.deadLetterMailbox - while (nextMessage ne null) { - val msg = nextMessage - nextMessage = nextMessage.next - msg.next = null + while (messageList.nonEmpty) { + val msg = messageList.head + messageList = messageList.tail + msg.unlink() try dlm.systemEnqueue(actor.self, msg) catch { case e: InterruptedException ⇒ interruption = e @@ -292,13 +301,13 @@ private[akka] abstract class Mailbox(val messageQueue: MessageQueue) protected[dispatch] def cleanUp(): Unit = if (actor ne null) { // actor is null for the deadLetterMailbox val dlm = actor.systemImpl.deadLetterMailbox - var message = systemDrain(NoMessage) - while (message ne null) { + var messageList = systemDrain(new LatestFirstSystemMessageList(NoMessage)) + while (messageList.nonEmpty) { // message must be “virgin” before being able to systemEnqueue again - val next = message.next - message.next = null - dlm.systemEnqueue(actor.self, message) - message = next + val msg = messageList.head + messageList = messageList.tail + msg.unlink() + dlm.systemEnqueue(actor.self, msg) } if (messageQueue ne null) // needed for CallingThreadDispatcher, which never calls Mailbox.run() @@ -355,7 +364,7 @@ private[akka] trait SystemMessageQueue { /** * Dequeue all messages from system queue and return them as single-linked list. */ - def systemDrain(newContents: SystemMessage): SystemMessage + def systemDrain(newContents: LatestFirstSystemMessageList): EarliestFirstSystemMessageList def hasSystemMessages: Boolean } @@ -367,36 +376,26 @@ private[akka] trait DefaultSystemMessageQueue { self: Mailbox ⇒ @tailrec final def systemEnqueue(receiver: ActorRef, message: SystemMessage): Unit = { - assert(message.next eq null) + assert(message.unlinked) if (Mailbox.debug) println(receiver + " having enqueued " + message) - val head = systemQueueGet - if (head == NoMessage) { + val currentList = systemQueueGet + if (currentList.head == NoMessage) { if (actor ne null) actor.systemImpl.deadLetterMailbox.systemEnqueue(receiver, message) } else { - /* - * This write is safely published by the compareAndSet contained within - * systemQueuePut; “Intra-Thread Semantics” on page 12 of the JSR133 spec - * guarantees that “head” uses the value obtained from systemQueueGet above. - * Hence, SystemMessage.next does not need to be volatile. - */ - message.next = head - if (!systemQueuePut(head, message)) { - message.next = null + if (!systemQueuePut(currentList, message :: currentList)) { + message.unlink() systemEnqueue(receiver, message) } } } @tailrec - final def systemDrain(newContents: SystemMessage): SystemMessage = systemQueueGet match { - case NoMessage ⇒ null - case head ⇒ if (systemQueuePut(head, newContents)) SystemMessage.reverse(head) else systemDrain(newContents) + final def systemDrain(newContents: LatestFirstSystemMessageList): EarliestFirstSystemMessageList = { + val currentList = systemQueueGet + if (systemQueuePut(currentList, newContents)) currentList.reverse else systemDrain(newContents) } - def hasSystemMessages: Boolean = systemQueueGet match { - case null | NoMessage ⇒ false - case _ ⇒ true - } + def hasSystemMessages: Boolean = systemQueueGet.nonEmpty } diff --git a/akka-actor/src/main/scala/akka/dispatch/sysmsg/SystemMessage.scala b/akka-actor/src/main/scala/akka/dispatch/sysmsg/SystemMessage.scala new file mode 100644 index 0000000000..a62dd03141 --- /dev/null +++ b/akka-actor/src/main/scala/akka/dispatch/sysmsg/SystemMessage.scala @@ -0,0 +1,257 @@ +/** + * Copyright (C) 2009-2013 Typesafe Inc. + */ +package akka.dispatch.sysmsg + +import scala.annotation.tailrec +import akka.actor.{ ActorRef, PossiblyHarmful } + +/** + * INTERNAL API + * + * Helper companion object for [[akka.dispatch.sysmsg.LatestFirstSystemMessageList]] and + * [[akka.dispatch.sysmsg.EarliestFirstSystemMessageList]] + */ +object SystemMessageList { + final val LNil: LatestFirstSystemMessageList = new LatestFirstSystemMessageList(null) + final val ENil: EarliestFirstSystemMessageList = new EarliestFirstSystemMessageList(null) + + @tailrec + private[sysmsg] def sizeInner(head: SystemMessage, acc: Int): Int = if (head eq null) acc else sizeInner(head.next, acc + 1) + + @tailrec + private[sysmsg] def reverseInner(head: SystemMessage, acc: SystemMessage): SystemMessage = { + if (head eq null) acc else { + val next = head.next + head.next = acc + reverseInner(next, head) + } + } +} + +/** + * + * INTERNAL API + * + * Value class supporting list operations on system messages. The `next` field of [[akka.dispatch.sysmsg.SystemMessage]] + * is hidden, and can only accessed through the value classes [[akka.dispatch.sysmsg.LatestFirstSystemMessageList]] and + * [[akka.dispatch.sysmsg.EarliestFirstSystemMessageList]], abstracting over the fact that system messages are the + * list nodes themselves. If used properly, this stays a compile time construct without any allocation overhead. + * + * This list is mutable. + * + * The type of the list also encodes that the messages contained are in reverse order, i.e. the head of the list is the + * latest appended element. + * + */ +class LatestFirstSystemMessageList(val head: SystemMessage) extends AnyVal { + import SystemMessageList._ + + /** + * Indicates if the list is empty or not. This operation has constant cost. + */ + final def isEmpty: Boolean = head eq null + + /** + * Indicates if the list has at least one element or not. This operation has constant cost. + */ + final def nonEmpty: Boolean = head ne null + + /** + * Indicates if the list is empty or not. This operation has constant cost. + */ + final def size: Int = sizeInner(head, 0) + + /** + * Gives back the list containing all the elements except the first. This operation has constant cost. + * + * *Warning:* as the underlying list nodes (the [[akka.dispatch.sysmsg.SystemMessage]] instances) are mutable, care + * should be taken when passing the tail to other methods. [[akka.dispatch.sysmsg.SystemMessage#unlink]] should be + * called on the head if one wants to detach the tail permanently. + */ + final def tail: LatestFirstSystemMessageList = new LatestFirstSystemMessageList(head.next) + + /** + * Reverses the list. This operation mutates the underlying list. The cost of the call to reverse is linear in the + * number of elements. + * + * The type of the returned list is of the opposite order: [[akka.dispatch.sysmsg.EarliestFirstSystemMessageList]] + */ + final def reverse: EarliestFirstSystemMessageList = new EarliestFirstSystemMessageList(reverseInner(head, null)) + + /** + * Attaches a message to the current head of the list. This operation has constant cost. + */ + final def ::(msg: SystemMessage): LatestFirstSystemMessageList = { + assert(msg ne null) + msg.next = head + new LatestFirstSystemMessageList(msg) + } + +} + +/** + * + * INTERNAL API + * + * Value class supporting list operations on system messages. The `next` field of [[akka.dispatch.sysmsg.SystemMessage]] + * is hidden, and can only accessed through the value classes [[akka.dispatch.sysmsg.LatestFirstSystemMessageList]] and + * [[akka.dispatch.sysmsg.EarliestFirstSystemMessageList]], abstracting over the fact that system messages are the + * list nodes themselves. If used properly, this stays a compile time construct without any allocation overhead. + * + * This list is mutable. + * + * This list type also encodes that the messages contained are in reverse order, i.e. the head of the list is the + * latest appended element. + * + */ +class EarliestFirstSystemMessageList(val head: SystemMessage) extends AnyVal { + import SystemMessageList._ + + /** + * Indicates if the list is empty or not. This operation has constant cost. + */ + final def isEmpty: Boolean = head eq null + + /** + * Indicates if the list has at least one element or not. This operation has constant cost. + */ + final def nonEmpty: Boolean = head ne null + + /** + * Indicates if the list is empty or not. This operation has constant cost. + */ + final def size: Int = sizeInner(head, 0) + + /** + * Gives back the list containing all the elements except the first. This operation has constant cost. + * + * *Warning:* as the underlying list nodes (the [[akka.dispatch.sysmsg.SystemMessage]] instances) are mutable, care + * should be taken when passing the tail to other methods. [[akka.dispatch.sysmsg.SystemMessage#unlink]] should be + * called on the head if one wants to detach the tail permanently. + */ + final def tail: EarliestFirstSystemMessageList = new EarliestFirstSystemMessageList(head.next) + + /** + * Reverses the list. This operation mutates the underlying list. The cost of the call to reverse is linear in the + * number of elements. + * + * The type of the returned list is of the opposite order: [[akka.dispatch.sysmsg.LatestFirstSystemMessageList]] + */ + final def reverse: LatestFirstSystemMessageList = new LatestFirstSystemMessageList(reverseInner(head, null)) + + /** + * Attaches a message to the current head of the list. This operation has constant cost. + */ + final def ::(msg: SystemMessage): EarliestFirstSystemMessageList = { + assert(msg ne null) + msg.next = head + new EarliestFirstSystemMessageList(msg) + } + + /** + * Prepends a list in a reversed order to the head of this list. The prepended list will be reversed during the process. + * + * Example: (3, 4, 5) reversePrepend (2, 1, 0) == (0, 1, 2, 3, 4, 5) + * + * The cost of this operation is linear in the size of the list that is to be prepended. + */ + final def reverse_:::(other: LatestFirstSystemMessageList): EarliestFirstSystemMessageList = { + var remaining = other + var result = this + while (remaining.nonEmpty) { + val msg = remaining.head + remaining = remaining.tail + result ::= msg + } + result + } + +} + +/** + * System messages are handled specially: they form their own queue within + * each actor’s mailbox. This queue is encoded in the messages themselves to + * avoid extra allocations and overhead. The next pointer is a normal var, and + * it does not need to be volatile because in the enqueuing method its update + * is immediately succeeded by a volatile write and all reads happen after the + * volatile read in the dequeuing thread. Afterwards, the obtained list of + * system messages is handled in a single thread only and not ever passed around, + * hence no further synchronization is needed. + * + * INTERNAL API + * + * ➡➡➡ NEVER SEND THE SAME SYSTEM MESSAGE OBJECT TO TWO ACTORS ⬅⬅⬅ + */ +private[akka] sealed trait SystemMessage extends PossiblyHarmful with Serializable { + // Next fields are only modifiable via the SystemMessageList value class + @transient + private[sysmsg] var next: SystemMessage = _ + + def unlink(): Unit = next = null + + def unlinked: Boolean = next eq null +} + +trait StashWhenWaitingForChildren + +trait StashWhenFailed + +/** + * INTERNAL API + */ +@SerialVersionUID(-4836972106317757555L) +private[akka] case class Create(uid: Int) extends SystemMessage // send to self from Dispatcher.register +/** + * INTERNAL API + */ +@SerialVersionUID(686735569005808256L) +private[akka] case class Recreate(cause: Throwable) extends SystemMessage with StashWhenWaitingForChildren // sent to self from ActorCell.restart +/** + * INTERNAL API + */ +@SerialVersionUID(7270271967867221401L) +private[akka] case class Suspend() extends SystemMessage with StashWhenWaitingForChildren // sent to self from ActorCell.suspend +/** + * INTERNAL API + */ +@SerialVersionUID(-2567504317093262591L) +private[akka] case class Resume(causedByFailure: Throwable) extends SystemMessage with StashWhenWaitingForChildren // sent to self from ActorCell.resume +/** + * INTERNAL API + */ +@SerialVersionUID(708873453777219599L) +private[akka] case class Terminate() extends SystemMessage // sent to self from ActorCell.stop +/** + * INTERNAL API + */ +@SerialVersionUID(3245747602115485675L) +private[akka] case class Supervise(child: ActorRef, async: Boolean, uid: Int) extends SystemMessage // sent to supervisor ActorRef from ActorCell.start +/** + * INTERNAL API + */ +@SerialVersionUID(5513569382760799668L) +private[akka] case class ChildTerminated(child: ActorRef) extends SystemMessage // sent to supervisor from ActorCell.doTerminate +/** + * INTERNAL API + */ +@SerialVersionUID(3323205435124174788L) +private[akka] case class Watch(watchee: ActorRef, watcher: ActorRef) extends SystemMessage // sent to establish a DeathWatch +/** + * INTERNAL API + */ +@SerialVersionUID(6363620903363658256L) +private[akka] case class Unwatch(watchee: ActorRef, watcher: ActorRef) extends SystemMessage // sent to tear down a DeathWatch +/** + * INTERNAL API + */ +@SerialVersionUID(-5475916034683997987L) +private[akka] case object NoMessage extends SystemMessage // switched into the mailbox to signal termination + +/** + * INTERNAL API + */ +@SerialVersionUID(3L) +private[akka] case class Failed(child: ActorRef, cause: Throwable, uid: Int) extends SystemMessage + with StashWhenFailed + with StashWhenWaitingForChildren \ No newline at end of file diff --git a/akka-actor/src/main/scala/akka/pattern/AskSupport.scala b/akka-actor/src/main/scala/akka/pattern/AskSupport.scala index 1937a81110..7905acbea3 100644 --- a/akka-actor/src/main/scala/akka/pattern/AskSupport.scala +++ b/akka-actor/src/main/scala/akka/pattern/AskSupport.scala @@ -7,7 +7,7 @@ import language.implicitConversions import java.util.concurrent.TimeoutException import akka.actor._ -import akka.dispatch._ +import akka.dispatch.sysmsg._ import scala.annotation.tailrec import scala.util.control.NonFatal import scala.concurrent.{ Future, Promise, ExecutionContext } diff --git a/akka-actor/src/main/scala/akka/pattern/GracefulStopSupport.scala b/akka-actor/src/main/scala/akka/pattern/GracefulStopSupport.scala index 84980e4ee0..b8f60ed00d 100644 --- a/akka-actor/src/main/scala/akka/pattern/GracefulStopSupport.scala +++ b/akka-actor/src/main/scala/akka/pattern/GracefulStopSupport.scala @@ -6,7 +6,7 @@ package akka.pattern import akka.actor._ import akka.util.{ Timeout } -import akka.dispatch.{ Unwatch, Watch } +import akka.dispatch.sysmsg.{ Unwatch, Watch } import scala.concurrent.Future import scala.concurrent.duration.Duration import scala.util.Success diff --git a/akka-cluster/src/main/scala/akka/cluster/ClusterActorRefProvider.scala b/akka-cluster/src/main/scala/akka/cluster/ClusterActorRefProvider.scala index 0d06d4d64b..bde47e3c34 100644 --- a/akka-cluster/src/main/scala/akka/cluster/ClusterActorRefProvider.scala +++ b/akka-cluster/src/main/scala/akka/cluster/ClusterActorRefProvider.scala @@ -18,7 +18,7 @@ import akka.actor.Props import akka.actor.Scheduler import akka.actor.Scope import akka.actor.Terminated -import akka.dispatch.ChildTerminated +import akka.dispatch.sysmsg.ChildTerminated import akka.event.EventStream import akka.japi.Util.immutableSeq import akka.remote.RemoteActorRefProvider diff --git a/akka-docs/rst/general/message-delivery-guarantees.rst b/akka-docs/rst/general/message-delivery-guarantees.rst index d9544d7016..e398346254 100644 --- a/akka-docs/rst/general/message-delivery-guarantees.rst +++ b/akka-docs/rst/general/message-delivery-guarantees.rst @@ -111,6 +111,8 @@ implementation; it is always possible to add stricter guarantees on top of basic ones, but it is not possible to retro-actively remove guarantees in order to gain more performance. +.. _message-ordering: + Discussion: Message Ordering ---------------------------- @@ -153,6 +155,22 @@ Causal transitive ordering would imply that ``M2`` is never received before violated due to different message delivery latencies when ``A``, ``B`` and ``C`` reside on different network hosts, see more below. +Communication of failure +........................ + +Please note, that the ordering guarantees discussed above only hold for user messages between actors. Failure of a child +of an actor is communicated by special system messages that are not ordered relative to ordinary user messages. In +particular: + + Child actor ``C`` sends message ``M`` to its parent ``P`` + + Child actor fails with failure ``F`` + + Parent actor ``P`` might receive the two events either in order ``M``, ``F`` or ``F``, ``M`` + +The reason for this is that internal system messages has their own mailboxes therefore the ordering of enqueue calls of +a user and system message cannot guarantee the ordering of their dequeue times. + The Rules for In-JVM (Local) Message Sends ========================================== diff --git a/akka-docs/rst/general/supervision.rst b/akka-docs/rst/general/supervision.rst index 470079639a..88097a5b88 100644 --- a/akka-docs/rst/general/supervision.rst +++ b/akka-docs/rst/general/supervision.rst @@ -55,6 +55,15 @@ actors cannot be orphaned or attached to supervisors from the outside, which might otherwise catch them unawares. In addition, this yields a natural and clean shutdown procedure for (sub-trees of) actor applications. +.. warning:: + + Supervision related parent-child communication happens by special system + messages that have their own mailboxes separate from user messages. This + implies that supervision related events are not deterministically + ordered relative to ordinary messages. In general, the user cannot influence + the order of normal messages and failure notifications. For details and + example see the :ref:`message-ordering` section. + .. _toplevel-supervisors: The Top-Level Supervisors diff --git a/akka-docs/rst/java/untyped-actors.rst b/akka-docs/rst/java/untyped-actors.rst index 5365aa5130..8ae06710c3 100644 --- a/akka-docs/rst/java/untyped-actors.rst +++ b/akka-docs/rst/java/untyped-actors.rst @@ -246,6 +246,14 @@ that triggered the exception will not be received again. Any message sent to an actor while it is being restarted will be queued to its mailbox as usual. +.. warning:: + + Be aware that the ordering of failure notifications relative to user messages + is not deterministic. In particular, a parent might restart its child before + it has processed the last messages sent by the child before the failure. + See :ref:`message-ordering` for details. + + Stop Hook --------- diff --git a/akka-docs/rst/scala/actors.rst b/akka-docs/rst/scala/actors.rst index 8ec241d046..a383d47228 100644 --- a/akka-docs/rst/scala/actors.rst +++ b/akka-docs/rst/scala/actors.rst @@ -359,6 +359,13 @@ that triggered the exception will not be received again. Any message sent to an actor while it is being restarted will be queued to its mailbox as usual. +.. warning:: + + Be aware that the ordering of failure notifications relative to user messages + is not deterministic. In particular, a parent might restart its child before + it has processed the last messages sent by the child before the failure. + See :ref:`message-ordering` for details. + Stop Hook --------- diff --git a/akka-remote/src/main/scala/akka/remote/Endpoint.scala b/akka-remote/src/main/scala/akka/remote/Endpoint.scala index fe3f96f6ca..f133453162 100644 --- a/akka-remote/src/main/scala/akka/remote/Endpoint.scala +++ b/akka-remote/src/main/scala/akka/remote/Endpoint.scala @@ -5,7 +5,7 @@ package akka.remote import akka.{ OnlyCauseStackTrace, AkkaException } import akka.actor._ -import akka.dispatch.SystemMessage +import akka.dispatch.sysmsg.SystemMessage import akka.event.LoggingAdapter import akka.pattern.pipe import akka.remote.EndpointManager.Send diff --git a/akka-remote/src/main/scala/akka/remote/RemoteActorRefProvider.scala b/akka-remote/src/main/scala/akka/remote/RemoteActorRefProvider.scala index c23e3c66b2..4ac2b95229 100644 --- a/akka-remote/src/main/scala/akka/remote/RemoteActorRefProvider.scala +++ b/akka-remote/src/main/scala/akka/remote/RemoteActorRefProvider.scala @@ -5,7 +5,7 @@ package akka.remote import akka.actor._ -import akka.dispatch._ +import akka.dispatch.sysmsg._ import akka.event.{ Logging, LoggingAdapter, EventStream } import akka.event.Logging.Error import akka.serialization.{ JavaSerializer, Serialization, SerializationExtension } diff --git a/akka-remote/src/main/scala/akka/remote/RemoteDaemon.scala b/akka-remote/src/main/scala/akka/remote/RemoteDaemon.scala index 6f8b994d54..ed0e9b3134 100644 --- a/akka-remote/src/main/scala/akka/remote/RemoteDaemon.scala +++ b/akka-remote/src/main/scala/akka/remote/RemoteDaemon.scala @@ -8,7 +8,7 @@ import scala.annotation.tailrec import scala.util.control.NonFatal import akka.actor.{ VirtualPathContainer, Terminated, Deploy, Props, Nobody, LocalActorRef, InternalActorRef, Address, ActorSystemImpl, ActorRef, ActorPathExtractor, ActorPath, Actor, AddressTerminated } import akka.event.LoggingAdapter -import akka.dispatch.Watch +import akka.dispatch.sysmsg.Watch import akka.actor.ActorRefWithCell import akka.actor.ActorRefScope import akka.util.Switch diff --git a/akka-remote/src/main/scala/akka/remote/RemoteTransport.scala b/akka-remote/src/main/scala/akka/remote/RemoteTransport.scala index 5304ed5c77..c697068b07 100644 --- a/akka-remote/src/main/scala/akka/remote/RemoteTransport.scala +++ b/akka-remote/src/main/scala/akka/remote/RemoteTransport.scala @@ -4,12 +4,9 @@ package akka.remote -import akka.dispatch.SystemMessage -import akka.event.{ LoggingAdapter, Logging } import akka.AkkaException -import akka.serialization.Serialization -import akka.remote.RemoteProtocol._ import akka.actor._ +import akka.event.LoggingAdapter import scala.collection.immutable import scala.concurrent.Future diff --git a/akka-testkit/src/main/scala/akka/testkit/CallingThreadDispatcher.scala b/akka-testkit/src/main/scala/akka/testkit/CallingThreadDispatcher.scala index cd026e1902..361c6d4c29 100644 --- a/akka-testkit/src/main/scala/akka/testkit/CallingThreadDispatcher.scala +++ b/akka-testkit/src/main/scala/akka/testkit/CallingThreadDispatcher.scala @@ -10,7 +10,8 @@ import java.util.concurrent.locks.ReentrantLock import scala.annotation.tailrec import com.typesafe.config.Config import akka.actor.{ ActorInitializationException, ExtensionIdProvider, ExtensionId, Extension, ExtendedActorSystem, ActorRef, ActorCell } -import akka.dispatch.{ MessageQueue, MailboxType, TaskInvocation, SystemMessage, Suspend, Resume, MessageDispatcherConfigurator, MessageDispatcher, Mailbox, Envelope, DispatcherPrerequisites, DefaultSystemMessageQueue } +import akka.dispatch.{ MessageQueue, MailboxType, TaskInvocation, MessageDispatcherConfigurator, MessageDispatcher, Mailbox, Envelope, DispatcherPrerequisites, DefaultSystemMessageQueue } +import akka.dispatch.sysmsg.{ SystemMessage, Suspend, Resume } import scala.concurrent.duration._ import akka.util.Switch import scala.concurrent.duration.Duration diff --git a/akka-testkit/src/main/scala/akka/testkit/TestEventListener.scala b/akka-testkit/src/main/scala/akka/testkit/TestEventListener.scala index 57e3230e98..51541814d7 100644 --- a/akka-testkit/src/main/scala/akka/testkit/TestEventListener.scala +++ b/akka-testkit/src/main/scala/akka/testkit/TestEventListener.scala @@ -10,7 +10,7 @@ import scala.collection.immutable import scala.concurrent.duration.Duration import scala.reflect.ClassTag import akka.actor.{ DeadLetter, ActorSystem, Terminated, UnhandledMessage } -import akka.dispatch.{ SystemMessage, Terminate } +import akka.dispatch.sysmsg.{ SystemMessage, Terminate } import akka.event.Logging.{ Warning, LogEvent, InitializeLogger, Info, Error, Debug, LoggerInitialized } import akka.event.Logging import akka.actor.NoSerializationVerificationNeeded From 0f432e38beecb0b89b690d413a2bdafa34e733c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Endre=20S=C3=A1ndor=20Varga?= Date: Fri, 22 Mar 2013 13:36:09 +0100 Subject: [PATCH 37/47] Fixes after merge --- .../akka/actor/SupervisorHierarchySpec.scala | 2 +- .../sysmsg/SystemMessageListSpec.scala | 36 +++++++++---------- .../akka/serialization/SerializeSpec.scala | 4 +-- .../src/main/scala/akka/actor/ActorCell.scala | 16 +++------ .../scala/akka/actor/ActorRefProvider.scala | 2 +- .../scala/akka/actor/dungeon/Dispatch.scala | 2 +- .../akka/dispatch/sysmsg/SystemMessage.scala | 4 +-- 7 files changed, 30 insertions(+), 36 deletions(-) diff --git a/akka-actor-tests/src/test/scala/akka/actor/SupervisorHierarchySpec.scala b/akka-actor-tests/src/test/scala/akka/actor/SupervisorHierarchySpec.scala index b94936ce84..024311b730 100644 --- a/akka-actor-tests/src/test/scala/akka/actor/SupervisorHierarchySpec.scala +++ b/akka-actor-tests/src/test/scala/akka/actor/SupervisorHierarchySpec.scala @@ -243,7 +243,7 @@ object SupervisorHierarchySpec { if (failed || suspended) { listener ! ErrorLog("not resumed (" + failed + ", " + suspended + ")", log) val state = stateCache.get(self) - stateCache.put(self.path, state.copy(log = log)) + if (state ne null) stateCache.put(self.path, state.copy(log = log)) } else { stateCache.put(self.path, HierarchyState(log, Map(), null)) } diff --git a/akka-actor-tests/src/test/scala/akka/dispatch/sysmsg/SystemMessageListSpec.scala b/akka-actor-tests/src/test/scala/akka/dispatch/sysmsg/SystemMessageListSpec.scala index a7059cb748..101b1fe35c 100644 --- a/akka-actor-tests/src/test/scala/akka/dispatch/sysmsg/SystemMessageListSpec.scala +++ b/akka-actor-tests/src/test/scala/akka/dispatch/sysmsg/SystemMessageListSpec.scala @@ -18,9 +18,9 @@ class SystemMessageListSpec extends AkkaSpec { } "able to append messages" in { - val create0 = Create(0) - val create1 = Create(1) - val create2 = Create(2) + val create0 = Failed(null, null, 0) + val create1 = Failed(null, null, 1) + val create2 = Failed(null, null, 2) ((create0 :: LNil).head eq create0) must be(true) ((create1 :: create0 :: LNil).head eq create1) must be(true) ((create2 :: create1 :: create0 :: LNil).head eq create2) must be(true) @@ -31,9 +31,9 @@ class SystemMessageListSpec extends AkkaSpec { } "able to deconstruct head and tail" in { - val create0 = Create(0) - val create1 = Create(1) - val create2 = Create(2) + val create0 = Failed(null, null, 0) + val create1 = Failed(null, null, 1) + val create2 = Failed(null, null, 2) val list = create2 :: create1 :: create0 :: LNil (list.head eq create2) must be(true) @@ -43,9 +43,9 @@ class SystemMessageListSpec extends AkkaSpec { } "properly report size and emptyness" in { - val create0 = Create(0) - val create1 = Create(1) - val create2 = Create(2) + val create0 = Failed(null, null, 0) + val create1 = Failed(null, null, 1) + val create2 = Failed(null, null, 2) val list = create2 :: create1 :: create0 :: LNil list.size must be === 3 @@ -63,9 +63,9 @@ class SystemMessageListSpec extends AkkaSpec { } "properly reverse contents" in { - val create0 = Create(0) - val create1 = Create(1) - val create2 = Create(2) + val create0 = Failed(null, null, 0) + val create1 = Failed(null, null, 1) + val create2 = Failed(null, null, 2) val list = create2 :: create1 :: create0 :: LNil val listRev: EarliestFirstSystemMessageList = list.reverse @@ -87,12 +87,12 @@ class SystemMessageListSpec extends AkkaSpec { "EarliestFirstSystemMessageList" must { "properly prepend reversed message lists to the front" in { - val create0 = Create(0) - val create1 = Create(1) - val create2 = Create(2) - val create3 = Create(3) - val create4 = Create(4) - val create5 = Create(5) + val create0 = Failed(null, null, 0) + val create1 = Failed(null, null, 1) + val create2 = Failed(null, null, 2) + val create3 = Failed(null, null, 3) + val create4 = Failed(null, null, 4) + val create5 = Failed(null, null, 5) val fwdList = create3 :: create4 :: create5 :: ENil val revList = create2 :: create1 :: create0 :: LNil diff --git a/akka-actor-tests/src/test/scala/akka/serialization/SerializeSpec.scala b/akka-actor-tests/src/test/scala/akka/serialization/SerializeSpec.scala index 26c551e8be..5b4ee232c0 100644 --- a/akka-actor-tests/src/test/scala/akka/serialization/SerializeSpec.scala +++ b/akka-actor-tests/src/test/scala/akka/serialization/SerializeSpec.scala @@ -333,7 +333,7 @@ class SerializationCompatibilitySpec extends AkkaSpec(SerializationTests.mostlyR String.valueOf(encodeHex(ser.serialize(obj, obj.getClass).get)) must be(asExpected) "be preserved for the Create SystemMessage" in { - verify(Create(), "aced00057372000c7363616c612e5475706c6532bc7daadf46211a990200024c00025f317400124c6a6176612f6c616e672f4f626a6563743b4c00025f3271007e0001787073720014616b6b612e64697370617463682e437265617465000000000000000302000078707671007e0003") + verify(Create(), "aced00057372000c7363616c612e5475706c6532bc7daadf46211a990200024c00025f317400124c6a6176612f6c616e672f4f626a6563743b4c00025f3271007e000178707372001b616b6b612e64697370617463682e7379736d73672e437265617465bcdf9f7f2675038d02000078707671007e0003") } "be preserved for the Recreate SystemMessage" in { verify(Recreate(null), "aced00057372000c7363616c612e5475706c6532bc7daadf46211a990200024c00025f317400124c6a6176612f6c616e672f4f626a6563743b4c00025f3271007e000178707372001d616b6b612e64697370617463682e7379736d73672e52656372656174650987c65c8d378a800200014c000563617573657400154c6a6176612f6c616e672f5468726f7761626c653b7870707671007e0003") @@ -348,7 +348,7 @@ class SerializationCompatibilitySpec extends AkkaSpec(SerializationTests.mostlyR verify(Terminate(), "aced00057372000c7363616c612e5475706c6532bc7daadf46211a990200024c00025f317400124c6a6176612f6c616e672f4f626a6563743b4c00025f3271007e000178707372001e616b6b612e64697370617463682e7379736d73672e5465726d696e61746509d66ca68318700f02000078707671007e0003") } "be preserved for the Supervise SystemMessage" in { - verify(Supervise(FakeActorRef("child"), true), "aced00057372000c7363616c612e5475706c6532bc7daadf46211a990200024c00025f317400124c6a6176612f6c616e672f4f626a6563743b4c00025f3271007e0001787073720017616b6b612e64697370617463682e53757065727669736500000000000000030200025a00056173796e634c00056368696c647400154c616b6b612f6163746f722f4163746f725265663b7870017372001f616b6b612e73657269616c697a6174696f6e2e46616b654163746f7252656600000000000000010200014c00046e616d657400124c6a6176612f6c616e672f537472696e673b7872001b616b6b612e6163746f722e496e7465726e616c4163746f725265660d0aa2ca1e82097602000078720013616b6b612e6163746f722e4163746f72526566c3585dde655f469402000078707400056368696c647671007e0003") + verify(Supervise(FakeActorRef("child"), true), "aced00057372000c7363616c612e5475706c6532bc7daadf46211a990200024c00025f317400124c6a6176612f6c616e672f4f626a6563743b4c00025f3271007e000178707372001e616b6b612e64697370617463682e7379736d73672e5375706572766973652d0b363f56ab5feb0200025a00056173796e634c00056368696c647400154c616b6b612f6163746f722f4163746f725265663b7870017372001f616b6b612e73657269616c697a6174696f6e2e46616b654163746f7252656600000000000000010200014c00046e616d657400124c6a6176612f6c616e672f537472696e673b7872001b616b6b612e6163746f722e496e7465726e616c4163746f72526566db6eaed9e69a356302000078720013616b6b612e6163746f722e4163746f72526566c3585dde655f469402000078707400056368696c647671007e0003") } "be preserved for the ChildTerminated SystemMessage" in { verify(ChildTerminated(FakeActorRef("child")), "aced00057372000c7363616c612e5475706c6532bc7daadf46211a990200024c00025f317400124c6a6176612f6c616e672f4f626a6563743b4c00025f3271007e0001787073720024616b6b612e64697370617463682e7379736d73672e4368696c645465726d696e617465644c84222437ed5db40200014c00056368696c647400154c616b6b612f6163746f722f4163746f725265663b78707372001f616b6b612e73657269616c697a6174696f6e2e46616b654163746f7252656600000000000000010200014c00046e616d657400124c6a6176612f6c616e672f537472696e673b7872001b616b6b612e6163746f722e496e7465726e616c4163746f72526566db6eaed9e69a356302000078720013616b6b612e6163746f722e4163746f72526566c3585dde655f469402000078707400056368696c647671007e0003") diff --git a/akka-actor/src/main/scala/akka/actor/ActorCell.scala b/akka-actor/src/main/scala/akka/actor/ActorCell.scala index 649db7cd02..efd968951e 100644 --- a/akka-actor/src/main/scala/akka/actor/ActorCell.scala +++ b/akka-actor/src/main/scala/akka/actor/ActorCell.scala @@ -8,22 +8,16 @@ import akka.actor.dungeon.ChildrenContainer import akka.dispatch.Envelope import akka.dispatch.NullMessage import akka.dispatch.sysmsg._ -import akka.event.Logging.Debug -import akka.event.Logging.{ LogEvent, Error } +import akka.dispatch.sysmsg.{ Watch, Unwatch, Terminate, SystemMessage, Suspend, Supervise, Resume, Recreate, NoMessage, Create, ChildTerminated } +import akka.event.Logging.{ LogEvent, Debug, Error } import akka.japi.Procedure import java.io.{ ObjectOutputStream, NotSerializableException } import scala.annotation.{ switch, tailrec } import scala.collection.immutable import scala.concurrent.ExecutionContext import scala.concurrent.duration.Duration +import scala.concurrent.forkjoin.ThreadLocalRandom import scala.util.control.NonFatal -import akka.actor.dungeon.ChildrenContainer -import akka.actor.dungeon.ChildrenContainer.WaitingForChildren -import akka.dispatch.{ Watch, Unwatch, Terminate, SystemMessage, Suspend, Supervise, Resume, Recreate, NoMessage, MessageDispatcher, Envelope, Create, ChildTerminated } -import akka.event.Logging.{ LogEvent, Debug, Error } -import akka.japi.Procedure -import akka.dispatch.NullMessage -import scala.concurrent.ExecutionContext /** * The actor context - the view of the actor cell from the actor. @@ -430,14 +424,14 @@ private[akka] class ActorCell( message match { case message: SystemMessage if shouldStash(message, currentState) ⇒ stash(message) case f: Failed ⇒ handleFailure(f) - case Create() ⇒ create(uid) + case Create() ⇒ create() case Watch(watchee, watcher) ⇒ addWatcher(watchee, watcher) case Unwatch(watchee, watcher) ⇒ remWatcher(watchee, watcher) case Recreate(cause) ⇒ faultRecreate(cause) case Suspend() ⇒ faultSuspend() case Resume(inRespToFailure) ⇒ faultResume(inRespToFailure) case Terminate() ⇒ terminate() - case Supervise(child, async) ⇒ supervise(child, async, uid) + case Supervise(child, async) ⇒ supervise(child, async) case ChildTerminated(child) ⇒ handleChildTerminated(child) case NoMessage ⇒ // only here to suppress warning } diff --git a/akka-actor/src/main/scala/akka/actor/ActorRefProvider.scala b/akka-actor/src/main/scala/akka/actor/ActorRefProvider.scala index 4a01b87f95..11f36cfc22 100644 --- a/akka-actor/src/main/scala/akka/actor/ActorRefProvider.scala +++ b/akka-actor/src/main/scala/akka/actor/ActorRefProvider.scala @@ -397,7 +397,7 @@ class LocalActorRefProvider private[akka] ( override def sendSystemMessage(message: SystemMessage): Unit = stopped ifOff { message match { case Failed(child, ex, _) ⇒ { causeOfTermination = Some(ex); child.asInstanceOf[InternalActorRef].stop() } - case Supervise(_, _) ⇒ // TODO register child in some map to keep track of it and enable shutdown after all dead + case Supervise(_, _) ⇒ // TODO register child in some map to keep track of it and enable shutdown after all dead case ChildTerminated(_) ⇒ stop() case _ ⇒ log.error(this + " received unexpected system message [" + message + "]") } diff --git a/akka-actor/src/main/scala/akka/actor/dungeon/Dispatch.scala b/akka-actor/src/main/scala/akka/actor/dungeon/Dispatch.scala index 4235330b7e..b6b1880bbe 100644 --- a/akka-actor/src/main/scala/akka/actor/dungeon/Dispatch.scala +++ b/akka-actor/src/main/scala/akka/actor/dungeon/Dispatch.scala @@ -54,7 +54,7 @@ private[akka] trait Dispatch { this: ActorCell ⇒ if (sendSupervise) { // ➡➡➡ NEVER SEND THE SAME SYSTEM MESSAGE OBJECT TO TWO ACTORS ⬅⬅⬅ - parent.sendSystemMessage(akka.dispatch.sysmsg.Supervise(self, async = false, uid)) + parent.sendSystemMessage(akka.dispatch.sysmsg.Supervise(self, async = false)) parent ! NullMessage // read ScalaDoc of NullMessage to see why } this diff --git a/akka-actor/src/main/scala/akka/dispatch/sysmsg/SystemMessage.scala b/akka-actor/src/main/scala/akka/dispatch/sysmsg/SystemMessage.scala index a62dd03141..7f043ebd3d 100644 --- a/akka-actor/src/main/scala/akka/dispatch/sysmsg/SystemMessage.scala +++ b/akka-actor/src/main/scala/akka/dispatch/sysmsg/SystemMessage.scala @@ -201,7 +201,7 @@ trait StashWhenFailed * INTERNAL API */ @SerialVersionUID(-4836972106317757555L) -private[akka] case class Create(uid: Int) extends SystemMessage // send to self from Dispatcher.register +private[akka] case class Create() extends SystemMessage // send to self from Dispatcher.register /** * INTERNAL API */ @@ -226,7 +226,7 @@ private[akka] case class Terminate() extends SystemMessage // sent to self from * INTERNAL API */ @SerialVersionUID(3245747602115485675L) -private[akka] case class Supervise(child: ActorRef, async: Boolean, uid: Int) extends SystemMessage // sent to supervisor ActorRef from ActorCell.start +private[akka] case class Supervise(child: ActorRef, async: Boolean) extends SystemMessage // sent to supervisor ActorRef from ActorCell.start /** * INTERNAL API */ From 61d1e67c1c1767cf796f7a3ae091b0a8fe2121ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Endre=20S=C3=A1ndor=20Varga?= Date: Fri, 22 Mar 2013 14:12:29 +0100 Subject: [PATCH 38/47] Minor fix to SupervisorHierarchySpec to ignore Terminated from old child --- .../scala/akka/actor/SupervisorHierarchySpec.scala | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/akka-actor-tests/src/test/scala/akka/actor/SupervisorHierarchySpec.scala b/akka-actor-tests/src/test/scala/akka/actor/SupervisorHierarchySpec.scala index 024311b730..f2a937112a 100644 --- a/akka-actor-tests/src/test/scala/akka/actor/SupervisorHierarchySpec.scala +++ b/akka-actor-tests/src/test/scala/akka/actor/SupervisorHierarchySpec.scala @@ -283,11 +283,14 @@ object SupervisorHierarchySpec { * (if the unwatch() came too late), so just ignore in this case. */ val name = ref.path.name - if (pongsToGo == 0 && context.child(name).isEmpty) { - listener ! Died(ref.path) - val kids = stateCache.get(self.path).kids(ref.path) - val props = Props(new Hierarchy(kids, breadth, listener, myLevel + 1)).withDispatcher("hierarchy") - context.watch(context.actorOf(props, name)) + if (pongsToGo == 0) { + if (!context.child(name).exists(_ != ref)) { + listener ! Died(ref.path) + val kids = stateCache.get(self.path).kids(ref.path) + val props = Props(new Hierarchy(kids, breadth, listener, myLevel + 1)).withDispatcher("hierarchy") + context.watch(context.actorOf(props, name)) + } + // Otherwise it is a Terminated from an old child. Ignore. } else { // WARNING: The Terminated that is logged by this is logged by check() above, too. It is not // an indication of duplicate Terminate messages From 405f34cc53f62a3537999987016e5b13fc4ad97f Mon Sep 17 00:00:00 2001 From: Patrik Nordwall Date: Fri, 22 Mar 2013 17:22:06 +0100 Subject: [PATCH 39/47] maven exec plugin doesn't have to be added to pom.xml --- akka-docs/rst/cluster/cluster-usage-java.rst | 21 +++----------------- 1 file changed, 3 insertions(+), 18 deletions(-) diff --git a/akka-docs/rst/cluster/cluster-usage-java.rst b/akka-docs/rst/cluster/cluster-usage-java.rst index a6c1542529..09a4f6d3c8 100644 --- a/akka-docs/rst/cluster/cluster-usage-java.rst +++ b/akka-docs/rst/cluster/cluster-usage-java.rst @@ -54,22 +54,7 @@ ip-addresses or host names of the machines in ``application.conf`` instead of `` .. literalinclude:: ../../../akka-samples/akka-sample-cluster/src/main/java/sample/cluster/simple/japi/SimpleClusterApp.java :language: java -3. Add `maven exec plugin `_ to your pom.xml:: - - - - - - org.codehaus.mojo - exec-maven-plugin - 1.2.1 - - - - - - -4. Start the first seed node. Open a terminal window and run (one line):: +3. Start the first seed node. Open a terminal window and run (one line):: mvn exec:java -Dexec.mainClass="sample.cluster.simple.japi.SimpleClusterApp" \ -Dexec.args="2551" @@ -77,7 +62,7 @@ ip-addresses or host names of the machines in ``application.conf`` instead of `` 2551 corresponds to the port of the first seed-nodes element in the configuration. In the log output you see that the cluster node has been started and changed status to 'Up'. -5. Start the second seed node. Open another terminal window and run:: +4. Start the second seed node. Open another terminal window and run:: mvn exec:java -Dexec.mainClass="sample.cluster.simple.japi.SimpleClusterApp" \ -Dexec.args="2552" @@ -89,7 +74,7 @@ and becomes a member of the cluster. Its status changed to 'Up'. Switch over to the first terminal window and see in the log output that the member joined. -6. Start another node. Open a maven session in yet another terminal window and run:: +5. Start another node. Open a maven session in yet another terminal window and run:: mvn exec:java -Dexec.mainClass="sample.cluster.simple.japi.SimpleClusterApp" From a8361eb22f93e8973fb988717d820175783034f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Antonsson?= Date: Thu, 21 Mar 2013 15:41:45 +0100 Subject: [PATCH 40/47] Ignore members that can't be leader during convergence check. See #3150 --- .../src/main/scala/akka/cluster/Gossip.scala | 19 +++++++++++-------- .../akka/cluster/ClusterDomainEventSpec.scala | 2 +- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/akka-cluster/src/main/scala/akka/cluster/Gossip.scala b/akka-cluster/src/main/scala/akka/cluster/Gossip.scala index a5eac87bfb..70d79e5c22 100644 --- a/akka-cluster/src/main/scala/akka/cluster/Gossip.scala +++ b/akka-cluster/src/main/scala/akka/cluster/Gossip.scala @@ -17,6 +17,8 @@ private[cluster] object Gossip { def apply(members: immutable.SortedSet[Member]) = if (members.isEmpty) empty else empty.copy(members = members) + + private val leaderMemberStatus = Set[MemberStatus](Up, Leaving) } /** @@ -68,11 +70,11 @@ private[cluster] case class Gossip( throw new IllegalArgumentException("Same nodes in both members and unreachable is not allowed, got [%s]" format unreachableAndLive.mkString(", ")) - val allowedLiveMemberStatuses: Set[MemberStatus] = Set(Joining, Up, Leaving, Exiting) - def hasNotAllowedLiveMemberStatus(m: Member) = !allowedLiveMemberStatuses.contains(m.status) + val allowedLiveMemberStatus: Set[MemberStatus] = Set(Joining, Up, Leaving, Exiting) + def hasNotAllowedLiveMemberStatus(m: Member) = !allowedLiveMemberStatus(m.status) if (members exists hasNotAllowedLiveMemberStatus) throw new IllegalArgumentException("Live members must have status [%s], got [%s]" - format (allowedLiveMemberStatuses.mkString(", "), + format (allowedLiveMemberStatus.mkString(", "), (members filter hasNotAllowedLiveMemberStatus).mkString(", "))) val seenButNotMember = overview.seen.keySet -- members.map(_.address) -- overview.unreachable.map(_.address) @@ -177,10 +179,11 @@ private[cluster] case class Gossip( // 1. we don't have any members that are unreachable, or // 2. all unreachable members in the set have status DOWN // Else we can't continue to check for convergence - // When that is done we check that all members exists in the seen table and - // have the latest vector clock version - - overview.unreachable.forall(_.status == Down) && members.forall(m ⇒ seenByAddress(m.address)) + // When that is done we check that all memebers with a leader + // status is in the seen table and has the latest vector clock + // version + overview.unreachable.forall(_.status == Down) && + !members.exists(m ⇒ Gossip.leaderMemberStatus(m.status) && !seenByAddress(m.address)) } def isLeader(address: Address): Boolean = leader == Some(address) @@ -191,7 +194,7 @@ private[cluster] case class Gossip( private def leaderOf(mbrs: immutable.SortedSet[Member]): Option[Address] = { if (mbrs.isEmpty) None - else mbrs.find(m ⇒ m.status != Joining && m.status != Exiting && m.status != Down). + else mbrs.find(m ⇒ Gossip.leaderMemberStatus(m.status)). orElse(Some(mbrs.min(Member.leaderStatusOrdering))).map(_.address) } diff --git a/akka-cluster/src/test/scala/akka/cluster/ClusterDomainEventSpec.scala b/akka-cluster/src/test/scala/akka/cluster/ClusterDomainEventSpec.scala index f624012307..fc16b503df 100644 --- a/akka-cluster/src/test/scala/akka/cluster/ClusterDomainEventSpec.scala +++ b/akka-cluster/src/test/scala/akka/cluster/ClusterDomainEventSpec.scala @@ -87,7 +87,7 @@ class ClusterDomainEventSpec extends WordSpec with MustMatchers { diffMemberEvents(g1, g2) must be(Seq.empty) diffUnreachable(g1, g2) must be(Seq.empty) - diffSeen(g1, g2) must be(Seq(SeenChanged(convergence = false, seenBy = Set(aUp.address, bUp.address)))) + diffSeen(g1, g2) must be(Seq(SeenChanged(convergence = true, seenBy = Set(aUp.address, bUp.address)))) diffMemberEvents(g2, g1) must be(Seq.empty) diffUnreachable(g2, g1) must be(Seq.empty) diffSeen(g2, g1) must be(Seq(SeenChanged(convergence = true, seenBy = Set(aUp.address, bUp.address, eJoining.address)))) From d49b8aa47c840c04bc03c92ce2b0b4d2f8b39a3c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Antonsson?= Date: Mon, 25 Mar 2013 11:48:12 +0100 Subject: [PATCH 41/47] Correct import statement. --- .../scala/akka/remote/transport/ThrottlerTransportAdapter.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/akka-remote/src/main/scala/akka/remote/transport/ThrottlerTransportAdapter.scala b/akka-remote/src/main/scala/akka/remote/transport/ThrottlerTransportAdapter.scala index f6c7c59e29..19b6b0b1d9 100644 --- a/akka-remote/src/main/scala/akka/remote/transport/ThrottlerTransportAdapter.scala +++ b/akka-remote/src/main/scala/akka/remote/transport/ThrottlerTransportAdapter.scala @@ -22,7 +22,7 @@ import scala.math.min import scala.util.{ Success, Failure } import scala.util.control.NonFatal import scala.concurrent.duration._ -import akka.dispatch.{ Unwatch, Watch } +import akka.dispatch.sysmsg.{ Unwatch, Watch } class ThrottlerProvider extends TransportAdapterProvider { From 118917d2be4858bb2ab0203ad8bb9b2f58c3a316 Mon Sep 17 00:00:00 2001 From: Patrik Nordwall Date: Fri, 22 Mar 2013 18:33:14 +0100 Subject: [PATCH 42/47] awaitCond = awaitCond with better error reporting, see #3168 --- .../code/docs/testkit/TestKitDocTest.java | 18 +++++ akka-docs/rst/java/testing.rst | 7 ++ akka-docs/rst/scala/testing.rst | 9 +++ .../main/java/akka/testkit/JavaTestKit.java | 81 ++++++++++++------- .../src/main/scala/akka/testkit/TestKit.scala | 34 +++++++- .../scala/akka/testkit/TestTimeSpec.scala | 12 ++- 6 files changed, 130 insertions(+), 31 deletions(-) diff --git a/akka-docs/rst/java/code/docs/testkit/TestKitDocTest.java b/akka-docs/rst/java/code/docs/testkit/TestKitDocTest.java index 0b0ec98848..ccdb1b38e4 100644 --- a/akka-docs/rst/java/code/docs/testkit/TestKitDocTest.java +++ b/akka-docs/rst/java/code/docs/testkit/TestKitDocTest.java @@ -187,6 +187,24 @@ public class TestKitDocTest { }}; //#test-awaitCond } + + @Test + public void demonstrateAwaitAssert() { + //#test-awaitAssert + new JavaTestKit(system) {{ + getRef().tell(42, null); + new AwaitAssert( + duration("1 second"), // maximum wait time + duration("100 millis") // interval at which to check the condition + ) { + // do not put code outside this method, will run afterwards + protected void check() { + assertEquals(msgAvailable(), true); + } + }; + }}; + //#test-awaitAssert + } @Test @SuppressWarnings("unchecked") // due to generic varargs diff --git a/akka-docs/rst/java/testing.rst b/akka-docs/rst/java/testing.rst index 0f8311821a..a9a0d40dd9 100644 --- a/akka-docs/rst/java/testing.rst +++ b/akka-docs/rst/java/testing.rst @@ -282,6 +282,13 @@ code blocks: reception, the embedded condition can compute the boolean result from anything in scope. + * **AwaitAssert** + + .. includecode:: code/docs/testkit/TestKitDocTest.java#test-awaitAssert + + This general construct is not connected with the test kit’s message + reception, the embedded assert can check anything in scope. + There are also cases where not all messages sent to the test kit are actually relevant to the test, but removing them would mean altering the actors under test. For this purpose it is possible to ignore certain messages: diff --git a/akka-docs/rst/scala/testing.rst b/akka-docs/rst/scala/testing.rst index 0de33fc35c..6a97784e15 100644 --- a/akka-docs/rst/scala/testing.rst +++ b/akka-docs/rst/scala/testing.rst @@ -304,6 +304,15 @@ with message flows: maximum defaults to the time remaining in the innermost enclosing :ref:`within ` block. + * :meth:`awaitAssert(a: => Any, max: Duration, interval: Duration)` + + Poll the given assert function every :obj:`interval` until it does not throw + an exception or the :obj:`max` duration is used up. If the timeout expires the + last exception is thrown. The interval defaults to 100 ms and the maximum defaults + to the time remaining in the innermost enclosing :ref:`within ` + block.The interval defaults to 100 ms and the maximum defaults to the time + remaining in the innermost enclosing :ref:`within ` block. + * :meth:`ignoreMsg(pf: PartialFunction[AnyRef, Boolean])` :meth:`ignoreNoMsg` diff --git a/akka-testkit/src/main/java/akka/testkit/JavaTestKit.java b/akka-testkit/src/main/java/akka/testkit/JavaTestKit.java index 6989a28fb8..69e5957eb5 100644 --- a/akka-testkit/src/main/java/akka/testkit/JavaTestKit.java +++ b/akka-testkit/src/main/java/akka/testkit/JavaTestKit.java @@ -14,7 +14,8 @@ import scala.concurrent.duration.Duration; import scala.concurrent.duration.FiniteDuration; /** - * Java API for the TestProbe. Proper JavaDocs to come once JavaDoccing is implemented. + * Java API for the TestProbe. Proper JavaDocs to come once JavaDoccing is + * implemented. */ public class JavaTestKit { private final TestProbe p; @@ -30,13 +31,15 @@ public class JavaTestKit { public ActorSystem getSystem() { return p.system(); } - + static public FiniteDuration duration(String s) { final Duration ret = Duration.apply(s); - if (ret instanceof FiniteDuration) return (FiniteDuration) ret; - else throw new IllegalArgumentException("duration() is only for finite durations, use Duration.Inf() and friends"); + if (ret instanceof FiniteDuration) + return (FiniteDuration) ret; + else + throw new IllegalArgumentException("duration() is only for finite durations, use Duration.Inf() and friends"); } - + public Duration dilated(Duration d) { return d.mul(TestKitExtension.get(p.system()).TestTimeFactor()); } @@ -137,7 +140,7 @@ public class JavaTestKit { } }, max, interval, p.awaitCond$default$4()); } - + public AwaitCond(Duration max, Duration interval, String message) { p.awaitCond(new AbstractFunction0() { public Object apply() { @@ -147,6 +150,27 @@ public class JavaTestKit { } } + public abstract class AwaitAssert { + protected abstract void check(); + + public AwaitAssert() { + this(Duration.Undefined(), p.awaitAssert$default$3()); + } + + public AwaitAssert(Duration max) { + this(max, p.awaitAssert$default$3()); + } + + public AwaitAssert(Duration max, Duration interval) { + p.awaitAssert(new AbstractFunction0() { + public Object apply() { + check(); + return null; + } + }, max, interval); + } + } + public abstract class ExpectMsg { private final T result; @@ -159,8 +183,7 @@ public class JavaTestKit { try { result = match(received); } catch (JavaPartialFunction.NoMatchException ex) { - throw new AssertionError("while expecting '" + hint - + "' received unexpected: " + received); + throw new AssertionError("while expecting '" + hint + "' received unexpected: " + received); } } @@ -200,13 +223,11 @@ public class JavaTestKit { } public Object[] expectMsgAllOf(Object... msgs) { - return (Object[]) p.expectMsgAllOf(Util.immutableSeq(msgs)).toArray( - Util.classTag(Object.class)); + return (Object[]) p.expectMsgAllOf(Util.immutableSeq(msgs)).toArray(Util.classTag(Object.class)); } public Object[] expectMsgAllOf(FiniteDuration max, Object... msgs) { - return (Object[]) p.expectMsgAllOf(max, Util.immutableSeq(msgs)).toArray( - Util.classTag(Object.class)); + return (Object[]) p.expectMsgAllOf(max, Util.immutableSeq(msgs)).toArray(Util.classTag(Object.class)); } @SuppressWarnings("unchecked") @@ -254,12 +275,11 @@ public class JavaTestKit { @SuppressWarnings("unchecked") public ReceiveWhile(Class clazz, Duration max, Duration idle, int messages) { - results = p.receiveWhile(max, idle, messages, - new CachingPartialFunction() { - public T match(Object msg) throws Exception { - return ReceiveWhile.this.match(msg); - } - }).toArray(Util.classTag(clazz)); + results = p.receiveWhile(max, idle, messages, new CachingPartialFunction() { + public T match(Object msg) throws Exception { + return ReceiveWhile.this.match(msg); + } + }).toArray(Util.classTag(clazz)); } protected RuntimeException noMatch() { @@ -274,16 +294,16 @@ public class JavaTestKit { public abstract class EventFilter { abstract protected T run(); - + private final Class clazz; - + private String source = null; private String message = null; private boolean pattern = false; private boolean complete = false; private int occurrences = Integer.MAX_VALUE; private Class exceptionType = null; - + @SuppressWarnings("unchecked") public EventFilter(Class clazz) { if (Throwable.class.isAssignableFrom(clazz)) { @@ -291,13 +311,15 @@ public class JavaTestKit { exceptionType = (Class) clazz; } else if (Logging.LogEvent.class.isAssignableFrom(clazz)) { this.clazz = (Class) clazz; - } else throw new IllegalArgumentException("supplied class must either be LogEvent or Throwable"); + } else + throw new IllegalArgumentException("supplied class must either be LogEvent or Throwable"); } public T exec() { akka.testkit.EventFilter filter; if (clazz == Logging.Error.class) { - if (exceptionType == null) exceptionType = Logging.noCause().getClass(); + if (exceptionType == null) + exceptionType = Logging.noCause().getClass(); filter = new ErrorFilter(exceptionType, source, message, pattern, complete, occurrences); } else if (clazz == Logging.Warning.class) { filter = new WarningFilter(source, message, pattern, complete, occurrences); @@ -305,39 +327,40 @@ public class JavaTestKit { filter = new InfoFilter(source, message, pattern, complete, occurrences); } else if (clazz == Logging.Debug.class) { filter = new DebugFilter(source, message, pattern, complete, occurrences); - } else throw new IllegalArgumentException("unknown LogLevel " + clazz); + } else + throw new IllegalArgumentException("unknown LogLevel " + clazz); return filter.intercept(new AbstractFunction0() { public T apply() { return run(); } }, p.system()); } - + public EventFilter message(String msg) { message = msg; pattern = false; complete = true; return this; } - + public EventFilter startsWith(String msg) { message = msg; pattern = false; complete = false; return this; } - + public EventFilter matches(String regex) { message = regex; pattern = true; return this; } - + public EventFilter from(String source) { this.source = source; return this; } - + public EventFilter occurrences(int number) { occurrences = number; return this; diff --git a/akka-testkit/src/main/scala/akka/testkit/TestKit.scala b/akka-testkit/src/main/scala/akka/testkit/TestKit.scala index da6b1363d3..cb48deb302 100644 --- a/akka-testkit/src/main/scala/akka/testkit/TestKit.scala +++ b/akka-testkit/src/main/scala/akka/testkit/TestKit.scala @@ -4,7 +4,6 @@ package akka.testkit import language.postfixOps - import scala.annotation.{ varargs, tailrec } import scala.collection.immutable import scala.concurrent.duration._ @@ -14,6 +13,7 @@ import java.util.concurrent.atomic.AtomicInteger import akka.actor._ import akka.actor.Actor._ import akka.util.{ Timeout, BoxedType } +import scala.util.control.NonFatal object TestActor { type Ignore = Option[PartialFunction[Any, Boolean]] @@ -227,6 +227,38 @@ trait TestKitBase { poll(_max min interval) } + /** + * Await until the given assert does not throw an exception or the timeout + * expires, whichever comes first. If the timeout expires the last exception + * is thrown. + * + * If no timeout is given, take it from the innermost enclosing `within` + * block. + * + * Note that the timeout is scaled using Duration.dilated, + * which uses the configuration entry "akka.test.timefactor". + */ + def awaitAssert(a: ⇒ Any, max: Duration = Duration.Undefined, interval: Duration = 100.millis) { + val _max = remainingOrDilated(max) + val stop = now + _max + + @tailrec + def poll(t: Duration) { + val failed = + try { a; false } catch { + case NonFatal(e) ⇒ + if (now >= stop) throw e + true + } + if (failed) { + Thread.sleep(t.toMillis) + poll((stop - now) min interval) + } + } + + poll(_max min interval) + } + /** * Execute code block while bounding its execution time between `min` and * `max`. `within` blocks may be nested. All methods in this trait which diff --git a/akka-testkit/src/test/scala/akka/testkit/TestTimeSpec.scala b/akka-testkit/src/test/scala/akka/testkit/TestTimeSpec.scala index 4ca3969ab0..f0f027b433 100644 --- a/akka-testkit/src/test/scala/akka/testkit/TestTimeSpec.scala +++ b/akka-testkit/src/test/scala/akka/testkit/TestTimeSpec.scala @@ -2,8 +2,9 @@ package akka.testkit import org.scalatest.matchers.MustMatchers import org.scalatest.{ BeforeAndAfterEach, WordSpec } -import scala.concurrent.duration.Duration +import scala.concurrent.duration._ import com.typesafe.config.Config +import org.scalatest.exceptions.TestFailedException @org.junit.runner.RunWith(classOf[org.scalatest.junit.JUnitRunner]) class TestTimeSpec extends AkkaSpec(Map("akka.test.timefactor" -> 2.0)) with BeforeAndAfterEach { @@ -20,6 +21,15 @@ class TestTimeSpec extends AkkaSpec(Map("akka.test.timefactor" -> 2.0)) with Bef diff must be < (target + 300000000l) } + "awaitAssert must throw correctly" in { + awaitAssert("foo" must be("foo")) + within(300.millis, 2.seconds) { + intercept[TestFailedException] { + awaitAssert("foo" must be("bar"), 500.millis) + } + } + } + } } From 806fc0c52570acfb0277de62286d8790be16758c Mon Sep 17 00:00:00 2001 From: Patrik Nordwall Date: Sun, 24 Mar 2013 22:01:57 +0100 Subject: [PATCH 43/47] Use awaitAssert in cluster tests, see #3168 --- .../akka/cluster/ClusterDeathWatchSpec.scala | 18 ++++----- .../akka/cluster/ClusterMetricsSpec.scala | 6 +-- .../scala/akka/cluster/ConvergenceSpec.scala | 8 ++-- .../akka/cluster/InitialHeartbeatSpec.scala | 4 +- .../akka/cluster/LeaderElectionSpec.scala | 6 +-- .../akka/cluster/LeaderLeavingSpec.scala | 8 ++-- .../scala/akka/cluster/MBeanSpec.scala | 25 ++++++------ .../akka/cluster/MinMembersBeforeUpSpec.scala | 10 ++--- .../akka/cluster/MultiNodeClusterSpec.scala | 13 +++--- ...LeavingAndExitingAndBeingRemovedSpec.scala | 4 +- .../cluster/NodeLeavingAndExitingSpec.scala | 2 +- .../akka/cluster/NodeMembershipSpec.scala | 12 ++---- .../cluster/RestartFirstSeedNodeSpec.scala | 10 ++--- .../scala/akka/cluster/StressSpec.scala | 8 ++-- .../scala/akka/cluster/TransitionSpec.scala | 40 +++++++++---------- .../UnreachableNodeRejoinsClusterSpec.scala | 22 +++++----- .../AdaptiveLoadBalancingRouterSpec.scala | 19 +++------ .../ClusterConsistentHashingRouterSpec.scala | 24 ++++------- .../ClusterRoundRobinRoutedActorSpec.scala | 19 ++++----- .../test/scala/akka/cluster/ClusterSpec.scala | 5 +-- 20 files changed, 119 insertions(+), 144 deletions(-) diff --git a/akka-cluster/src/multi-jvm/scala/akka/cluster/ClusterDeathWatchSpec.scala b/akka-cluster/src/multi-jvm/scala/akka/cluster/ClusterDeathWatchSpec.scala index 0e9ffdc1e0..f3793fb2eb 100644 --- a/akka-cluster/src/multi-jvm/scala/akka/cluster/ClusterDeathWatchSpec.scala +++ b/akka-cluster/src/multi-jvm/scala/akka/cluster/ClusterDeathWatchSpec.scala @@ -82,11 +82,11 @@ abstract class ClusterDeathWatchSpec enterBarrier("second-terminated") markNodeAsUnavailable(third) - awaitCond(clusterView.members.forall(_.address != address(third))) - awaitCond(clusterView.unreachableMembers.exists(_.address == address(third))) + awaitAssert(clusterView.members.map(_.address) must not contain (address(third))) + awaitAssert(clusterView.unreachableMembers.map(_.address) must contain(address(third))) cluster.down(third) // removed - awaitCond(clusterView.unreachableMembers.forall(_.address != address(third))) + awaitAssert(clusterView.unreachableMembers.map(_.address) must not contain (address(third))) expectMsg(path3) enterBarrier("third-terminated") @@ -98,11 +98,11 @@ abstract class ClusterDeathWatchSpec enterBarrier("watch-established") runOn(third) { markNodeAsUnavailable(second) - awaitCond(clusterView.members.forall(_.address != address(second))) - awaitCond(clusterView.unreachableMembers.exists(_.address == address(second))) + awaitAssert(clusterView.members.map(_.address) must not contain (address(second))) + awaitAssert(clusterView.unreachableMembers.map(_.address) must contain(address(second))) cluster.down(second) // removed - awaitCond(clusterView.unreachableMembers.forall(_.address != address(second))) + awaitAssert(clusterView.unreachableMembers.map(_.address) must not contain (address(second))) } enterBarrier("second-terminated") enterBarrier("third-terminated") @@ -137,11 +137,11 @@ abstract class ClusterDeathWatchSpec enterBarrier("hello-deployed") markNodeAsUnavailable(first) - awaitCond(clusterView.members.forall(_.address != address(first))) - awaitCond(clusterView.unreachableMembers.exists(_.address == address(first))) + awaitAssert(clusterView.members.map(_.address) must not contain (address(first))) + awaitAssert(clusterView.unreachableMembers.map(_.address) must contain(address(first))) cluster.down(first) // removed - awaitCond(clusterView.unreachableMembers.forall(_.address != address(first))) + awaitAssert(clusterView.unreachableMembers.map(_.address) must not contain (address(first))) val t = expectMsgType[Terminated] t.actor must be(hello) diff --git a/akka-cluster/src/multi-jvm/scala/akka/cluster/ClusterMetricsSpec.scala b/akka-cluster/src/multi-jvm/scala/akka/cluster/ClusterMetricsSpec.scala index dc16f8dfe7..1b6fabc63e 100644 --- a/akka-cluster/src/multi-jvm/scala/akka/cluster/ClusterMetricsSpec.scala +++ b/akka-cluster/src/multi-jvm/scala/akka/cluster/ClusterMetricsSpec.scala @@ -38,8 +38,8 @@ abstract class ClusterMetricsSpec extends MultiNodeSpec(ClusterMetricsMultiJvmSp "and gossip metrics around the node ring" taggedAs LongRunningTest in within(60 seconds) { awaitClusterUp(roles: _*) enterBarrier("cluster-started") - awaitCond(clusterView.members.filter(_.status == MemberStatus.Up).size == roles.size) - awaitCond(clusterView.clusterMetrics.size == roles.size) + awaitAssert(clusterView.members.count(_.status == MemberStatus.Up) must be(roles.size)) + awaitAssert(clusterView.clusterMetrics.size must be(roles.size)) val collector = MetricsCollector(cluster.system, cluster.settings) collector.sample.metrics.size must be > (3) enterBarrier("after") @@ -50,7 +50,7 @@ abstract class ClusterMetricsSpec extends MultiNodeSpec(ClusterMetricsMultiJvmSp } enterBarrier("first-left") runOn(second, third, fourth, fifth) { - awaitCond(clusterView.clusterMetrics.size == (roles.size - 1)) + awaitAssert(clusterView.clusterMetrics.size must be(roles.size - 1)) } enterBarrier("finished") } diff --git a/akka-cluster/src/multi-jvm/scala/akka/cluster/ConvergenceSpec.scala b/akka-cluster/src/multi-jvm/scala/akka/cluster/ConvergenceSpec.scala index 0554144015..327a5cb52f 100644 --- a/akka-cluster/src/multi-jvm/scala/akka/cluster/ConvergenceSpec.scala +++ b/akka-cluster/src/multi-jvm/scala/akka/cluster/ConvergenceSpec.scala @@ -69,9 +69,9 @@ abstract class ConvergenceSpec(multiNodeConfig: ConvergenceMultiNodeConfig) within(28 seconds) { // third becomes unreachable - awaitCond(clusterView.unreachableMembers.size == 1) - awaitCond(clusterView.members.size == 2) - awaitCond(clusterView.members.forall(_.status == MemberStatus.Up)) + awaitAssert(clusterView.unreachableMembers.size must be(1)) + awaitAssert(clusterView.members.size must be(2)) + awaitAssert(clusterView.members.map(_.status) must be(Set(MemberStatus.Up))) awaitSeenSameState(first, second) // still one unreachable clusterView.unreachableMembers.size must be(1) @@ -96,7 +96,7 @@ abstract class ConvergenceSpec(multiNodeConfig: ConvergenceMultiNodeConfig) runOn(first, second, fourth) { for (n ← 1 to 5) { - awaitCond(clusterView.members.size == 2) + awaitAssert(clusterView.members.size must be(2)) awaitSeenSameState(first, second, fourth) memberStatus(first) must be(Some(MemberStatus.Up)) memberStatus(second) must be(Some(MemberStatus.Up)) diff --git a/akka-cluster/src/multi-jvm/scala/akka/cluster/InitialHeartbeatSpec.scala b/akka-cluster/src/multi-jvm/scala/akka/cluster/InitialHeartbeatSpec.scala index 707b6a9c47..111f643d76 100644 --- a/akka-cluster/src/multi-jvm/scala/akka/cluster/InitialHeartbeatSpec.scala +++ b/akka-cluster/src/multi-jvm/scala/akka/cluster/InitialHeartbeatSpec.scala @@ -48,9 +48,9 @@ abstract class InitialHeartbeatSpec runOn(first) { within(10 seconds) { - awaitCond { + awaitAssert { cluster.sendCurrentClusterState(testActor) - expectMsgType[CurrentClusterState].members.exists(_.address == secondAddress) + expectMsgType[CurrentClusterState].members.map(_.address) must contain(secondAddress) } } } diff --git a/akka-cluster/src/multi-jvm/scala/akka/cluster/LeaderElectionSpec.scala b/akka-cluster/src/multi-jvm/scala/akka/cluster/LeaderElectionSpec.scala index 95f8f29250..51787df18c 100644 --- a/akka-cluster/src/multi-jvm/scala/akka/cluster/LeaderElectionSpec.scala +++ b/akka-cluster/src/multi-jvm/scala/akka/cluster/LeaderElectionSpec.scala @@ -84,13 +84,13 @@ abstract class LeaderElectionSpec(multiNodeConfig: LeaderElectionMultiNodeConfig // detect failure markNodeAsUnavailable(leaderAddress) - awaitCond(clusterView.unreachableMembers.exists(_.address == leaderAddress)) + awaitAssert(clusterView.unreachableMembers.map(_.address) must contain(leaderAddress)) enterBarrier("after-unavailable" + n) // user marks the shutdown leader as DOWN cluster.down(leaderAddress) // removed - awaitCond(clusterView.unreachableMembers.forall(_.address != leaderAddress)) + awaitAssert(clusterView.unreachableMembers.map(_.address) must not contain (leaderAddress)) enterBarrier("after-down" + n, "completed" + n) case _ if remainingRoles.contains(myself) ⇒ @@ -98,7 +98,7 @@ abstract class LeaderElectionSpec(multiNodeConfig: LeaderElectionMultiNodeConfig val leaderAddress = address(leader) enterBarrier("before-shutdown" + n, "after-shutdown" + n) - awaitCond(clusterView.unreachableMembers.exists(_.address == leaderAddress)) + awaitAssert(clusterView.unreachableMembers.map(_.address) must contain(leaderAddress)) enterBarrier("after-unavailable" + n) enterBarrier("after-down" + n) diff --git a/akka-cluster/src/multi-jvm/scala/akka/cluster/LeaderLeavingSpec.scala b/akka-cluster/src/multi-jvm/scala/akka/cluster/LeaderLeavingSpec.scala index 1e52674e62..fafbbee921 100644 --- a/akka-cluster/src/multi-jvm/scala/akka/cluster/LeaderLeavingSpec.scala +++ b/akka-cluster/src/multi-jvm/scala/akka/cluster/LeaderLeavingSpec.scala @@ -77,19 +77,19 @@ abstract class LeaderLeavingSpec enterBarrier("leader-left") val expectedAddresses = roles.toSet map address - awaitCond(clusterView.members.map(_.address) == expectedAddresses) + awaitAssert(clusterView.members.map(_.address) must be(expectedAddresses)) // verify that the LEADER is EXITING exitingLatch.await // verify that the LEADER is no longer part of the 'members' set - awaitCond(clusterView.members.forall(_.address != oldLeaderAddress)) + awaitAssert(clusterView.members.map(_.address) must not contain (oldLeaderAddress)) // verify that the LEADER is not part of the 'unreachable' set - awaitCond(clusterView.unreachableMembers.forall(_.address != oldLeaderAddress)) + awaitAssert(clusterView.unreachableMembers.map(_.address) must not contain (oldLeaderAddress)) // verify that we have a new LEADER - awaitCond(clusterView.leader != oldLeaderAddress) + awaitAssert(clusterView.leader must not be (oldLeaderAddress)) } enterBarrier("finished") diff --git a/akka-cluster/src/multi-jvm/scala/akka/cluster/MBeanSpec.scala b/akka-cluster/src/multi-jvm/scala/akka/cluster/MBeanSpec.scala index ad8c41124a..9b2959702e 100644 --- a/akka-cluster/src/multi-jvm/scala/akka/cluster/MBeanSpec.scala +++ b/akka-cluster/src/multi-jvm/scala/akka/cluster/MBeanSpec.scala @@ -67,8 +67,8 @@ abstract class MBeanSpec } awaitClusterUp(first) runOn(first) { - awaitCond(mbeanServer.getAttribute(mbeanName, "MemberStatus") == "Up") - awaitCond(mbeanServer.getAttribute(mbeanName, "Leader") == address(first).toString) + awaitAssert(mbeanServer.getAttribute(mbeanName, "MemberStatus") must be("Up")) + awaitAssert(mbeanServer.getAttribute(mbeanName, "Leader") must be(address(first).toString)) mbeanServer.getAttribute(mbeanName, "Singleton").asInstanceOf[Boolean] must be(true) mbeanServer.getAttribute(mbeanName, "Members") must be(address(first).toString) mbeanServer.getAttribute(mbeanName, "Unreachable") must be("") @@ -85,11 +85,11 @@ abstract class MBeanSpec awaitMembersUp(4) assertMembers(clusterView.members, roles.map(address(_)): _*) - awaitCond(mbeanServer.getAttribute(mbeanName, "MemberStatus") == "Up") + awaitAssert(mbeanServer.getAttribute(mbeanName, "MemberStatus") must be("Up")) val expectedMembers = roles.sorted.map(address(_)).mkString(",") - awaitCond(mbeanServer.getAttribute(mbeanName, "Members") == expectedMembers) + awaitAssert(mbeanServer.getAttribute(mbeanName, "Members") must be(expectedMembers)) val expectedLeader = address(roleOfLeader()) - awaitCond(mbeanServer.getAttribute(mbeanName, "Leader") == expectedLeader.toString) + awaitAssert(mbeanServer.getAttribute(mbeanName, "Leader") must be(expectedLeader.toString)) mbeanServer.getAttribute(mbeanName, "Singleton").asInstanceOf[Boolean] must be(false) enterBarrier("after-4") @@ -103,9 +103,9 @@ abstract class MBeanSpec enterBarrier("fourth-shutdown") runOn(first, second, third) { - awaitCond(mbeanServer.getAttribute(mbeanName, "Unreachable") == fourthAddress.toString) + awaitAssert(mbeanServer.getAttribute(mbeanName, "Unreachable") must be(fourthAddress.toString)) val expectedMembers = Seq(first, second, third).sorted.map(address(_)).mkString(",") - awaitCond(mbeanServer.getAttribute(mbeanName, "Members") == expectedMembers) + awaitAssert(mbeanServer.getAttribute(mbeanName, "Members") must be(expectedMembers)) } enterBarrier("fourth-unreachable") @@ -117,7 +117,7 @@ abstract class MBeanSpec runOn(first, second, third) { awaitMembersUp(3, canNotBePartOfMemberRing = Set(fourthAddress)) assertMembers(clusterView.members, first, second, third) - awaitCond(mbeanServer.getAttribute(mbeanName, "Unreachable") == "") + awaitAssert(mbeanServer.getAttribute(mbeanName, "Unreachable") must be("")) } enterBarrier("after-5") @@ -132,15 +132,14 @@ abstract class MBeanSpec awaitMembersUp(2) assertMembers(clusterView.members, first, second) val expectedMembers = Seq(first, second).sorted.map(address(_)).mkString(",") - awaitCond(mbeanServer.getAttribute(mbeanName, "Members") == expectedMembers) + awaitAssert(mbeanServer.getAttribute(mbeanName, "Members") must be(expectedMembers)) } runOn(third) { awaitCond(cluster.isTerminated) // mbean should be unregistered, i.e. throw InstanceNotFoundException - awaitCond(Try { mbeanServer.getMBeanInfo(mbeanName); false } recover { - case e: InstanceNotFoundException ⇒ true - case _ ⇒ false - } get) + awaitAssert(intercept[InstanceNotFoundException] { + mbeanServer.getMBeanInfo(mbeanName) + }) } enterBarrier("after-6") diff --git a/akka-cluster/src/multi-jvm/scala/akka/cluster/MinMembersBeforeUpSpec.scala b/akka-cluster/src/multi-jvm/scala/akka/cluster/MinMembersBeforeUpSpec.scala index a91d672326..84ef5624b7 100644 --- a/akka-cluster/src/multi-jvm/scala/akka/cluster/MinMembersBeforeUpSpec.scala +++ b/akka-cluster/src/multi-jvm/scala/akka/cluster/MinMembersBeforeUpSpec.scala @@ -92,10 +92,9 @@ abstract class MinMembersBeforeUpBase(multiNodeConfig: MultiNodeConfig) runOn(first) { cluster join myself - awaitCond { - val result = clusterView.status == Joining + awaitAssert { clusterView.refreshCurrentState() - result + clusterView.status must be(Joining) } } enterBarrier("first-started") @@ -107,10 +106,9 @@ abstract class MinMembersBeforeUpBase(multiNodeConfig: MultiNodeConfig) } runOn(first, second) { val expectedAddresses = Set(first, second) map address - awaitCond { - val result = clusterView.members.map(_.address) == expectedAddresses + awaitAssert { clusterView.refreshCurrentState() - result + clusterView.members.map(_.address) must be(expectedAddresses) } clusterView.members.map(_.status) must be(Set(Joining)) // and it should not change diff --git a/akka-cluster/src/multi-jvm/scala/akka/cluster/MultiNodeClusterSpec.scala b/akka-cluster/src/multi-jvm/scala/akka/cluster/MultiNodeClusterSpec.scala index 52135b47d9..51eab3fea5 100644 --- a/akka-cluster/src/multi-jvm/scala/akka/cluster/MultiNodeClusterSpec.scala +++ b/akka-cluster/src/multi-jvm/scala/akka/cluster/MultiNodeClusterSpec.scala @@ -156,7 +156,7 @@ trait MultiNodeClusterSpec extends Suite with STMultiNodeSpec with WatchedByCoro def startClusterNode(): Unit = { if (clusterView.members.isEmpty) { cluster join myself - awaitCond(clusterView.members.exists(_.address == address(myself))) + awaitAssert(clusterView.members.map(_.address) must contain(address(myself))) } else clusterView.self } @@ -256,13 +256,12 @@ trait MultiNodeClusterSpec extends Suite with STMultiNodeSpec with WatchedByCoro timeout: FiniteDuration = 20.seconds): Unit = { within(timeout) { if (!canNotBePartOfMemberRing.isEmpty) // don't run this on an empty set - awaitCond( - canNotBePartOfMemberRing forall (address ⇒ !(clusterView.members exists (_.address == address)))) - awaitCond(clusterView.members.size == numberOfMembers) - awaitCond(clusterView.members.forall(_.status == MemberStatus.Up)) + awaitAssert(canNotBePartOfMemberRing foreach (a ⇒ clusterView.members.map(_.address) must not contain (a))) + awaitAssert(clusterView.members.size must be(numberOfMembers)) + awaitAssert(clusterView.members.map(_.status) must be(Set(MemberStatus.Up))) // clusterView.leader is updated by LeaderChanged, await that to be updated also val expectedLeader = clusterView.members.headOption.map(_.address) - awaitCond(clusterView.leader == expectedLeader) + awaitAssert(clusterView.leader must be(expectedLeader)) } } @@ -270,7 +269,7 @@ trait MultiNodeClusterSpec extends Suite with STMultiNodeSpec with WatchedByCoro * Wait until the specified nodes have seen the same gossip overview. */ def awaitSeenSameState(addresses: Address*): Unit = - awaitCond((addresses.toSet -- clusterView.seenBy).isEmpty) + awaitAssert((addresses.toSet -- clusterView.seenBy) must be(Set.empty)) /** * Leader according to the address ordering of the roles. diff --git a/akka-cluster/src/multi-jvm/scala/akka/cluster/NodeLeavingAndExitingAndBeingRemovedSpec.scala b/akka-cluster/src/multi-jvm/scala/akka/cluster/NodeLeavingAndExitingAndBeingRemovedSpec.scala index e3c5f6d1c7..d7ce04301e 100644 --- a/akka-cluster/src/multi-jvm/scala/akka/cluster/NodeLeavingAndExitingAndBeingRemovedSpec.scala +++ b/akka-cluster/src/multi-jvm/scala/akka/cluster/NodeLeavingAndExitingAndBeingRemovedSpec.scala @@ -43,10 +43,10 @@ abstract class NodeLeavingAndExitingAndBeingRemovedSpec runOn(first, third) { // verify that the 'second' node is no longer part of the 'members' set - awaitCond(clusterView.members.forall(_.address != address(second)), reaperWaitingTime) + awaitAssert(clusterView.members.map(_.address) must not contain (address(second)), reaperWaitingTime) // verify that the 'second' node is not part of the 'unreachable' set - awaitCond(clusterView.unreachableMembers.forall(_.address != address(second)), reaperWaitingTime) + awaitAssert(clusterView.unreachableMembers.map(_.address) must not contain (address(second)), reaperWaitingTime) } runOn(second) { diff --git a/akka-cluster/src/multi-jvm/scala/akka/cluster/NodeLeavingAndExitingSpec.scala b/akka-cluster/src/multi-jvm/scala/akka/cluster/NodeLeavingAndExitingSpec.scala index 341a8528bb..72c0eeb089 100644 --- a/akka-cluster/src/multi-jvm/scala/akka/cluster/NodeLeavingAndExitingSpec.scala +++ b/akka-cluster/src/multi-jvm/scala/akka/cluster/NodeLeavingAndExitingSpec.scala @@ -64,7 +64,7 @@ abstract class NodeLeavingAndExitingSpec enterBarrier("second-left") val expectedAddresses = roles.toSet map address - awaitCond(clusterView.members.map(_.address) == expectedAddresses) + awaitAssert(clusterView.members.map(_.address) must be(expectedAddresses)) // Verify that 'second' node is set to EXITING exitingLatch.await diff --git a/akka-cluster/src/multi-jvm/scala/akka/cluster/NodeMembershipSpec.scala b/akka-cluster/src/multi-jvm/scala/akka/cluster/NodeMembershipSpec.scala index da11b5b7d0..21c87c59c4 100644 --- a/akka-cluster/src/multi-jvm/scala/akka/cluster/NodeMembershipSpec.scala +++ b/akka-cluster/src/multi-jvm/scala/akka/cluster/NodeMembershipSpec.scala @@ -38,11 +38,9 @@ abstract class NodeMembershipSpec runOn(first, second) { cluster.join(first) - awaitCond(clusterView.members.size == 2) + awaitAssert(clusterView.members.size must be(2)) assertMembers(clusterView.members, first, second) - awaitCond { - clusterView.members.forall(_.status == MemberStatus.Up) - } + awaitAssert(clusterView.members.map(_.status) must be(Set(MemberStatus.Up))) } enterBarrier("after-1") @@ -54,11 +52,9 @@ abstract class NodeMembershipSpec cluster.join(first) } - awaitCond(clusterView.members.size == 3) + awaitAssert(clusterView.members.size must be(3)) assertMembers(clusterView.members, first, second, third) - awaitCond { - clusterView.members.forall(_.status == MemberStatus.Up) - } + awaitAssert(clusterView.members.map(_.status) must be(Set(MemberStatus.Up))) enterBarrier("after-2") } diff --git a/akka-cluster/src/multi-jvm/scala/akka/cluster/RestartFirstSeedNodeSpec.scala b/akka-cluster/src/multi-jvm/scala/akka/cluster/RestartFirstSeedNodeSpec.scala index 716a3cdcf8..7bec0f0292 100644 --- a/akka-cluster/src/multi-jvm/scala/akka/cluster/RestartFirstSeedNodeSpec.scala +++ b/akka-cluster/src/multi-jvm/scala/akka/cluster/RestartFirstSeedNodeSpec.scala @@ -92,8 +92,8 @@ abstract class RestartFirstSeedNodeSpec // now we can join seed1System, seed2, seed3 together runOn(seed1) { Cluster(seed1System).joinSeedNodes(seedNodes) - awaitCond(Cluster(seed1System).readView.members.size == 3) - awaitCond(Cluster(seed1System).readView.members.forall(_.status == Up)) + awaitAssert(Cluster(seed1System).readView.members.size must be(3)) + awaitAssert(Cluster(seed1System).readView.members.map(_.status) must be(Set(Up))) } runOn(seed2, seed3) { cluster.joinSeedNodes(seedNodes) @@ -108,15 +108,15 @@ abstract class RestartFirstSeedNodeSpec } runOn(seed2, seed3) { awaitMembersUp(2, canNotBePartOfMemberRing = Set(seedNodes.head)) - awaitCond(clusterView.unreachableMembers.forall(_.address != seedNodes.head)) + awaitAssert(clusterView.unreachableMembers.map(_.address) must not contain (seedNodes.head)) } enterBarrier("seed1-shutdown") // then start restartedSeed1System, which has the same address as seed1System runOn(seed1) { Cluster(restartedSeed1System).joinSeedNodes(seedNodes) - awaitCond(Cluster(restartedSeed1System).readView.members.size == 3) - awaitCond(Cluster(restartedSeed1System).readView.members.forall(_.status == Up)) + awaitAssert(Cluster(restartedSeed1System).readView.members.size must be(3)) + awaitAssert(Cluster(restartedSeed1System).readView.members.map(_.status) must be(Set(Up))) } runOn(seed2, seed3) { awaitMembersUp(3) diff --git a/akka-cluster/src/multi-jvm/scala/akka/cluster/StressSpec.scala b/akka-cluster/src/multi-jvm/scala/akka/cluster/StressSpec.scala index fdf33cb391..332e2792d5 100644 --- a/akka-cluster/src/multi-jvm/scala/akka/cluster/StressSpec.scala +++ b/akka-cluster/src/multi-jvm/scala/akka/cluster/StressSpec.scala @@ -965,19 +965,19 @@ abstract class StressSpec expectMsgType[ChildrenCount] must be(ChildrenCount(nbrUsedRoles, 0)) 1 to 5 foreach { _ ⇒ supervisor ! new RuntimeException("Simulated exception") } - awaitCond { + awaitAssert { supervisor ! GetChildrenCount val c = expectMsgType[ChildrenCount] - c == ChildrenCount(nbrUsedRoles, 5 * nbrUsedRoles) + c must be(ChildrenCount(nbrUsedRoles, 5 * nbrUsedRoles)) } // after 5 restart attempts the children should be stopped supervisor ! new RuntimeException("Simulated exception") - awaitCond { + awaitAssert { supervisor ! GetChildrenCount val c = expectMsgType[ChildrenCount] // zero children - c == ChildrenCount(0, 6 * nbrUsedRoles) + c must be(ChildrenCount(0, 6 * nbrUsedRoles)) } supervisor ! Reset diff --git a/akka-cluster/src/multi-jvm/scala/akka/cluster/TransitionSpec.scala b/akka-cluster/src/multi-jvm/scala/akka/cluster/TransitionSpec.scala index dd379382e8..9cf6ba710c 100644 --- a/akka-cluster/src/multi-jvm/scala/akka/cluster/TransitionSpec.scala +++ b/akka-cluster/src/multi-jvm/scala/akka/cluster/TransitionSpec.scala @@ -55,20 +55,18 @@ abstract class TransitionSpec def seenLatestGossip: Set[RoleName] = clusterView.seenBy flatMap roleName - def awaitSeen(addresses: Address*): Unit = awaitCond { - (seenLatestGossip map address) == addresses.toSet + def awaitSeen(addresses: Address*): Unit = awaitAssert { + (seenLatestGossip map address) must be(addresses.toSet) } - def awaitMembers(addresses: Address*): Unit = awaitCond { - val result = memberAddresses == addresses.toSet + def awaitMembers(addresses: Address*): Unit = awaitAssert { clusterView.refreshCurrentState() - result + memberAddresses must be(addresses.toSet) } - def awaitMemberStatus(address: Address, status: MemberStatus): Unit = awaitCond { - val result = memberStatus(address) == status + def awaitMemberStatus(address: Address, status: MemberStatus): Unit = awaitAssert { clusterView.refreshCurrentState() - result + memberStatus(address) must be(status) } def leaderActions(): Unit = @@ -133,7 +131,7 @@ abstract class TransitionSpec awaitMembers(first, second) awaitMemberStatus(first, Up) awaitMemberStatus(second, Joining) - awaitCond(seenLatestGossip == Set(first, second)) + awaitAssert(seenLatestGossip must be(Set(first, second))) } enterBarrier("convergence-joining-2") @@ -148,7 +146,7 @@ abstract class TransitionSpec runOn(first, second) { // gossip chat will synchronize the views awaitMemberStatus(second, Up) - awaitCond(seenLatestGossip == Set(first, second)) + awaitAssert(seenLatestGossip must be(Set(first, second))) awaitMemberStatus(first, Up) } @@ -162,7 +160,7 @@ abstract class TransitionSpec } runOn(second, third) { // gossip chat from the join will synchronize the views - awaitCond(seenLatestGossip == Set(second, third)) + awaitAssert(seenLatestGossip must be(Set(second, third))) } enterBarrier("third-joined-second") @@ -172,7 +170,7 @@ abstract class TransitionSpec awaitMembers(first, second, third) awaitMemberStatus(third, Joining) awaitMemberStatus(second, Up) - awaitCond(seenLatestGossip == Set(first, second, third)) + awaitAssert(seenLatestGossip must be(Set(first, second, third))) } first gossipTo third @@ -181,7 +179,7 @@ abstract class TransitionSpec awaitMemberStatus(first, Up) awaitMemberStatus(second, Up) awaitMemberStatus(third, Joining) - awaitCond(seenLatestGossip == Set(first, second, third)) + awaitAssert(seenLatestGossip must be(Set(first, second, third))) } enterBarrier("convergence-joining-3") @@ -200,7 +198,7 @@ abstract class TransitionSpec leader12 gossipTo other1 runOn(other1) { awaitMemberStatus(third, Up) - awaitCond(seenLatestGossip == Set(leader12, myself)) + awaitAssert(seenLatestGossip must be(Set(leader12, myself))) } // first non-leader gossipTo the other non-leader @@ -211,7 +209,7 @@ abstract class TransitionSpec } runOn(other2) { awaitMemberStatus(third, Up) - awaitCond(seenLatestGossip == Set(first, second, third)) + awaitAssert(seenLatestGossip must be(Set(first, second, third))) } // first non-leader gossipTo the leader @@ -220,7 +218,7 @@ abstract class TransitionSpec awaitMemberStatus(first, Up) awaitMemberStatus(second, Up) awaitMemberStatus(third, Up) - awaitCond(seenLatestGossip == Set(first, second, third)) + awaitAssert(seenLatestGossip must be(Set(first, second, third))) } enterBarrier("after-3") @@ -230,8 +228,8 @@ abstract class TransitionSpec runOn(third) { markNodeAsUnavailable(second) reapUnreachable() - awaitCond(clusterView.unreachableMembers.contains(Member(second, Up, Set.empty))) - awaitCond(seenLatestGossip == Set(third)) + awaitAssert(clusterView.unreachableMembers must contain(Member(second, Up, Set.empty))) + awaitAssert(seenLatestGossip must be(Set(third))) } enterBarrier("after-second-unavailble") @@ -239,7 +237,7 @@ abstract class TransitionSpec third gossipTo first runOn(first, third) { - awaitCond(clusterView.unreachableMembers.contains(Member(second, Up, Set.empty))) + awaitAssert(clusterView.unreachableMembers must contain(Member(second, Up, Set.empty))) } runOn(first) { @@ -251,9 +249,9 @@ abstract class TransitionSpec first gossipTo third runOn(first, third) { - awaitCond(clusterView.unreachableMembers.contains(Member(second, Down, Set.empty))) + awaitAssert(clusterView.unreachableMembers must contain(Member(second, Down, Set.empty))) awaitMemberStatus(second, Down) - awaitCond(seenLatestGossip == Set(first, third)) + awaitAssert(seenLatestGossip must be(Set(first, third))) } enterBarrier("after-6") diff --git a/akka-cluster/src/multi-jvm/scala/akka/cluster/UnreachableNodeRejoinsClusterSpec.scala b/akka-cluster/src/multi-jvm/scala/akka/cluster/UnreachableNodeRejoinsClusterSpec.scala index fe1061e8a8..7063212643 100644 --- a/akka-cluster/src/multi-jvm/scala/akka/cluster/UnreachableNodeRejoinsClusterSpec.scala +++ b/akka-cluster/src/multi-jvm/scala/akka/cluster/UnreachableNodeRejoinsClusterSpec.scala @@ -98,12 +98,12 @@ abstract class UnreachableNodeRejoinsClusterSpec(multiNodeConfig: UnreachableNod allButVictim.foreach(markNodeAsUnavailable(_)) within(30 seconds) { // victim becomes all alone - awaitCond({ + awaitAssert { val members = clusterView.members - clusterView.unreachableMembers.size == (roles.size - 1) && - members.size == 1 && - members.forall(_.status == MemberStatus.Up) - }) + clusterView.unreachableMembers.size must be(roles.size - 1) + members.size must be(1) + members.map(_.status) must be(Set(MemberStatus.Up)) + } clusterView.unreachableMembers.map(_.address) must be((allButVictim map address).toSet) } } @@ -112,12 +112,12 @@ abstract class UnreachableNodeRejoinsClusterSpec(multiNodeConfig: UnreachableNod markNodeAsUnavailable(victim) within(30 seconds) { // victim becomes unreachable - awaitCond({ + awaitAssert { val members = clusterView.members - clusterView.unreachableMembers.size == 1 && - members.size == (roles.size - 1) && - members.forall(_.status == MemberStatus.Up) - }) + clusterView.unreachableMembers.size must be(1) + members.size must be(roles.size - 1) + members.map(_.status) must be(Set(MemberStatus.Up)) + } awaitSeenSameState(allButVictim map address: _*) // still one unreachable clusterView.unreachableMembers.size must be(1) @@ -136,7 +136,7 @@ abstract class UnreachableNodeRejoinsClusterSpec(multiNodeConfig: UnreachableNod runOn(allBut(victim): _*) { awaitMembersUp(roles.size - 1, Set(victim)) // eventually removed - awaitCond(clusterView.unreachableMembers.isEmpty, 15 seconds) + awaitAssert(clusterView.unreachableMembers must be(Set.empty), 15 seconds) } endBarrier diff --git a/akka-cluster/src/multi-jvm/scala/akka/cluster/routing/AdaptiveLoadBalancingRouterSpec.scala b/akka-cluster/src/multi-jvm/scala/akka/cluster/routing/AdaptiveLoadBalancingRouterSpec.scala index 70477d03b7..0954d4f3c4 100644 --- a/akka-cluster/src/multi-jvm/scala/akka/cluster/routing/AdaptiveLoadBalancingRouterSpec.scala +++ b/akka-cluster/src/multi-jvm/scala/akka/cluster/routing/AdaptiveLoadBalancingRouterSpec.scala @@ -116,10 +116,8 @@ abstract class AdaptiveLoadBalancingRouterSpec extends MultiNodeSpec(AdaptiveLoa val router = system.actorOf(Props[Routee].withRouter(ClusterRouterConfig( local = AdaptiveLoadBalancingRouter(HeapMetricsSelector), settings = ClusterRouterSettings(totalInstances = 10, maxInstancesPerNode = 1, useRole = None))), name) - awaitCond { - // it may take some time until router receives cluster member events - currentRoutees(router).size == roles.size - } + // it may take some time until router receives cluster member events + awaitAssert { currentRoutees(router).size must be(roles.size) } currentRoutees(router).map(fullAddress).toSet must be(roles.map(address).toSet) router } @@ -170,7 +168,6 @@ abstract class AdaptiveLoadBalancingRouterSpec extends MultiNodeSpec(AdaptiveLoa runOn(first) { val router2 = startRouter("router2") - router2 // collect some metrics before we start Thread.sleep(cluster.settings.MetricsInterval.toMillis * 10) @@ -193,10 +190,8 @@ abstract class AdaptiveLoadBalancingRouterSpec extends MultiNodeSpec(AdaptiveLoa "create routees from configuration" taggedAs LongRunningTest in { runOn(first) { val router3 = system.actorOf(Props[Memory].withRouter(FromConfig()), "router3") - awaitCond { - // it may take some time until router receives cluster member events - currentRoutees(router3).size == 9 - } + // it may take some time until router receives cluster member events + awaitAssert { currentRoutees(router3).size must be(9) } currentRoutees(router3).map(fullAddress).toSet must be(Set(address(first))) } enterBarrier("after-4") @@ -205,10 +200,8 @@ abstract class AdaptiveLoadBalancingRouterSpec extends MultiNodeSpec(AdaptiveLoa "create routees from cluster.enabled configuration" taggedAs LongRunningTest in { runOn(first) { val router4 = system.actorOf(Props[Memory].withRouter(FromConfig()), "router4") - awaitCond { - // it may take some time until router receives cluster member events - currentRoutees(router4).size == 6 - } + // it may take some time until router receives cluster member events + awaitAssert { currentRoutees(router4).size must be(6) } currentRoutees(router4).map(fullAddress).toSet must be(Set( address(first), address(second), address(third))) } diff --git a/akka-cluster/src/multi-jvm/scala/akka/cluster/routing/ClusterConsistentHashingRouterSpec.scala b/akka-cluster/src/multi-jvm/scala/akka/cluster/routing/ClusterConsistentHashingRouterSpec.scala index b72f793228..9b6ca0d91a 100644 --- a/akka-cluster/src/multi-jvm/scala/akka/cluster/routing/ClusterConsistentHashingRouterSpec.scala +++ b/akka-cluster/src/multi-jvm/scala/akka/cluster/routing/ClusterConsistentHashingRouterSpec.scala @@ -87,10 +87,8 @@ abstract class ClusterConsistentHashingRouterSpec extends MultiNodeSpec(ClusterC "create routees from configuration" in { runOn(first) { - awaitCond { - // it may take some time until router receives cluster member events - currentRoutees(router1).size == 4 - } + // it may take some time until router receives cluster member events + awaitAssert { currentRoutees(router1).size must be(4) } currentRoutees(router1).map(fullAddress).toSet must be(Set(address(first), address(second))) } enterBarrier("after-2") @@ -111,10 +109,8 @@ abstract class ClusterConsistentHashingRouterSpec extends MultiNodeSpec(ClusterC awaitClusterUp(first, second, third) runOn(first) { - awaitCond { - // it may take some time until router receives cluster member events - currentRoutees(router1).size == 6 - } + // it may take some time until router receives cluster member events + awaitAssert { currentRoutees(router1).size must be(6) } currentRoutees(router1).map(fullAddress).toSet must be(roles.map(address).toSet) } @@ -125,10 +121,8 @@ abstract class ClusterConsistentHashingRouterSpec extends MultiNodeSpec(ClusterC runOn(first) { val router2 = system.actorOf(Props[Echo].withRouter(ClusterRouterConfig(local = ConsistentHashingRouter(), settings = ClusterRouterSettings(totalInstances = 10, maxInstancesPerNode = 2, useRole = None))), "router2") - awaitCond { - // it may take some time until router receives cluster member events - currentRoutees(router2).size == 6 - } + // it may take some time until router receives cluster member events + awaitAssert { currentRoutees(router2).size must be(6) } currentRoutees(router2).map(fullAddress).toSet must be(roles.map(address).toSet) } @@ -166,10 +160,8 @@ abstract class ClusterConsistentHashingRouterSpec extends MultiNodeSpec(ClusterC } def assertHashMapping(router: ActorRef): Unit = { - awaitCond { - // it may take some time until router receives cluster member events - currentRoutees(router).size == 6 - } + // it may take some time until router receives cluster member events + awaitAssert { currentRoutees(router).size must be(6) } currentRoutees(router).map(fullAddress).toSet must be(roles.map(address).toSet) router ! "a" diff --git a/akka-cluster/src/multi-jvm/scala/akka/cluster/routing/ClusterRoundRobinRoutedActorSpec.scala b/akka-cluster/src/multi-jvm/scala/akka/cluster/routing/ClusterRoundRobinRoutedActorSpec.scala index 430df7ab50..7148b4ee9f 100644 --- a/akka-cluster/src/multi-jvm/scala/akka/cluster/routing/ClusterRoundRobinRoutedActorSpec.scala +++ b/akka-cluster/src/multi-jvm/scala/akka/cluster/routing/ClusterRoundRobinRoutedActorSpec.scala @@ -137,7 +137,7 @@ abstract class ClusterRoundRobinRoutedActorSpec extends MultiNodeSpec(ClusterRou router1.isInstanceOf[RoutedActorRef] must be(true) // max-nr-of-instances-per-node=2 times 2 nodes - awaitCond(currentRoutees(router1).size == 4) + awaitAssert(currentRoutees(router1).size must be(4)) val iterationCount = 10 for (i ← 0 until iterationCount) { @@ -165,7 +165,7 @@ abstract class ClusterRoundRobinRoutedActorSpec extends MultiNodeSpec(ClusterRou runOn(first) { // 2 nodes, 1 routee on each node - awaitCond(currentRoutees(router4).size == 2) + awaitAssert(currentRoutees(router4).size must be(2)) val iterationCount = 10 for (i ← 0 until iterationCount) { @@ -191,7 +191,7 @@ abstract class ClusterRoundRobinRoutedActorSpec extends MultiNodeSpec(ClusterRou runOn(first) { // max-nr-of-instances-per-node=2 times 4 nodes - awaitCond(currentRoutees(router1).size == 8) + awaitAssert(currentRoutees(router1).size must be(8)) val iterationCount = 10 for (i ← 0 until iterationCount) { @@ -213,7 +213,7 @@ abstract class ClusterRoundRobinRoutedActorSpec extends MultiNodeSpec(ClusterRou runOn(first) { // 4 nodes, 1 routee on each node - awaitCond(currentRoutees(router4).size == 4) + awaitAssert(currentRoutees(router4).size must be(4)) val iterationCount = 10 for (i ← 0 until iterationCount) { @@ -233,7 +233,7 @@ abstract class ClusterRoundRobinRoutedActorSpec extends MultiNodeSpec(ClusterRou runOn(first) { // max-nr-of-instances-per-node=1 times 3 nodes - awaitCond(currentRoutees(router3).size == 3) + awaitAssert(currentRoutees(router3).size must be(3)) val iterationCount = 10 for (i ← 0 until iterationCount) { @@ -255,7 +255,7 @@ abstract class ClusterRoundRobinRoutedActorSpec extends MultiNodeSpec(ClusterRou "deploy routees to specified node role" taggedAs LongRunningTest in { runOn(first) { - awaitCond(currentRoutees(router5).size == 2) + awaitAssert(currentRoutees(router5).size must be(2)) val iterationCount = 10 for (i ← 0 until iterationCount) { @@ -280,7 +280,7 @@ abstract class ClusterRoundRobinRoutedActorSpec extends MultiNodeSpec(ClusterRou router2.isInstanceOf[RoutedActorRef] must be(true) // totalInstances = 3, maxInstancesPerNode = 1 - awaitCond(currentRoutees(router2).size == 3) + awaitAssert(currentRoutees(router2).size must be(3)) val iterationCount = 10 for (i ← 0 until iterationCount) { @@ -311,8 +311,9 @@ abstract class ClusterRoundRobinRoutedActorSpec extends MultiNodeSpec(ClusterRou val downAddress = routeeAddresses.find(_ != address(first)).get cluster.down(downAddress) - awaitCond { - routeeAddresses.contains(notUsedAddress) && !routeeAddresses.contains(downAddress) + awaitAssert { + routeeAddresses must contain(notUsedAddress) + routeeAddresses must not contain (downAddress) } val iterationCount = 10 diff --git a/akka-cluster/src/test/scala/akka/cluster/ClusterSpec.scala b/akka-cluster/src/test/scala/akka/cluster/ClusterSpec.scala index a2738ea9ef..47cfcb19a1 100644 --- a/akka-cluster/src/test/scala/akka/cluster/ClusterSpec.scala +++ b/akka-cluster/src/test/scala/akka/cluster/ClusterSpec.scala @@ -38,8 +38,7 @@ object ClusterSpec { class ClusterSpec extends AkkaSpec(ClusterSpec.config) with ImplicitSender { import ClusterSpec._ - // FIXME: temporary workaround. See #2663 - val selfAddress = system.asInstanceOf[ExtendedActorSystem].provider.asInstanceOf[ClusterActorRefProvider].transport.defaultAddress + val selfAddress = system.asInstanceOf[ExtendedActorSystem].provider.getDefaultAddress val cluster = Cluster(system) def clusterView = cluster.readView @@ -67,7 +66,7 @@ class ClusterSpec extends AkkaSpec(ClusterSpec.config) with ImplicitSender { awaitCond(clusterView.isSingletonCluster) clusterView.self.address must be(selfAddress) clusterView.members.map(_.address) must be(Set(selfAddress)) - awaitCond(clusterView.status == MemberStatus.Up) + awaitAssert(clusterView.status must be(MemberStatus.Up)) } "publish CurrentClusterState to subscribers when requested" in { From 3340f84154d7daab1a6ccf376082d65443865031 Mon Sep 17 00:00:00 2001 From: Patrik Nordwall Date: Mon, 25 Mar 2013 14:33:32 +0100 Subject: [PATCH 44/47] Exiting members must still have seen the gossip for convergence, see #3171 * Otherwise MemberExited might not be published on the leaving node, since the leader may act Leaving->Exiting->Removed without the leaving node seeing and publishing the transition. --- akka-cluster/src/main/scala/akka/cluster/Gossip.scala | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/akka-cluster/src/main/scala/akka/cluster/Gossip.scala b/akka-cluster/src/main/scala/akka/cluster/Gossip.scala index 70d79e5c22..a3c3c0d908 100644 --- a/akka-cluster/src/main/scala/akka/cluster/Gossip.scala +++ b/akka-cluster/src/main/scala/akka/cluster/Gossip.scala @@ -19,6 +19,7 @@ private[cluster] object Gossip { if (members.isEmpty) empty else empty.copy(members = members) private val leaderMemberStatus = Set[MemberStatus](Up, Leaving) + private val convergenceMemberStatus = Set[MemberStatus](Up, Leaving, Exiting) } /** @@ -179,11 +180,11 @@ private[cluster] case class Gossip( // 1. we don't have any members that are unreachable, or // 2. all unreachable members in the set have status DOWN // Else we can't continue to check for convergence - // When that is done we check that all memebers with a leader + // When that is done we check that all members with a convergence // status is in the seen table and has the latest vector clock // version overview.unreachable.forall(_.status == Down) && - !members.exists(m ⇒ Gossip.leaderMemberStatus(m.status) && !seenByAddress(m.address)) + !members.exists(m ⇒ Gossip.convergenceMemberStatus(m.status) && !seenByAddress(m.address)) } def isLeader(address: Address): Boolean = leader == Some(address) From f7ad87ce8d21481678600ff6b606cebc6bdd3c84 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Antonsson?= Date: Mon, 25 Mar 2013 15:11:40 +0100 Subject: [PATCH 45/47] Make The Coroner print full stack traces. --- .../src/test/scala/akka/testkit/Coroner.scala | 83 ++++++++++++++++--- 1 file changed, 73 insertions(+), 10 deletions(-) diff --git a/akka-testkit/src/test/scala/akka/testkit/Coroner.scala b/akka-testkit/src/test/scala/akka/testkit/Coroner.scala index 601c23abd4..3207d08fa0 100644 --- a/akka-testkit/src/test/scala/akka/testkit/Coroner.scala +++ b/akka-testkit/src/test/scala/akka/testkit/Coroner.scala @@ -98,13 +98,13 @@ object Coroner { #Heap usage: ${memMx.getHeapMemoryUsage()} #Non-heap usage: ${memMx.getNonHeapMemoryUsage()}""".stripMargin('#')) - def dumpAllThreads(): Seq[ThreadInfo] = { + def dumpAllThreads: Seq[ThreadInfo] = { threadMx.dumpAllThreads( - threadMx.isObjectMonitorUsageSupported(), - threadMx.isSynchronizerUsageSupported()) + threadMx.isObjectMonitorUsageSupported, + threadMx.isSynchronizerUsageSupported) } - def findDeadlockedThreads(): (Seq[ThreadInfo], String) = { + def findDeadlockedThreads: (Seq[ThreadInfo], String) = { val (ids, desc) = if (threadMx.isSynchronizerUsageSupported()) { (threadMx.findDeadlockedThreads(), "monitors and ownable synchronizers") } else { @@ -118,20 +118,83 @@ object Coroner { } } - def printThreadInfo(threadInfos: Seq[ThreadInfo]) = { + def printThreadInfos(threadInfos: Seq[ThreadInfo]) = { if (threadInfos.isEmpty) { println("None") } else { - for (ti ← threadInfos.sortBy(_.getThreadName)) { println(ti) } + for (ti ← threadInfos.sortBy(_.getThreadName)) { println(threadInfoToString(ti)) } } } - println("All threads:") - printThreadInfo(dumpAllThreads()) + def threadInfoToString(ti: ThreadInfo): String = { + val sb = new java.lang.StringBuilder + sb.append("\"") + sb.append(ti.getThreadName) + sb.append("\" Id=") + sb.append(ti.getThreadId) + sb.append(" ") + sb.append(ti.getThreadState) - val (deadlockedThreads, deadlockDesc) = findDeadlockedThreads() + if (ti.getLockName != null) { + sb.append(" on " + ti.getLockName) + } + + if (ti.getLockOwnerName != null) { + sb.append(" owned by \"") + sb.append(ti.getLockOwnerName) + sb.append("\" Id=") + sb.append(ti.getLockOwnerId) + } + + if (ti.isSuspended) { + sb.append(" (suspended)") + } + + if (ti.isInNative) { + sb.append(" (in native)") + } + + sb.append('\n') + + def appendMsg(msg: String, o: Any) = { + sb.append(msg) + sb.append(o) + sb.append('\n') + } + + val stackTrace = ti.getStackTrace + for (i ← 0 until stackTrace.length) { + val ste = stackTrace(i) + appendMsg("\tat ", ste) + if (i == 0 && ti.getLockInfo != null) { + import java.lang.Thread.State._ + ti.getThreadState match { + case BLOCKED ⇒ appendMsg("\t- blocked on ", ti.getLockInfo) + case WAITING ⇒ appendMsg("\t- waiting on ", ti.getLockInfo) + case TIMED_WAITING ⇒ appendMsg("\t- waiting on ", ti.getLockInfo) + case _ ⇒ + } + } + + for (mi ← ti.getLockedMonitors if mi.getLockedStackDepth == i) + appendMsg("\t- locked ", mi) + } + + val locks = ti.getLockedSynchronizers + if (locks.length > 0) { + appendMsg("\n\tNumber of locked synchronizers = ", locks.length) + for (li ← locks) appendMsg("\t- ", li) + } + sb.append('\n') + return sb.toString + } + + println("All threads:") + printThreadInfos(dumpAllThreads) + + val (deadlockedThreads, deadlockDesc) = findDeadlockedThreads println(s"Deadlocks found for $deadlockDesc:") - printThreadInfo(deadlockedThreads) + printThreadInfos(deadlockedThreads) } } From a041a295edc6dd378f74cc6ba9f91395f5334a61 Mon Sep 17 00:00:00 2001 From: Rich Dougherty Date: Wed, 20 Mar 2013 20:38:49 +1300 Subject: [PATCH 46/47] Drop unserializable and oversized messages, preserving association. Fixes #3070 --- .../src/main/scala/akka/remote/Endpoint.scala | 82 +++++++++++++------ .../test/scala/akka/remote/RemotingSpec.scala | 76 ++++++++++++++++- 2 files changed, 132 insertions(+), 26 deletions(-) diff --git a/akka-remote/src/main/scala/akka/remote/Endpoint.scala b/akka-remote/src/main/scala/akka/remote/Endpoint.scala index fe3f96f6ca..1fca182aee 100644 --- a/akka-remote/src/main/scala/akka/remote/Endpoint.scala +++ b/akka-remote/src/main/scala/akka/remote/Endpoint.scala @@ -15,8 +15,9 @@ import akka.remote.transport.AssociationHandle._ import akka.remote.transport.{ AkkaPduCodec, Transport, AssociationHandle } import akka.serialization.Serialization import akka.util.ByteString -import scala.util.control.{ NoStackTrace, NonFatal } import akka.remote.transport.Transport.InvalidAssociationException +import java.io.NotSerializableException +import scala.util.control.{ NoStackTrace, NonFatal } /** * INTERNAL API @@ -143,19 +144,42 @@ private[remote] class EndpointAssociationException(msg: String, cause: Throwable /** * INTERNAL API */ -private[remote] class EndpointWriter( - handleOrActive: Option[AssociationHandle], +@SerialVersionUID(1L) +private[remote] class OversizedPayloadException(msg: String) extends EndpointException(msg) + +private[remote] abstract class EndpointActor( val localAddress: Address, val remoteAddress: Address, val transport: Transport, val settings: RemoteSettings, - val codec: AkkaPduCodec) extends Actor with Stash with FSM[EndpointWriter.State, Unit] { + val codec: AkkaPduCodec) extends Actor with ActorLogging { + + def inbound: Boolean + + val eventPublisher = new EventPublisher(context.system, log, settings.LogRemoteLifecycleEvents) + + def publishError(reason: Throwable): Unit = { + try + eventPublisher.notifyListeners(AssociationErrorEvent(reason, localAddress, remoteAddress, inbound)) + catch { case NonFatal(e) ⇒ log.error(e, "Unable to publish error event to EventStream.") } + } +} + +/** + * INTERNAL API + */ +private[remote] class EndpointWriter( + handleOrActive: Option[AssociationHandle], + localAddress: Address, + remoteAddress: Address, + transport: Transport, + settings: RemoteSettings, + codec: AkkaPduCodec) extends EndpointActor(localAddress, remoteAddress, transport, settings, codec) with Stash with FSM[EndpointWriter.State, Unit] { import EndpointWriter._ import context.dispatcher val extendedSystem: ExtendedActorSystem = context.system.asInstanceOf[ExtendedActorSystem] - val eventPublisher = new EventPublisher(context.system, log, settings.LogRemoteLifecycleEvents) var reader: Option[ActorRef] = None var handle: Option[AssociationHandle] = handleOrActive // FIXME: refactor into state data @@ -168,12 +192,15 @@ private[remote] class EndpointWriter( var inbound = handle.isDefined private def publishAndThrow(reason: Throwable): Nothing = { - try - eventPublisher.notifyListeners(AssociationErrorEvent(reason, localAddress, remoteAddress, inbound)) - catch { case NonFatal(e) ⇒ log.error(e, "Unable to publish error event to EventStream.") } + publishError(reason) throw reason } + private def publishAndStay(reason: Throwable): State = { + publishError(reason) + stay() + } + override def postRestart(reason: Throwable): Unit = { handle = None // Wipe out the possibly injected handle inbound = false @@ -226,7 +253,11 @@ private[remote] class EndpointWriter( handle match { case Some(h) ⇒ val pdu = codec.constructMessage(recipient.localAddressToUse, recipient, serializeMessage(msg), senderOption) - if (h.write(pdu)) stay() else { + if (pdu.size > transport.maximumPayloadBytes) { + publishAndStay(new OversizedPayloadException(s"Discarding oversized payload sent to ${recipient}: max allowed size ${transport.maximumPayloadBytes} bytes, actual size of encoded ${msg.getClass} was ${pdu.size} bytes.")) + } else if (h.write(pdu)) { + stay() + } else { stash() goto(Buffering) } @@ -234,11 +265,12 @@ private[remote] class EndpointWriter( throw new EndpointException("Internal error: Endpoint is in state Writing, but no association handle is present.") } } catch { - case NonFatal(e: EndpointException) ⇒ publishAndThrow(e) - case NonFatal(e) ⇒ publishAndThrow(new EndpointException("Failed to write message to the transport", e)) + case e: NotSerializableException ⇒ publishAndStay(e) + case e: EndpointException ⇒ publishAndThrow(e) + case NonFatal(e) ⇒ publishAndThrow(new EndpointException("Failed to write message to the transport", e)) } - // We are in Writing state, so stash is emtpy, safe to stop here + // We are in Writing state, so stash is empty, safe to stop here case Event(FlushAndStop, _) ⇒ stop() } @@ -290,19 +322,15 @@ private[remote] class EndpointWriter( } private def startReadEndpoint(handle: AssociationHandle): Some[ActorRef] = { - val readerLocalAddress = handle.localAddress - val readerCodec = codec - val readerDispatcher = msgDispatch val newReader = - context.watch(context.actorOf(Props(new EndpointReader(readerCodec, readerLocalAddress, readerDispatcher)), + context.watch(context.actorOf( + Props(new EndpointReader(localAddress, remoteAddress, transport, settings, codec, msgDispatch, inbound)), "endpointReader-" + AddressUrlEncoder(remoteAddress) + "-" + readerId.next())) handle.readHandlerPromise.success(ActorHandleEventListener(newReader)) Some(newReader) } private def serializeMessage(msg: Any): MessageProtocol = handle match { - // FIXME: Unserializable messages should be dropped without closing the association. Should be logged, - // but without flooding the log. case Some(h) ⇒ Serialization.currentTransportAddress.withValue(h.localAddress) { (MessageSerializer.serialize(extendedSystem, msg.asInstanceOf[AnyRef])) @@ -317,9 +345,13 @@ private[remote] class EndpointWriter( * INTERNAL API */ private[remote] class EndpointReader( - val codec: AkkaPduCodec, - val localAddress: Address, - val msgDispatch: InboundMessageDispatcher) extends Actor { + localAddress: Address, + remoteAddress: Address, + transport: Transport, + settings: RemoteSettings, + codec: AkkaPduCodec, + msgDispatch: InboundMessageDispatcher, + val inbound: Boolean) extends EndpointActor(localAddress, remoteAddress, transport, settings, codec) { val provider = RARP(context.system).provider @@ -327,8 +359,12 @@ private[remote] class EndpointReader( case Disassociated ⇒ context.stop(self) case InboundPayload(p) ⇒ - val msg = decodePdu(p) - msgDispatch.dispatch(msg.recipient, msg.recipientAddress, msg.serializedMessage, msg.senderOption) + if (p.size > transport.maximumPayloadBytes) { + publishError(new OversizedPayloadException(s"Discarding oversized payload received: max allowed size ${transport.maximumPayloadBytes} bytes, actual size ${p.size} bytes.")) + } else { + val msg = decodePdu(p) + msgDispatch.dispatch(msg.recipient, msg.recipientAddress, msg.serializedMessage, msg.senderOption) + } } private def decodePdu(pdu: ByteString): Message = try { diff --git a/akka-remote/src/test/scala/akka/remote/RemotingSpec.scala b/akka-remote/src/test/scala/akka/remote/RemotingSpec.scala index 98b4cd455c..37be9b5553 100644 --- a/akka-remote/src/test/scala/akka/remote/RemotingSpec.scala +++ b/akka-remote/src/test/scala/akka/remote/RemotingSpec.scala @@ -5,12 +5,14 @@ package akka.remote import akka.actor._ import akka.pattern.ask +import akka.remote.transport.AssociationRegistry import akka.testkit._ +import akka.util.ByteString import com.typesafe.config._ +import java.io.NotSerializableException import scala.concurrent.Await import scala.concurrent.Future import scala.concurrent.duration._ -import akka.remote.transport.AssociationRegistry object RemotingSpec { class Echo1 extends Actor { @@ -115,8 +117,9 @@ class RemotingSpec extends AkkaSpec(RemotingSpec.cfg) with ImplicitSender with D val conf = ConfigFactory.parseString( """ - akka.remote { - test.local-address = "test://remote-sys@localhost:12346" + akka.remote.test { + local-address = "test://remote-sys@localhost:12346" + maximum-payload-bytes = 48000 bytes } """).withFallback(system.settings.config).resolve() val otherSystem = ActorSystem("remote-sys", conf) @@ -139,6 +142,38 @@ class RemotingSpec extends AkkaSpec(RemotingSpec.cfg) with ImplicitSender with D val here = system.actorFor("akka.test://remote-sys@localhost:12346/user/echo") + private def verifySend(msg: Any)(afterSend: ⇒ Unit) { + val bigBounceOther = otherSystem.actorOf(Props(new Actor { + def receive = { + case x: Int ⇒ sender ! byteStringOfSize(x) + case x ⇒ sender ! x + } + }), "bigBounce") + val bigBounceHere = system.actorFor("akka.test://remote-sys@localhost:12346/user/bigBounce") + + val eventForwarder = system.actorOf(Props(new Actor { + def receive = { + case x ⇒ testActor ! x + } + })) + system.eventStream.subscribe(eventForwarder, classOf[AssociationErrorEvent]) + system.eventStream.subscribe(eventForwarder, classOf[DisassociatedEvent]) + try { + bigBounceHere ! msg + afterSend + expectNoMsg(500.millis.dilated) + } finally { + system.eventStream.unsubscribe(eventForwarder, classOf[AssociationErrorEvent]) + system.eventStream.unsubscribe(eventForwarder, classOf[DisassociatedEvent]) + system.stop(eventForwarder) + otherSystem.stop(bigBounceOther) + } + } + + private def byteStringOfSize(size: Int) = ByteString.fromArray(Array.fill(size)(42: Byte)) + + val maxPayloadBytes = system.settings.config.getBytes("akka.remote.test.maximum-payload-bytes").toInt + override def afterTermination() { otherSystem.shutdown() AssociationRegistry.clear() @@ -345,6 +380,41 @@ class RemotingSpec extends AkkaSpec(RemotingSpec.cfg) with ImplicitSender with D expectMsg("postStop") } + "drop unserializable messages" in { + object Unserializable + verifySend(Unserializable) { + expectMsgPF(1.second) { + case AssociationErrorEvent(_: NotSerializableException, _, _, _) ⇒ () + } + } + } + + "allow messages up to payload size" in { + val maxProtocolOverhead = 500 // Make sure we're still under size after the message is serialized, etc + val big = byteStringOfSize(maxPayloadBytes - maxProtocolOverhead) + verifySend(big) { + expectMsg(1.second, big) + } + } + + "drop sent messages over payload size" in { + val oversized = byteStringOfSize(maxPayloadBytes + 1) + verifySend(oversized) { + expectMsgPF(1.second) { + case AssociationErrorEvent(e: OversizedPayloadException, _, _, _) if e.getMessage.startsWith("Discarding oversized payload sent") ⇒ () + } + } + } + + "drop received messages over payload size" in { + // Receiver should reply with a message of size maxPayload + 1, which will be dropped and an error logged + verifySend(maxPayloadBytes + 1) { + expectMsgPF(1.second) { + case AssociationErrorEvent(e: OversizedPayloadException, _, _, _) if e.getMessage.startsWith("Discarding oversized payload received") ⇒ () + } + } + } + } override def beforeTermination() { From 27491b89481cee7e4b3c08b35111fae2b7b678bd Mon Sep 17 00:00:00 2001 From: Patrik Nordwall Date: Wed, 27 Mar 2013 14:33:43 +0100 Subject: [PATCH 47/47] NPE in logFailure, see #3181 --- akka-actor/src/main/scala/akka/actor/FaultHandling.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/akka-actor/src/main/scala/akka/actor/FaultHandling.scala b/akka-actor/src/main/scala/akka/actor/FaultHandling.scala index 4612bff4e5..04fbdc69eb 100644 --- a/akka-actor/src/main/scala/akka/actor/FaultHandling.scala +++ b/akka-actor/src/main/scala/akka/actor/FaultHandling.scala @@ -320,8 +320,8 @@ abstract class SupervisorStrategy { protected def logFailure(context: ActorContext, child: ActorRef, cause: Throwable, decision: Directive): Unit = if (loggingEnabled) { val logMessage = cause match { - case e: ActorInitializationException ⇒ e.getCause.getMessage - case e ⇒ e.getMessage + case e: ActorInitializationException if e.getCause ne null ⇒ e.getCause.getMessage + case e ⇒ e.getMessage } decision match { case Resume ⇒ publish(context, Warning(child.path.toString, getClass, logMessage))