diff --git a/.scala-steward.conf b/.scala-steward.conf index 9fe6a9ba2b..4cc938e48a 100644 --- a/.scala-steward.conf +++ b/.scala-steward.conf @@ -1,3 +1,5 @@ +pullRequests.frequency = "@monthly" + updates.ignore = [ { groupId = "com.google.protobuf", artifactId = "protobuf-java" }, { groupId = "org.scalameta", artifactId = "scalafmt-core" }, @@ -17,3 +19,4 @@ updates.ignore = [ { groupId = "org.mockito", artifactId = "mockito-core" } ] +updatePullRequests = false diff --git a/.scalafix.conf b/.scalafix.conf index c4c957b7a4..f6a1fcdbde 100644 --- a/.scalafix.conf +++ b/.scalafix.conf @@ -31,6 +31,7 @@ ignored-files = [ //ignored packages ignored-packages = [ + "docs", "doc", "jdoc" ] @@ -38,7 +39,7 @@ ignored-packages = [ //sort imports, see https://github.com/NeQuissimus/sort-imports SortImports.asciiSort = false SortImports.blocks = [ - "java.", + "re:javax?\\.", "scala.", "*", "com.sun." diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index dfd02e04bb..8a3ef26d48 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -205,8 +205,8 @@ target PR branch you can do so by setting the PR_TARGET_BRANCH environment varia PR_TARGET_BRANCH=origin/example sbt validatePullRequest ``` -If you have already run all tests and now just need to check that everything is formatted and or mima passes there -are a set of `all*` commands aliases for running `test:compile` (also formats), `mimaReportBinaryIssues`, and `validateCompile` +If you already ran all tests and just need to check formatting and mima, there +is a set of `all*` command aliases that run `test:compile` (also formats), `mimaReportBinaryIssues`, and `validateCompile` (compiles `multi-jvm` if enabled for that project). See `build.sbt` or use completion to find the most appropriate one e.g. `allCluster`, `allTyped`. diff --git a/akka-actor-testkit-typed/src/main/scala/akka/actor/testkit/typed/internal/BehaviorTestKitImpl.scala b/akka-actor-testkit-typed/src/main/scala/akka/actor/testkit/typed/internal/BehaviorTestKitImpl.scala index 776bfd7ed4..bcfce93d48 100644 --- a/akka-actor-testkit-typed/src/main/scala/akka/actor/testkit/typed/internal/BehaviorTestKitImpl.scala +++ b/akka-actor-testkit-typed/src/main/scala/akka/actor/testkit/typed/internal/BehaviorTestKitImpl.scala @@ -33,7 +33,14 @@ private[akka] final class BehaviorTestKitImpl[T](_path: ActorPath, _initialBehav private[akka] def as[U]: BehaviorTestKitImpl[U] = this.asInstanceOf[BehaviorTestKitImpl[U]] private var currentUncanonical = _initialBehavior - private var current = Behavior.validateAsInitial(Behavior.start(_initialBehavior, context)) + private var current = { + try { + context.setCurrentActorThread() + Behavior.validateAsInitial(Behavior.start(_initialBehavior, context)) + } finally { + context.clearCurrentActorThread() + } + } // execute any future tasks scheduled in Actor's constructor runAllTasks() @@ -122,8 +129,13 @@ private[akka] final class BehaviorTestKitImpl[T](_path: ActorPath, _initialBehav override def run(message: T): Unit = { try { - currentUncanonical = Behavior.interpretMessage(current, context, message) - current = Behavior.canonicalize(currentUncanonical, current, context) + context.setCurrentActorThread() + try { + currentUncanonical = Behavior.interpretMessage(current, context, message) + current = Behavior.canonicalize(currentUncanonical, current, context) + } finally { + context.clearCurrentActorThread() + } runAllTasks() } catch handleException } diff --git a/akka-actor-testkit-typed/src/main/scala/akka/actor/testkit/typed/internal/LogbackUtil.scala b/akka-actor-testkit-typed/src/main/scala/akka/actor/testkit/typed/internal/LogbackUtil.scala index 9d6c683c59..c4113a4311 100644 --- a/akka-actor-testkit-typed/src/main/scala/akka/actor/testkit/typed/internal/LogbackUtil.scala +++ b/akka-actor-testkit-typed/src/main/scala/akka/actor/testkit/typed/internal/LogbackUtil.scala @@ -6,9 +6,10 @@ package akka.actor.testkit.typed.internal import org.slf4j.LoggerFactory import org.slf4j.event.Level - import akka.annotation.InternalApi +import scala.annotation.tailrec + /** * INTERNAL API */ @@ -16,9 +17,17 @@ import akka.annotation.InternalApi def loggerNameOrRoot(loggerName: String): String = if (loggerName == "") org.slf4j.Logger.ROOT_LOGGER_NAME else loggerName - def getLogbackLogger(loggerName: String): ch.qos.logback.classic.Logger = { + def getLogbackLogger(loggerName: String): ch.qos.logback.classic.Logger = + getLogbackLoggerInternal(loggerName, 50) + + @tailrec + private def getLogbackLoggerInternal(loggerName: String, count: Int): ch.qos.logback.classic.Logger = { LoggerFactory.getLogger(loggerNameOrRoot(loggerName)) match { - case logger: ch.qos.logback.classic.Logger => logger + case logger: ch.qos.logback.classic.Logger => logger + case _: org.slf4j.helpers.SubstituteLogger if count > 0 => + // Wait for logging initialisation http://www.slf4j.org/codes.html#substituteLogger + Thread.sleep(50) + getLogbackLoggerInternal(loggerName, count - 1) case null => throw new IllegalArgumentException(s"Couldn't find logger for [$loggerName].") case other => diff --git a/akka-actor-testkit-typed/src/main/scala/akka/actor/testkit/typed/internal/StubbedActorContext.scala b/akka-actor-testkit-typed/src/main/scala/akka/actor/testkit/typed/internal/StubbedActorContext.scala index f6fe6e083a..4deba6cc09 100644 --- a/akka-actor-testkit-typed/src/main/scala/akka/actor/testkit/typed/internal/StubbedActorContext.scala +++ b/akka-actor-testkit-typed/src/main/scala/akka/actor/testkit/typed/internal/StubbedActorContext.scala @@ -87,17 +87,25 @@ private[akka] final class FunctionRef[-T](override val path: ActorPath, send: (T throw new UnsupportedOperationException( "No classic ActorContext available with the stubbed actor context, to spawn materializers and run streams you will need a real actor") - override def children: Iterable[ActorRef[Nothing]] = _children.values.map(_.context.self) + override def children: Iterable[ActorRef[Nothing]] = { + checkCurrentActorThread() + _children.values.map(_.context.self) + } def childrenNames: Iterable[String] = _children.keys - override def child(name: String): Option[ActorRef[Nothing]] = _children.get(name).map(_.context.self) + override def child(name: String): Option[ActorRef[Nothing]] = { + checkCurrentActorThread() + _children.get(name).map(_.context.self) + } override def spawnAnonymous[U](behavior: Behavior[U], props: Props = Props.empty): ActorRef[U] = { + checkCurrentActorThread() val btk = new BehaviorTestKitImpl[U]((path / childName.next()).withUid(rnd().nextInt()), behavior) _children += btk.context.self.path.name -> btk btk.context.self } - override def spawn[U](behavior: Behavior[U], name: String, props: Props = Props.empty): ActorRef[U] = + override def spawn[U](behavior: Behavior[U], name: String, props: Props = Props.empty): ActorRef[U] = { + checkCurrentActorThread() _children.get(name) match { case Some(_) => throw classic.InvalidActorNameException(s"actor name $name is already taken") case None => @@ -105,12 +113,14 @@ private[akka] final class FunctionRef[-T](override val path: ActorPath, send: (T _children += name -> btk btk.context.self } + } /** * Do not actually stop the child inbox, only simulate the liveness check. * Removal is asynchronous, explicit removeInbox is needed from outside afterwards. */ override def stop[U](child: ActorRef[U]): Unit = { + checkCurrentActorThread() if (child.path.parent != self.path) throw new IllegalArgumentException( "Only direct children of an actor can be stopped through the actor context, " + @@ -120,11 +130,21 @@ private[akka] final class FunctionRef[-T](override val path: ActorPath, send: (T _children -= child.path.name } } - override def watch[U](other: ActorRef[U]): Unit = () - override def watchWith[U](other: ActorRef[U], message: T): Unit = () - override def unwatch[U](other: ActorRef[U]): Unit = () - override def setReceiveTimeout(d: FiniteDuration, message: T): Unit = () - override def cancelReceiveTimeout(): Unit = () + override def watch[U](other: ActorRef[U]): Unit = { + checkCurrentActorThread() + } + override def watchWith[U](other: ActorRef[U], message: T): Unit = { + checkCurrentActorThread() + } + override def unwatch[U](other: ActorRef[U]): Unit = { + checkCurrentActorThread() + } + override def setReceiveTimeout(d: FiniteDuration, message: T): Unit = { + checkCurrentActorThread() + } + override def cancelReceiveTimeout(): Unit = { + checkCurrentActorThread() + } override def scheduleOnce[U](delay: FiniteDuration, target: ActorRef[U], message: U): classic.Cancellable = new classic.Cancellable { @@ -186,11 +206,20 @@ private[akka] final class FunctionRef[-T](override val path: ActorPath, send: (T override def toString: String = s"Inbox($self)" - override def log: Logger = logger + override def log: Logger = { + checkCurrentActorThread() + logger + } - override def setLoggerName(name: String): Unit = () // nop as we don't track logger + override def setLoggerName(name: String): Unit = { + // nop as we don't track logger + checkCurrentActorThread() + } - override def setLoggerName(clazz: Class[_]): Unit = () // nop as we don't track logger + override def setLoggerName(clazz: Class[_]): Unit = { + // nop as we don't track logger + checkCurrentActorThread() + } /** * The log entries logged through context.log.{debug, info, warn, error} are captured and can be inspected through diff --git a/akka-actor-tests/src/test/scala/akka/actor/ActorLifeCycleSpec.scala b/akka-actor-tests/src/test/scala/akka/actor/ActorLifeCycleSpec.scala index 6e344a941f..a88c7c5baa 100644 --- a/akka-actor-tests/src/test/scala/akka/actor/ActorLifeCycleSpec.scala +++ b/akka-actor-tests/src/test/scala/akka/actor/ActorLifeCycleSpec.scala @@ -5,12 +5,11 @@ package akka.actor import java.util.UUID.{ randomUUID => newUuid } +import java.util.concurrent.CountDownLatch import java.util.concurrent.atomic._ -import scala.concurrent.Await - +import scala.concurrent.{ Await, Future } import org.scalatest.BeforeAndAfterEach - import akka.actor.Actor._ import akka.pattern.ask import akka.testkit._ @@ -19,7 +18,7 @@ object ActorLifeCycleSpec { class LifeCycleTestActor(testActor: ActorRef, id: String, generationProvider: AtomicInteger) extends Actor { def report(msg: Any) = testActor ! message(msg) - def message(msg: Any): Tuple3[Any, String, Int] = (msg, id, currentGen) + def message(msg: Any): (Any, String, Int) = (msg, id, currentGen) val currentGen = generationProvider.getAndIncrement() override def preStart(): Unit = { report("preStart") } override def postStop(): Unit = { report("postStop") } @@ -151,4 +150,41 @@ class ActorLifeCycleSpec extends AkkaSpec with BeforeAndAfterEach with ImplicitS } } + "have a non null context after termination" in { + class StopBeforeFutureFinishes(val latch: CountDownLatch) extends Actor { + import context.dispatcher + import akka.pattern._ + + override def receive: Receive = { + case "ping" => + val replyTo = sender() + + context.stop(self) + + Future { + latch.await() + Thread.sleep(50) + "po" + } + // Here, we implicitly close over the actor instance and access the context + // when the flatMap thunk is run. Previously, the context was nulled when the actor + // was terminated. This isn't done any more. Still, the pattern of `import context.dispatcher` + // is discouraged as closing over `context` is unsafe in general. + .flatMap(x => Future { x + "ng" } /* implicitly: (this.context.dispatcher) */ ) + .recover { case _: NullPointerException => "npe" } + .pipeTo(replyTo) + } + } + + val latch = new CountDownLatch(1) + val actor = system.actorOf(Props(new StopBeforeFutureFinishes(latch))) + watch(actor) + + actor ! "ping" + + expectTerminated(actor) + latch.countDown() + + expectMsg("pong") + } } 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 2f9808744b..4506ef36ba 100644 --- a/akka-actor-tests/src/test/scala/akka/actor/DeathWatchSpec.scala +++ b/akka-actor-tests/src/test/scala/akka/actor/DeathWatchSpec.scala @@ -18,6 +18,10 @@ import akka.testkit._ class LocalDeathWatchSpec extends AkkaSpec with ImplicitSender with DefaultTimeout with DeathWatchSpec object DeathWatchSpec { + object Watcher { + def props(target: ActorRef, testActor: ActorRef) = + Props(classOf[Watcher], target, testActor) + } class Watcher(target: ActorRef, testActor: ActorRef) extends Actor { context.watch(target) def receive = { @@ -26,9 +30,6 @@ object DeathWatchSpec { } } - def props(target: ActorRef, testActor: ActorRef) = - Props(classOf[Watcher], target, testActor) - class EmptyWatcher(target: ActorRef) extends Actor { context.watch(target) def receive = Actor.emptyBehavior @@ -70,6 +71,40 @@ object DeathWatchSpec { final case class FF(fail: Failed) final case class Latches(t1: TestLatch, t2: TestLatch) extends NoSerializationVerificationNeeded + + object WatchWithVerifier { + case class WatchThis(ref: ActorRef) + case object Watching + case class CustomWatchMsg(ref: ActorRef) + case class StartStashing(numberOfMessagesToStash: Int) + case object StashingStarted + + def props(probe: ActorRef) = Props(new WatchWithVerifier(probe)) + } + class WatchWithVerifier(probe: ActorRef) extends Actor with Stash { + import WatchWithVerifier._ + private var stashing = false + private var stashNMessages = 0 + + override def receive: Receive = { + case StartStashing(messagesToStash) => + stashing = true + stashNMessages = messagesToStash + sender() ! StashingStarted + case WatchThis(ref) => + context.watchWith(ref, CustomWatchMsg(ref)) + sender() ! Watching + case _ if stashing => + stash() + stashNMessages -= 1 + if (stashNMessages == 0) { + stashing = false + unstashAll() + } + case msg: CustomWatchMsg => + probe ! msg + } + } } @silent @@ -79,7 +114,8 @@ trait DeathWatchSpec { this: AkkaSpec with ImplicitSender with DefaultTimeout => lazy val supervisor = system.actorOf(Props(classOf[Supervisor], SupervisorStrategy.defaultStrategy), "watchers") - def startWatching(target: ActorRef) = Await.result((supervisor ? props(target, testActor)).mapTo[ActorRef], 3 seconds) + def startWatching(target: ActorRef) = + Await.result((supervisor ? Watcher.props(target, testActor)).mapTo[ActorRef], 3 seconds) "The Death Watch" must { def expectTerminationOf(actorRef: ActorRef) = @@ -244,6 +280,31 @@ trait DeathWatchSpec { this: AkkaSpec with ImplicitSender with DefaultTimeout => w ! Identify(()) expectMsg(ActorIdentity((), Some(w))) } + + "watch with custom message" in { + val verifierProbe = TestProbe() + val verifier = system.actorOf(WatchWithVerifier.props(verifierProbe.ref)) + val subject = system.actorOf(Props[EmptyActor]()) + verifier ! WatchWithVerifier.WatchThis(subject) + expectMsg(WatchWithVerifier.Watching) + + subject ! PoisonPill + verifierProbe.expectMsg(WatchWithVerifier.CustomWatchMsg(subject)) + } + + // Coverage for #29101 + "stash watchWith termination message correctly" in { + val verifierProbe = TestProbe() + val verifier = system.actorOf(WatchWithVerifier.props(verifierProbe.ref)) + val subject = system.actorOf(Props[EmptyActor]()) + verifier ! WatchWithVerifier.WatchThis(subject) + expectMsg(WatchWithVerifier.Watching) + verifier ! WatchWithVerifier.StartStashing(numberOfMessagesToStash = 1) + expectMsg(WatchWithVerifier.StashingStarted) + + subject ! PoisonPill + verifierProbe.expectMsg(WatchWithVerifier.CustomWatchMsg(subject)) + } } } diff --git a/akka-actor-tests/src/test/scala/akka/actor/ExtensionSpec.scala b/akka-actor-tests/src/test/scala/akka/actor/ExtensionSpec.scala index 1996020497..90deed788a 100644 --- a/akka-actor-tests/src/test/scala/akka/actor/ExtensionSpec.scala +++ b/akka-actor-tests/src/test/scala/akka/actor/ExtensionSpec.scala @@ -34,16 +34,17 @@ object FailingTestExtension extends ExtensionId[FailingTestExtension] with Exten class TestException extends IllegalArgumentException("ERR") with NoStackTrace } -object InstanceCountingExtension extends ExtensionId[DummyExtensionImpl] with ExtensionIdProvider { +object InstanceCountingExtension extends ExtensionId[InstanceCountingExtension] with ExtensionIdProvider { val createCount = new AtomicInteger(0) - override def createExtension(system: ExtendedActorSystem): DummyExtensionImpl = { - createCount.addAndGet(1) - new DummyExtensionImpl + override def createExtension(system: ExtendedActorSystem): InstanceCountingExtension = { + new InstanceCountingExtension } override def lookup(): ExtensionId[_ <: Extension] = this } -class DummyExtensionImpl extends Extension +class InstanceCountingExtension extends Extension { + InstanceCountingExtension.createCount.incrementAndGet() +} // Dont't place inside ActorSystemSpec object, since it will not be garbage collected and reference to system remains class FailingTestExtension(val system: ExtendedActorSystem) extends Extension { @@ -111,12 +112,33 @@ class ExtensionSpec extends AnyWordSpec with Matchers { shutdownActorSystem(system) } - "allow for auto-loading of library-extensions" in { + "allow for auto-loading of library-extensions from reference.conf" in { + import akka.util.ccompat.JavaConverters._ + // could be initialized by other tests, but assuming tests are not running in parallel + val countBefore = InstanceCountingExtension.createCount.get() val system = ActorSystem("extensions") - val listedExtensions = system.settings.config.getStringList("akka.library-extensions") - listedExtensions.size should be > 0 - // could be initialized by other tests, so at least once - InstanceCountingExtension.createCount.get() should be > 0 + val listedExtensions = system.settings.config.getStringList("akka.library-extensions").asScala + listedExtensions.count(_.contains("InstanceCountingExtension")) should ===(1) + + InstanceCountingExtension.createCount.get() - countBefore should ===(1) + + shutdownActorSystem(system) + } + + "not create duplicate instances when auto-loading of library-extensions" in { + import akka.util.ccompat.JavaConverters._ + // could be initialized by other tests, but assuming tests are not running in parallel + val countBefore = InstanceCountingExtension.createCount.get() + val system = ActorSystem( + "extensions", + ConfigFactory.parseString( + """ + akka.library-extensions = ["akka.actor.InstanceCountingExtension", "akka.actor.InstanceCountingExtension", "akka.actor.InstanceCountingExtension$"] + """)) + val listedExtensions = system.settings.config.getStringList("akka.library-extensions").asScala + listedExtensions.count(_.contains("InstanceCountingExtension")) should ===(3) // testing duplicate names + + InstanceCountingExtension.createCount.get() - countBefore should ===(1) shutdownActorSystem(system) } diff --git a/akka-actor-tests/src/test/scala/akka/pattern/BackoffOnRestartSupervisorSpec.scala b/akka-actor-tests/src/test/scala/akka/pattern/BackoffOnRestartSupervisorSpec.scala index 4be5f55f34..e6fa3b5a78 100644 --- a/akka-actor-tests/src/test/scala/akka/pattern/BackoffOnRestartSupervisorSpec.scala +++ b/akka-actor-tests/src/test/scala/akka/pattern/BackoffOnRestartSupervisorSpec.scala @@ -31,7 +31,7 @@ class TestActor(probe: ActorRef) extends Actor { probe ! "STARTED" - def receive = { + def receive: Receive = { case "DIE" => context.stop(self) case "THROW" => throw new TestActor.NormalException case "THROW_STOPPING_EXCEPTION" => throw new TestActor.StoppingException @@ -46,9 +46,9 @@ object TestParentActor { } class TestParentActor(probe: ActorRef, supervisorProps: Props) extends Actor { - val supervisor = context.actorOf(supervisorProps) + val supervisor: ActorRef = context.actorOf(supervisorProps) - def receive = { + def receive: Receive = { case other => probe.forward(other) } } @@ -58,10 +58,10 @@ class BackoffOnRestartSupervisorSpec extends AkkaSpec(""" akka.loggers = ["akka.testkit.SilenceAllTestEventListener"] """) with WithLogCapturing with ImplicitSender { - @silent def supervisorProps(probeRef: ActorRef) = { - val options = Backoff - .onFailure(TestActor.props(probeRef), "someChildName", 200 millis, 10 seconds, 0.0, maxNrOfRetries = -1) + val options = BackoffOpts + .onFailure(TestActor.props(probeRef), "someChildName", 200 millis, 10 seconds, 0.0) + .withMaxNrOfRetries(-1) .withSupervisorStrategy(OneForOneStrategy(maxNrOfRetries = 5, withinTimeRange = 30 seconds) { case _: TestActor.StoppingException => SupervisorStrategy.Stop }) @@ -69,16 +69,16 @@ class BackoffOnRestartSupervisorSpec extends AkkaSpec(""" } trait Setup { - val probe = TestProbe() - val supervisor = system.actorOf(supervisorProps(probe.ref)) + val probe: TestProbe = TestProbe() + val supervisor: ActorRef = system.actorOf(supervisorProps(probe.ref)) probe.expectMsg("STARTED") } trait Setup2 { - val probe = TestProbe() - val parent = system.actorOf(TestParentActor.props(probe.ref, supervisorProps(probe.ref))) + val probe: TestProbe = TestProbe() + val parent: ActorRef = system.actorOf(TestParentActor.props(probe.ref, supervisorProps(probe.ref))) probe.expectMsg("STARTED") - val child = probe.lastSender + val child: ActorRef = probe.lastSender } "BackoffOnRestartSupervisor" must { @@ -139,7 +139,7 @@ class BackoffOnRestartSupervisorSpec extends AkkaSpec(""" } class SlowlyFailingActor(latch: CountDownLatch) extends Actor { - def receive = { + def receive: Receive = { case "THROW" => sender ! "THROWN" throw new NormalException @@ -155,18 +155,12 @@ class BackoffOnRestartSupervisorSpec extends AkkaSpec(""" "accept commands while child is terminating" in { val postStopLatch = new CountDownLatch(1) @silent - val options = Backoff - .onFailure( - Props(new SlowlyFailingActor(postStopLatch)), - "someChildName", - 1 nanos, - 1 nanos, - 0.0, - maxNrOfRetries = -1) + val options = BackoffOpts + .onFailure(Props(new SlowlyFailingActor(postStopLatch)), "someChildName", 1 nanos, 1 nanos, 0.0) + .withMaxNrOfRetries(-1) .withSupervisorStrategy(OneForOneStrategy(loggingEnabled = false) { case _: TestActor.StoppingException => SupervisorStrategy.Stop }) - @silent val supervisor = system.actorOf(BackoffSupervisor.props(options)) supervisor ! BackoffSupervisor.GetCurrentChild @@ -221,13 +215,12 @@ class BackoffOnRestartSupervisorSpec extends AkkaSpec(""" // withinTimeRange indicates the time range in which maxNrOfRetries will cause the child to // stop. IE: If we restart more than maxNrOfRetries in a time range longer than withinTimeRange // that is acceptable. - @silent - val options = Backoff - .onFailure(TestActor.props(probe.ref), "someChildName", 300.millis, 10.seconds, 0.0, maxNrOfRetries = -1) + val options = BackoffOpts + .onFailure(TestActor.props(probe.ref), "someChildName", 300.millis, 10.seconds, 0.0) + .withMaxNrOfRetries(-1) .withSupervisorStrategy(OneForOneStrategy(withinTimeRange = 1 seconds, maxNrOfRetries = 3) { case _: TestActor.StoppingException => SupervisorStrategy.Stop }) - @silent val supervisor = system.actorOf(BackoffSupervisor.props(options)) probe.expectMsg("STARTED") filterException[TestActor.TestException] { diff --git a/akka-actor-tests/src/test/scala/akka/pattern/BackoffSupervisorSpec.scala b/akka-actor-tests/src/test/scala/akka/pattern/BackoffSupervisorSpec.scala index 48039c90fe..dcccd99b1b 100644 --- a/akka-actor-tests/src/test/scala/akka/pattern/BackoffSupervisorSpec.scala +++ b/akka-actor-tests/src/test/scala/akka/pattern/BackoffSupervisorSpec.scala @@ -7,7 +7,6 @@ package akka.pattern import scala.concurrent.duration._ import scala.util.control.NoStackTrace -import com.github.ghik.silencer.silent import org.scalatest.concurrent.Eventually import org.scalatest.prop.TableDrivenPropertyChecks._ @@ -24,7 +23,7 @@ object BackoffSupervisorSpec { } class Child(probe: ActorRef) extends Actor { - def receive = { + def receive: Receive = { case "boom" => throw new TestException case msg => probe ! msg } @@ -36,7 +35,7 @@ object BackoffSupervisorSpec { } class ManualChild(probe: ActorRef) extends Actor { - def receive = { + def receive: Receive = { case "boom" => throw new TestException case msg => probe ! msg @@ -48,14 +47,13 @@ object BackoffSupervisorSpec { class BackoffSupervisorSpec extends AkkaSpec with ImplicitSender with Eventually { import BackoffSupervisorSpec._ - @silent("deprecated") - def onStopOptions(props: Props = Child.props(testActor), maxNrOfRetries: Int = -1) = - Backoff.onStop(props, "c1", 100.millis, 3.seconds, 0.2, maxNrOfRetries) - @silent("deprecated") - def onFailureOptions(props: Props = Child.props(testActor), maxNrOfRetries: Int = -1) = - Backoff.onFailure(props, "c1", 100.millis, 3.seconds, 0.2, maxNrOfRetries) - @silent("deprecated") - def create(options: BackoffOptions) = system.actorOf(BackoffSupervisor.props(options)) + def onStopOptions(props: Props = Child.props(testActor), maxNrOfRetries: Int = -1): BackoffOnStopOptions = + BackoffOpts.onStop(props, "c1", 100.millis, 3.seconds, 0.2).withMaxNrOfRetries(maxNrOfRetries) + def onFailureOptions(props: Props = Child.props(testActor), maxNrOfRetries: Int = -1): BackoffOnFailureOptions = + BackoffOpts.onFailure(props, "c1", 100.millis, 3.seconds, 0.2).withMaxNrOfRetries(maxNrOfRetries) + + def create(options: BackoffOnStopOptions): ActorRef = system.actorOf(BackoffSupervisor.props(options)) + def create(options: BackoffOnFailureOptions): ActorRef = system.actorOf(BackoffSupervisor.props(options)) "BackoffSupervisor" must { "start child again when it stops when using `Backoff.onStop`" in { @@ -179,10 +177,10 @@ class BackoffSupervisorSpec extends AkkaSpec with ImplicitSender with Eventually "reply to sender if replyWhileStopped is specified" in { filterException[TestException] { - @silent("deprecated") val supervisor = create( - Backoff - .onFailure(Child.props(testActor), "c1", 100.seconds, 300.seconds, 0.2, maxNrOfRetries = -1) + BackoffOpts + .onFailure(Child.props(testActor), "c1", 100.seconds, 300.seconds, 0.2) + .withMaxNrOfRetries(-1) .withReplyWhileStopped("child was stopped")) supervisor ! BackoffSupervisor.GetCurrentChild val c1 = expectMsgType[BackoffSupervisor.CurrentChild].ref.get @@ -203,11 +201,43 @@ class BackoffSupervisorSpec extends AkkaSpec with ImplicitSender with Eventually } } + "use provided actor while stopped and withHandlerWhileStopped is specified" in { + val handler = system.actorOf(Props(new Actor { + override def receive: Receive = { + case "still there?" => + sender() ! "not here!" + } + })) + filterException[TestException] { + val supervisor = create( + BackoffOpts + .onFailure(Child.props(testActor), "c1", 100.seconds, 300.seconds, 0.2) + .withMaxNrOfRetries(-1) + .withHandlerWhileStopped(handler)) + supervisor ! BackoffSupervisor.GetCurrentChild + val c1 = expectMsgType[BackoffSupervisor.CurrentChild].ref.get + watch(c1) + supervisor ! BackoffSupervisor.GetRestartCount + expectMsg(BackoffSupervisor.RestartCount(0)) + + c1 ! "boom" + expectTerminated(c1) + + awaitAssert { + supervisor ! BackoffSupervisor.GetRestartCount + expectMsg(BackoffSupervisor.RestartCount(1)) + } + + supervisor ! "still there?" + expectMsg("not here!") + } + } + "not reply to sender if replyWhileStopped is NOT specified" in { filterException[TestException] { - @silent("deprecated") val supervisor = - create(Backoff.onFailure(Child.props(testActor), "c1", 100.seconds, 300.seconds, 0.2, maxNrOfRetries = -1)) + create( + BackoffOpts.onFailure(Child.props(testActor), "c1", 100.seconds, 300.seconds, 0.2).withMaxNrOfRetries(-1)) supervisor ! BackoffSupervisor.GetCurrentChild val c1 = expectMsgType[BackoffSupervisor.CurrentChild].ref.get watch(c1) @@ -382,7 +412,7 @@ class BackoffSupervisorSpec extends AkkaSpec with ImplicitSender with Eventually c1 ! PoisonPill expectTerminated(c1) // since actor stopped we can expect the two messages to end up in dead letters - EventFilter.warning(pattern = ".*(ping|stop).*", occurrences = 2).intercept { + EventFilter.warning(pattern = ".*(ping|stop).*", occurrences = 1).intercept { supervisor ! "ping" supervisorWatcher.expectNoMessage(20.millis) // supervisor must not terminate diff --git a/akka-actor-tests/src/test/scala/akka/pattern/RetrySpec.scala b/akka-actor-tests/src/test/scala/akka/pattern/RetrySpec.scala index f079edfb9f..cc0fc56d67 100644 --- a/akka-actor-tests/src/test/scala/akka/pattern/RetrySpec.scala +++ b/akka-actor-tests/src/test/scala/akka/pattern/RetrySpec.scala @@ -124,6 +124,23 @@ class RetrySpec extends AkkaSpec with RetrySupport { elapse <= 100 shouldBe true } } + + "handle thrown exceptions in same way as failed Future" in { + @volatile var failCount = 0 + + def attempt() = { + if (failCount < 5) { + failCount += 1 + throw new IllegalStateException(failCount.toString) + } else Future.successful(5) + } + + val retried = retry(() => attempt(), 10, 100 milliseconds) + + within(3 seconds) { + Await.result(retried, remaining) should ===(5) + } + } } } 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 af8b93291c..f4cc926d49 100644 --- a/akka-actor-tests/src/test/scala/akka/serialization/SerializeSpec.scala +++ b/akka-actor-tests/src/test/scala/akka/serialization/SerializeSpec.scala @@ -270,6 +270,29 @@ class SerializeSpec extends AkkaSpec(SerializationTests.serializeConf) { ser.serialize(new Other).get } } + + "detect duplicate serializer ids" in { + (intercept[IllegalArgumentException] { + val sys = ActorSystem( + "SerializeSpec", + ConfigFactory.parseString(s""" + akka { + actor { + serializers { + test = "akka.serialization.NoopSerializer" + test-same = "akka.serialization.NoopSerializerSameId" + } + + serialization-bindings { + "akka.serialization.SerializationTests$$Person" = test + "akka.serialization.SerializationTests$$Address" = test-same + } + } + } + """)) + shutdown(sys) + }.getMessage should include).regex("Serializer identifier \\[9999\\].*is not unique") + } } } @@ -578,6 +601,8 @@ protected[akka] class NoopSerializer2 extends Serializer { def fromBinary(bytes: Array[Byte], clazz: Option[Class[_]]): AnyRef = null } +protected[akka] class NoopSerializerSameId extends NoopSerializer + @SerialVersionUID(1) protected[akka] final case class FakeThrowable(msg: String) extends Throwable(msg) with Serializable { override def fillInStackTrace = null diff --git a/akka-actor-typed-tests/src/test/scala/akka/actor/typed/scaladsl/ActorThreadSpec.scala b/akka-actor-typed-tests/src/test/scala/akka/actor/typed/scaladsl/ActorThreadSpec.scala new file mode 100644 index 0000000000..0f15aa09e1 --- /dev/null +++ b/akka-actor-typed-tests/src/test/scala/akka/actor/typed/scaladsl/ActorThreadSpec.scala @@ -0,0 +1,271 @@ +/* + * Copyright (C) 2020 Lightbend Inc. + */ + +package akka.actor.typed.scaladsl + +import java.util.concurrent.CountDownLatch +import java.util.concurrent.TimeUnit + +import scala.concurrent.Future +import scala.util.Failure +import scala.util.Success + +import akka.actor.testkit.typed.scaladsl.LogCapturing +import akka.actor.testkit.typed.scaladsl.LoggingTestKit +import akka.actor.testkit.typed.scaladsl.ScalaTestWithActorTestKit +import akka.actor.typed.ActorRef +import akka.actor.typed.Behavior +import akka.actor.typed.scaladsl.ActorThreadSpec.Echo +import org.scalatest.wordspec.AnyWordSpecLike + +object ActorThreadSpec { + object Echo { + final case class Msg(i: Int, replyTo: ActorRef[Int]) + + def apply(): Behavior[Msg] = + Behaviors.receiveMessage { + case Msg(i, replyTo) => + replyTo ! i + Behaviors.same + } + } + +} + +class ActorThreadSpec extends ScalaTestWithActorTestKit with AnyWordSpecLike with LogCapturing { + + "Actor thread-safety checks" must { + + "detect illegal access to ActorContext from outside" in { + @volatile var context: ActorContext[String] = null + val probe = createTestProbe[String]() + + spawn(Behaviors.setup[String] { ctx => + // here it's ok + ctx.children + context = ctx + probe.ref ! "initialized" + Behaviors.empty + }) + + probe.expectMessage("initialized") + intercept[UnsupportedOperationException] { + context.children + }.getMessage should include("Unsupported access to ActorContext") + + } + + "detect illegal access to ActorContext from other thread when processing message" in { + val probe = createTestProbe[UnsupportedOperationException]() + + val ref = spawn(Behaviors.receive[CountDownLatch] { + case (context, latch) => + Future { + try { + context.children + } catch { + case e: UnsupportedOperationException => + probe.ref ! e + } + }(context.executionContext) + latch.await(5, TimeUnit.SECONDS) + Behaviors.same + }) + + val l = new CountDownLatch(1) + try { + ref ! l + probe.receiveMessage().getMessage should include("Unsupported access to ActorContext") + } finally { + l.countDown() + } + } + + "detect illegal access to ActorContext from other thread after processing message" in { + val probe = createTestProbe[UnsupportedOperationException]() + + val ref = spawn(Behaviors.receive[CountDownLatch] { + case (context, latch) => + Future { + try { + latch.await(5, TimeUnit.SECONDS) + context.children + } catch { + case e: UnsupportedOperationException => + probe.ref ! e + } + }(context.executionContext) + + Behaviors.stopped + }) + + val l = new CountDownLatch(1) + try { + ref ! l + probe.expectTerminated(ref) + } finally { + l.countDown() + } + probe.receiveMessage().getMessage should include("Unsupported access to ActorContext") + } + + "detect illegal access from child" in { + val probe = createTestProbe[UnsupportedOperationException]() + + val ref = spawn(Behaviors.receive[String] { + case (context, _) => + // really bad idea to define a child actor like this + context.spawnAnonymous(Behaviors.setup[String] { _ => + try { + context.children + } catch { + case e: UnsupportedOperationException => + probe.ref ! e + } + Behaviors.empty + }) + Behaviors.same + }) + + ref ! "hello" + probe.receiveMessage().getMessage should include("Unsupported access to ActorContext") + } + + "allow access from message adapter" in { + val probe = createTestProbe[String]() + val echo = spawn(Echo()) + + spawn(Behaviors.setup[String] { context => + val replyAdapter = context.messageAdapter[Int] { i => + // this is allowed because the mapping function is running in the target actor + context.children + i.toString + } + echo ! Echo.Msg(17, replyAdapter) + + Behaviors.receiveMessage { msg => + probe.ref ! msg + Behaviors.same + } + }) + + probe.expectMessage("17") + } + + "allow access from ask response mapper" in { + val probe = createTestProbe[String]() + val echo = spawn(Echo()) + + spawn(Behaviors.setup[String] { context => + context.ask[Echo.Msg, Int](echo, Echo.Msg(18, _)) { + case Success(i) => + // this is allowed because the mapping function is running in the target actor + context.children + i.toString + case Failure(e) => throw e + } + + Behaviors.receiveMessage { msg => + probe.ref ! msg + Behaviors.same + } + }) + + probe.expectMessage("18") + } + + "detect wrong context in construction of AbstractBehavior" in { + val probe = createTestProbe[String]() + val ref = spawn(Behaviors.setup[String] { context => + // missing setup new AbstractBehavior and passing in parent's context + val child = context.spawnAnonymous(new AbstractBehavior[String](context) { + override def onMessage(msg: String): Behavior[String] = { + probe.ref ! msg + Behaviors.same + } + }) + + Behaviors.receiveMessage { msg => + child ! msg + Behaviors.same + } + }) + + // 2 occurrences because one from PostStop also + LoggingTestKit + .error[IllegalStateException] + .withMessageContains("was created with wrong ActorContext") + .withOccurrences(2) + .expect { + // it's not detected when spawned, but when processing message + ref ! "hello" + probe.expectNoMessage() + } + } + + "detect illegal access from AbstractBehavior constructor" in { + val probe = createTestProbe[UnsupportedOperationException]() + + spawn(Behaviors.setup[String] { context => + context.spawnAnonymous( + Behaviors.setup[String](_ => + // wrongly using parent's context + new AbstractBehavior[String](context) { + try { + this.context.children + } catch { + case e: UnsupportedOperationException => + probe.ref ! e + } + + override def onMessage(msg: String): Behavior[String] = { + Behaviors.same + } + })) + + Behaviors.empty + }) + + probe.receiveMessage().getMessage should include("Unsupported access to ActorContext") + } + + "detect sharing of same AbstractBehavior instance" in { + // extremely contrived example, but the creativity among users can be great + @volatile var behv: Behavior[CountDownLatch] = null + + val ref1 = spawn(Behaviors.setup[CountDownLatch] { context => + behv = new AbstractBehavior[CountDownLatch](context) { + override def onMessage(latch: CountDownLatch): Behavior[CountDownLatch] = { + latch.await(5, TimeUnit.SECONDS) + Behaviors.same + } + } + behv + }) + + eventually(behv shouldNot equal(null)) + + // spawning same instance again + val ref2 = spawn(behv) + + val latch1 = new CountDownLatch(1) + try { + ref1 ! latch1 + + // 2 occurrences because one from PostStop also + LoggingTestKit + .error[IllegalStateException] + .withMessageContains("was created with wrong ActorContext") + .withOccurrences(2) + .expect { + ref2 ! new CountDownLatch(0) + } + } finally { + latch1.countDown() + } + } + + } + +} diff --git a/akka-actor-typed-tests/src/test/scala/akka/actor/typed/scaladsl/MessageAdapterSpec.scala b/akka-actor-typed-tests/src/test/scala/akka/actor/typed/scaladsl/MessageAdapterSpec.scala index 0e0280f3fd..d6829e2a2e 100644 --- a/akka-actor-typed-tests/src/test/scala/akka/actor/typed/scaladsl/MessageAdapterSpec.scala +++ b/akka-actor-typed-tests/src/test/scala/akka/actor/typed/scaladsl/MessageAdapterSpec.scala @@ -6,7 +6,6 @@ package akka.actor.typed.scaladsl import com.typesafe.config.ConfigFactory import org.scalatest.wordspec.AnyWordSpecLike -import org.slf4j.event.Level import akka.actor.testkit.typed.TestException import akka.actor.testkit.typed.scaladsl.LogCapturing @@ -17,6 +16,7 @@ import akka.actor.typed.ActorRef import akka.actor.typed.Behavior import akka.actor.typed.PostStop import akka.actor.typed.Props +import akka.actor.typed.internal.AdaptMessage object MessageAdapterSpec { val config = ConfigFactory.parseString(""" @@ -271,13 +271,15 @@ class MessageAdapterSpec } - "log wrapped message of DeadLetter" in { + "redirect to DeadLetter after termination" in { case class Ping(sender: ActorRef[Pong]) case class Pong(greeting: String) case class PingReply(response: Pong) val pingProbe = createTestProbe[Ping]() + val deadLetterProbe = testKit.createDeadLetterProbe() + val snitch = Behaviors.setup[PingReply] { context => val replyTo = context.messageAdapter[Pong](PingReply) pingProbe.ref ! Ping(replyTo) @@ -287,13 +289,13 @@ class MessageAdapterSpec createTestProbe().expectTerminated(ref) - LoggingTestKit.empty - .withLogLevel(Level.INFO) - .withMessageRegex("Pong.*wrapped in.*AdaptMessage.*dead letters encountered") - .expect { - pingProbe.receiveMessage().sender ! Pong("hi") - } - + pingProbe.receiveMessage().sender ! Pong("hi") + val deadLetter = deadLetterProbe.receiveMessage() + deadLetter.message match { + case AdaptMessage(Pong("hi"), _) => // passed through the FunctionRef + case Pong("hi") => // FunctionRef stopped + case unexpected => fail(s"Unexpected message [$unexpected], expected Pong or AdaptMessage(Pong)") + } } } diff --git a/akka-actor-typed/src/main/mima-filters/2.6.5.backwards.excludes/current-actor-thread.excludes b/akka-actor-typed/src/main/mima-filters/2.6.5.backwards.excludes/current-actor-thread.excludes new file mode 100644 index 0000000000..f4eae2851d --- /dev/null +++ b/akka-actor-typed/src/main/mima-filters/2.6.5.backwards.excludes/current-actor-thread.excludes @@ -0,0 +1,7 @@ +# add internal currentActorThread to ActorContext +ProblemFilters.exclude[ReversedMissingMethodProblem]("akka.actor.typed.scaladsl.ActorContext.setCurrentActorThread") +ProblemFilters.exclude[ReversedMissingMethodProblem]("akka.actor.typed.scaladsl.ActorContext.clearCurrentActorThread") +ProblemFilters.exclude[ReversedMissingMethodProblem]("akka.actor.typed.scaladsl.ActorContext.checkCurrentActorThread") +ProblemFilters.exclude[ReversedMissingMethodProblem]("akka.actor.typed.internal.ActorContextImpl.akka$actor$typed$internal$ActorContextImpl$$_currentActorThread_=") +ProblemFilters.exclude[ReversedMissingMethodProblem]("akka.actor.typed.internal.ActorContextImpl.akka$actor$typed$internal$ActorContextImpl$$_currentActorThread") + diff --git a/akka-actor-typed/src/main/resources/reference.conf b/akka-actor-typed/src/main/resources/reference.conf index f3f36a1f40..3cb166b939 100644 --- a/akka-actor-typed/src/main/resources/reference.conf +++ b/akka-actor-typed/src/main/resources/reference.conf @@ -16,7 +16,7 @@ akka.actor.typed { library-extensions = ${?akka.actor.typed.library-extensions} [] # Receptionist is started eagerly to allow clustered receptionist to gather remote registrations early on. - library-extensions += "akka.actor.typed.receptionist.Receptionist" + library-extensions += "akka.actor.typed.receptionist.Receptionist$" # While an actor is restarted (waiting for backoff to expire and children to stop) # incoming messages and signals are stashed, and delivered later to the newly restarted diff --git a/akka-actor-typed/src/main/scala/akka/actor/typed/internal/ActorContextImpl.scala b/akka-actor-typed/src/main/scala/akka/actor/typed/internal/ActorContextImpl.scala index 66acdb81c2..a1ba9b6e41 100644 --- a/akka-actor-typed/src/main/scala/akka/actor/typed/internal/ActorContextImpl.scala +++ b/akka-actor-typed/src/main/scala/akka/actor/typed/internal/ActorContextImpl.scala @@ -95,10 +95,16 @@ import akka.util.Timeout private var _messageAdapters: List[(Class[_], Any => T)] = Nil private var _timer: OptionVal[TimerSchedulerImpl[T]] = OptionVal.None + // _currentActorThread is on purpose not volatile. Used from `checkCurrentActorThread`. + // It will always see the right value when accessed from the right thread. + // Possible that it would NOT detect illegal access sometimes but that's ok. + private var _currentActorThread: OptionVal[Thread] = OptionVal.None + // context-shared timer needed to allow for nested timer usage def timer: TimerSchedulerImpl[T] = _timer match { case OptionVal.Some(timer) => timer case OptionVal.None => + checkCurrentActorThread() val timer = new TimerSchedulerImpl[T](this) _timer = OptionVal.Some(timer) timer @@ -152,6 +158,7 @@ import akka.util.Timeout } override def log: Logger = { + checkCurrentActorThread() val logging = loggingContext() ActorMdc.setMdc(logging) logging.logger @@ -160,6 +167,7 @@ import akka.util.Timeout override def getLog: Logger = log override def setLoggerName(name: String): Unit = { + checkCurrentActorThread() _logging = OptionVal.Some(loggingContext().withLogger(LoggerFactory.getLogger(name))) } @@ -247,6 +255,7 @@ import akka.util.Timeout internalMessageAdapter(messageClass, f.apply) private def internalMessageAdapter[U](messageClass: Class[U], f: U => T): ActorRef[U] = { + checkCurrentActorThread() // replace existing adapter for same class, only one per class is supported to avoid unbounded growth // in case "same" adapter is added repeatedly val boxedMessageClass = BoxedType(messageClass).asInstanceOf[Class[U]] @@ -268,4 +277,44 @@ import akka.util.Timeout * INTERNAL API */ @InternalApi private[akka] def messageAdapters: List[(Class[_], Any => T)] = _messageAdapters + + /** + * INTERNAL API + */ + @InternalApi private[akka] def setCurrentActorThread(): Unit = { + _currentActorThread match { + case OptionVal.None => + _currentActorThread = OptionVal.Some(Thread.currentThread()) + case OptionVal.Some(t) => + throw new IllegalStateException( + s"Invalid access by thread from the outside of $self. " + + s"Current message is processed by $t, but also accessed from from ${Thread.currentThread()}.") + } + } + + /** + * INTERNAL API + */ + @InternalApi private[akka] def clearCurrentActorThread(): Unit = { + _currentActorThread = OptionVal.None + } + + /** + * INTERNAL API + */ + @InternalApi private[akka] def checkCurrentActorThread(): Unit = { + val callerThread = Thread.currentThread() + _currentActorThread match { + case OptionVal.Some(t) => + if (callerThread ne t) { + throw new UnsupportedOperationException( + s"Unsupported access to ActorContext operation from the outside of $self. " + + s"Current message is processed by $t, but ActorContext was called from $callerThread.") + } + case OptionVal.None => + throw new UnsupportedOperationException( + s"Unsupported access to ActorContext from the outside of $self. " + + s"No message is currently processed by the actor, but ActorContext was called from $callerThread.") + } + } } diff --git a/akka-actor-typed/src/main/scala/akka/actor/typed/internal/adapter/ActorAdapter.scala b/akka-actor-typed/src/main/scala/akka/actor/typed/internal/adapter/ActorAdapter.scala index 13ea8e77d9..c57d90b3c2 100644 --- a/akka-actor-typed/src/main/scala/akka/actor/typed/internal/adapter/ActorAdapter.scala +++ b/akka-actor-typed/src/main/scala/akka/actor/typed/internal/adapter/ActorAdapter.scala @@ -75,6 +75,7 @@ import akka.util.OptionVal def receive: Receive = ActorAdapter.DummyReceive override protected[akka] def aroundReceive(receive: Receive, msg: Any): Unit = { + ctx.setCurrentActorThread() try { // as we know we never become in "normal" typed actors, it is just the current behavior that // changes, we can avoid some overhead with the partial function/behavior stack of untyped entirely @@ -104,7 +105,10 @@ import akka.util.OptionVal case msg: T @unchecked => handleMessage(msg) } - } finally ctx.clearMdc() + } finally { + ctx.clearCurrentActorThread() + ctx.clearMdc() + } } private def handleMessage(msg: T): Unit = { @@ -206,32 +210,38 @@ import akka.util.OptionVal } override val supervisorStrategy = classic.OneForOneStrategy(loggingEnabled = false) { - case TypedActorFailedException(cause) => - // These have already been optionally logged by typed supervision - recordChildFailure(cause) - classic.SupervisorStrategy.Stop case ex => - val isTypedActor = sender() match { - case afwc: ActorRefWithCell => - afwc.underlying.props.producer.actorClass == classOf[ActorAdapter[_]] + ctx.setCurrentActorThread() + try ex match { + case TypedActorFailedException(cause) => + // These have already been optionally logged by typed supervision + recordChildFailure(cause) + classic.SupervisorStrategy.Stop case _ => - false - } - recordChildFailure(ex) - val logMessage = ex match { - case e: ActorInitializationException if e.getCause ne null => - e.getCause match { - case ex: InvocationTargetException if ex.getCause ne null => ex.getCause.getMessage - case ex => ex.getMessage + val isTypedActor = sender() match { + case afwc: ActorRefWithCell => + afwc.underlying.props.producer.actorClass == classOf[ActorAdapter[_]] + case _ => + false } - case e => e.getMessage + recordChildFailure(ex) + val logMessage = ex match { + case e: ActorInitializationException if e.getCause ne null => + e.getCause match { + case ex: InvocationTargetException if ex.getCause ne null => ex.getCause.getMessage + case ex => ex.getMessage + } + case e => e.getMessage + } + // log at Error as that is what the supervision strategy would have done. + ctx.log.error(logMessage, ex) + if (isTypedActor) + classic.SupervisorStrategy.Stop + else + ActorAdapter.classicSupervisorDecider(ex) + } finally { + ctx.clearCurrentActorThread() } - // log at Error as that is what the supervision strategy would have done. - ctx.log.error(logMessage, ex) - if (isTypedActor) - classic.SupervisorStrategy.Stop - else - ActorAdapter.classicSupervisorDecider(ex) } private def recordChildFailure(ex: Throwable): Unit = { @@ -241,6 +251,30 @@ import akka.util.OptionVal } } + override protected[akka] def aroundPreStart(): Unit = { + ctx.setCurrentActorThread() + try super.aroundPreStart() + finally ctx.clearCurrentActorThread() + } + + override protected[akka] def aroundPreRestart(reason: Throwable, message: Option[Any]): Unit = { + ctx.setCurrentActorThread() + try super.aroundPreRestart(reason, message) + finally ctx.clearCurrentActorThread() + } + + override protected[akka] def aroundPostRestart(reason: Throwable): Unit = { + ctx.setCurrentActorThread() + try super.aroundPostRestart(reason) + finally ctx.clearCurrentActorThread() + } + + override protected[akka] def aroundPostStop(): Unit = { + ctx.setCurrentActorThread() + try super.aroundPostStop() + finally ctx.clearCurrentActorThread() + } + override def preStart(): Unit = { try { if (Behavior.isAlive(behavior)) { diff --git a/akka-actor-typed/src/main/scala/akka/actor/typed/internal/adapter/ActorContextAdapter.scala b/akka-actor-typed/src/main/scala/akka/actor/typed/internal/adapter/ActorContextAdapter.scala index d75edffc68..b29a44111d 100644 --- a/akka-actor-typed/src/main/scala/akka/actor/typed/internal/adapter/ActorContextAdapter.scala +++ b/akka-actor-typed/src/main/scala/akka/actor/typed/internal/adapter/ActorContextAdapter.scala @@ -58,13 +58,26 @@ private[akka] object ActorContextAdapter { final override val self = ActorRefAdapter(classicContext.self) final override val system = ActorSystemAdapter(classicContext.system) private[akka] def classicActorContext = classicContext - override def children: Iterable[ActorRef[Nothing]] = classicContext.children.map(ActorRefAdapter(_)) - override def child(name: String): Option[ActorRef[Nothing]] = classicContext.child(name).map(ActorRefAdapter(_)) - override def spawnAnonymous[U](behavior: Behavior[U], props: Props = Props.empty): ActorRef[U] = + override def children: Iterable[ActorRef[Nothing]] = { + checkCurrentActorThread() + classicContext.children.map(ActorRefAdapter(_)) + } + override def child(name: String): Option[ActorRef[Nothing]] = { + checkCurrentActorThread() + classicContext.child(name).map(ActorRefAdapter(_)) + } + override def spawnAnonymous[U](behavior: Behavior[U], props: Props = Props.empty): ActorRef[U] = { + checkCurrentActorThread() ActorRefFactoryAdapter.spawnAnonymous(classicContext, behavior, props, rethrowTypedFailure = true) - override def spawn[U](behavior: Behavior[U], name: String, props: Props = Props.empty): ActorRef[U] = + } + + override def spawn[U](behavior: Behavior[U], name: String, props: Props = Props.empty): ActorRef[U] = { + checkCurrentActorThread() ActorRefFactoryAdapter.spawn(classicContext, behavior, name, props, rethrowTypedFailure = true) - override def stop[U](child: ActorRef[U]): Unit = + } + + override def stop[U](child: ActorRef[U]): Unit = { + checkCurrentActorThread() if (child.path.parent == self.path) { // only if a direct child toClassic(child) match { case f: akka.actor.FunctionRef => @@ -90,16 +103,29 @@ private[akka] object ActorContextAdapter { s"but [$child] is not a child of [$self]. Stopping other actors has to be expressed as " + "an explicit stop message that the actor accepts.") } + } - override def watch[U](other: ActorRef[U]): Unit = { classicContext.watch(toClassic(other)) } - override def watchWith[U](other: ActorRef[U], msg: T): Unit = { classicContext.watchWith(toClassic(other), msg) } - override def unwatch[U](other: ActorRef[U]): Unit = { classicContext.unwatch(toClassic(other)) } + override def watch[U](other: ActorRef[U]): Unit = { + checkCurrentActorThread() + classicContext.watch(toClassic(other)) + } + override def watchWith[U](other: ActorRef[U], msg: T): Unit = { + checkCurrentActorThread() + classicContext.watchWith(toClassic(other), msg) + } + override def unwatch[U](other: ActorRef[U]): Unit = { + checkCurrentActorThread() + classicContext.unwatch(toClassic(other)) + } var receiveTimeoutMsg: T = null.asInstanceOf[T] override def setReceiveTimeout(d: FiniteDuration, msg: T): Unit = { + checkCurrentActorThread() receiveTimeoutMsg = msg classicContext.setReceiveTimeout(d) } override def cancelReceiveTimeout(): Unit = { + checkCurrentActorThread() + receiveTimeoutMsg = null.asInstanceOf[T] classicContext.setReceiveTimeout(Duration.Undefined) } diff --git a/akka-actor-typed/src/main/scala/akka/actor/typed/javadsl/AbstractBehavior.scala b/akka-actor-typed/src/main/scala/akka/actor/typed/javadsl/AbstractBehavior.scala index 7ccc276c0e..23a7dfc2a3 100644 --- a/akka-actor-typed/src/main/scala/akka/actor/typed/javadsl/AbstractBehavior.scala +++ b/akka-actor-typed/src/main/scala/akka/actor/typed/javadsl/AbstractBehavior.scala @@ -50,12 +50,13 @@ abstract class AbstractBehavior[T](context: ActorContext[T]) extends ExtensibleB protected def getContext: ActorContext[T] = context - private def checkRightContext(ctx: TypedActorContext[T]): Unit = + private def checkRightContext(ctx: TypedActorContext[T]): Unit = { if (ctx.asJava ne context) throw new IllegalStateException( s"Actor [${ctx.asJava.getSelf}] of AbstractBehavior class " + s"[${getClass.getName}] was created with wrong ActorContext [${context.asJava.getSelf}]. " + "Wrap in Behaviors.setup and pass the context to the constructor of AbstractBehavior.") + } @throws(classOf[Exception]) override final def receive(ctx: TypedActorContext[T], msg: T): Behavior[T] = { diff --git a/akka-actor-typed/src/main/scala/akka/actor/typed/scaladsl/AbstractBehavior.scala b/akka-actor-typed/src/main/scala/akka/actor/typed/scaladsl/AbstractBehavior.scala index 0bc42f229b..d830cd4eb1 100644 --- a/akka-actor-typed/src/main/scala/akka/actor/typed/scaladsl/AbstractBehavior.scala +++ b/akka-actor-typed/src/main/scala/akka/actor/typed/scaladsl/AbstractBehavior.scala @@ -70,12 +70,13 @@ abstract class AbstractBehavior[T](protected val context: ActorContext[T]) exten @throws(classOf[Exception]) def onSignal: PartialFunction[Signal, Behavior[T]] = PartialFunction.empty - private def checkRightContext(ctx: TypedActorContext[T]): Unit = + private def checkRightContext(ctx: TypedActorContext[T]): Unit = { if (ctx.asJava ne context) throw new IllegalStateException( s"Actor [${ctx.asJava.getSelf}] of AbstractBehavior class " + s"[${getClass.getName}] was created with wrong ActorContext [${context.asJava.getSelf}]. " + "Wrap in Behaviors.setup and pass the context to the constructor of AbstractBehavior.") + } @throws(classOf[Exception]) override final def receive(ctx: TypedActorContext[T], msg: T): Behavior[T] = { diff --git a/akka-actor-typed/src/main/scala/akka/actor/typed/scaladsl/ActorContext.scala b/akka-actor-typed/src/main/scala/akka/actor/typed/scaladsl/ActorContext.scala index cd5658bfeb..86dfb5f1d2 100644 --- a/akka-actor-typed/src/main/scala/akka/actor/typed/scaladsl/ActorContext.scala +++ b/akka-actor-typed/src/main/scala/akka/actor/typed/scaladsl/ActorContext.scala @@ -337,4 +337,19 @@ trait ActorContext[T] extends TypedActorContext[T] with ClassicActorContextProvi @InternalApi private[akka] def clearMdc(): Unit + /** + * INTERNAL API + */ + @InternalApi private[akka] def setCurrentActorThread(): Unit + + /** + * INTERNAL API + */ + @InternalApi private[akka] def clearCurrentActorThread(): Unit + + /** + * INTERNAL API + */ + @InternalApi private[akka] def checkCurrentActorThread(): Unit + } diff --git a/akka-actor-typed/src/main/scala/akka/actor/typed/scaladsl/AskPattern.scala b/akka-actor-typed/src/main/scala/akka/actor/typed/scaladsl/AskPattern.scala index 60359de5e6..5894b194e1 100644 --- a/akka-actor-typed/src/main/scala/akka/actor/typed/scaladsl/AskPattern.scala +++ b/akka-actor-typed/src/main/scala/akka/actor/typed/scaladsl/AskPattern.scala @@ -17,9 +17,9 @@ import akka.actor.typed.RecipientRef import akka.actor.typed.Scheduler import akka.actor.typed.internal.{ adapter => adapt } import akka.actor.typed.internal.InternalRecipientRef -import akka.annotation.InternalApi +import akka.annotation.{ InternalApi, InternalStableApi } import akka.pattern.PromiseActorRef -import akka.util.Timeout +import akka.util.{ unused, Timeout } /** * The ask-pattern implements the initiator side of a request–reply protocol. @@ -146,14 +146,19 @@ object AskPattern { val ref: ActorRef[U] = _ref val future: Future[U] = _future val promiseRef: PromiseActorRef = _promiseRef + + @InternalStableApi + private[akka] def ask[T](target: InternalRecipientRef[T], message: T, @unused timeout: Timeout): Future[U] = { + target ! message + future + } } private def askClassic[T, U](target: InternalRecipientRef[T], timeout: Timeout, f: ActorRef[U] => T): Future[U] = { val p = new PromiseRef[U](target, timeout) val m = f(p.ref) if (p.promiseRef ne null) p.promiseRef.messageClassName = m.getClass.getName - target ! m - p.future + p.ask(target, m, timeout) } /** diff --git a/akka-actor/src/main/mima-filters/2.6.5.backwards.excludes/27614-no-reflection-in-actorcell.excludes b/akka-actor/src/main/mima-filters/2.6.5.backwards.excludes/27614-no-reflection-in-actorcell.excludes new file mode 100644 index 0000000000..e131db5de0 --- /dev/null +++ b/akka-actor/src/main/mima-filters/2.6.5.backwards.excludes/27614-no-reflection-in-actorcell.excludes @@ -0,0 +1,7 @@ +# #25040 changes to ActorCell internals +ProblemFilters.exclude[DirectMissingMethodProblem]("akka.actor.ActorCell.setActorFields") +ProblemFilters.exclude[DirectMissingMethodProblem]("akka.actor.ActorCell.clearActorCellFields") +ProblemFilters.exclude[DirectMissingMethodProblem]("akka.actor.ActorCell.actor_=") +ProblemFilters.exclude[ReversedMissingMethodProblem]("akka.actor.dungeon.FaultHandling.akka$actor$dungeon$FaultHandling$$_failed") +ProblemFilters.exclude[ReversedMissingMethodProblem]("akka.actor.dungeon.FaultHandling.akka$actor$dungeon$FaultHandling$$_failed_=") +ProblemFilters.exclude[DirectMissingMethodProblem]("akka.util.Reflect.lookupAndSetField") diff --git a/akka-actor/src/main/mima-filters/2.6.5.backwards.excludes/29082-backoff-reply.excludes b/akka-actor/src/main/mima-filters/2.6.5.backwards.excludes/29082-backoff-reply.excludes new file mode 100644 index 0000000000..a653bde9fb --- /dev/null +++ b/akka-actor/src/main/mima-filters/2.6.5.backwards.excludes/29082-backoff-reply.excludes @@ -0,0 +1,27 @@ +# Internals changed +ProblemFilters.exclude[ReversedMissingMethodProblem]("akka.pattern.ExtendedBackoffOptions.withHandlerWhileStopped") +ProblemFilters.exclude[IncompatibleResultTypeProblem]("akka.pattern.BackoffOnFailureOptionsImpl.$default$8") +ProblemFilters.exclude[IncompatibleMethTypeProblem]("akka.pattern.BackoffOnFailureOptionsImpl.apply") +ProblemFilters.exclude[IncompatibleResultTypeProblem]("akka.pattern.BackoffOnFailureOptionsImpl.apply$default$8") +ProblemFilters.exclude[IncompatibleResultTypeProblem]("akka.pattern.BackoffOnStopOptionsImpl.$default$8") +ProblemFilters.exclude[IncompatibleMethTypeProblem]("akka.pattern.BackoffOnStopOptionsImpl.apply") +ProblemFilters.exclude[IncompatibleResultTypeProblem]("akka.pattern.BackoffOnStopOptionsImpl.apply$default$8") +ProblemFilters.exclude[IncompatibleResultTypeProblem]("akka.pattern.BackoffOnFailureOptionsImpl.$default$8") +ProblemFilters.exclude[IncompatibleResultTypeProblem]("akka.pattern.BackoffOnFailureOptionsImpl.apply$default$8") +ProblemFilters.exclude[IncompatibleMethTypeProblem]("akka.pattern.BackoffOnFailureOptionsImpl.apply") +ProblemFilters.exclude[DirectMissingMethodProblem]("akka.pattern.BackoffOnFailureOptionsImpl.replyWhileStopped") +ProblemFilters.exclude[IncompatibleMethTypeProblem]("akka.pattern.BackoffOnFailureOptionsImpl.copy") +ProblemFilters.exclude[IncompatibleResultTypeProblem]("akka.pattern.BackoffOnFailureOptionsImpl.copy$default$8") +ProblemFilters.exclude[IncompatibleMethTypeProblem]("akka.pattern.BackoffOnFailureOptionsImpl.this") +ProblemFilters.exclude[IncompatibleResultTypeProblem]("akka.pattern.BackoffOnStopOptionsImpl.$default$8") +ProblemFilters.exclude[IncompatibleResultTypeProblem]("akka.pattern.BackoffOnStopOptionsImpl.apply$default$8") +ProblemFilters.exclude[IncompatibleMethTypeProblem]("akka.pattern.BackoffOnStopOptionsImpl.apply") +ProblemFilters.exclude[DirectMissingMethodProblem]("akka.pattern.BackoffOnStopOptionsImpl.replyWhileStopped") +ProblemFilters.exclude[IncompatibleMethTypeProblem]("akka.pattern.BackoffOnStopOptionsImpl.copy") +ProblemFilters.exclude[IncompatibleResultTypeProblem]("akka.pattern.BackoffOnStopOptionsImpl.copy$default$8") +ProblemFilters.exclude[IncompatibleMethTypeProblem]("akka.pattern.BackoffOnStopOptionsImpl.this") +ProblemFilters.exclude[IncompatibleMethTypeProblem]("akka.pattern.internal.BackoffOnRestartSupervisor.this") +ProblemFilters.exclude[IncompatibleMethTypeProblem]("akka.pattern.internal.BackoffOnStopSupervisor.this") +ProblemFilters.exclude[IncompatibleSignatureProblem]("akka.pattern.BackoffOnFailureOptionsImpl.unapply") +ProblemFilters.exclude[IncompatibleSignatureProblem]("akka.pattern.BackoffOnStopOptionsImpl.unapply") +ProblemFilters.exclude[IncompatibleSignatureProblem]("akka.pattern.BackoffOnFailureOptionsImpl.unapply") \ No newline at end of file diff --git a/akka-actor/src/main/resources/reference.conf b/akka-actor/src/main/resources/reference.conf index 9d3ba13270..76f4eceef8 100644 --- a/akka-actor/src/main/resources/reference.conf +++ b/akka-actor/src/main/resources/reference.conf @@ -73,7 +73,7 @@ akka { # # Should not be set by end user applications in 'application.conf', use the extensions property for that # - library-extensions = ${?akka.library-extensions} ["akka.serialization.SerializationExtension"] + library-extensions = ${?akka.library-extensions} ["akka.serialization.SerializationExtension$"] # List FQCN of extensions which shall be loaded at actor system startup. # Should be on the format: 'extensions = ["foo", "bar"]' etc. diff --git a/akka-actor/src/main/scala-2.13/akka/compat/Future.scala b/akka-actor/src/main/scala-2.13/akka/compat/Future.scala index baf9ddd557..6cae239b68 100644 --- a/akka-actor/src/main/scala-2.13/akka/compat/Future.scala +++ b/akka-actor/src/main/scala-2.13/akka/compat/Future.scala @@ -4,9 +4,10 @@ package akka.compat -import akka.annotation.InternalApi -import scala.concurrent.{ ExecutionContext, Future => SFuture } import scala.collection.immutable +import scala.concurrent.{ ExecutionContext, Future => SFuture } + +import akka.annotation.InternalApi /** * INTERNAL API diff --git a/akka-actor/src/main/scala-2.13/akka/dispatch/internal/SameThreadExecutionContext.scala b/akka-actor/src/main/scala-2.13/akka/dispatch/internal/SameThreadExecutionContext.scala index 005b0ee59d..5599c7fdd4 100644 --- a/akka-actor/src/main/scala-2.13/akka/dispatch/internal/SameThreadExecutionContext.scala +++ b/akka-actor/src/main/scala-2.13/akka/dispatch/internal/SameThreadExecutionContext.scala @@ -4,10 +4,10 @@ package akka.dispatch.internal -import akka.annotation.InternalApi - import scala.concurrent.ExecutionContext +import akka.annotation.InternalApi + /** * Factory to create same thread ec. Not intended to be called from any other site than to create [[akka.dispatch.ExecutionContexts#parasitic]] * diff --git a/akka-actor/src/main/scala-2.13/akka/util/ByteIterator.scala b/akka-actor/src/main/scala-2.13/akka/util/ByteIterator.scala index 073b0f9b77..b5252803bc 100644 --- a/akka-actor/src/main/scala-2.13/akka/util/ByteIterator.scala +++ b/akka-actor/src/main/scala-2.13/akka/util/ByteIterator.scala @@ -4,8 +4,6 @@ package akka.util -import akka.util.Collections.EmptyImmutableSeq - import java.nio.{ ByteBuffer, ByteOrder } import scala.annotation.tailrec @@ -14,6 +12,8 @@ import scala.collection.LinearSeq import scala.collection.mutable.ListBuffer import scala.reflect.ClassTag +import akka.util.Collections.EmptyImmutableSeq + object ByteIterator { object ByteArrayIterator { diff --git a/akka-actor/src/main/scala-2.13/akka/util/ByteString.scala b/akka-actor/src/main/scala-2.13/akka/util/ByteString.scala index 4baeccc56e..e6009e1f0f 100644 --- a/akka-actor/src/main/scala-2.13/akka/util/ByteString.scala +++ b/akka-actor/src/main/scala-2.13/akka/util/ByteString.scala @@ -5,16 +5,17 @@ package akka.util import java.io.{ ObjectInputStream, ObjectOutputStream } -import java.nio.{ ByteBuffer, ByteOrder } import java.lang.{ Iterable => JIterable } +import java.nio.{ ByteBuffer, ByteOrder } import java.nio.charset.{ Charset, StandardCharsets } import java.util.Base64 import scala.annotation.{ tailrec, varargs } -import scala.collection.mutable.{ Builder, WrappedArray } import scala.collection.{ immutable, mutable } import scala.collection.immutable.{ IndexedSeq, IndexedSeqOps, StrictOptimizedSeqOps, VectorBuilder } +import scala.collection.mutable.{ Builder, WrappedArray } import scala.reflect.ClassTag + import com.github.ghik.silencer.silent object ByteString { diff --git a/akka-actor/src/main/scala/akka/actor/ActorCell.scala b/akka-actor/src/main/scala/akka/actor/ActorCell.scala index 1d2a01f579..ec3c26a980 100644 --- a/akka-actor/src/main/scala/akka/actor/ActorCell.scala +++ b/akka-actor/src/main/scala/akka/actor/ActorCell.scala @@ -21,7 +21,7 @@ import akka.dispatch.{ Envelope, MessageDispatcher } import akka.dispatch.sysmsg._ import akka.event.Logging.{ Debug, Error, LogEvent } import akka.japi.Procedure -import akka.util.{ unused, Reflect } +import akka.util.unused /** * The actor context - the view of the actor cell from the actor. @@ -410,7 +410,7 @@ private[akka] object ActorCell { private[akka] class ActorCell( val system: ActorSystemImpl, val self: InternalActorRef, - final val props: Props, // Must be final so that it can be properly cleared in clearActorCellFields + _initialProps: Props, val dispatcher: MessageDispatcher, val parent: InternalActorRef) extends AbstractActor.ActorContext @@ -421,6 +421,9 @@ private[akka] class ActorCell( with dungeon.DeathWatch with dungeon.FaultHandling { + private[this] var _props = _initialProps + def props: Props = _props + import ActorCell._ final def isLocal = true @@ -435,7 +438,6 @@ private[akka] class ActorCell( protected def uid: Int = self.path.uid private[this] var _actor: Actor = _ def actor: Actor = _actor - 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 @@ -615,6 +617,7 @@ private[akka] class ActorCell( // If no becomes were issued, the actors behavior is its receive method behaviorStack = if (behaviorStack.isEmpty) instance.receive :: behaviorStack else behaviorStack + _actor = instance instance } finally { val stackAfter = contextStack.get @@ -624,29 +627,28 @@ private[akka] class ActorCell( } protected def create(failure: Option[ActorInitializationException]): Unit = { - def clearOutActorIfNonNull(): Unit = { - if (actor != null) { + def failActor(): Unit = + if (_actor != null) { clearActorFields(actor, recreate = false) - actor = null // ensure that we know that we failed during creation + setFailedFatally() + _actor = null // ensure that we know that we failed during creation } - } failure.foreach { throw _ } try { val created = newActor() - actor = created created.aroundPreStart() checkReceiveTimeout(reschedule = true) if (system.settings.DebugLifecycle) publish(Debug(self.path.toString, clazz(created), "started (" + created + ")")) } catch { case e: InterruptedException => - clearOutActorIfNonNull() + failActor() Thread.currentThread().interrupt() throw ActorInitializationException(self, "interruption during creation", e) case NonFatal(e) => - clearOutActorIfNonNull() + failActor() e match { case i: InstantiationException => throw ActorInitializationException( @@ -684,25 +686,17 @@ private[akka] class ActorCell( case _ => } - final protected def clearActorCellFields(cell: ActorCell): Unit = { - cell.unstashAll() - if (!Reflect.lookupAndSetField(classOf[ActorCell], cell, "props", ActorCell.terminatedProps)) - throw new IllegalArgumentException("ActorCell has no props field") - } - + @InternalStableApi + @silent("never used") final protected def clearActorFields(actorInstance: Actor, recreate: Boolean): Unit = { - setActorFields(actorInstance, context = null, self = if (recreate) self else system.deadLetters) currentMessage = null behaviorStack = emptyBehaviorStack } - - final protected def setActorFields(actorInstance: Actor, context: ActorContext, self: ActorRef): Unit = - if (actorInstance ne null) { - if (!Reflect.lookupAndSetField(actorInstance.getClass, actorInstance, "context", context) - || !Reflect.lookupAndSetField(actorInstance.getClass, actorInstance, "self", self)) - throw IllegalActorStateException( - s"${actorInstance.getClass} is not an Actor class. It doesn't extend the 'Actor' trait") - } + final protected def clearFieldsForTermination(): Unit = { + unstashAll() + _props = ActorCell.terminatedProps + _actor = null + } // logging is not the main purpose, and if it fails there’s nothing we can do protected final def publish(e: LogEvent): Unit = diff --git a/akka-actor/src/main/scala/akka/actor/ActorRef.scala b/akka-actor/src/main/scala/akka/actor/ActorRef.scala index 1a2b7313bc..268bb923f0 100644 --- a/akka-actor/src/main/scala/akka/actor/ActorRef.scala +++ b/akka-actor/src/main/scala/akka/actor/ActorRef.scala @@ -809,6 +809,15 @@ private[akka] class VirtualPathContainer( } } +/** + * INTERNAL API + */ +@InternalApi private[akka] object FunctionRef { + def deadLetterMessageHandler(system: ActorSystem): (ActorRef, Any) => Unit = { (sender, msg) => + system.deadLetters.tell(msg, sender) + } +} + /** * INTERNAL API * @@ -826,17 +835,20 @@ private[akka] class VirtualPathContainer( * [[FunctionRef#unwatch]] must be called to avoid a resource leak, which is different * from an ordinary actor. */ -private[akka] final class FunctionRef( +@InternalApi private[akka] final class FunctionRef( override val path: ActorPath, override val provider: ActorRefProvider, system: ActorSystem, f: (ActorRef, Any) => Unit) extends MinimalActorRef { + // var because it's replaced in `stop` + private var messageHandler: (ActorRef, Any) => Unit = f + override def !(message: Any)(implicit sender: ActorRef = Actor.noSender): Unit = { message match { case AddressTerminated(address) => addressTerminated(address) - case _ => f(sender, message) + case _ => messageHandler(sender, message) } } @@ -922,7 +934,13 @@ private[akka] final class FunctionRef( } } - override def stop(): Unit = sendTerminated() + override def stop(): Unit = { + sendTerminated() + // The messageHandler function may close over a large object graph (such as an Akka Stream) + // so we replace the messageHandler function to make that available for garbage collection. + // Doesn't matter if the change isn't visible immediately, volatile not needed. + messageHandler = FunctionRef.deadLetterMessageHandler(system) + } private def addWatcher(watchee: ActorRef, watcher: ActorRef): Unit = { val selfTerminated = this.synchronized { diff --git a/akka-actor/src/main/scala/akka/actor/ActorSystem.scala b/akka-actor/src/main/scala/akka/actor/ActorSystem.scala index d7e358a452..ea63ad7591 100644 --- a/akka-actor/src/main/scala/akka/actor/ActorSystem.scala +++ b/akka-actor/src/main/scala/akka/actor/ActorSystem.scala @@ -1178,9 +1178,11 @@ private[akka] class ActorSystemImpl( * when the extension cannot be found at all we throw regardless of this setting) */ def loadExtensions(key: String, throwOnLoadFail: Boolean): Unit = { + immutableSeq(settings.config.getStringList(key)).foreach { fqcn => dynamicAccess.getObjectFor[AnyRef](fqcn).recoverWith { - case _ => dynamicAccess.createInstanceFor[AnyRef](fqcn, Nil) + case firstProblem => + dynamicAccess.createInstanceFor[AnyRef](fqcn, Nil).recoverWith { case _ => Failure(firstProblem) } } match { case Success(p: ExtensionIdProvider) => registerExtension(p.lookup()) diff --git a/akka-actor/src/main/scala/akka/actor/Deployer.scala b/akka-actor/src/main/scala/akka/actor/Deployer.scala index c5c0f3fd87..0ea6c929f9 100644 --- a/akka-actor/src/main/scala/akka/actor/Deployer.scala +++ b/akka-actor/src/main/scala/akka/actor/Deployer.scala @@ -120,13 +120,13 @@ final class Deploy( new Deploy(path, config, routerConfig, scope, dispatcher, mailbox, tags) override def productElement(n: Int): Any = n match { - case 1 => path - case 2 => config - case 3 => routerConfig - case 4 => scope - case 5 => dispatcher - case 6 => mailbox - case 7 => tags + case 0 => path + case 1 => config + case 2 => routerConfig + case 3 => scope + case 4 => dispatcher + case 5 => mailbox + case 6 => tags } override def productArity: Int = 7 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 ced705651f..d493e42dc4 100644 --- a/akka-actor/src/main/scala/akka/actor/dungeon/Children.scala +++ b/akka-actor/src/main/scala/akka/actor/dungeon/Children.scala @@ -9,10 +9,9 @@ import java.util.Optional import scala.annotation.tailrec import scala.collection.immutable import scala.util.control.NonFatal - import com.github.ghik.silencer.silent - import akka.actor._ +import akka.annotation.InternalStableApi import akka.serialization.{ Serialization, SerializationExtension, Serializers } import akka.util.{ Helpers, Unsafe } @@ -182,6 +181,7 @@ private[akka] trait Children { this: ActorCell => case _ => null } + @InternalStableApi protected def suspendChildren(exceptFor: Set[ActorRef] = Set.empty): Unit = childrenRefs.stats.foreach { case ChildRestartStats(child, _, _) if !(exceptFor contains child) => 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 cd15ce390f..468e5865d9 100644 --- a/akka-actor/src/main/scala/akka/actor/dungeon/DeathWatch.scala +++ b/akka-actor/src/main/scala/akka/actor/dungeon/DeathWatch.scala @@ -63,7 +63,15 @@ private[akka] trait DeathWatch { this: ActorCell => protected def receivedTerminated(t: Terminated): Unit = terminatedQueued.get(t.actor).foreach { optionalMessage => terminatedQueued -= t.actor // here we know that it is the SAME ref which was put in - receiveMessage(optionalMessage.getOrElse(t)) + optionalMessage match { + case Some(customTermination) => + // needed for stashing of custom watch messages to work (or stash will stash the Terminated message instead) + currentMessage = currentMessage.copy(message = customTermination) + receiveMessage(customTermination) + + case None => + receiveMessage(t) + } } /** 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 a6ae5a5169..8c498c788f 100644 --- a/akka-actor/src/main/scala/akka/actor/dungeon/FaultHandling.scala +++ b/akka-actor/src/main/scala/akka/actor/dungeon/FaultHandling.scala @@ -4,22 +4,35 @@ package akka.actor.dungeon -import scala.collection.immutable -import scala.concurrent.duration.Duration -import scala.util.control.Exception._ -import scala.util.control.NonFatal - -import akka.actor.{ Actor, ActorCell, ActorInterruptedException, ActorRef, InternalActorRef } +import akka.actor.{ ActorCell, ActorInterruptedException, ActorRef, InternalActorRef } import akka.actor.ActorRefScope import akka.actor.PostRestartException import akka.actor.PreRestartException +import akka.annotation.InternalApi +import akka.annotation.InternalStableApi import akka.dispatch._ 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 + +/** + * INTERNAL API + */ +@InternalApi private[akka] object FaultHandling { + sealed trait FailedInfo + private case object NoFailedInfo extends FailedInfo + private final case class FailedRef(ref: ActorRef) extends FailedInfo + private case object FailedFatally extends FailedInfo +} + private[akka] trait FaultHandling { this: ActorCell => + import FaultHandling._ /* ================= * T H E R U L E S @@ -44,11 +57,22 @@ private[akka] trait FaultHandling { this: ActorCell => * a restart with dying children) * might well be replaced by ref to a Cancellable in the future (see #2299) */ - private var _failed: ActorRef = null - private def isFailed: Boolean = _failed != null - private def setFailed(perpetrator: ActorRef): Unit = _failed = perpetrator - private def clearFailed(): Unit = _failed = null - private def perpetrator: ActorRef = _failed + private var _failed: FailedInfo = NoFailedInfo + private def isFailed: Boolean = _failed.isInstanceOf[FailedRef] + private def isFailedFatally: Boolean = _failed eq FailedFatally + private def perpetrator: ActorRef = _failed match { + case FailedRef(ref) => ref + case _ => null + } + private def setFailed(perpetrator: ActorRef): Unit = _failed = _failed match { + case FailedFatally => FailedFatally + case _ => FailedRef(perpetrator) + } + private def clearFailed(): Unit = _failed = _failed match { + case FailedRef(_) => NoFailedInfo + case other => other + } + protected def setFailedFatally(): Unit = _failed = FailedFatally /** * Do re-create the actor in response to a failure. @@ -65,7 +89,7 @@ private[akka] trait FaultHandling { this: ActorCell => val optionalMessage = if (currentMessage ne null) Some(currentMessage.message) else None try { // if the actor fails in preRestart, we can do nothing but log it: it’s best-effort - if (failedActor.context ne null) failedActor.aroundPreRestart(cause, optionalMessage) + if (!isFailedFatally) failedActor.aroundPreRestart(cause, optionalMessage) } catch handleNonFatalOrInterruptedException { e => val ex = PreRestartException(self, e, cause, optionalMessage) publish(Error(ex, self.path.toString, clazz(failedActor), e.getMessage)) @@ -74,7 +98,7 @@ private[akka] trait FaultHandling { this: ActorCell => } } assert(mailbox.isSuspended, "mailbox must be suspended during restart, status=" + mailbox.currentStatus) - if (!setChildrenTerminationReason(ChildrenContainer.Recreation(cause))) finishRecreate(cause, failedActor) + if (!setChildrenTerminationReason(ChildrenContainer.Recreation(cause))) finishRecreate(cause) } else { // need to keep that suspend counter balanced faultResume(causedByFailure = null) @@ -101,7 +125,7 @@ private[akka] trait FaultHandling { this: ActorCell => system.eventStream.publish( Error(self.path.toString, clazz(actor), "changing Resume into Create after " + causedByFailure)) faultCreate() - } else if (actor.context == null && causedByFailure != null) { + } else if (isFailedFatally && causedByFailure != null) { system.eventStream.publish( Error(self.path.toString, clazz(actor), "changing Resume into Restart after " + causedByFailure)) faultRecreate(causedByFailure) @@ -174,6 +198,7 @@ private[akka] trait FaultHandling { this: ActorCell => } } + @InternalStableApi 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 { @@ -226,12 +251,11 @@ private[akka] trait FaultHandling { this: ActorCell => publish(Debug(self.path.toString, clazz(a), "stopped")) clearActorFields(a, recreate = false) - clearActorCellFields(this) - actor = null + clearFieldsForTermination() } } - private def finishRecreate(cause: Throwable, failedActor: Actor): Unit = { + private def finishRecreate(cause: Throwable): Unit = { // need to keep a snapshot of the surviving children before the new actor instance creates new ones val survivors = children @@ -240,8 +264,6 @@ private[akka] trait FaultHandling { this: ActorCell => finally clearFailed() // must happen in any case, so that failure is propagated val freshActor = newActor() - actor = freshActor // this must happen before postRestart has a chance to fail - if (freshActor eq failedActor) setActorFields(freshActor, this, self) // If the creator returns the same instance, we need to restore our nulled out fields. freshActor.aroundPostRestart(cause) checkReceiveTimeout(reschedule = true) // user may have set a receive timeout in preStart which is called from postRestart @@ -255,6 +277,7 @@ private[akka] trait FaultHandling { this: ActorCell => publish(Error(e, self.path.toString, clazz(freshActor), "restarting " + child)) }) } catch handleNonFatalOrInterruptedException { e => + setFailedFatally() clearActorFields(actor, recreate = false) // in order to prevent preRestart() from happening again handleInvokeFailure(survivors, PostRestartException(self, e, cause)) } @@ -301,7 +324,7 @@ private[akka] trait FaultHandling { this: ActorCell => * then we are continuing the previously suspended recreate/create/terminate action */ status match { - case Some(ChildrenContainer.Recreation(cause)) => finishRecreate(cause, actor) + case Some(ChildrenContainer.Recreation(cause)) => finishRecreate(cause) case Some(ChildrenContainer.Creation()) => finishCreate() case Some(ChildrenContainer.Termination) => finishTerminate() case _ => diff --git a/akka-actor/src/main/scala/akka/dispatch/Future.scala b/akka-actor/src/main/scala/akka/dispatch/Future.scala index 7e6d6d9010..344c6d4437 100644 --- a/akka-actor/src/main/scala/akka/dispatch/Future.scala +++ b/akka-actor/src/main/scala/akka/dispatch/Future.scala @@ -78,15 +78,16 @@ object ExecutionContexts { def global(): ExecutionContextExecutor = ExecutionContext.global /** + * INTERNAL API + * * WARNING: Not A General Purpose ExecutionContext! * * This is an execution context which runs everything on the calling thread. * It is very useful for actions which are known to be non-blocking and * non-throwing in order to save a round-trip to the thread pool. * - * INTERNAL API + * Once Scala 2.12 is no longer supported this can be dropped in favour of directly using `ExecutionContext.parasitic` */ - // Once Scala 2.12 is no longer supported this can be dropped in favour of directly using [[ExecutionContext.parasitic]] @InternalStableApi private[akka] val parasitic: ExecutionContext = SameThreadExecutionContext() diff --git a/akka-actor/src/main/scala/akka/dispatch/affinity/AffinityPool.scala b/akka-actor/src/main/scala/akka/dispatch/affinity/AffinityPool.scala index 66e32f8591..6fb5cddeac 100644 --- a/akka-actor/src/main/scala/akka/dispatch/affinity/AffinityPool.scala +++ b/akka-actor/src/main/scala/akka/dispatch/affinity/AffinityPool.scala @@ -22,9 +22,6 @@ import akka.event.Logging import akka.util.{ ImmutableIntMap, ReentrantGuard } import akka.util.Helpers.Requiring -import scala.annotation.{ switch, tailrec } -import scala.collection.{ immutable, mutable } - @InternalApi @ApiMayChange private[affinity] object AffinityPool { diff --git a/akka-actor/src/main/scala/akka/io/dns/DnsSettings.scala b/akka-actor/src/main/scala/akka/io/dns/DnsSettings.scala index 91bbfc5f47..71cb80a64b 100644 --- a/akka-actor/src/main/scala/akka/io/dns/DnsSettings.scala +++ b/akka-actor/src/main/scala/akka/io/dns/DnsSettings.scala @@ -165,7 +165,6 @@ object DnsSettings { def getNameserversUsingJNDI: Try[List[InetSocketAddress]] = { import java.util - import javax.naming.Context import javax.naming.directory.InitialDirContext // Using jndi-dns to obtain the default name servers. diff --git a/akka-actor/src/main/scala/akka/pattern/AskSupport.scala b/akka-actor/src/main/scala/akka/pattern/AskSupport.scala index 215d849b6e..c59427c22f 100644 --- a/akka-actor/src/main/scala/akka/pattern/AskSupport.scala +++ b/akka-actor/src/main/scala/akka/pattern/AskSupport.scala @@ -14,10 +14,11 @@ import scala.util.{ Failure, Success } import com.github.ghik.silencer.silent import akka.actor._ -import akka.annotation.InternalApi +import akka.annotation.{ InternalApi, InternalStableApi } import akka.dispatch.ExecutionContexts import akka.dispatch.sysmsg._ import akka.util.{ Timeout, Unsafe } +import akka.util.unused /** * This is what is used to complete a Future that is returned from an ask/? call, @@ -339,9 +340,8 @@ final class AskableActorRef(val actorRef: ActorRef) extends AnyVal { if (timeout.duration.length <= 0) Future.failed[Any](AskableActorRef.negativeTimeoutException(actorRef, message, sender)) else { - val a = PromiseActorRef(ref.provider, timeout, targetName = actorRef, message.getClass.getName, sender) - actorRef.tell(message, a) - a.result.future + PromiseActorRef(ref.provider, timeout, targetName = actorRef, message.getClass.getName, sender) + .ask(actorRef, message, timeout) } case _ => Future.failed[Any](AskableActorRef.unsupportedRecipientType(actorRef, message, sender)) } @@ -376,8 +376,7 @@ final class ExplicitlyAskableActorRef(val actorRef: ActorRef) extends AnyVal { val a = PromiseActorRef(ref.provider, timeout, targetName = actorRef, "unknown", sender) val message = messageFactory(a) a.messageClassName = message.getClass.getName - actorRef.tell(message, a) - a.result.future + a.ask(actorRef, message, timeout) } case _ if sender eq null => Future.failed[Any]( @@ -423,9 +422,8 @@ final class AskableActorSelection(val actorSel: ActorSelection) extends AnyVal { if (timeout.duration.length <= 0) Future.failed[Any](AskableActorRef.negativeTimeoutException(actorSel, message, sender)) else { - val a = PromiseActorRef(ref.provider, timeout, targetName = actorSel, message.getClass.getName, sender) - actorSel.tell(message, a) - a.result.future + PromiseActorRef(ref.provider, timeout, targetName = actorSel, message.getClass.getName, sender) + .ask(actorSel, message, timeout) } case _ => Future.failed[Any](AskableActorRef.unsupportedRecipientType(actorSel, message, sender)) } @@ -455,8 +453,7 @@ final class ExplicitlyAskableActorSelection(val actorSel: ActorSelection) extend val a = PromiseActorRef(ref.provider, timeout, targetName = actorSel, "unknown", sender) val message = messageFactory(a) a.messageClassName = message.getClass.getName - actorSel.tell(message, a) - a.result.future + a.ask(actorSel, message, timeout) } case _ if sender eq null => Future.failed[Any]( @@ -573,7 +570,9 @@ private[akka] final class PromiseActorRef private ( } override def !(message: Any)(implicit sender: ActorRef = Actor.noSender): Unit = state match { - case Stopped | _: StoppedWithPath => provider.deadLetters ! message + case Stopped | _: StoppedWithPath => + provider.deadLetters ! message + onComplete(message, alreadyCompleted = true) case _ => if (message == null) throw InvalidMessageException("Message is null") val promiseResult = message match { @@ -581,8 +580,10 @@ private[akka] final class PromiseActorRef private ( case Status.Failure(f) => Failure(f) case other => Success(other) } - if (!result.tryComplete(promiseResult)) + val alreadyCompleted = !result.tryComplete(promiseResult) + if (alreadyCompleted) provider.deadLetters ! message + onComplete(message, alreadyCompleted) } override def sendSystemMessage(message: SystemMessage): Unit = message match { @@ -632,6 +633,24 @@ private[akka] final class PromiseActorRef private ( case Registering => stop() // spin until registration is completed before stopping } } + + @InternalStableApi + private[akka] def ask(actorSel: ActorSelection, message: Any, @unused timeout: Timeout): Future[Any] = { + actorSel.tell(message, this) + result.future + } + + @InternalStableApi + private[akka] def ask(actorRef: ActorRef, message: Any, @unused timeout: Timeout): Future[Any] = { + actorRef.tell(message, this) + result.future + } + + @InternalStableApi + private[akka] def onComplete(@unused message: Any, @unused alreadyCompleted: Boolean): Unit = {} + + @InternalStableApi + private[akka] def onTimeout(@unused timeout: Timeout): Unit = {} } /** @@ -658,7 +677,7 @@ private[akka] object PromiseActorRef { val a = new PromiseActorRef(provider, result, messageClassName) implicit val ec = ExecutionContexts.parasitic val f = scheduler.scheduleOnce(timeout.duration) { - result.tryComplete { + val timedOut = result.tryComplete { val wasSentBy = if (sender == ActorRef.noSender) "" else s" was sent by [$sender]" val messagePart = s"Message of type [${a.messageClassName}]$wasSentBy." Failure( @@ -667,6 +686,9 @@ private[akka] object PromiseActorRef { messagePart + " A typical reason for `AskTimeoutException` is that the recipient actor didn't send a reply.")) } + if (timedOut) { + a.onTimeout(timeout) + } } result.future.onComplete { _ => try a.stop() diff --git a/akka-actor/src/main/scala/akka/pattern/Backoff.scala b/akka-actor/src/main/scala/akka/pattern/Backoff.scala index 45f628959c..6cc78aeed6 100644 --- a/akka-actor/src/main/scala/akka/pattern/Backoff.scala +++ b/akka-actor/src/main/scala/akka/pattern/Backoff.scala @@ -617,7 +617,7 @@ private final case class BackoffOptionsImpl( backoffReset, randomFactor, supervisorStrategy, - replyWhileStopped)) + replyWhileStopped.map(msg => ReplyWith(msg)).getOrElse(ForwardDeathLetters))) //onStop method in companion object case StopImpliesFailure => Props( @@ -629,7 +629,7 @@ private final case class BackoffOptionsImpl( backoffReset, randomFactor, supervisorStrategy, - replyWhileStopped, + replyWhileStopped.map(msg => ReplyWith(msg)).getOrElse(ForwardDeathLetters), finalStopMessage)) } } diff --git a/akka-actor/src/main/scala/akka/pattern/BackoffOptions.scala b/akka-actor/src/main/scala/akka/pattern/BackoffOptions.scala index 6e893978ac..8b2c5e61e8 100644 --- a/akka-actor/src/main/scala/akka/pattern/BackoffOptions.scala +++ b/akka-actor/src/main/scala/akka/pattern/BackoffOptions.scala @@ -5,9 +5,8 @@ package akka.pattern import scala.concurrent.duration.{ Duration, FiniteDuration } - -import akka.actor.{ OneForOneStrategy, Props, SupervisorStrategy } -import akka.annotation.DoNotInherit +import akka.actor.{ ActorRef, OneForOneStrategy, Props, SupervisorStrategy } +import akka.annotation.{ DoNotInherit, InternalApi } import akka.pattern.internal.{ BackoffOnRestartSupervisor, BackoffOnStopSupervisor } import akka.util.JavaDurationConverters._ @@ -299,6 +298,15 @@ private[akka] sealed trait ExtendedBackoffOptions[T <: ExtendedBackoffOptions[T] */ def withReplyWhileStopped(replyWhileStopped: Any): T + /** + * Returns a new BackoffOptions with a custom handler for messages that the supervisor receives while its child is stopped. + * By default, a message received while the child is stopped is forwarded to `deadLetters`. + * Essentially, this handler replaces `deadLetters` allowing to implement custom handling instead of a static reply. + * + * @param handler PartialFunction of the received message and sender + */ + def withHandlerWhileStopped(handler: ActorRef): T + /** * Returns the props to create the back-off supervisor. */ @@ -334,7 +342,7 @@ private final case class BackoffOnStopOptionsImpl[T]( randomFactor: Double, reset: Option[BackoffReset] = None, supervisorStrategy: OneForOneStrategy = OneForOneStrategy()(SupervisorStrategy.defaultStrategy.decider), - replyWhileStopped: Option[Any] = None, + handlingWhileStopped: HandlingWhileStopped = ForwardDeathLetters, finalStopMessage: Option[Any => Boolean] = None) extends BackoffOnStopOptions { @@ -344,7 +352,9 @@ private final case class BackoffOnStopOptionsImpl[T]( def withAutoReset(resetBackoff: FiniteDuration) = copy(reset = Some(AutoReset(resetBackoff))) def withManualReset = copy(reset = Some(ManualReset)) def withSupervisorStrategy(supervisorStrategy: OneForOneStrategy) = copy(supervisorStrategy = supervisorStrategy) - def withReplyWhileStopped(replyWhileStopped: Any) = copy(replyWhileStopped = Some(replyWhileStopped)) + def withReplyWhileStopped(replyWhileStopped: Any) = copy(handlingWhileStopped = ReplyWith(replyWhileStopped)) + def withHandlerWhileStopped(handlerWhileStopped: ActorRef) = + copy(handlingWhileStopped = ForwardTo(handlerWhileStopped)) def withMaxNrOfRetries(maxNrOfRetries: Int) = copy(supervisorStrategy = supervisorStrategy.withMaxNrOfRetries(maxNrOfRetries)) @@ -374,7 +384,7 @@ private final case class BackoffOnStopOptionsImpl[T]( backoffReset, randomFactor, supervisorStrategy, - replyWhileStopped, + handlingWhileStopped, finalStopMessage)) } } @@ -387,7 +397,7 @@ private final case class BackoffOnFailureOptionsImpl[T]( randomFactor: Double, reset: Option[BackoffReset] = None, supervisorStrategy: OneForOneStrategy = OneForOneStrategy()(SupervisorStrategy.defaultStrategy.decider), - replyWhileStopped: Option[Any] = None) + handlingWhileStopped: HandlingWhileStopped = ForwardDeathLetters) extends BackoffOnFailureOptions { private val backoffReset = reset.getOrElse(AutoReset(minBackoff)) @@ -396,7 +406,9 @@ private final case class BackoffOnFailureOptionsImpl[T]( def withAutoReset(resetBackoff: FiniteDuration) = copy(reset = Some(AutoReset(resetBackoff))) def withManualReset = copy(reset = Some(ManualReset)) def withSupervisorStrategy(supervisorStrategy: OneForOneStrategy) = copy(supervisorStrategy = supervisorStrategy) - def withReplyWhileStopped(replyWhileStopped: Any) = copy(replyWhileStopped = Some(replyWhileStopped)) + def withReplyWhileStopped(replyWhileStopped: Any) = copy(handlingWhileStopped = ReplyWith(replyWhileStopped)) + def withHandlerWhileStopped(handlerWhileStopped: ActorRef) = + copy(handlingWhileStopped = ForwardTo(handlerWhileStopped)) def withMaxNrOfRetries(maxNrOfRetries: Int) = copy(supervisorStrategy = supervisorStrategy.withMaxNrOfRetries(maxNrOfRetries)) @@ -419,10 +431,17 @@ private final case class BackoffOnFailureOptionsImpl[T]( backoffReset, randomFactor, supervisorStrategy, - replyWhileStopped)) + handlingWhileStopped)) } } +@InternalApi private[akka] sealed trait BackoffReset private[akka] case object ManualReset extends BackoffReset private[akka] final case class AutoReset(resetBackoff: FiniteDuration) extends BackoffReset + +@InternalApi +private[akka] sealed trait HandlingWhileStopped +private[akka] case object ForwardDeathLetters extends HandlingWhileStopped +private[akka] case class ForwardTo(handler: ActorRef) extends HandlingWhileStopped +private[akka] case class ReplyWith(msg: Any) extends HandlingWhileStopped diff --git a/akka-actor/src/main/scala/akka/pattern/BackoffSupervisor.scala b/akka-actor/src/main/scala/akka/pattern/BackoffSupervisor.scala index e85fe90dbe..112406f47a 100644 --- a/akka-actor/src/main/scala/akka/pattern/BackoffSupervisor.scala +++ b/akka-actor/src/main/scala/akka/pattern/BackoffSupervisor.scala @@ -184,7 +184,7 @@ object BackoffSupervisor { AutoReset(minBackoff), randomFactor, strategy, - None, + ForwardDeathLetters, None)) } @@ -341,7 +341,7 @@ final class BackoffSupervisor @deprecated("Use `BackoffSupervisor.props` method reset, randomFactor, strategy, - replyWhileStopped, + replyWhileStopped.map(msg => ReplyWith(msg)).getOrElse(ForwardDeathLetters), finalStopMessage) { // for binary compatibility with 2.5.18 diff --git a/akka-actor/src/main/scala/akka/pattern/RetrySupport.scala b/akka-actor/src/main/scala/akka/pattern/RetrySupport.scala index 0380f2ba0b..7c9a111481 100644 --- a/akka-actor/src/main/scala/akka/pattern/RetrySupport.scala +++ b/akka-actor/src/main/scala/akka/pattern/RetrySupport.scala @@ -153,39 +153,44 @@ object RetrySupport extends RetrySupport { maxAttempts: Int, delayFunction: Int => Option[FiniteDuration], attempted: Int)(implicit ec: ExecutionContext, scheduler: Scheduler): Future[T] = { - try { - require(maxAttempts >= 0, "Parameter maxAttempts must >= 0.") - require(attempt != null, "Parameter attempt should not be null.") - if (maxAttempts - attempted > 0) { - val result = attempt() - if (result eq null) - result - else { - val nextAttempt = attempted + 1 - result.recoverWith { - case NonFatal(_) => - delayFunction(nextAttempt) match { - case Some(delay) => - if (delay.length < 1) - retry(attempt, maxAttempts, delayFunction, nextAttempt) - else - after(delay, scheduler) { - retry(attempt, maxAttempts, delayFunction, nextAttempt) - } - case None => - retry(attempt, maxAttempts, delayFunction, nextAttempt) - case _ => - Future.failed(new IllegalArgumentException("The delayFunction of retry should not return null.")) - } - } - } - - } else { + def tryAttempt(): Future[T] = { + try { attempt() + } catch { + case NonFatal(exc) => Future.failed(exc) // in case the `attempt` function throws } - } catch { - case NonFatal(error) => Future.failed(error) + } + + require(maxAttempts >= 0, "Parameter maxAttempts must >= 0.") + require(attempt != null, "Parameter attempt should not be null.") + if (maxAttempts - attempted > 0) { + val result = tryAttempt() + if (result eq null) + result + else { + val nextAttempt = attempted + 1 + result.recoverWith { + case NonFatal(_) => + delayFunction(nextAttempt) match { + case Some(delay) => + if (delay.length < 1) + retry(attempt, maxAttempts, delayFunction, nextAttempt) + else + after(delay, scheduler) { + retry(attempt, maxAttempts, delayFunction, nextAttempt) + } + case None => + retry(attempt, maxAttempts, delayFunction, nextAttempt) + case _ => + Future.failed(new IllegalArgumentException("The delayFunction of retry should not return null.")) + } + + } + } + + } else { + tryAttempt() } } } diff --git a/akka-actor/src/main/scala/akka/pattern/internal/BackoffOnRestartSupervisor.scala b/akka-actor/src/main/scala/akka/pattern/internal/BackoffOnRestartSupervisor.scala index 3f799dc361..e7e5bf8de3 100644 --- a/akka-actor/src/main/scala/akka/pattern/internal/BackoffOnRestartSupervisor.scala +++ b/akka-actor/src/main/scala/akka/pattern/internal/BackoffOnRestartSupervisor.scala @@ -4,12 +4,20 @@ package akka.pattern.internal -import scala.concurrent.duration._ - -import akka.actor.{ OneForOneStrategy, _ } import akka.actor.SupervisorStrategy._ +import akka.actor.{ OneForOneStrategy, _ } import akka.annotation.InternalApi -import akka.pattern.{ BackoffReset, BackoffSupervisor, HandleBackoff } +import akka.pattern.{ + BackoffReset, + BackoffSupervisor, + ForwardDeathLetters, + ForwardTo, + HandleBackoff, + HandlingWhileStopped, + ReplyWith +} + +import scala.concurrent.duration._ /** * INTERNAL API @@ -26,7 +34,7 @@ import akka.pattern.{ BackoffReset, BackoffSupervisor, HandleBackoff } val reset: BackoffReset, randomFactor: Double, strategy: OneForOneStrategy, - replyWhileStopped: Option[Any]) + handlingWhileStopped: HandlingWhileStopped) extends Actor with HandleBackoff with ActorLogging { @@ -34,7 +42,7 @@ import akka.pattern.{ BackoffReset, BackoffSupervisor, HandleBackoff } import BackoffSupervisor._ import context._ - override val supervisorStrategy = + override val supervisorStrategy: OneForOneStrategy = OneForOneStrategy(strategy.maxNrOfRetries, strategy.withinTimeRange, strategy.loggingEnabled) { case ex => val defaultDirective: Directive = @@ -94,9 +102,10 @@ import akka.pattern.{ BackoffReset, BackoffSupervisor, HandleBackoff } case Some(c) => c.forward(msg) case None => - replyWhileStopped match { - case None => context.system.deadLetters.forward(msg) - case Some(r) => sender() ! r + handlingWhileStopped match { + case ForwardDeathLetters => context.system.deadLetters.forward(msg) + case ForwardTo(h) => h.forward(msg) + case ReplyWith(r) => sender() ! r } } } diff --git a/akka-actor/src/main/scala/akka/pattern/internal/BackoffOnStopSupervisor.scala b/akka-actor/src/main/scala/akka/pattern/internal/BackoffOnStopSupervisor.scala index af94d4fa57..492f939bf5 100644 --- a/akka-actor/src/main/scala/akka/pattern/internal/BackoffOnStopSupervisor.scala +++ b/akka-actor/src/main/scala/akka/pattern/internal/BackoffOnStopSupervisor.scala @@ -4,12 +4,20 @@ package akka.pattern.internal -import scala.concurrent.duration.FiniteDuration - -import akka.actor.{ Actor, ActorLogging, OneForOneStrategy, Props, SupervisorStrategy, Terminated } import akka.actor.SupervisorStrategy.{ Directive, Escalate } +import akka.actor.{ Actor, ActorLogging, OneForOneStrategy, Props, SupervisorStrategy, Terminated } import akka.annotation.InternalApi -import akka.pattern.{ BackoffReset, BackoffSupervisor, HandleBackoff } +import akka.pattern.{ + BackoffReset, + BackoffSupervisor, + ForwardDeathLetters, + ForwardTo, + HandleBackoff, + HandlingWhileStopped, + ReplyWith +} + +import scala.concurrent.duration.FiniteDuration /** * INTERNAL API @@ -26,7 +34,7 @@ import akka.pattern.{ BackoffReset, BackoffSupervisor, HandleBackoff } val reset: BackoffReset, randomFactor: Double, strategy: SupervisorStrategy, - replyWhileStopped: Option[Any], + handlingWhileStopped: HandlingWhileStopped, finalStopMessage: Option[Any => Boolean]) extends Actor with HandleBackoff @@ -35,7 +43,7 @@ import akka.pattern.{ BackoffReset, BackoffSupervisor, HandleBackoff } import BackoffSupervisor._ import context.dispatcher - override val supervisorStrategy = strategy match { + override val supervisorStrategy: SupervisorStrategy = strategy match { case oneForOne: OneForOneStrategy => OneForOneStrategy(oneForOne.maxNrOfRetries, oneForOne.withinTimeRange, oneForOne.loggingEnabled) { case ex => @@ -84,13 +92,14 @@ import akka.pattern.{ BackoffReset, BackoffSupervisor, HandleBackoff } case None => } case None => - replyWhileStopped match { - case Some(r) => sender() ! r - case None => context.system.deadLetters.forward(msg) - } finalStopMessage match { case Some(fsm) if fsm(msg) => context.stop(self) - case _ => + case _ => + handlingWhileStopped match { + case ForwardDeathLetters => context.system.deadLetters.forward(msg) + case ForwardTo(h) => h.forward(msg) + case ReplyWith(r) => sender() ! r + } } } } diff --git a/akka-actor/src/main/scala/akka/serialization/Serialization.scala b/akka-actor/src/main/scala/akka/serialization/Serialization.scala index 86bd55e2a7..0fc71e1fac 100644 --- a/akka-actor/src/main/scala/akka/serialization/Serialization.scala +++ b/akka-actor/src/main/scala/akka/serialization/Serialization.scala @@ -503,8 +503,21 @@ class Serialization(val system: ExtendedActorSystem) extends Extension { /** * Maps from a Serializer Identity (Int) to a Serializer instance (optimization) */ - val serializerByIdentity: Map[Int, Serializer] = - Map(NullSerializer.identifier -> NullSerializer) ++ serializers.map { case (_, v) => (v.identifier, v) } + val serializerByIdentity: Map[Int, Serializer] = { + val zero: Map[Int, Serializer] = Map(NullSerializer.identifier -> NullSerializer) + serializers.foldLeft(zero) { + case (acc, (_, ser)) => + val id = ser.identifier + acc.get(id) match { + case Some(existing) if existing != ser => + throw new IllegalArgumentException( + s"Serializer identifier [$id] of [${ser.getClass.getName}] " + + s"is not unique. It is also used by [${acc(id).getClass.getName}].") + case _ => + acc.updated(id, ser) + } + } + } /** * Serializers with id 0 - 1023 are stored in an array for quick allocation free access diff --git a/akka-actor/src/main/scala/akka/util/JavaDurationConverters.scala b/akka-actor/src/main/scala/akka/util/JavaDurationConverters.scala index c344f72058..cefe1a2e28 100644 --- a/akka-actor/src/main/scala/akka/util/JavaDurationConverters.scala +++ b/akka-actor/src/main/scala/akka/util/JavaDurationConverters.scala @@ -7,9 +7,12 @@ import java.time.{ Duration => JDuration } import scala.concurrent.duration.{ Duration, FiniteDuration } +import akka.annotation.InternalStableApi + /** * INTERNAL API */ +@InternalStableApi private[akka] object JavaDurationConverters { def asFiniteDuration(duration: JDuration): FiniteDuration = duration.asScala diff --git a/akka-actor/src/main/scala/akka/util/Reflect.scala b/akka-actor/src/main/scala/akka/util/Reflect.scala index bd087f01e4..c0245da59b 100644 --- a/akka-actor/src/main/scala/akka/util/Reflect.scala +++ b/akka-actor/src/main/scala/akka/util/Reflect.scala @@ -7,6 +7,8 @@ import java.lang.reflect.Constructor import java.lang.reflect.ParameterizedType import java.lang.reflect.Type +import akka.annotation.InternalApi + import scala.annotation.tailrec import scala.collection.immutable import scala.util.Try @@ -18,6 +20,7 @@ import scala.util.control.NonFatal * * INTERNAL API */ +@InternalApi private[akka] object Reflect { /** @@ -138,33 +141,6 @@ private[akka] object Reflect { rec(root) } - /** - * INTERNAL API - * Set a val inside a class. - */ - @tailrec protected[akka] final def lookupAndSetField( - clazz: Class[_], - instance: AnyRef, - name: String, - value: Any): Boolean = { - @tailrec def clearFirst(fields: Array[java.lang.reflect.Field], idx: Int): Boolean = - if (idx < fields.length) { - val field = fields(idx) - if (field.getName == name) { - field.setAccessible(true) - field.set(instance, value) - true - } else clearFirst(fields, idx + 1) - } else false - - clearFirst(clazz.getDeclaredFields, 0) || { - clazz.getSuperclass match { - case null => false // clazz == classOf[AnyRef] - case sc => lookupAndSetField(sc, instance, name, value) - } - } - } - /** * INTERNAL API */ diff --git a/akka-bench-jmh/src/main/scala/akka/serialization/jackson/JacksonSerializationBench.scala b/akka-bench-jmh/src/main/scala/akka/serialization/jackson/JacksonSerializationBench.scala index 2289f214db..e11418911c 100644 --- a/akka-bench-jmh/src/main/scala/akka/serialization/jackson/JacksonSerializationBench.scala +++ b/akka-bench-jmh/src/main/scala/akka/serialization/jackson/JacksonSerializationBench.scala @@ -190,6 +190,10 @@ class JacksonSerializationBench { @Param(Array("jackson-json", "jackson-cbor")) // "java" private var serializerName: String = _ + @silent("immutable val") + @Param(Array("off", "gzip", "lz4")) + private var compression: String = _ + @Setup(Level.Trial) def setupTrial(): Unit = { val config = ConfigFactory.parseString(s""" @@ -208,7 +212,7 @@ class JacksonSerializationBench { } } akka.serialization.jackson.jackson-json.compression { - algorithm = off + algorithm = $compression compress-larger-than = 100 b } """) @@ -222,10 +226,18 @@ class JacksonSerializationBench { Await.result(system.terminate(), 5.seconds) } + private var size = 0L + private def serializeDeserialize[T <: AnyRef](msg: T): T = { serialization.findSerializerFor(msg) match { case serializer: SerializerWithStringManifest => val blob = serializer.toBinary(msg) + if (size != blob.length) { + size = blob.length + println( + s"# Size is $size of ${msg.getClass.getName} with " + + s"${system.settings.config.getString("akka.serialization.jackson.jackson-json.compression.algorithm")}") + } serializer.fromBinary(blob, serializer.manifest(msg)).asInstanceOf[T] case serializer => val blob = serializer.toBinary(msg) diff --git a/akka-cluster-metrics/src/main/java/akka/cluster/metrics/protobuf/msg/ClusterMetricsMessages.java b/akka-cluster-metrics/src/main/java/akka/cluster/metrics/protobuf/msg/ClusterMetricsMessages.java index badaa04cb6..0dd86a8e15 100644 --- a/akka-cluster-metrics/src/main/java/akka/cluster/metrics/protobuf/msg/ClusterMetricsMessages.java +++ b/akka-cluster-metrics/src/main/java/akka/cluster/metrics/protobuf/msg/ClusterMetricsMessages.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2019-2020 Lightbend Inc. + * Copyright (C) 2020 Lightbend Inc. */ // Generated by the protocol buffer compiler. DO NOT EDIT! diff --git a/akka-cluster-sharding-typed/src/main/java/akka/cluster/sharding/typed/internal/protobuf/ShardingMessages.java b/akka-cluster-sharding-typed/src/main/java/akka/cluster/sharding/typed/internal/protobuf/ShardingMessages.java index 48833056e2..85a2305110 100644 --- a/akka-cluster-sharding-typed/src/main/java/akka/cluster/sharding/typed/internal/protobuf/ShardingMessages.java +++ b/akka-cluster-sharding-typed/src/main/java/akka/cluster/sharding/typed/internal/protobuf/ShardingMessages.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2019-2020 Lightbend Inc. + * Copyright (C) 2020 Lightbend Inc. */ // Generated by the protocol buffer compiler. DO NOT EDIT! diff --git a/akka-cluster-sharding-typed/src/main/scala/akka/cluster/sharding/typed/internal/ClusterShardingImpl.scala b/akka-cluster-sharding-typed/src/main/scala/akka/cluster/sharding/typed/internal/ClusterShardingImpl.scala index ff7a5f5cdd..dbf2aa39cd 100644 --- a/akka-cluster-sharding-typed/src/main/scala/akka/cluster/sharding/typed/internal/ClusterShardingImpl.scala +++ b/akka-cluster-sharding-typed/src/main/scala/akka/cluster/sharding/typed/internal/ClusterShardingImpl.scala @@ -27,7 +27,7 @@ import akka.actor.typed.internal.PoisonPillInterceptor import akka.actor.typed.internal.adapter.ActorRefAdapter import akka.actor.typed.internal.adapter.ActorSystemAdapter import akka.actor.typed.scaladsl.Behaviors -import akka.annotation.InternalApi +import akka.annotation.{ InternalApi, InternalStableApi } import akka.cluster.ClusterSettings.DataCenter import akka.cluster.sharding.ShardCoordinator.LeastShardAllocationStrategy import akka.cluster.sharding.ShardCoordinator.ShardAllocationStrategy @@ -40,9 +40,8 @@ import akka.event.LoggingAdapter import akka.japi.function.{ Function => JFunction } import akka.pattern.AskTimeoutException import akka.pattern.PromiseActorRef -import akka.util.ByteString +import akka.util.{ unused, ByteString, Timeout } import akka.util.JavaDurationConverters._ -import akka.util.Timeout /** * INTERNAL API @@ -311,8 +310,7 @@ import akka.util.Timeout val replyTo = new EntityPromiseRef[U](shardRegion.asInstanceOf[InternalActorRef], timeout) val m = message(replyTo.ref) if (replyTo.promiseRef ne null) replyTo.promiseRef.messageClassName = m.getClass.getName - shardRegion ! ShardingEnvelope(entityId, m) - replyTo.future + replyTo.ask(shardRegion, entityId, m, timeout) } def ask[U](message: JFunction[ActorRef[U], M], timeout: Duration): CompletionStage[U] = @@ -349,6 +347,16 @@ import akka.util.Timeout val ref: ActorRef[U] = _ref val future: Future[U] = _future val promiseRef: PromiseActorRef = _promiseRef + + @InternalStableApi + private[akka] def ask[T]( + shardRegion: akka.actor.ActorRef, + entityId: String, + message: T, + @unused timeout: Timeout): Future[U] = { + shardRegion ! ShardingEnvelope(entityId, message) + future + } } // impl InternalRecipientRef diff --git a/akka-cluster-sharding-typed/src/multi-jvm/scala/akka/cluster/sharding/typed/delivery/DeliveryThroughputSpec.scala b/akka-cluster-sharding-typed/src/multi-jvm/scala/akka/cluster/sharding/typed/delivery/DeliveryThroughputSpec.scala index 9995042c79..c0ff1853e6 100644 --- a/akka-cluster-sharding-typed/src/multi-jvm/scala/akka/cluster/sharding/typed/delivery/DeliveryThroughputSpec.scala +++ b/akka-cluster-sharding-typed/src/multi-jvm/scala/akka/cluster/sharding/typed/delivery/DeliveryThroughputSpec.scala @@ -37,6 +37,7 @@ import akka.remote.testkit.MultiNodeConfig import akka.remote.testkit.MultiNodeSpec import akka.remote.testkit.PerfFlamesSupport import akka.serialization.jackson.CborSerializable +import akka.actor.typed.scaladsl.LoggerOps object DeliveryThroughputSpec extends MultiNodeConfig { val first = role("first") @@ -232,6 +233,7 @@ object DeliveryThroughputSpec extends MultiNodeConfig { case object Run extends Command private case class WrappedRequestNext(r: ShardingProducerController.RequestNext[Consumer.Command]) extends Command + private case object PrintStatus extends Command def apply( producerController: ActorRef[ShardingProducerController.Command[Consumer.Command]], @@ -240,35 +242,54 @@ object DeliveryThroughputSpec extends MultiNodeConfig { resultReporter: BenchmarkFileReporter): Behavior[Command] = { val numberOfMessages = testSettings.totalMessages - Behaviors.setup { context => - val requestNextAdapter = - context.messageAdapter[ShardingProducerController.RequestNext[Consumer.Command]](WrappedRequestNext(_)) - var startTime = System.nanoTime() - var remaining = numberOfMessages + context.system.settings.config - .getInt("akka.reliable-delivery.sharding.consumer-controller.flow-control-window") + Behaviors.withTimers { timers => + Behaviors.setup { context => + timers.startTimerWithFixedDelay(PrintStatus, 1.second) + val requestNextAdapter = + context.messageAdapter[ShardingProducerController.RequestNext[Consumer.Command]](WrappedRequestNext(_)) + var startTime = System.nanoTime() + var remaining = numberOfMessages + context.system.settings.config + .getInt("akka.reliable-delivery.sharding.consumer-controller.flow-control-window") + var latestDemand: ShardingProducerController.RequestNext[Consumer.Command] = null + var messagesSentToEachEntity: Map[String, Long] = Map.empty[String, Long].withDefaultValue(0L) - Behaviors.receiveMessage { - case WrappedRequestNext(next) => - remaining -= 1 - if (remaining == 0) { - context.log.info("Completed {} messages", numberOfMessages) - Producer.reportEnd(startTime, testSettings, plotRef, resultReporter) - Behaviors.stopped - } else { - val entityId = (remaining % testSettings.numberOfConsumers).toString - if (next.entitiesWithDemand(entityId) || !next.bufferedForEntitiesWithoutDemand.contains(entityId)) - next.sendNextTo ! ShardingEnvelope(entityId, Consumer.TheMessage) + Behaviors.receiveMessage { + case WrappedRequestNext(next) => + latestDemand = next + remaining -= 1 + if (remaining == 0) { + context.log.info("Completed {} messages", numberOfMessages) + Producer.reportEnd(startTime, testSettings, plotRef, resultReporter) + Behaviors.stopped + } else { + val entityId = (remaining % testSettings.numberOfConsumers).toString + if (next.entitiesWithDemand(entityId) || !next.bufferedForEntitiesWithoutDemand.contains(entityId)) { + messagesSentToEachEntity = + messagesSentToEachEntity.updated(entityId, messagesSentToEachEntity(entityId) + 1L) + + next.sendNextTo ! ShardingEnvelope(entityId, Consumer.TheMessage) + } + Behaviors.same + } + case Run => + context.log.info("Starting {} messages", numberOfMessages) + startTime = System.nanoTime() + producerController ! ShardingProducerController.Start(requestNextAdapter) Behaviors.same - } - case Run => - context.log.info("Starting {} messages", numberOfMessages) - startTime = System.nanoTime() - producerController ! ShardingProducerController.Start(requestNextAdapter) - Behaviors.same + case PrintStatus => + context.log.infoN( + "Remaining {}. Latest demand {}. Messages sent {}. Expecting demand from {}", + remaining, + latestDemand, + messagesSentToEachEntity, + (remaining % testSettings.numberOfConsumers)) + Behaviors.same + } } } } + } final case class TestSettings(testName: String, totalMessages: Long, numberOfConsumers: Int) diff --git a/akka-cluster-sharding-typed/src/test/java/jdocs/akka/cluster/sharding/typed/AccountExampleWithEventHandlersInState.java b/akka-cluster-sharding-typed/src/test/java/jdocs/akka/cluster/sharding/typed/AccountExampleWithEventHandlersInState.java index be6e5ba857..ef9f2c586d 100644 --- a/akka-cluster-sharding-typed/src/test/java/jdocs/akka/cluster/sharding/typed/AccountExampleWithEventHandlersInState.java +++ b/akka-cluster-sharding-typed/src/test/java/jdocs/akka/cluster/sharding/typed/AccountExampleWithEventHandlersInState.java @@ -38,10 +38,10 @@ public interface AccountExampleWithEventHandlersInState { // Command // #reply-command - interface Command extends CborSerializable {} + interface Command extends CborSerializable {} // #reply-command - public static class CreateAccount implements Command { + public static class CreateAccount implements Command { public final ActorRef replyTo; @JsonCreator @@ -50,7 +50,7 @@ public interface AccountExampleWithEventHandlersInState { } } - public static class Deposit implements Command { + public static class Deposit implements Command { public final BigDecimal amount; public final ActorRef replyTo; @@ -60,7 +60,7 @@ public interface AccountExampleWithEventHandlersInState { } } - public static class Withdraw implements Command { + public static class Withdraw implements Command { public final BigDecimal amount; public final ActorRef replyTo; @@ -70,7 +70,7 @@ public interface AccountExampleWithEventHandlersInState { } } - public static class GetBalance implements Command { + public static class GetBalance implements Command { public final ActorRef replyTo; @JsonCreator @@ -79,7 +79,7 @@ public interface AccountExampleWithEventHandlersInState { } } - public static class CloseAccount implements Command { + public static class CloseAccount implements Command { public final ActorRef replyTo; @JsonCreator diff --git a/akka-cluster-sharding-typed/src/test/java/jdocs/akka/cluster/sharding/typed/AccountExampleWithMutableState.java b/akka-cluster-sharding-typed/src/test/java/jdocs/akka/cluster/sharding/typed/AccountExampleWithMutableState.java index 70da5c1bf3..2c9b9ac4fc 100644 --- a/akka-cluster-sharding-typed/src/test/java/jdocs/akka/cluster/sharding/typed/AccountExampleWithMutableState.java +++ b/akka-cluster-sharding-typed/src/test/java/jdocs/akka/cluster/sharding/typed/AccountExampleWithMutableState.java @@ -35,9 +35,9 @@ public interface AccountExampleWithMutableState { EntityTypeKey.create(Command.class, "Account"); // Command - interface Command extends CborSerializable {} + interface Command extends CborSerializable {} - public static class CreateAccount implements Command { + public static class CreateAccount implements Command { public final ActorRef replyTo; @JsonCreator @@ -46,7 +46,7 @@ public interface AccountExampleWithMutableState { } } - public static class Deposit implements Command { + public static class Deposit implements Command { public final BigDecimal amount; public final ActorRef replyTo; @@ -56,7 +56,7 @@ public interface AccountExampleWithMutableState { } } - public static class Withdraw implements Command { + public static class Withdraw implements Command { public final BigDecimal amount; public final ActorRef replyTo; @@ -66,7 +66,7 @@ public interface AccountExampleWithMutableState { } } - public static class GetBalance implements Command { + public static class GetBalance implements Command { public final ActorRef replyTo; @JsonCreator @@ -75,7 +75,7 @@ public interface AccountExampleWithMutableState { } } - public static class CloseAccount implements Command { + public static class CloseAccount implements Command { public final ActorRef replyTo; @JsonCreator diff --git a/akka-cluster-sharding-typed/src/test/java/jdocs/akka/cluster/sharding/typed/AccountExampleWithNullState.java b/akka-cluster-sharding-typed/src/test/java/jdocs/akka/cluster/sharding/typed/AccountExampleWithNullState.java index 0402cce01a..0afce2091e 100644 --- a/akka-cluster-sharding-typed/src/test/java/jdocs/akka/cluster/sharding/typed/AccountExampleWithNullState.java +++ b/akka-cluster-sharding-typed/src/test/java/jdocs/akka/cluster/sharding/typed/AccountExampleWithNullState.java @@ -35,9 +35,9 @@ public interface AccountExampleWithNullState { EntityTypeKey.create(Command.class, "Account"); // Command - interface Command extends CborSerializable {} + interface Command extends CborSerializable {} - public static class CreateAccount implements Command { + public static class CreateAccount implements Command { public final ActorRef replyTo; @JsonCreator @@ -46,7 +46,7 @@ public interface AccountExampleWithNullState { } } - public static class Deposit implements Command { + public static class Deposit implements Command { public final BigDecimal amount; public final ActorRef replyTo; @@ -56,7 +56,7 @@ public interface AccountExampleWithNullState { } } - public static class Withdraw implements Command { + public static class Withdraw implements Command { public final BigDecimal amount; public final ActorRef replyTo; @@ -66,7 +66,7 @@ public interface AccountExampleWithNullState { } } - public static class GetBalance implements Command { + public static class GetBalance implements Command { public final ActorRef replyTo; @JsonCreator @@ -75,7 +75,7 @@ public interface AccountExampleWithNullState { } } - public static class CloseAccount implements Command { + public static class CloseAccount implements Command { public final ActorRef replyTo; @JsonCreator diff --git a/akka-cluster-sharding-typed/src/test/scala/docs/akka/cluster/sharding/typed/AccountExampleDocSpec.scala b/akka-cluster-sharding-typed/src/test/scala/docs/akka/cluster/sharding/typed/AccountExampleDocSpec.scala index a32dfe3e4a..fff3609ea3 100644 --- a/akka-cluster-sharding-typed/src/test/scala/docs/akka/cluster/sharding/typed/AccountExampleDocSpec.scala +++ b/akka-cluster-sharding-typed/src/test/scala/docs/akka/cluster/sharding/typed/AccountExampleDocSpec.scala @@ -26,7 +26,7 @@ class AccountExampleDocSpec with LogCapturing { private val eventSourcedTestKit = - EventSourcedBehaviorTestKit[AccountEntity.Command[_], AccountEntity.Event, AccountEntity.Account]( + EventSourcedBehaviorTestKit[AccountEntity.Command, AccountEntity.Event, AccountEntity.Account]( system, AccountEntity("1", PersistenceId("Account", "1"))) diff --git a/akka-cluster-sharding-typed/src/test/scala/docs/akka/cluster/sharding/typed/AccountExampleSpec.scala b/akka-cluster-sharding-typed/src/test/scala/docs/akka/cluster/sharding/typed/AccountExampleSpec.scala index 42a954b749..6574180b95 100644 --- a/akka-cluster-sharding-typed/src/test/scala/docs/akka/cluster/sharding/typed/AccountExampleSpec.scala +++ b/akka-cluster-sharding-typed/src/test/scala/docs/akka/cluster/sharding/typed/AccountExampleSpec.scala @@ -79,10 +79,8 @@ class AccountExampleSpec } "reject Withdraw overdraft" in { - // AccountCommand[_] is the command type, but it should also be possible to narrow it to - // AccountCommand[OperationResult] val probe = createTestProbe[OperationResult]() - val ref = ClusterSharding(system).entityRefFor[Command[OperationResult]](AccountEntity.TypeKey, "3") + val ref = ClusterSharding(system).entityRefFor[Command](AccountEntity.TypeKey, "3") ref ! CreateAccount(probe.ref) probe.expectMessage(Confirmed) ref ! Deposit(100, probe.ref) @@ -90,10 +88,12 @@ class AccountExampleSpec ref ! Withdraw(110, probe.ref) probe.expectMessageType[Rejected] + // Account.Command is the command type, but it should also be possible to narrow it // ... thus restricting the entity ref from being sent other commands, e.g.: + // val ref2 = ClusterSharding(system).entityRefFor[Deposit](AccountEntity.TypeKey, "3") // val probe2 = createTestProbe[CurrentBalance]() // val msg = GetBalance(probe2.ref) - // ref ! msg // type mismatch: GetBalance NOT =:= AccountCommand[OperationResult] + // ref2 ! msg // type mismatch: GetBalance NOT =:= Deposit } "handle GetBalance" in { diff --git a/akka-cluster-sharding-typed/src/test/scala/docs/akka/cluster/sharding/typed/AccountExampleWithCommandHandlersInState.scala b/akka-cluster-sharding-typed/src/test/scala/docs/akka/cluster/sharding/typed/AccountExampleWithCommandHandlersInState.scala index e41ff492de..c5f3772b16 100644 --- a/akka-cluster-sharding-typed/src/test/scala/docs/akka/cluster/sharding/typed/AccountExampleWithCommandHandlersInState.scala +++ b/akka-cluster-sharding-typed/src/test/scala/docs/akka/cluster/sharding/typed/AccountExampleWithCommandHandlersInState.scala @@ -24,14 +24,12 @@ object AccountExampleWithCommandHandlersInState { //#account-entity object AccountEntity { // Command - sealed trait Command[Reply <: CommandReply] extends CborSerializable { - def replyTo: ActorRef[Reply] - } - final case class CreateAccount(replyTo: ActorRef[OperationResult]) extends Command[OperationResult] - final case class Deposit(amount: BigDecimal, replyTo: ActorRef[OperationResult]) extends Command[OperationResult] - final case class Withdraw(amount: BigDecimal, replyTo: ActorRef[OperationResult]) extends Command[OperationResult] - final case class GetBalance(replyTo: ActorRef[CurrentBalance]) extends Command[CurrentBalance] - final case class CloseAccount(replyTo: ActorRef[OperationResult]) extends Command[OperationResult] + sealed trait Command extends CborSerializable + final case class CreateAccount(replyTo: ActorRef[OperationResult]) extends Command + final case class Deposit(amount: BigDecimal, replyTo: ActorRef[OperationResult]) extends Command + final case class Withdraw(amount: BigDecimal, replyTo: ActorRef[OperationResult]) extends Command + final case class GetBalance(replyTo: ActorRef[CurrentBalance]) extends Command + final case class CloseAccount(replyTo: ActorRef[OperationResult]) extends Command // Reply sealed trait CommandReply extends CborSerializable @@ -54,11 +52,11 @@ object AccountExampleWithCommandHandlersInState { // State sealed trait Account extends CborSerializable { - def applyCommand(cmd: Command[_]): ReplyEffect + def applyCommand(cmd: Command): ReplyEffect def applyEvent(event: Event): Account } case object EmptyAccount extends Account { - override def applyCommand(cmd: Command[_]): ReplyEffect = + override def applyCommand(cmd: Command): ReplyEffect = cmd match { case CreateAccount(replyTo) => Effect.persist(AccountCreated).thenReply(replyTo)(_ => Confirmed) @@ -76,7 +74,7 @@ object AccountExampleWithCommandHandlersInState { case class OpenedAccount(balance: BigDecimal) extends Account { require(balance >= Zero, "Account balance can't be negative") - override def applyCommand(cmd: Command[_]): ReplyEffect = + override def applyCommand(cmd: Command): ReplyEffect = cmd match { case Deposit(amount, replyTo) => Effect.persist(Deposited(amount)).thenReply(replyTo)(_ => Confirmed) @@ -115,28 +113,33 @@ object AccountExampleWithCommandHandlersInState { } case object ClosedAccount extends Account { - override def applyCommand(cmd: Command[_]): ReplyEffect = + override def applyCommand(cmd: Command): ReplyEffect = cmd match { - case c @ (_: Deposit | _: Withdraw) => - Effect.reply(c.replyTo)(Rejected("Account is closed")) + case c: Deposit => + replyClosed(c.replyTo) + case c: Withdraw => + replyClosed(c.replyTo) case GetBalance(replyTo) => Effect.reply(replyTo)(CurrentBalance(Zero)) case CloseAccount(replyTo) => - Effect.reply(replyTo)(Rejected("Account is already closed")) + replyClosed(replyTo) case CreateAccount(replyTo) => - Effect.reply(replyTo)(Rejected("Account is already created")) + replyClosed(replyTo) } + private def replyClosed(replyTo: ActorRef[AccountEntity.OperationResult]): ReplyEffect = + Effect.reply(replyTo)(Rejected(s"Account is closed")) + override def applyEvent(event: Event): Account = throw new IllegalStateException(s"unexpected event [$event] in state [ClosedAccount]") } // when used with sharding, this TypeKey can be used in `sharding.init` and `sharding.entityRefFor`: - val TypeKey: EntityTypeKey[Command[_]] = - EntityTypeKey[Command[_]]("Account") + val TypeKey: EntityTypeKey[Command] = + EntityTypeKey[Command]("Account") - def apply(persistenceId: PersistenceId): Behavior[Command[_]] = { - EventSourcedBehavior.withEnforcedReplies[Command[_], Event, Account]( + def apply(persistenceId: PersistenceId): Behavior[Command] = { + EventSourcedBehavior.withEnforcedReplies[Command, Event, Account]( persistenceId, EmptyAccount, (state, cmd) => state.applyCommand(cmd), diff --git a/akka-cluster-sharding-typed/src/test/scala/docs/akka/cluster/sharding/typed/AccountExampleWithEventHandlersInState.scala b/akka-cluster-sharding-typed/src/test/scala/docs/akka/cluster/sharding/typed/AccountExampleWithEventHandlersInState.scala index 7c4ca1cf46..dc4c888136 100644 --- a/akka-cluster-sharding-typed/src/test/scala/docs/akka/cluster/sharding/typed/AccountExampleWithEventHandlersInState.scala +++ b/akka-cluster-sharding-typed/src/test/scala/docs/akka/cluster/sharding/typed/AccountExampleWithEventHandlersInState.scala @@ -27,17 +27,15 @@ object AccountExampleWithEventHandlersInState { object AccountEntity { // Command //#reply-command - sealed trait Command[Reply <: CommandReply] extends CborSerializable { - def replyTo: ActorRef[Reply] - } + sealed trait Command extends CborSerializable //#reply-command - final case class CreateAccount(replyTo: ActorRef[OperationResult]) extends Command[OperationResult] - final case class Deposit(amount: BigDecimal, replyTo: ActorRef[OperationResult]) extends Command[OperationResult] + final case class CreateAccount(replyTo: ActorRef[OperationResult]) extends Command + final case class Deposit(amount: BigDecimal, replyTo: ActorRef[OperationResult]) extends Command //#reply-command - final case class Withdraw(amount: BigDecimal, replyTo: ActorRef[OperationResult]) extends Command[OperationResult] + final case class Withdraw(amount: BigDecimal, replyTo: ActorRef[OperationResult]) extends Command //#reply-command - final case class GetBalance(replyTo: ActorRef[CurrentBalance]) extends Command[CurrentBalance] - final case class CloseAccount(replyTo: ActorRef[OperationResult]) extends Command[OperationResult] + final case class GetBalance(replyTo: ActorRef[CurrentBalance]) extends Command + final case class CloseAccount(replyTo: ActorRef[OperationResult]) extends Command // Reply //#reply-command @@ -89,20 +87,20 @@ object AccountExampleWithEventHandlersInState { } // when used with sharding, this TypeKey can be used in `sharding.init` and `sharding.entityRefFor`: - val TypeKey: EntityTypeKey[Command[_]] = - EntityTypeKey[Command[_]]("Account") + val TypeKey: EntityTypeKey[Command] = + EntityTypeKey[Command]("Account") // Note that after defining command, event and state classes you would probably start here when writing this. // When filling in the parameters of EventSourcedBehavior.apply you can use IntelliJ alt+Enter > createValue // to generate the stub with types for the command and event handlers. //#withEnforcedReplies - def apply(accountNumber: String, persistenceId: PersistenceId): Behavior[Command[_]] = { + def apply(accountNumber: String, persistenceId: PersistenceId): Behavior[Command] = { EventSourcedBehavior.withEnforcedReplies(persistenceId, EmptyAccount, commandHandler(accountNumber), eventHandler) } //#withEnforcedReplies - private def commandHandler(accountNumber: String): (Account, Command[_]) => ReplyEffect[Event, Account] = { + private def commandHandler(accountNumber: String): (Account, Command) => ReplyEffect[Event, Account] = { (state, cmd) => state match { case EmptyAccount => @@ -122,18 +120,26 @@ object AccountExampleWithEventHandlersInState { case ClosedAccount => cmd match { - case c @ (_: Deposit | _: Withdraw) => - Effect.reply(c.replyTo)(Rejected(s"Account $accountNumber is closed")) + case c: Deposit => + replyClosed(accountNumber, c.replyTo) + case c: Withdraw => + replyClosed(accountNumber, c.replyTo) case GetBalance(replyTo) => Effect.reply(replyTo)(CurrentBalance(Zero)) case CloseAccount(replyTo) => - Effect.reply(replyTo)(Rejected(s"Account $accountNumber is already closed")) + replyClosed(accountNumber, replyTo) case CreateAccount(replyTo) => - Effect.reply(replyTo)(Rejected(s"Account $accountNumber is already closed")) + replyClosed(accountNumber, replyTo) } } } + private def replyClosed( + accountNumber: String, + replyTo: ActorRef[AccountEntity.OperationResult]): ReplyEffect[Event, Account] = { + Effect.reply(replyTo)(Rejected(s"Account $accountNumber is closed")) + } + private val eventHandler: (Account, Event) => Account = { (state, event) => state.applyEvent(event) } diff --git a/akka-cluster-sharding-typed/src/test/scala/docs/akka/cluster/sharding/typed/AccountExampleWithOptionState.scala b/akka-cluster-sharding-typed/src/test/scala/docs/akka/cluster/sharding/typed/AccountExampleWithOptionState.scala index 0061f125ff..16197c6023 100644 --- a/akka-cluster-sharding-typed/src/test/scala/docs/akka/cluster/sharding/typed/AccountExampleWithOptionState.scala +++ b/akka-cluster-sharding-typed/src/test/scala/docs/akka/cluster/sharding/typed/AccountExampleWithOptionState.scala @@ -24,14 +24,12 @@ object AccountExampleWithOptionState { //#account-entity object AccountEntity { // Command - sealed trait Command[Reply <: CommandReply] extends CborSerializable { - def replyTo: ActorRef[Reply] - } - final case class CreateAccount(replyTo: ActorRef[OperationResult]) extends Command[OperationResult] - final case class Deposit(amount: BigDecimal, replyTo: ActorRef[OperationResult]) extends Command[OperationResult] - final case class Withdraw(amount: BigDecimal, replyTo: ActorRef[OperationResult]) extends Command[OperationResult] - final case class GetBalance(replyTo: ActorRef[CurrentBalance]) extends Command[CurrentBalance] - final case class CloseAccount(replyTo: ActorRef[OperationResult]) extends Command[OperationResult] + sealed trait Command extends CborSerializable + final case class CreateAccount(replyTo: ActorRef[OperationResult]) extends Command + final case class Deposit(amount: BigDecimal, replyTo: ActorRef[OperationResult]) extends Command + final case class Withdraw(amount: BigDecimal, replyTo: ActorRef[OperationResult]) extends Command + final case class GetBalance(replyTo: ActorRef[CurrentBalance]) extends Command + final case class CloseAccount(replyTo: ActorRef[OperationResult]) extends Command // Reply sealed trait CommandReply extends CborSerializable @@ -54,13 +52,13 @@ object AccountExampleWithOptionState { // State sealed trait Account extends CborSerializable { - def applyCommand(cmd: Command[_]): ReplyEffect + def applyCommand(cmd: Command): ReplyEffect def applyEvent(event: Event): Account } case class OpenedAccount(balance: BigDecimal) extends Account { require(balance >= Zero, "Account balance can't be negative") - override def applyCommand(cmd: Command[_]): ReplyEffect = + override def applyCommand(cmd: Command): ReplyEffect = cmd match { case Deposit(amount, replyTo) => Effect.persist(Deposited(amount)).thenReply(replyTo)(_ => Confirmed) @@ -99,28 +97,33 @@ object AccountExampleWithOptionState { } case object ClosedAccount extends Account { - override def applyCommand(cmd: Command[_]): ReplyEffect = + override def applyCommand(cmd: Command): ReplyEffect = cmd match { - case c @ (_: Deposit | _: Withdraw) => - Effect.reply(c.replyTo)(Rejected("Account is closed")) + case c: Deposit => + replyClosed(c.replyTo) + case c: Withdraw => + replyClosed(c.replyTo) case GetBalance(replyTo) => Effect.reply(replyTo)(CurrentBalance(Zero)) case CloseAccount(replyTo) => - Effect.reply(replyTo)(Rejected("Account is already closed")) + replyClosed(replyTo) case CreateAccount(replyTo) => - Effect.reply(replyTo)(Rejected("Account is already created")) + replyClosed(replyTo) } + private def replyClosed(replyTo: ActorRef[AccountEntity.OperationResult]): ReplyEffect = + Effect.reply(replyTo)(Rejected(s"Account is closed")) + override def applyEvent(event: Event): Account = throw new IllegalStateException(s"unexpected event [$event] in state [ClosedAccount]") } // when used with sharding, this TypeKey can be used in `sharding.init` and `sharding.entityRefFor`: - val TypeKey: EntityTypeKey[Command[_]] = - EntityTypeKey[Command[_]]("Account") + val TypeKey: EntityTypeKey[Command] = + EntityTypeKey[Command]("Account") - def apply(persistenceId: PersistenceId): Behavior[Command[_]] = { - EventSourcedBehavior.withEnforcedReplies[Command[_], Event, Option[Account]]( + def apply(persistenceId: PersistenceId): Behavior[Command] = { + EventSourcedBehavior.withEnforcedReplies[Command, Event, Option[Account]]( persistenceId, None, (state, cmd) => @@ -135,7 +138,7 @@ object AccountExampleWithOptionState { }) } - def onFirstCommand(cmd: Command[_]): ReplyEffect = { + def onFirstCommand(cmd: Command): ReplyEffect = { cmd match { case CreateAccount(replyTo) => Effect.persist(AccountCreated).thenReply(replyTo)(_ => Confirmed) diff --git a/akka-cluster-sharding/src/main/mima-filters/2.6.5.backwards.excludes/shard-allocation-client.excludes b/akka-cluster-sharding/src/main/mima-filters/2.6.5.backwards.excludes/shard-allocation-client.excludes new file mode 100644 index 0000000000..ca2bcab05a --- /dev/null +++ b/akka-cluster-sharding/src/main/mima-filters/2.6.5.backwards.excludes/shard-allocation-client.excludes @@ -0,0 +1,4 @@ +# Add methods to trait not for user extension +ProblemFilters.exclude[ReversedMissingMethodProblem]("akka.cluster.sharding.external.javadsl.ExternalShardAllocationClient.setShardLocations") +ProblemFilters.exclude[ReversedMissingMethodProblem]("akka.cluster.sharding.external.scaladsl.ExternalShardAllocationClient.updateShardLocations") + diff --git a/akka-cluster-sharding/src/main/scala/akka/cluster/sharding/ShardCoordinator.scala b/akka-cluster-sharding/src/main/scala/akka/cluster/sharding/ShardCoordinator.scala index 3d3d622ee2..8542a1d05a 100644 --- a/akka-cluster-sharding/src/main/scala/akka/cluster/sharding/ShardCoordinator.scala +++ b/akka-cluster-sharding/src/main/scala/akka/cluster/sharding/ShardCoordinator.scala @@ -614,7 +614,7 @@ abstract class ShardCoordinator( case GetShardHome(shard) => if (!handleGetShardHome(shard)) { // location not know, yet - val activeRegions = state.regions -- gracefulShutdownInProgress + val activeRegions = (state.regions -- gracefulShutdownInProgress) -- regionTerminationInProgress if (activeRegions.nonEmpty) { val getShardHomeSender = sender() val regionFuture = allocationStrategy.allocateShard(getShardHomeSender, shard, activeRegions) @@ -923,7 +923,8 @@ abstract class ShardCoordinator( state.shards.get(shard) match { case Some(ref) => getShardHomeSender ! ShardHome(shard, ref) case None => - if (state.regions.contains(region) && !gracefulShutdownInProgress.contains(region)) { + if (state.regions.contains(region) && !gracefulShutdownInProgress.contains(region) && !regionTerminationInProgress + .contains(region)) { update(ShardHomeAllocated(shard, region)) { evt => state = state.updated(evt) log.debug( diff --git a/akka-cluster-sharding/src/main/scala/akka/cluster/sharding/external/internal/ExternalShardAllocationClientImpl.scala b/akka-cluster-sharding/src/main/scala/akka/cluster/sharding/external/internal/ExternalShardAllocationClientImpl.scala index 5af5f9f28c..b6646697b5 100644 --- a/akka-cluster-sharding/src/main/scala/akka/cluster/sharding/external/internal/ExternalShardAllocationClientImpl.scala +++ b/akka-cluster-sharding/src/main/scala/akka/cluster/sharding/external/internal/ExternalShardAllocationClientImpl.scala @@ -38,6 +38,7 @@ import akka.pattern.ask import akka.util.JavaDurationConverters._ import akka.util.PrettyDuration._ import akka.util.Timeout +import akka.util.ccompat.JavaConverters._ /** * INTERNAL API @@ -92,4 +93,21 @@ final private[external] class ExternalShardAllocationClientImpl(system: ActorSys } override def getShardLocations(): CompletionStage[ShardLocations] = shardLocations().toJava + + override def updateShardLocations(locations: Map[ShardId, Address]): Future[Done] = { + log.debug("updateShardLocations {} for {}", locations, Key) + (replicator ? Update(Key, LWWMap.empty[ShardId, String], WriteLocal, None) { existing => + locations.foldLeft(existing) { + case (acc, (shardId, address)) => acc.put(self, shardId, address.toString) + } + }).flatMap { + case UpdateSuccess(_, _) => Future.successful(Done) + case UpdateTimeout => + Future.failed(new ClientTimeoutException(s"Unable to update shard location after ${timeout.duration.pretty}")) + } + } + + override def setShardLocations(locations: java.util.Map[ShardId, Address]): CompletionStage[Done] = { + updateShardLocations(locations.asScala.toMap).toJava + } } diff --git a/akka-cluster-sharding/src/main/scala/akka/cluster/sharding/external/javadsl/ExternalShardAllocationClient.scala b/akka-cluster-sharding/src/main/scala/akka/cluster/sharding/external/javadsl/ExternalShardAllocationClient.scala index fd561757c3..ee8d73360f 100644 --- a/akka-cluster-sharding/src/main/scala/akka/cluster/sharding/external/javadsl/ExternalShardAllocationClient.scala +++ b/akka-cluster-sharding/src/main/scala/akka/cluster/sharding/external/javadsl/ExternalShardAllocationClient.scala @@ -28,10 +28,21 @@ trait ExternalShardAllocationClient { * * @param shard The shard identifier * @param location Location (akka node) to allocate the shard to - * @return Confirmation that the update has been propagated to a majority of cluster nodes + * @return Conformation that the update has been written to the local node */ def setShardLocation(shard: ShardId, location: Address): CompletionStage[Done] + /** + * Update all of the provided ShardLocations. + * The [[Address]] should match one of the nodes in the cluster. If the node has not joined + * the cluster yet it will be moved to that node after the first cluster + * sharding rebalance it does. + * + * @param locations to update + * @return Confirmation that the update has been written to the local node + */ + def setShardLocations(locations: java.util.Map[ShardId, Address]): CompletionStage[Done] + /** * Get all the current shard locations that have been set via setShardLocation */ diff --git a/akka-cluster-sharding/src/main/scala/akka/cluster/sharding/external/scaladsl/ExternalShardAllocationClient.scala b/akka-cluster-sharding/src/main/scala/akka/cluster/sharding/external/scaladsl/ExternalShardAllocationClient.scala index aa9e21af03..694e03c918 100644 --- a/akka-cluster-sharding/src/main/scala/akka/cluster/sharding/external/scaladsl/ExternalShardAllocationClient.scala +++ b/akka-cluster-sharding/src/main/scala/akka/cluster/sharding/external/scaladsl/ExternalShardAllocationClient.scala @@ -24,7 +24,7 @@ trait ExternalShardAllocationClient { * Update the given shard's location. The [[Address]] should * match one of the nodes in the cluster. If the node has not joined * the cluster yet it will be moved to that node after the first cluster - * sharding rebalance. + * sharding rebalance it does. * * @param shard The shard identifier * @param location Location (akka node) to allocate the shard to @@ -32,6 +32,17 @@ trait ExternalShardAllocationClient { */ def updateShardLocation(shard: ShardId, location: Address): Future[Done] + /** + * Update all of the provided ShardLocations. + * The [[Address]] should match one of the nodes in the cluster. If the node has not joined + * the cluster yet it will be moved to that node after the first cluster + * sharding rebalance it does. + * + * @param locations to update + * @return Confirmation that the update has been propagates to a majority of cluster nodes + */ + def updateShardLocations(locations: Map[ShardId, Address]): Future[Done] + /** * Get all the current shard locations that have been set via updateShardLocation */ diff --git a/akka-cluster-sharding/src/multi-jvm/scala/akka/cluster/sbr/GlobalRegistry.scala b/akka-cluster-sharding/src/multi-jvm/scala/akka/cluster/sbr/GlobalRegistry.scala new file mode 100644 index 0000000000..5e834b71b5 --- /dev/null +++ b/akka-cluster-sharding/src/multi-jvm/scala/akka/cluster/sbr/GlobalRegistry.scala @@ -0,0 +1,97 @@ +/* + * Copyright (C) 2015-2020 Lightbend Inc. + */ + +package akka.cluster.sbr + +import scala.concurrent.duration._ + +import akka.actor.Actor +import akka.actor.ActorLogging +import akka.actor.ActorRef +import akka.actor.Address +import akka.actor.Props +import akka.cluster.Cluster +import akka.cluster.sharding.ShardRegion +import akka.serialization.jackson.CborSerializable + +object GlobalRegistry { + final case class Register(key: String, address: Address) extends CborSerializable + final case class Unregister(key: String, address: Address) extends CborSerializable + final case class DoubleRegister(key: String, msg: String) extends CborSerializable + + def props(probe: ActorRef, onlyErrors: Boolean): Props = + Props(new GlobalRegistry(probe, onlyErrors)) + + object SingletonActor { + def props(registry: ActorRef): Props = + Props(new SingletonActor(registry)) + + val extractEntityId: ShardRegion.ExtractEntityId = { + case id: Int => (id.toString, id) + } + + val extractShardId: ShardRegion.ExtractShardId = msg => + msg match { + case id: Int => (id % 10).toString + } + } + + class SingletonActor(registry: ActorRef) extends Actor with ActorLogging { + val key = self.path.toStringWithoutAddress + "-" + Cluster(context.system).selfDataCenter + + override def preStart(): Unit = { + log.info("Starting") + registry ! Register(key, Cluster(context.system).selfAddress) + } + + override def preRestart(reason: Throwable, message: Option[Any]): Unit = { + // don't call postStop + } + + override def postStop(): Unit = { + log.info("Stopping") + registry ! Unregister(key, Cluster(context.system).selfAddress) + } + + override def receive = { + case i: Int => sender() ! i + } + } +} + +class GlobalRegistry(probe: ActorRef, onlyErrors: Boolean) extends Actor with ActorLogging { + import GlobalRegistry._ + + var registry = Map.empty[String, Address] + var unregisterTimestamp = Map.empty[String, Long] + + override def receive = { + case r @ Register(key, address) => + log.info("{}", r) + if (registry.contains(key)) { + val errMsg = s"trying to register $address, but ${registry(key)} was already registered for $key" + log.error(errMsg) + probe ! DoubleRegister(key, errMsg) + } else { + unregisterTimestamp.get(key).foreach { t => + log.info("Unregister/register margin for [{}] was [{}] ms", key, (System.nanoTime() - t).nanos.toMillis) + } + registry += key -> address + if (!onlyErrors) probe ! r + } + + case u @ Unregister(key, address) => + log.info("{}", u) + if (!registry.contains(key)) + probe ! s"$key was not registered" + else if (registry(key) != address) + probe ! s"${registry(key)} instead of $address was registered for $key" + else { + registry -= key + unregisterTimestamp += key -> System.nanoTime() + if (!onlyErrors) probe ! u + } + } + +} diff --git a/akka-cluster-sharding/src/multi-jvm/scala/akka/cluster/sbr/GremlinController.scala b/akka-cluster-sharding/src/multi-jvm/scala/akka/cluster/sbr/GremlinController.scala new file mode 100644 index 0000000000..c76e38359d --- /dev/null +++ b/akka-cluster-sharding/src/multi-jvm/scala/akka/cluster/sbr/GremlinController.scala @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2019-2020 Lightbend Inc. + */ + +package akka.cluster.sbr + +import akka.actor.Actor +import akka.actor.ActorLogging +import akka.actor.ActorRef +import akka.actor.Address +import akka.actor.ExtendedActorSystem +import akka.actor.Props +import akka.cluster.Cluster +import akka.pattern.pipe +import akka.remote.RemoteActorRefProvider +import akka.remote.transport.ThrottlerTransportAdapter.Blackhole +import akka.remote.transport.ThrottlerTransportAdapter.Direction +import akka.remote.transport.ThrottlerTransportAdapter.SetThrottle +import akka.remote.transport.ThrottlerTransportAdapter.Unthrottled +import akka.serialization.jackson.CborSerializable + +object GremlinController { + final case class BlackholeNode(target: Address) extends CborSerializable + final case class PassThroughNode(target: Address) extends CborSerializable + case object GetAddress extends CborSerializable + + def props: Props = + Props(new GremlinController) +} + +class GremlinController extends Actor with ActorLogging { + import context.dispatcher + + import GremlinController._ + val transport = + context.system.asInstanceOf[ExtendedActorSystem].provider.asInstanceOf[RemoteActorRefProvider].transport + val selfAddress = Cluster(context.system).selfAddress + + override def receive = { + case GetAddress => + sender() ! selfAddress + case BlackholeNode(target) => + log.debug("Blackhole {} <-> {}", selfAddress, target) + transport.managementCommand(SetThrottle(target, Direction.Both, Blackhole)).pipeTo(sender()) + case PassThroughNode(target) => + log.debug("PassThrough {} <-> {}", selfAddress, target) + transport.managementCommand(SetThrottle(target, Direction.Both, Unthrottled)).pipeTo(sender()) + } +} + +object GremlinControllerProxy { + def props(target: ActorRef): Props = + Props(new GremlinControllerProxy(target)) +} + +class GremlinControllerProxy(target: ActorRef) extends Actor { + override def receive = { + case msg => target.forward(msg) + } +} diff --git a/akka-cluster-sharding/src/multi-jvm/scala/akka/cluster/sbr/RandomizedBrainResolverIntegrationSpec.scala b/akka-cluster-sharding/src/multi-jvm/scala/akka/cluster/sbr/RandomizedBrainResolverIntegrationSpec.scala new file mode 100644 index 0000000000..f03e3ec557 --- /dev/null +++ b/akka-cluster-sharding/src/multi-jvm/scala/akka/cluster/sbr/RandomizedBrainResolverIntegrationSpec.scala @@ -0,0 +1,409 @@ +/* + * Copyright (C) 2015-2020 Lightbend Inc. + */ + +package akka.cluster.sbr + +import scala.concurrent.Await +import scala.concurrent.duration._ +import scala.util.Random + +import com.typesafe.config.Config +import com.typesafe.config.ConfigFactory +import org.scalatest.BeforeAndAfterEach + +import akka.actor._ +import akka.cluster.Cluster +import akka.cluster.MemberStatus +import akka.cluster.MultiNodeClusterSpec +import akka.cluster.sharding.ClusterSharding +import akka.cluster.sharding.ClusterShardingSettings +import akka.cluster.singleton.ClusterSingletonManager +import akka.cluster.singleton.ClusterSingletonManagerSettings +import akka.pattern.ask +import akka.remote.testconductor.RoleName +import akka.remote.testkit.MultiNodeConfig +import akka.remote.testkit.MultiNodeSpec +import akka.testkit.ImplicitSender +import akka.testkit.LongRunningTest +import akka.testkit.TestKit +import akka.testkit.TestProbe +import akka.util.Timeout + +/* + * Depends on akka private classes so needs to be in this package + */ +object RandomizedSplitBrainResolverIntegrationSpec extends MultiNodeConfig { + val node1 = role("node1") + val node2 = role("node2") + val node3 = role("node3") + val node4 = role("node4") + val node5 = role("node5") + val node6 = role("node6") + val node7 = role("node7") + val node8 = role("node8") + val node9 = role("node9") + + commonConfig(ConfigFactory.parseString(s""" + akka { + loglevel = INFO + cluster { + downing-provider-class = "akka.cluster.sbr.SplitBrainResolverProvider" + split-brain-resolver { + stable-after = 10s + + active-strategy = lease-majority + lease-majority { + lease-implementation = test-lease + } + } + + #failure-detector.acceptable-heartbeat-pause = 10s + + # speedup timeout + sharding.handoff-timeout = 10 s + + # this is starting singleton more aggressively than default (15) + singleton.min-number-of-hand-over-retries = 10 + } + actor.provider = cluster + } + + test-lease { + lease-class = akka.cluster.sbr.SbrTestLeaseActorClient + heartbeat-interval = 1s + heartbeat-timeout = 120s + lease-operation-timeout = 3s + } + + test.random-seed = ${System.currentTimeMillis()} + + akka.testconductor.barrier-timeout = 120 s + akka.cluster.run-coordinated-shutdown-when-down = off + """)) + + testTransport(on = true) + +} + +class RandomizedSplitBrainResolverIntegrationSpecMultiJvmNode1 extends RandomizedSplitBrainResolverIntegrationSpec +class RandomizedSplitBrainResolverIntegrationSpecMultiJvmNode2 extends RandomizedSplitBrainResolverIntegrationSpec +class RandomizedSplitBrainResolverIntegrationSpecMultiJvmNode3 extends RandomizedSplitBrainResolverIntegrationSpec +class RandomizedSplitBrainResolverIntegrationSpecMultiJvmNode4 extends RandomizedSplitBrainResolverIntegrationSpec +class RandomizedSplitBrainResolverIntegrationSpecMultiJvmNode5 extends RandomizedSplitBrainResolverIntegrationSpec +class RandomizedSplitBrainResolverIntegrationSpecMultiJvmNode6 extends RandomizedSplitBrainResolverIntegrationSpec +class RandomizedSplitBrainResolverIntegrationSpecMultiJvmNode7 extends RandomizedSplitBrainResolverIntegrationSpec +class RandomizedSplitBrainResolverIntegrationSpecMultiJvmNode8 extends RandomizedSplitBrainResolverIntegrationSpec +class RandomizedSplitBrainResolverIntegrationSpecMultiJvmNode9 extends RandomizedSplitBrainResolverIntegrationSpec + +class RandomizedSplitBrainResolverIntegrationSpec + extends MultiNodeSpec(RandomizedSplitBrainResolverIntegrationSpec) + with MultiNodeClusterSpec + with ImplicitSender + with BeforeAndAfterEach { + import GlobalRegistry._ + import GremlinController._ + import RandomizedSplitBrainResolverIntegrationSpec._ + + // counter for unique naming for each test + var c = 0 + // to be shutdown in afterEach + var disposableSys: DisposableSys = _ + + override def expectedTestDuration = 3.minutes + + object DisposableSys { + def apply(scenario: Scenario): DisposableSys = { + disposableSys = new DisposableSys(scenario) + disposableSys + } + } + + override def afterEach(): Unit = { + if (disposableSys ne null) + disposableSys.shutdownSys() + } + + class DisposableSys(scenario: Scenario) { + + c += 1 + + val sys: ActorSystem = { + + val sys = ActorSystem(system.name + "-" + c, system.settings.config) + val gremlinController = sys.actorOf(GremlinController.props, "gremlinController") + system.actorOf(GremlinControllerProxy.props(gremlinController), s"gremlinControllerProxy-$c") + sys + } + + val singletonProbe = TestProbe() + val shardingProbe = TestProbe() + runOn(node1) { + system.actorOf(GlobalRegistry.props(singletonProbe.ref, true), s"singletonRegistry-$c") + system.actorOf(GlobalRegistry.props(shardingProbe.ref, true), s"shardingRegistry-$c") + if (scenario.usingLease) + system.actorOf(SbrTestLeaseActor.props, s"lease-${sys.name}") + } + enterBarrier("registry-started") + + system.actorSelection(node(node1) / "user" / s"singletonRegistry-$c") ! Identify(None) + val singletonRegistry: ActorRef = expectMsgType[ActorIdentity].ref.get + system.actorSelection(node(node1) / "user" / s"shardingRegistry-$c") ! Identify(None) + val shardingRegistry: ActorRef = expectMsgType[ActorIdentity].ref.get + + if (scenario.usingLease) { + system.actorSelection(node(node1) / "user" / s"lease-${sys.name}") ! Identify(None) + val leaseRef: ActorRef = expectMsgType[ActorIdentity].ref.get + SbrTestLeaseActorClientExt(sys).getActorLeaseClient().setActorLeaseRef(leaseRef) + } + enterBarrier("registry-located") + + lazy val region = ClusterSharding(sys).shardRegion(s"Entity-$c") + + def shutdownSys(): Unit = { + TestKit.shutdownActorSystem(sys, 10.seconds, verifySystemShutdown = true) + } + + def gremlinControllerProxy(at: RoleName): ActorRef = { + system.actorSelection(node(at) / "user" / s"gremlinControllerProxy-$c") ! Identify(None) + expectMsgType[ActorIdentity].ref.get + } + + def sysAddress(at: RoleName): Address = { + implicit val timeout = Timeout(3.seconds) + Await.result((gremlinControllerProxy(at) ? GetAddress).mapTo[Address], timeout.duration) + } + + def blackhole(from: RoleName, to: RoleName): Unit = { + implicit val timeout = Timeout(3.seconds) + import system.dispatcher + val f = for { + target <- (gremlinControllerProxy(to) ? GetAddress).mapTo[Address] + done <- gremlinControllerProxy(from) ? BlackholeNode(target) + } yield done + Await.ready(f, timeout.duration * 2) + log.info("Blackhole {} <-> {}", from.name, to.name) + } + + def passThrough(from: RoleName, to: RoleName): Unit = { + implicit val timeout = Timeout(3.seconds) + import system.dispatcher + val f = for { + target <- (gremlinControllerProxy(to) ? GetAddress).mapTo[Address] + done <- gremlinControllerProxy(from) ? PassThroughNode(target) + } yield done + Await.ready(f, timeout.duration * 2) + log.info("PassThrough {} <-> {}", from.name, to.name) + } + + def join(from: RoleName, to: RoleName, awaitUp: Boolean): Unit = { + runOn(from) { + Cluster(sys).join(sysAddress(to)) + createSingleton() + startSharding() + if (awaitUp) + awaitMemberUp() + } + enterBarrier(from.name + s"-joined-$c") + } + + def awaitMemberUp(): Unit = + within(10.seconds) { + awaitAssert { + Cluster(sys).state.members.exists { m => + m.address == Cluster(sys).selfAddress && m.status == MemberStatus.Up + } should be(true) + } + } + + def createSingleton(): ActorRef = { + sys.actorOf( + ClusterSingletonManager.props( + singletonProps = SingletonActor.props(singletonRegistry), + terminationMessage = PoisonPill, + settings = ClusterSingletonManagerSettings(system)), + name = "singletonRegistry") + } + + def startSharding(): Unit = { + ClusterSharding(sys).start( + typeName = s"Entity-$c", + entityProps = SingletonActor.props(shardingRegistry), + settings = ClusterShardingSettings(system), + extractEntityId = SingletonActor.extractEntityId, + extractShardId = SingletonActor.extractShardId) + } + + def verify(): Unit = { + val nodes = roles.take(scenario.numberOfNodes) + + def sendToSharding(expectReply: Boolean): Unit = { + runOn(nodes: _*) { + if (!Cluster(sys).isTerminated) { + val probe = TestProbe()(sys) + for (i <- 0 until 10) { + region.tell(i, probe.ref) + if (expectReply) + probe.expectMsg(3.seconds, i) + } + } + } + } + + runOn(nodes: _*) { + log.info("Running {} {} in round {}", myself.name, Cluster(sys).selfUniqueAddress, c) + } + val randomSeed = sys.settings.config.getLong("test.random-seed") + val random = new Random(randomSeed) + enterBarrier(s"log-startup-$c") + + within(3.minutes) { + + join(nodes.head, nodes.head, awaitUp = true) // oldest + join(nodes.last, nodes.head, awaitUp = true) // next oldest + for (n <- nodes.tail.dropRight(1)) + join(n, nodes.head, awaitUp = false) + runOn(nodes: _*) { + awaitMemberUp() + } + enterBarrier(s"all-up-$c") + + singletonProbe.expectNoMessage(1.second) + shardingProbe.expectNoMessage(10.millis) + + sendToSharding(expectReply = true) + + enterBarrier(s"initialized-$c") + runOn(nodes: _*) { + log.info("Initialized {} {} in round {}", myself.name, Cluster(sys).selfUniqueAddress, c) + } + + runOn(node1) { + val cleanSplit = random.nextBoolean() + val healCleanSplit = cleanSplit && random.nextBoolean() + val side1 = nodes.take(1 + random.nextInt(nodes.size - 1)) + val side2 = nodes.drop(side1.size) + + val numberOfFlaky = random.nextInt(5) + val healLastFlay = numberOfFlaky > 0 && random.nextBoolean() + val flaky: Map[Int, (RoleName, List[RoleName])] = + (0 until numberOfFlaky).map { i => + val from = nodes(random.nextInt(nodes.size)) + val targets = nodes.filterNot(_ == from) + val to = (0 to random.nextInt(math.min(5, targets.size))).map(j => targets(j)).toList + i -> (from -> to) + }.toMap + + val delays = (0 until 10).map(_ => 2 + random.nextInt(13)) + + log.info(s"Generated $scenario with random seed [$randomSeed] in round [$c]: " + + s"cleanSplit [$cleanSplit], healCleanSplit [$healCleanSplit] " + + (if (cleanSplit) s"side1 [${side1.map(_.name).mkString(", ")}], side2 [${side2.map(_.name).mkString(", ")}] ") + + s"flaky [${flaky.map { case (_, (from, to)) => from.name -> to.map(_.name).mkString("(", ", ", ")") }.mkString("; ")}] " + + s"delays [${delays.mkString(", ")}]") + + var delayIndex = 0 + def nextDelay(): Unit = { + Thread.sleep(delays(delayIndex) * 1000) + delayIndex += 1 + } + + if (cleanSplit) { + for (n1 <- side1; n2 <- side2) + blackhole(n1, n2) + + nextDelay() + } + + flaky.foreach { + case (i, (from, to)) => + if (i != 0) { + // heal previous flakiness + val (prevFrom, prevTo) = flaky(i - 1) + for (n <- prevTo) + passThrough(prevFrom, n) + } + + for (n <- to) + blackhole(from, n) + + nextDelay() + } + + if (healLastFlay) { + val (prevFrom, prevTo) = flaky(flaky.size - 1) + for (n <- prevTo) + passThrough(prevFrom, n) + + nextDelay() + } + + if (healCleanSplit) { + for (n1 <- side1; n2 <- side2) + passThrough(n1, n2) + } + } + enterBarrier(s"scenario-done-$c") + + runOn(nodes: _*) { + sendToSharding(expectReply = false) + singletonProbe.expectNoMessage(10.seconds) + shardingProbe.expectNoMessage(10.millis) + + var loopLimit = 20 + while (loopLimit != 0 && !Cluster(sys).isTerminated && Cluster(sys).state.unreachable.nonEmpty) { + sendToSharding(expectReply = false) + singletonProbe.expectNoMessage(5.seconds) + shardingProbe.expectNoMessage(10.millis) + loopLimit -= 1 + } + } + enterBarrier(s"terminated-or-unreachable-removed-$c") + + runOn(nodes: _*) { + (Cluster(sys).isTerminated || Cluster(sys).state.unreachable.isEmpty) should ===(true) + within(30.seconds) { + awaitAssert { + sendToSharding(expectReply = true) + } + } + singletonProbe.expectNoMessage(5.seconds) + shardingProbe.expectNoMessage(10.millis) + if (!Cluster(sys).isTerminated) + log.info(s"Survived ${Cluster(sys).state.members.size} members in round $c") + } + + enterBarrier(s"verified-$c") + } + enterBarrier(s"after-$c") + } + + } + + private val leaseMajorityConfig = ConfigFactory.parseString("""akka.cluster.split-brain-resolver { + active-strategy = lease-majority + }""") + + case class Scenario(cfg: Config, numberOfNodes: Int) { + + val activeStrategy: String = cfg.getString("akka.cluster.split-brain-resolver.active-strategy") + + override def toString: String = + s"Scenario($activeStrategy, $numberOfNodes)" + + def usingLease: Boolean = activeStrategy.contains("lease") + } + + val scenarios = + List(Scenario(leaseMajorityConfig, 3), Scenario(leaseMajorityConfig, 5), Scenario(leaseMajorityConfig, 9)) + + "SplitBrainResolver with lease" must { + + for (scenario <- scenarios) { + scenario.toString taggedAs LongRunningTest in { + DisposableSys(scenario).verify() + } + } + } + +} diff --git a/akka-cluster-sharding/src/multi-jvm/scala/akka/cluster/sbr/SbrTestLeaseActor.scala b/akka-cluster-sharding/src/multi-jvm/scala/akka/cluster/sbr/SbrTestLeaseActor.scala new file mode 100644 index 0000000000..2622327267 --- /dev/null +++ b/akka-cluster-sharding/src/multi-jvm/scala/akka/cluster/sbr/SbrTestLeaseActor.scala @@ -0,0 +1,125 @@ +/* + * Copyright (C) 2019-2020 Lightbend Inc. + */ + +package akka.cluster.sbr + +import java.util.concurrent.atomic.AtomicReference + +import scala.concurrent.Future +import scala.concurrent.duration._ + +import akka.actor.Actor +import akka.actor.ActorLogging +import akka.actor.ActorRef +import akka.actor.ActorSystem +import akka.actor.ExtendedActorSystem +import akka.actor.Extension +import akka.actor.ExtensionId +import akka.actor.ExtensionIdProvider +import akka.actor.Props +import akka.coordination.lease.LeaseSettings +import akka.coordination.lease.scaladsl.Lease +import akka.pattern.ask +import akka.serialization.jackson.CborSerializable +import akka.util.Timeout + +object SbrTestLeaseActor { + def props: Props = + Props(new SbrTestLeaseActor) + + final case class Acquire(owner: String) extends CborSerializable + final case class Release(owner: String) extends CborSerializable +} + +class SbrTestLeaseActor extends Actor with ActorLogging { + import SbrTestLeaseActor._ + + var owner: Option[String] = None + + override def receive = { + case Acquire(o) => + owner match { + case None => + log.info("ActorLease: acquired by [{}]", o) + owner = Some(o) + sender() ! true + case Some(`o`) => + log.info("ActorLease: renewed by [{}]", o) + sender() ! true + case Some(existingOwner) => + log.info("ActorLease: requested by [{}], but already held by [{}]", o, existingOwner) + sender() ! false + } + + case Release(o) => + owner match { + case None => + log.info("ActorLease: released by [{}] but no owner", o) + owner = Some(o) + sender() ! true + case Some(`o`) => + log.info("ActorLease: released by [{}]", o) + sender() ! true + case Some(existingOwner) => + log.info("ActorLease: release attempt by [{}], but held by [{}]", o, existingOwner) + sender() ! false + } + } + +} + +object SbrTestLeaseActorClientExt extends ExtensionId[SbrTestLeaseActorClientExt] with ExtensionIdProvider { + override def get(system: ActorSystem): SbrTestLeaseActorClientExt = super.get(system) + override def lookup = SbrTestLeaseActorClientExt + override def createExtension(system: ExtendedActorSystem): SbrTestLeaseActorClientExt = + new SbrTestLeaseActorClientExt(system) +} + +class SbrTestLeaseActorClientExt(val system: ExtendedActorSystem) extends Extension { + + private val leaseClient = new AtomicReference[SbrTestLeaseActorClient]() + + def getActorLeaseClient(): SbrTestLeaseActorClient = { + val lease = leaseClient.get + if (lease == null) throw new IllegalStateException("ActorLeaseClient must be set first") + lease + } + + def setActorLeaseClient(client: SbrTestLeaseActorClient): Unit = + leaseClient.set(client) + +} + +class SbrTestLeaseActorClient(settings: LeaseSettings, system: ExtendedActorSystem) extends Lease(settings) { + import SbrTestLeaseActor.Acquire + import SbrTestLeaseActor.Release + + SbrTestLeaseActorClientExt(system).setActorLeaseClient(this) + + private implicit val timeout = Timeout(3.seconds) + + private val _leaseRef = new AtomicReference[ActorRef] + + private def leaseRef: ActorRef = { + val ref = _leaseRef.get + if (ref == null) throw new IllegalStateException("ActorLeaseRef must be set first") + ref + } + + def setActorLeaseRef(ref: ActorRef): Unit = + _leaseRef.set(ref) + + override def acquire(): Future[Boolean] = { + (leaseRef ? Acquire(settings.ownerName)).mapTo[Boolean] + } + + override def acquire(leaseLostCallback: Option[Throwable] => Unit): Future[Boolean] = + acquire() + + override def release(): Future[Boolean] = { + (leaseRef ? Release(settings.ownerName)).mapTo[Boolean] + } + + override def checkLease(): Boolean = false +} diff --git a/akka-cluster-sharding/src/multi-jvm/scala/akka/cluster/sbr/SplitBrainResolverIntegrationSpec.scala b/akka-cluster-sharding/src/multi-jvm/scala/akka/cluster/sbr/SplitBrainResolverIntegrationSpec.scala new file mode 100644 index 0000000000..718edef025 --- /dev/null +++ b/akka-cluster-sharding/src/multi-jvm/scala/akka/cluster/sbr/SplitBrainResolverIntegrationSpec.scala @@ -0,0 +1,465 @@ +/* + * Copyright (C) 2015-2020 Lightbend Inc. + */ + +package akka.cluster.sbr + +import scala.concurrent.Await +import scala.concurrent.duration._ + +import com.typesafe.config.Config +import com.typesafe.config.ConfigFactory +import com.typesafe.config.ConfigValueFactory +import org.scalatest.BeforeAndAfterEach + +import akka.actor._ +import akka.cluster.Cluster +import akka.cluster.ClusterSettings.DataCenter +import akka.cluster.ClusterSettings.DefaultDataCenter +import akka.cluster.Member +import akka.cluster.MemberStatus +import akka.cluster.MultiNodeClusterSpec +import akka.cluster.sharding.ClusterSharding +import akka.cluster.sharding.ClusterShardingSettings +import akka.cluster.singleton.ClusterSingletonManager +import akka.cluster.singleton.ClusterSingletonManagerSettings +import akka.pattern.ask +import akka.remote.testconductor.RoleName +import akka.remote.testkit.MultiNodeConfig +import akka.remote.testkit.MultiNodeSpec +import akka.testkit.ImplicitSender +import akka.testkit.LongRunningTest +import akka.testkit.TestKit +import akka.testkit.TestProbe +import akka.util.Timeout + +/* + * Depends on akka private classes so needs to be in this package + */ +object SplitBrainResolverIntegrationSpec extends MultiNodeConfig { + val node1 = role("node1") + val node2 = role("node2") + val node3 = role("node3") + val node4 = role("node4") + val node5 = role("node5") + val node6 = role("node6") + val node7 = role("node7") + val node8 = role("node8") + val node9 = role("node9") + + commonConfig(ConfigFactory.parseString(""" + akka { + loglevel = INFO + cluster { + downing-provider-class = "akka.cluster.sbr.SplitBrainResolverProvider" + split-brain-resolver.active-strategy = keep-majority + split-brain-resolver.stable-after = 10s + + sharding.handoff-timeout = 5s + } + + actor.provider = cluster + remote.log-remote-lifecycle-events = off + } + + akka.coordinated-shutdown.run-by-jvm-shutdown-hook = off + akka.coordinated-shutdown.terminate-actor-system = off + akka.cluster.run-coordinated-shutdown-when-down = off + """)) + + testTransport(on = true) + +} + +class SplitBrainResolverIntegrationSpecMultiJvmNode1 extends SplitBrainResolverIntegrationSpec +class SplitBrainResolverIntegrationSpecMultiJvmNode2 extends SplitBrainResolverIntegrationSpec +class SplitBrainResolverIntegrationSpecMultiJvmNode3 extends SplitBrainResolverIntegrationSpec +class SplitBrainResolverIntegrationSpecMultiJvmNode4 extends SplitBrainResolverIntegrationSpec +class SplitBrainResolverIntegrationSpecMultiJvmNode5 extends SplitBrainResolverIntegrationSpec +class SplitBrainResolverIntegrationSpecMultiJvmNode6 extends SplitBrainResolverIntegrationSpec +class SplitBrainResolverIntegrationSpecMultiJvmNode7 extends SplitBrainResolverIntegrationSpec +class SplitBrainResolverIntegrationSpecMultiJvmNode8 extends SplitBrainResolverIntegrationSpec +class SplitBrainResolverIntegrationSpecMultiJvmNode9 extends SplitBrainResolverIntegrationSpec + +class SplitBrainResolverIntegrationSpec + extends MultiNodeSpec(SplitBrainResolverIntegrationSpec) + with MultiNodeClusterSpec + with ImplicitSender + with BeforeAndAfterEach { + import GlobalRegistry._ + import GremlinController._ + import SplitBrainResolverIntegrationSpec._ + + override def initialParticipants = roles.size + + override def afterEach(): Unit = { + if (disposableSys ne null) + disposableSys.shutdownSys() + } + + // counter for unique naming for each test + var c = 0 + // to be shutdown in afterEach + var disposableSys: DisposableSys = _ + + override def expectedTestDuration = 10.minutes + + object DisposableSys { + def apply(scenario: Scenario): DisposableSys = { + disposableSys = new DisposableSys(scenario) + disposableSys + } + } + + class DisposableSys(scenario: Scenario) { + + c += 1 + + val sys: ActorSystem = { + val dcName = scenario.dcDecider(myself) + + val sys = ActorSystem( + system.name + "-" + c, + scenario.cfg + .withValue("akka.cluster.multi-data-center.self-data-center", ConfigValueFactory.fromAnyRef(dcName)) + .withFallback(system.settings.config)) + val gremlinController = sys.actorOf(GremlinController.props, "gremlinController") + system.actorOf(GremlinControllerProxy.props(gremlinController), s"gremlinControllerProxy-$c") + sys + } + + val singletonProbe = TestProbe() + val shardingProbe = TestProbe() + runOn(node1) { + system.actorOf(GlobalRegistry.props(singletonProbe.ref, false), s"singletonRegistry-$c") + system.actorOf(GlobalRegistry.props(shardingProbe.ref, true), s"shardingRegistry-$c") + if (scenario.usingLease) + system.actorOf(SbrTestLeaseActor.props, s"lease-${sys.name}") + } + enterBarrier("registry-started") + + system.actorSelection(node(node1) / "user" / s"singletonRegistry-$c") ! Identify(None) + val singletonRegistry: ActorRef = expectMsgType[ActorIdentity].ref.get + system.actorSelection(node(node1) / "user" / s"shardingRegistry-$c") ! Identify(None) + val shardingRegistry: ActorRef = expectMsgType[ActorIdentity].ref.get + + if (scenario.usingLease) { + system.actorSelection(node(node1) / "user" / s"lease-${sys.name}") ! Identify(None) + val leaseRef: ActorRef = expectMsgType[ActorIdentity].ref.get + SbrTestLeaseActorClientExt(sys).getActorLeaseClient().setActorLeaseRef(leaseRef) + } + + enterBarrier("registry-located") + + lazy val region = ClusterSharding(sys).shardRegion(s"Entity-$c") + + def shutdownSys(): Unit = { + TestKit.shutdownActorSystem(sys, 10.seconds, verifySystemShutdown = true) + } + + def gremlinControllerProxy(at: RoleName): ActorRef = { + system.actorSelection(node(at) / "user" / s"gremlinControllerProxy-$c") ! Identify(None) + expectMsgType[ActorIdentity].ref.get + } + + def sysAddress(at: RoleName): Address = { + implicit val timeout = Timeout(3.seconds) + Await.result((gremlinControllerProxy(at) ? GetAddress).mapTo[Address], timeout.duration) + } + + def blackhole(from: RoleName, to: RoleName): Unit = { + implicit val timeout = Timeout(3.seconds) + import system.dispatcher + val f = for { + target <- (gremlinControllerProxy(to) ? GetAddress).mapTo[Address] + done <- gremlinControllerProxy(from) ? BlackholeNode(target) + } yield done + Await.ready(f, timeout.duration * 2) + log.info("Blackhole {} <-> {}", from.name, to.name) + } + + def join(from: RoleName, to: RoleName, awaitUp: Boolean): Unit = { + runOn(from) { + Cluster(sys).join(sysAddress(to)) + createSingleton() + startSharding() + if (awaitUp) + awaitMemberUp() + } + enterBarrier(from.name + s"-joined-$c") + } + + def awaitMemberUp(): Unit = + within(10.seconds) { + awaitAssert { + Cluster(sys).state.members.exists { m => + m.address == Cluster(sys).selfAddress && m.status == MemberStatus.Up + } should be(true) + } + } + + def awaitAllMembersUp(nodes: RoleName*): Unit = { + val addresses = nodes.map(sysAddress).toSet + within(15.seconds) { + awaitAssert { + Cluster(sys).state.members.map(_.address) should ===(addresses) + Cluster(sys).state.members.foreach { + _.status should ===(MemberStatus.Up) + } + } + } + } + + def createSingleton(): ActorRef = { + sys.actorOf( + ClusterSingletonManager.props( + singletonProps = SingletonActor.props(singletonRegistry), + terminationMessage = PoisonPill, + settings = ClusterSingletonManagerSettings(system)), + name = "singletonRegistry") + } + + def startSharding(): Unit = { + ClusterSharding(sys).start( + typeName = s"Entity-$c", + entityProps = SingletonActor.props(shardingRegistry), + settings = ClusterShardingSettings(system), + extractEntityId = SingletonActor.extractEntityId, + extractShardId = SingletonActor.extractShardId) + } + + def verify(): Unit = { + val side1 = roles.take(scenario.side1Size) + val side2 = roles.drop(scenario.side1Size).take(scenario.side2Size) + + def singletonRegisterKey(node: RoleName): String = + "/user/singletonRegistry/singleton-" + scenario.dcDecider(node) + + runOn(side1 ++ side2: _*) { + log.info("Running {} {} in round {}", myself.name, Cluster(sys).selfUniqueAddress, c) + } + enterBarrier(s"log-startup-$c") + + within(90.seconds) { + + join(side1.head, side1.head, awaitUp = true) // oldest + join(side2.head, side1.head, awaitUp = true) // next oldest + for (n <- side1.tail ++ side2.tail) + join(n, side1.head, awaitUp = false) + runOn(side1 ++ side2: _*) { + awaitAllMembersUp(side1 ++ side2: _*) + } + enterBarrier(s"all-up-$c") + + runOn(node1) { + singletonProbe.within(25.seconds) { + singletonProbe.expectMsg(Register(singletonRegisterKey(node1), sysAddress(node1))) + } + shardingProbe.expectNoMessage(100.millis) + } + + runOn(side1 ++ side2: _*) { + val probe = TestProbe()(sys) + for (i <- 0 until 10) { + region.tell(i, probe.ref) + probe.expectMsg(5.seconds, i) + } + } + + enterBarrier(s"initialized-$c") + runOn(side1 ++ side2: _*) { + log.info("Initialized {} {} in round {}", myself.name, Cluster(sys).selfUniqueAddress, c) + } + + runOn(node1) { + for (n1 <- side1; n2 <- side2) + blackhole(n1, n2) + } + enterBarrier(s"blackhole-$c") + + val resolvedExpected = scenario.expected match { + case KeepLeader => + import Member.addressOrdering + val address = (side1 ++ side2).map(sysAddress).min + if (side1.exists(sysAddress(_) == address)) KeepSide1 + else if (side2.exists(sysAddress(_) == address)) KeepSide2 + else ShutdownBoth + case other => other + } + + resolvedExpected match { + case ShutdownBoth => + runOn(side1 ++ side2: _*) { + awaitCond(Cluster(sys).isTerminated, max = 30.seconds) + } + enterBarrier(s"sys-terminated-$c") + runOn(node1) { + singletonProbe.within(20.seconds) { + singletonProbe.expectMsg(Unregister(singletonRegisterKey(side1.head), sysAddress(side1.head))) + } + shardingProbe.expectNoMessage(100.millis) + } + + case KeepSide1 => + runOn(side1: _*) { + val expectedAddresses = side1.map(sysAddress).toSet + within(remaining - 3.seconds) { + awaitAssert { + val probe = TestProbe()(sys) + for (i <- 0 until 10) { + region.tell(i, probe.ref) + probe.expectMsg(2.seconds, i) + } + + Cluster(sys).state.members.map(_.address) should be(expectedAddresses) + } + } + } + runOn(side2: _*) { + awaitCond(Cluster(sys).isTerminated, max = 30.seconds) + } + enterBarrier(s"cluster-shutdown-verified-$c") + singletonProbe.expectNoMessage(1.second) + shardingProbe.expectNoMessage(100.millis) + + case KeepSide2 => + runOn(side1: _*) { + awaitCond(Cluster(sys).isTerminated, max = 30.seconds) + } + enterBarrier(s"sys-terminated-$c") + runOn(node1) { + singletonProbe.within(30.seconds) { + singletonProbe.expectMsg(Unregister(singletonRegisterKey(side1.head), sysAddress(side1.head))) + singletonProbe.expectMsg(Register(singletonRegisterKey(side2.head), sysAddress(side2.head))) + } + shardingProbe.expectNoMessage(100.millis) + } + runOn(side2: _*) { + val expectedAddresses = side2.map(sysAddress).toSet + within(remaining - 3.seconds) { + awaitAssert { + val probe = TestProbe()(sys) + for (i <- 0 until 10) { + region.tell(i, probe.ref) + probe.expectMsg(2.seconds, i) + } + + Cluster(sys).state.members.map(_.address) should be(expectedAddresses) + } + } + } + + case KeepAll => + runOn((side1 ++ side2): _*) { + val expectedAddresses = (side1 ++ side2).map(sysAddress).toSet + within(remaining - 3.seconds) { + awaitAssert { + val probe = TestProbe()(sys) + for (i <- 0 until 10) { + region.tell(i, probe.ref) + probe.expectMsg(2.seconds, i) + } + + Cluster(sys).state.members.map(_.address) should be(expectedAddresses) + } + } + Cluster(sys).isTerminated should be(false) + } + enterBarrier(s"cluster-intact-verified-$c") + + case KeepLeader => throw new IllegalStateException // already resolved to other case + } + + enterBarrier(s"verified-$c") + } + enterBarrier(s"after-$c") + } + + } + + private val staticQuorumConfig = ConfigFactory.parseString("""akka.cluster.split-brain-resolver { + active-strategy = static-quorum + static-quorum.quorum-size = 5 + }""") + + private val keepMajorityConfig = ConfigFactory.parseString("""akka.cluster.split-brain-resolver { + active-strategy = keep-majority + }""") + private val keepOldestConfig = ConfigFactory.parseString("""akka.cluster.split-brain-resolver { + active-strategy = keep-oldest + }""") + private val downAllConfig = ConfigFactory.parseString("""akka.cluster.split-brain-resolver { + active-strategy = down-all + }""") + private val leaseMajorityConfig = ConfigFactory.parseString("""akka.cluster.split-brain-resolver { + active-strategy = lease-majority + lease-majority { + lease-implementation = test-lease + acquire-lease-delay-for-minority = 3s + } + } + test-lease { + lease-class = akka.cluster.sbr.SbrTestLeaseActorClient + heartbeat-interval = 1s + heartbeat-timeout = 120s + lease-operation-timeout = 3s + } + """) + + sealed trait Expected + case object KeepSide1 extends Expected + case object KeepSide2 extends Expected + case object ShutdownBoth extends Expected + case object KeepLeader extends Expected + case object KeepAll extends Expected + + val defaultDcDecider: RoleName => DataCenter = _ => DefaultDataCenter + + case class Scenario( + cfg: Config, + side1Size: Int, + side2Size: Int, + expected: Expected, + dcDecider: RoleName => DataCenter = defaultDcDecider // allows to set the dc per indexed node + ) { + + val activeStrategy: String = cfg.getString("akka.cluster.split-brain-resolver.active-strategy") + + override def toString: String = { + s"$expected when using $activeStrategy and side1=$side1Size and side2=$side2Size" + + (if (dcDecider ne defaultDcDecider) "with multi-DC" else "") + } + + def usingLease: Boolean = activeStrategy.contains("lease") + } + + val scenarios = List( + Scenario(staticQuorumConfig, 1, 2, ShutdownBoth), + Scenario(staticQuorumConfig, 4, 4, ShutdownBoth), + Scenario(staticQuorumConfig, 5, 4, KeepSide1), + Scenario(staticQuorumConfig, 1, 5, KeepSide2), + Scenario(staticQuorumConfig, 4, 5, KeepSide2), + Scenario(keepMajorityConfig, 2, 1, KeepSide1), + Scenario(keepMajorityConfig, 1, 2, KeepSide2), + Scenario(keepMajorityConfig, 4, 5, KeepSide2), + Scenario(keepMajorityConfig, 4, 4, KeepLeader), + Scenario(keepOldestConfig, 3, 3, KeepSide1), + Scenario(keepOldestConfig, 1, 1, KeepSide1), + Scenario(keepOldestConfig, 1, 2, KeepSide2), // because down-if-alone + Scenario(keepMajorityConfig, 3, 2, KeepAll, { + case `node1` | `node2` | `node3` => "dcA" + case _ => "dcB" + }), + Scenario(downAllConfig, 1, 2, ShutdownBoth), + Scenario(leaseMajorityConfig, 4, 5, KeepSide2)) + + "Cluster SplitBrainResolver" must { + + for (scenario <- scenarios) { + scenario.toString taggedAs LongRunningTest in { + DisposableSys(scenario).verify() + } + } + } + +} diff --git a/akka-cluster-sharding/src/multi-jvm/scala/akka/cluster/sharding/ClusterShardCoordinatorDowning2Spec.scala b/akka-cluster-sharding/src/multi-jvm/scala/akka/cluster/sharding/ClusterShardCoordinatorDowning2Spec.scala new file mode 100644 index 0000000000..bea4817d86 --- /dev/null +++ b/akka-cluster-sharding/src/multi-jvm/scala/akka/cluster/sharding/ClusterShardCoordinatorDowning2Spec.scala @@ -0,0 +1,181 @@ +/* + * Copyright (C) 2020 Lightbend Inc. + */ + +package akka.cluster.sharding + +import scala.concurrent.duration._ + +import akka.actor.Actor +import akka.actor.ActorRef +import akka.actor.Props +import akka.cluster.MemberStatus +import akka.remote.transport.ThrottlerTransportAdapter.Direction +import akka.serialization.jackson.CborSerializable +import akka.testkit._ +import akka.util.ccompat._ + +@ccompatUsedUntil213 +object ClusterShardCoordinatorDowning2Spec { + case class Ping(id: String) extends CborSerializable + + class Entity extends Actor { + def receive = { + case Ping(_) => sender() ! self + } + } + + case object GetLocations extends CborSerializable + case class Locations(locations: Map[String, ActorRef]) extends CborSerializable + + class ShardLocations extends Actor { + var locations: Locations = _ + def receive = { + case GetLocations => sender() ! locations + case l: Locations => locations = l + } + } + + val extractEntityId: ShardRegion.ExtractEntityId = { + case m @ Ping(id) => (id, m) + } + + val extractShardId: ShardRegion.ExtractShardId = { + case Ping(id: String) => id.charAt(0).toString + } +} + +abstract class ClusterShardCoordinatorDowning2SpecConfig(mode: String) + extends MultiNodeClusterShardingConfig( + mode, + loglevel = "INFO", + additionalConfig = """ + akka.cluster.sharding.rebalance-interval = 120 s + # setting down-removal-margin, for testing of issue #29131 + akka.cluster.down-removal-margin = 3 s + akka.remote.watch-failure-detector.acceptable-heartbeat-pause = 3s + """) { + val first = role("first") + val second = role("second") + + testTransport(on = true) + +} + +object PersistentClusterShardCoordinatorDowning2SpecConfig + extends ClusterShardCoordinatorDowning2SpecConfig(ClusterShardingSettings.StateStoreModePersistence) +object DDataClusterShardCoordinatorDowning2SpecConfig + extends ClusterShardCoordinatorDowning2SpecConfig(ClusterShardingSettings.StateStoreModeDData) + +class PersistentClusterShardCoordinatorDowning2Spec + extends ClusterShardCoordinatorDowning2Spec(PersistentClusterShardCoordinatorDowning2SpecConfig) +class DDataClusterShardCoordinatorDowning2Spec + extends ClusterShardCoordinatorDowning2Spec(DDataClusterShardCoordinatorDowning2SpecConfig) + +class PersistentClusterShardCoordinatorDowning2MultiJvmNode1 extends PersistentClusterShardCoordinatorDowning2Spec +class PersistentClusterShardCoordinatorDowning2MultiJvmNode2 extends PersistentClusterShardCoordinatorDowning2Spec + +class DDataClusterShardCoordinatorDowning2MultiJvmNode1 extends DDataClusterShardCoordinatorDowning2Spec +class DDataClusterShardCoordinatorDowning2MultiJvmNode2 extends DDataClusterShardCoordinatorDowning2Spec + +abstract class ClusterShardCoordinatorDowning2Spec(multiNodeConfig: ClusterShardCoordinatorDowning2SpecConfig) + extends MultiNodeClusterShardingSpec(multiNodeConfig) + with ImplicitSender { + import multiNodeConfig._ + + import ClusterShardCoordinatorDowning2Spec._ + + def startSharding(): Unit = { + startSharding( + system, + typeName = "Entity", + entityProps = Props[Entity](), + extractEntityId = extractEntityId, + extractShardId = extractShardId) + } + + lazy val region = ClusterSharding(system).shardRegion("Entity") + + s"Cluster sharding ($mode) with down member, scenario 2" must { + + "join cluster" in within(20.seconds) { + startPersistenceIfNotDdataMode(startOn = first, setStoreOn = Seq(first, second)) + + join(first, first, onJoinedRunOnFrom = startSharding()) + join(second, first, onJoinedRunOnFrom = startSharding(), assertNodeUp = false) + + // all Up, everywhere before continuing + runOn(first, second) { + awaitAssert { + cluster.state.members.size should ===(2) + cluster.state.members.unsorted.map(_.status) should ===(Set(MemberStatus.Up)) + } + } + + enterBarrier("after-2") + } + + "initialize shards" in { + runOn(first) { + val shardLocations = system.actorOf(Props[ShardLocations](), "shardLocations") + val locations = (for (n <- 1 to 4) yield { + val id = n.toString + region ! Ping(id) + id -> expectMsgType[ActorRef] + }).toMap + shardLocations ! Locations(locations) + system.log.debug("Original locations: {}", locations) + } + enterBarrier("after-3") + } + + "recover after downing other node (not coordinator)" in within(20.seconds) { + val secondAddress = address(second) + + runOn(first) { + testConductor.blackhole(first, second, Direction.Both).await + } + + Thread.sleep(3000) + + runOn(first) { + cluster.down(second) + awaitAssert { + cluster.state.members.size should ===(1) + } + + // start a few more new shards, could be allocated to second but should notice that it's terminated + val additionalLocations = + awaitAssert { + val probe = TestProbe() + (for (n <- 5 to 8) yield { + val id = n.toString + region.tell(Ping(id), probe.ref) + id -> probe.expectMsgType[ActorRef](1.second) + }).toMap + } + system.log.debug("Additional locations: {}", additionalLocations) + + system.actorSelection(node(first) / "user" / "shardLocations") ! GetLocations + val Locations(originalLocations) = expectMsgType[Locations] + + awaitAssert { + val probe = TestProbe() + (originalLocations ++ additionalLocations).foreach { + case (id, ref) => + region.tell(Ping(id), probe.ref) + if (ref.path.address == secondAddress) { + val newRef = probe.expectMsgType[ActorRef](1.second) + newRef should not be (ref) + system.log.debug("Moved [{}] from [{}] to [{}]", id, ref, newRef) + } else + probe.expectMsg(1.second, ref) // should not move + } + } + } + + enterBarrier("after-4") + } + + } +} diff --git a/akka-cluster-sharding/src/multi-jvm/scala/akka/cluster/sharding/ClusterShardCoordinatorDowningSpec.scala b/akka-cluster-sharding/src/multi-jvm/scala/akka/cluster/sharding/ClusterShardCoordinatorDowningSpec.scala new file mode 100644 index 0000000000..e50cfcf511 --- /dev/null +++ b/akka-cluster-sharding/src/multi-jvm/scala/akka/cluster/sharding/ClusterShardCoordinatorDowningSpec.scala @@ -0,0 +1,183 @@ +/* + * Copyright (C) 2020 Lightbend Inc. + */ + +package akka.cluster.sharding + +import scala.concurrent.duration._ + +import akka.actor.Actor +import akka.actor.ActorRef +import akka.actor.Props +import akka.cluster.MemberStatus +import akka.remote.transport.ThrottlerTransportAdapter.Direction +import akka.serialization.jackson.CborSerializable +import akka.testkit._ +import akka.util.ccompat._ + +@ccompatUsedUntil213 +object ClusterShardCoordinatorDowningSpec { + case class Ping(id: String) extends CborSerializable + + class Entity extends Actor { + def receive = { + case Ping(_) => sender() ! self + } + } + + case object GetLocations extends CborSerializable + case class Locations(locations: Map[String, ActorRef]) extends CborSerializable + + class ShardLocations extends Actor { + var locations: Locations = _ + def receive = { + case GetLocations => sender() ! locations + case l: Locations => locations = l + } + } + + val extractEntityId: ShardRegion.ExtractEntityId = { + case m @ Ping(id) => (id, m) + } + + val extractShardId: ShardRegion.ExtractShardId = { + case Ping(id: String) => id.charAt(0).toString + } +} + +abstract class ClusterShardCoordinatorDowningSpecConfig(mode: String) + extends MultiNodeClusterShardingConfig( + mode, + loglevel = "INFO", + additionalConfig = """ + akka.cluster.sharding.rebalance-interval = 120 s + # setting down-removal-margin, for testing of issue #29131 + akka.cluster.down-removal-margin = 3 s + akka.remote.watch-failure-detector.acceptable-heartbeat-pause = 3s + """) { + val controller = role("controller") + val first = role("first") + val second = role("second") + + testTransport(on = true) + +} + +object PersistentClusterShardCoordinatorDowningSpecConfig + extends ClusterShardCoordinatorDowningSpecConfig(ClusterShardingSettings.StateStoreModePersistence) +object DDataClusterShardCoordinatorDowningSpecConfig + extends ClusterShardCoordinatorDowningSpecConfig(ClusterShardingSettings.StateStoreModeDData) + +class PersistentClusterShardCoordinatorDowningSpec + extends ClusterShardCoordinatorDowningSpec(PersistentClusterShardCoordinatorDowningSpecConfig) +class DDataClusterShardCoordinatorDowningSpec + extends ClusterShardCoordinatorDowningSpec(DDataClusterShardCoordinatorDowningSpecConfig) + +class PersistentClusterShardCoordinatorDowningMultiJvmNode1 extends PersistentClusterShardCoordinatorDowningSpec +class PersistentClusterShardCoordinatorDowningMultiJvmNode2 extends PersistentClusterShardCoordinatorDowningSpec +class PersistentClusterShardCoordinatorDowningMultiJvmNode3 extends PersistentClusterShardCoordinatorDowningSpec + +class DDataClusterShardCoordinatorDowningMultiJvmNode1 extends DDataClusterShardCoordinatorDowningSpec +class DDataClusterShardCoordinatorDowningMultiJvmNode2 extends DDataClusterShardCoordinatorDowningSpec +class DDataClusterShardCoordinatorDowningMultiJvmNode3 extends DDataClusterShardCoordinatorDowningSpec + +abstract class ClusterShardCoordinatorDowningSpec(multiNodeConfig: ClusterShardCoordinatorDowningSpecConfig) + extends MultiNodeClusterShardingSpec(multiNodeConfig) + with ImplicitSender { + import multiNodeConfig._ + + import ClusterShardCoordinatorDowningSpec._ + + def startSharding(): Unit = { + startSharding( + system, + typeName = "Entity", + entityProps = Props[Entity](), + extractEntityId = extractEntityId, + extractShardId = extractShardId) + } + + lazy val region = ClusterSharding(system).shardRegion("Entity") + + s"Cluster sharding ($mode) with down member, scenario 1" must { + + "join cluster" in within(20.seconds) { + startPersistenceIfNotDdataMode(startOn = controller, setStoreOn = Seq(first, second)) + + join(first, first, onJoinedRunOnFrom = startSharding()) + join(second, first, onJoinedRunOnFrom = startSharding(), assertNodeUp = false) + + // all Up, everywhere before continuing + runOn(first, second) { + awaitAssert { + cluster.state.members.size should ===(2) + cluster.state.members.unsorted.map(_.status) should ===(Set(MemberStatus.Up)) + } + } + + enterBarrier("after-2") + } + + "initialize shards" in { + runOn(first) { + val shardLocations = system.actorOf(Props[ShardLocations](), "shardLocations") + val locations = (for (n <- 1 to 4) yield { + val id = n.toString + region ! Ping(id) + id -> expectMsgType[ActorRef] + }).toMap + shardLocations ! Locations(locations) + system.log.debug("Original locations: {}", locations) + } + enterBarrier("after-3") + } + + "recover after downing coordinator node" in within(20.seconds) { + val firstAddress = address(first) + system.actorSelection(node(first) / "user" / "shardLocations") ! GetLocations + val Locations(originalLocations) = expectMsgType[Locations] + + runOn(controller) { + testConductor.blackhole(first, second, Direction.Both).await + } + + Thread.sleep(3000) + + runOn(second) { + cluster.down(first) + awaitAssert { + cluster.state.members.size should ===(1) + } + + // start a few more new shards, could be allocated to first but should notice that it's terminated + val additionalLocations = + awaitAssert { + val probe = TestProbe() + (for (n <- 5 to 8) yield { + val id = n.toString + region.tell(Ping(id), probe.ref) + id -> probe.expectMsgType[ActorRef](1.second) + }).toMap + } + system.log.debug("Additional locations: {}", additionalLocations) + + awaitAssert { + val probe = TestProbe() + (originalLocations ++ additionalLocations).foreach { + case (id, ref) => + region.tell(Ping(id), probe.ref) + if (ref.path.address == firstAddress) { + val newRef = probe.expectMsgType[ActorRef](1.second) + newRef should not be (ref) + system.log.debug("Moved [{}] from [{}] to [{}]", id, ref, newRef) + } else + probe.expectMsg(1.second, ref) // should not move + } + } + } + + enterBarrier("after-4") + } + + } +} diff --git a/akka-cluster-sharding/src/multi-jvm/scala/akka/cluster/sharding/ExternalShardAllocationSpec.scala b/akka-cluster-sharding/src/multi-jvm/scala/akka/cluster/sharding/ExternalShardAllocationSpec.scala index dbf977d8cd..89ee7a38ee 100644 --- a/akka-cluster-sharding/src/multi-jvm/scala/akka/cluster/sharding/ExternalShardAllocationSpec.scala +++ b/akka-cluster-sharding/src/multi-jvm/scala/akka/cluster/sharding/ExternalShardAllocationSpec.scala @@ -131,7 +131,7 @@ abstract class ExternalShardAllocationSpec val forthAddress = address(forth) runOn(second) { system.log.info("Allocating {} on {}", onForthShardId, forthAddress) - ExternalShardAllocation(system).clientFor(typeName).updateShardLocation(onForthShardId, forthAddress) + ExternalShardAllocation(system).clientFor(typeName).updateShardLocations(Map(onForthShardId -> forthAddress)) } enterBarrier("allocated-to-new-node") runOn(forth) { diff --git a/akka-cluster-sharding/src/test/scala/akka/cluster/sharding/ClusterShardingLeaseSpec.scala b/akka-cluster-sharding/src/test/scala/akka/cluster/sharding/ClusterShardingLeaseSpec.scala index 477febf80b..b1c02b5b15 100644 --- a/akka-cluster-sharding/src/test/scala/akka/cluster/sharding/ClusterShardingLeaseSpec.scala +++ b/akka-cluster-sharding/src/test/scala/akka/cluster/sharding/ClusterShardingLeaseSpec.scala @@ -11,7 +11,9 @@ import scala.util.control.NoStackTrace import com.typesafe.config.{ Config, ConfigFactory } import akka.actor.Props -import akka.cluster.{ Cluster, MemberStatus, TestLease, TestLeaseExt } +import akka.cluster.{ Cluster, MemberStatus } +import akka.coordination.lease.TestLease +import akka.coordination.lease.TestLeaseExt import akka.testkit.{ AkkaSpec, ImplicitSender } import akka.testkit.TestActors.EchoActor diff --git a/akka-cluster-sharding/src/test/scala/akka/cluster/sharding/ShardSpec.scala b/akka-cluster-sharding/src/test/scala/akka/cluster/sharding/ShardSpec.scala index b67ba6c1cd..5d670b9f32 100644 --- a/akka-cluster-sharding/src/test/scala/akka/cluster/sharding/ShardSpec.scala +++ b/akka-cluster-sharding/src/test/scala/akka/cluster/sharding/ShardSpec.scala @@ -12,20 +12,21 @@ import scala.util.Success import scala.util.control.NoStackTrace import akka.actor.{ Actor, ActorLogging, PoisonPill, Props } -import akka.cluster.TestLeaseExt import akka.cluster.sharding.ShardRegion.ShardInitialized import akka.coordination.lease.LeaseUsageSettings +import akka.coordination.lease.TestLease +import akka.coordination.lease.TestLeaseExt import akka.testkit.{ AkkaSpec, ImplicitSender, TestProbe } object ShardSpec { val config = - """ + s""" akka.loglevel = INFO akka.actor.provider = "cluster" akka.remote.classic.netty.tcp.port = 0 akka.remote.artery.canonical.port = 0 test-lease { - lease-class = akka.cluster.TestLease + lease-class = ${classOf[TestLease].getName} heartbeat-interval = 1s heartbeat-timeout = 120s lease-operation-timeout = 3s diff --git a/akka-cluster-tools/src/main/java/akka/cluster/client/protobuf/msg/ClusterClientMessages.java b/akka-cluster-tools/src/main/java/akka/cluster/client/protobuf/msg/ClusterClientMessages.java index f363f6b23c..f88b1dce3e 100644 --- a/akka-cluster-tools/src/main/java/akka/cluster/client/protobuf/msg/ClusterClientMessages.java +++ b/akka-cluster-tools/src/main/java/akka/cluster/client/protobuf/msg/ClusterClientMessages.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2019-2020 Lightbend Inc. + * Copyright (C) 2020 Lightbend Inc. */ // Generated by the protocol buffer compiler. DO NOT EDIT! diff --git a/akka-cluster-tools/src/main/java/akka/cluster/pubsub/protobuf/msg/DistributedPubSubMessages.java b/akka-cluster-tools/src/main/java/akka/cluster/pubsub/protobuf/msg/DistributedPubSubMessages.java index 75b3a7a9ca..5475b9086d 100644 --- a/akka-cluster-tools/src/main/java/akka/cluster/pubsub/protobuf/msg/DistributedPubSubMessages.java +++ b/akka-cluster-tools/src/main/java/akka/cluster/pubsub/protobuf/msg/DistributedPubSubMessages.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2019-2020 Lightbend Inc. + * Copyright (C) 2020 Lightbend Inc. */ // Generated by the protocol buffer compiler. DO NOT EDIT! diff --git a/akka-cluster-tools/src/main/resources/reference.conf b/akka-cluster-tools/src/main/resources/reference.conf index 7e00206259..51c032e3f5 100644 --- a/akka-cluster-tools/src/main/resources/reference.conf +++ b/akka-cluster-tools/src/main/resources/reference.conf @@ -197,8 +197,10 @@ akka.cluster.singleton-proxy { # The actor name of the singleton actor that is started by the ClusterSingletonManager singleton-name = ${akka.cluster.singleton.singleton-name} - # The role of the cluster nodes where the singleton can be deployed. - # If the role is not specified then any node will do. + # The role of the cluster nodes where the singleton can be deployed. + # Corresponding to the role used by the `ClusterSingletonManager`. If the role is not + # specified it's a singleton among all nodes in the cluster, and the `ClusterSingletonManager` + # must then also be configured in same way. role = "" # Interval at which the proxy will try to resolve the singleton instance. diff --git a/akka-cluster-tools/src/main/scala/akka/cluster/singleton/ClusterSingletonManager.scala b/akka-cluster-tools/src/main/scala/akka/cluster/singleton/ClusterSingletonManager.scala index 9a25bfa461..70dc6f22d1 100644 --- a/akka-cluster-tools/src/main/scala/akka/cluster/singleton/ClusterSingletonManager.scala +++ b/akka-cluster-tools/src/main/scala/akka/cluster/singleton/ClusterSingletonManager.scala @@ -626,6 +626,10 @@ class ClusterSingletonManager(singletonProps: Props, terminationMessage: Any, se goto(BecomingOldest).using(BecomingOldestData(oldest.filterNot(_ == cluster.selfUniqueAddress))) else goto(Younger).using(YoungerData(oldest.filterNot(_ == cluster.selfUniqueAddress))) + + case Event(HandOverToMe, _) => + // nothing to hand over in start + stay() } when(Younger) { diff --git a/akka-cluster-tools/src/main/scala/akka/cluster/singleton/ClusterSingletonProxy.scala b/akka-cluster-tools/src/main/scala/akka/cluster/singleton/ClusterSingletonProxy.scala index 6fac0342c8..4d7718528c 100644 --- a/akka-cluster-tools/src/main/scala/akka/cluster/singleton/ClusterSingletonProxy.scala +++ b/akka-cluster-tools/src/main/scala/akka/cluster/singleton/ClusterSingletonProxy.scala @@ -66,8 +66,11 @@ object ClusterSingletonProxySettings { /** * @param singletonName The actor name of the singleton actor that is started by the [[ClusterSingletonManager]]. - * @param role The role of the cluster nodes where the singleton can be deployed. If None, then any node will do. - * @param dataCenter The data center of the cluster nodes where the singleton is running. If None then the same data center as current node. + * @param role The role of the cluster nodes where the singleton can be deployed. Corresponding to the `role` + * used by the `ClusterSingletonManager`. If the role is not specified it's a singleton among all + * nodes in the cluster, and the `ClusterSingletonManager` must then also be configured in + * same way. + * @param dataCenter The data center of the cluster nodes where the singleton is running. If None then the same data center as current node. * @param singletonIdentificationInterval Interval at which the proxy will try to resolve the singleton instance. * @param bufferSize If the location of the singleton is unknown the proxy will buffer this number of messages * and deliver them when the singleton is identified. When the buffer is full old messages will be dropped diff --git a/akka-cluster-tools/src/multi-jvm/scala/akka/cluster/singleton/ClusterSingletonManagerLeaseSpec.scala b/akka-cluster-tools/src/multi-jvm/scala/akka/cluster/singleton/ClusterSingletonManagerLeaseSpec.scala index c551b362ce..4ee6fbb708 100644 --- a/akka-cluster-tools/src/multi-jvm/scala/akka/cluster/singleton/ClusterSingletonManagerLeaseSpec.scala +++ b/akka-cluster-tools/src/multi-jvm/scala/akka/cluster/singleton/ClusterSingletonManagerLeaseSpec.scala @@ -11,8 +11,10 @@ import com.typesafe.config.ConfigFactory import akka.actor.{ Actor, ActorIdentity, ActorLogging, ActorRef, Address, Identify, PoisonPill, Props } import akka.cluster._ import akka.cluster.MemberStatus.Up -import akka.cluster.TestLeaseActor._ import akka.cluster.singleton.ClusterSingletonManagerLeaseSpec.ImportantSingleton.Response +import akka.coordination.lease.TestLeaseActor +import akka.coordination.lease.TestLeaseActorClient +import akka.coordination.lease.TestLeaseActorClientExt import akka.remote.testkit.{ MultiNodeConfig, MultiNodeSpec, STMultiNodeSpec } import akka.testkit._ @@ -25,14 +27,14 @@ object ClusterSingletonManagerLeaseSpec extends MultiNodeConfig { testTransport(true) - commonConfig(ConfigFactory.parseString(""" + commonConfig(ConfigFactory.parseString(s""" akka.loglevel = INFO akka.actor.provider = "cluster" akka.remote.log-remote-lifecycle-events = off akka.cluster.downing-provider-class = akka.cluster.testkit.AutoDowning akka.cluster.testkit.auto-down-unreachable-after = 0s test-lease { - lease-class = akka.cluster.TestLeaseActorClient + lease-class = ${classOf[TestLeaseActorClient].getName} heartbeat-interval = 1s heartbeat-timeout = 120s lease-operation-timeout = 3s @@ -79,6 +81,7 @@ class ClusterSingletonManagerLeaseSpec import ClusterSingletonManagerLeaseSpec._ import ClusterSingletonManagerLeaseSpec.ImportantSingleton._ + import TestLeaseActor._ override def initialParticipants = roles.size @@ -128,10 +131,11 @@ class ClusterSingletonManagerLeaseSpec } "Start singleton and ping from all nodes" in { - runOn(first, second, third, fourth) { + // fourth doesn't have the worker role + runOn(first, second, third) { system.actorOf( ClusterSingletonManager - .props(props(), PoisonPill, ClusterSingletonManagerSettings(system).withRole("worker")), + .props(ImportantSingleton.props(), PoisonPill, ClusterSingletonManagerSettings(system).withRole("worker")), "important") } enterBarrier("singleton-started") diff --git a/akka-cluster-tools/src/test/scala/akka/cluster/singleton/ClusterSingletonLeaseSpec.scala b/akka-cluster-tools/src/test/scala/akka/cluster/singleton/ClusterSingletonLeaseSpec.scala index 210d980e70..63bc4e4eab 100644 --- a/akka-cluster-tools/src/test/scala/akka/cluster/singleton/ClusterSingletonLeaseSpec.scala +++ b/akka-cluster-tools/src/test/scala/akka/cluster/singleton/ClusterSingletonLeaseSpec.scala @@ -20,10 +20,8 @@ import akka.actor.PoisonPill import akka.actor.Props import akka.cluster.Cluster import akka.cluster.MemberStatus -import akka.cluster.TestLease -import akka.cluster.TestLease.AcquireReq -import akka.cluster.TestLease.ReleaseReq -import akka.cluster.TestLeaseExt +import akka.coordination.lease.TestLease +import akka.coordination.lease.TestLeaseExt import akka.testkit.AkkaSpec import akka.testkit.TestException import akka.testkit.TestProbe @@ -55,6 +53,7 @@ class ClusterSingletonLeaseSpec extends AkkaSpec(ConfigFactory.parseString(""" lease-retry-interval = 2000ms } """).withFallback(TestLease.config)) { + import TestLease.{ AcquireReq, ReleaseReq } val cluster = Cluster(system) val testLeaseExt = TestLeaseExt(system) diff --git a/akka-cluster-typed/src/test/scala/docs/akka/cluster/typed/BasicClusterExampleSpec.scala b/akka-cluster-typed/src/test/scala/docs/akka/cluster/typed/BasicClusterExampleSpec.scala index 09db42afec..594451cbbf 100644 --- a/akka-cluster-typed/src/test/scala/docs/akka/cluster/typed/BasicClusterExampleSpec.scala +++ b/akka-cluster-typed/src/test/scala/docs/akka/cluster/typed/BasicClusterExampleSpec.scala @@ -41,6 +41,8 @@ akka { seed-nodes = [ "akka://ClusterSystem@127.0.0.1:2551", "akka://ClusterSystem@127.0.0.1:2552"] + + downing-provider-class = "akka.cluster.sbr.SplitBrainResolverProvider" } } #config-seeds diff --git a/akka-cluster/src/main/java/akka/cluster/protobuf/msg/ClusterMessages.java b/akka-cluster/src/main/java/akka/cluster/protobuf/msg/ClusterMessages.java index 6af29aef04..f3a07fa363 100644 --- a/akka-cluster/src/main/java/akka/cluster/protobuf/msg/ClusterMessages.java +++ b/akka-cluster/src/main/java/akka/cluster/protobuf/msg/ClusterMessages.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2019-2020 Lightbend Inc. + * Copyright (C) 2020 Lightbend Inc. */ // Generated by the protocol buffer compiler. DO NOT EDIT! diff --git a/akka-cluster/src/main/resources/reference.conf b/akka-cluster/src/main/resources/reference.conf index 4317057bbc..53b29c2210 100644 --- a/akka-cluster/src/main/resources/reference.conf +++ b/akka-cluster/src/main/resources/reference.conf @@ -42,6 +42,10 @@ akka { # This is useful if you implement downing strategies that handle network partitions, # e.g. by keeping the larger side of the partition and shutting down the smaller side. # Disable with "off" or specify a duration to enable. + # + # When using the `akka.cluster.sbr.SplitBrainResolver` as downing provider it will use + # the akka.cluster.split-brain-resolver.stable-after as the default down-removal-margin + # if this down-removal-margin is undefined. down-removal-margin = off # Pluggable support for downing of nodes in the cluster. @@ -364,3 +368,113 @@ akka { } } + +#//#split-brain-resolver + +# To enable the split brain resolver you first need to enable the provider in your application.conf: +# akka.cluster.downing-provider-class = "akka.cluster.sbr.SplitBrainResolverProvider" + +akka.cluster.split-brain-resolver { + # Select one of the available strategies (see descriptions below): + # static-quorum, keep-majority, keep-oldest, down-all, lease-majority + active-strategy = keep-majority + + #//#stable-after + # Time margin after which shards or singletons that belonged to a downed/removed + # partition are created in surviving partition. The purpose of this margin is that + # in case of a network partition the persistent actors in the non-surviving partitions + # must be stopped before corresponding persistent actors are started somewhere else. + # This is useful if you implement downing strategies that handle network partitions, + # e.g. by keeping the larger side of the partition and shutting down the smaller side. + # Decision is taken by the strategy when there has been no membership or + # reachability changes for this duration, i.e. the cluster state is stable. + stable-after = 20s + #//#stable-after + + # When reachability observations by the failure detector are changed the SBR decisions + # are deferred until there are no changes within the 'stable-after' duration. + # If this continues for too long it might be an indication of an unstable system/network + # and it could result in delayed or conflicting decisions on separate sides of a network + # partition. + # As a precaution for that scenario all nodes are downed if no decision is made within + # `stable-after + down-all-when-unstable` from the first unreachability event. + # The measurement is reset if all unreachable have been healed, downed or removed, or + # if there are no changes within `stable-after * 2`. + # The value can be on, off, or a duration. + # By default it is 'on' and then it is derived to be 3/4 of stable-after. + down-all-when-unstable = on + +} +#//#split-brain-resolver + +# Down the unreachable nodes if the number of remaining nodes are greater than or equal to +# the given 'quorum-size'. Otherwise down the reachable nodes, i.e. it will shut down that +# side of the partition. In other words, the 'size' defines the minimum number of nodes +# that the cluster must have to be operational. If there are unreachable nodes when starting +# up the cluster, before reaching this limit, the cluster may shutdown itself immediately. +# This is not an issue if you start all nodes at approximately the same time. +# +# Note that you must not add more members to the cluster than 'quorum-size * 2 - 1', because +# then both sides may down each other and thereby form two separate clusters. For example, +# quorum-size configured to 3 in a 6 node cluster may result in a split where each side +# consists of 3 nodes each, i.e. each side thinks it has enough nodes to continue by +# itself. A warning is logged if this recommendation is violated. +#//#static-quorum +akka.cluster.split-brain-resolver.static-quorum { + # minimum number of nodes that the cluster must have + quorum-size = undefined + + # if the 'role' is defined the decision is based only on members with that 'role' + role = "" +} +#//#static-quorum + +# Down the unreachable nodes if the current node is in the majority part based the last known +# membership information. Otherwise down the reachable nodes, i.e. the own part. If the +# the parts are of equal size the part containing the node with the lowest address is kept. +# Note that if there are more than two partitions and none is in majority each part +# will shutdown itself, terminating the whole cluster. +#//#keep-majority +akka.cluster.split-brain-resolver.keep-majority { + # if the 'role' is defined the decision is based only on members with that 'role' + role = "" +} +#//#keep-majority + +# Down the part that does not contain the oldest member (current singleton). +# +# There is one exception to this rule if 'down-if-alone' is defined to 'on'. +# Then, if the oldest node has partitioned from all other nodes the oldest +# will down itself and keep all other nodes running. The strategy will not +# down the single oldest node when it is the only remaining node in the cluster. +# +# Note that if the oldest node crashes the others will remove it from the cluster +# when 'down-if-alone' is 'on', otherwise they will down themselves if the +# oldest node crashes, i.e. shutdown the whole cluster together with the oldest node. +#//#keep-oldest +akka.cluster.split-brain-resolver.keep-oldest { + # Enable downing of the oldest node when it is partitioned from all other nodes + down-if-alone = on + + # if the 'role' is defined the decision is based only on members with that 'role', + # i.e. using the oldest member (singleton) within the nodes with that role + role = "" +} +#//#keep-oldest + +# Keep the part that can acquire the lease, and down the other part. +# Best effort is to keep the side that has most nodes, i.e. the majority side. +# This is achieved by adding a delay before trying to acquire the lease on the +# minority side. +#//#lease-majority +akka.cluster.split-brain-resolver.lease-majority { + lease-implementation = "" + + # This delay is used on the minority side before trying to acquire the lease, + # as an best effort to try to keep the majority side. + acquire-lease-delay-for-minority = 2s + + # If the 'role' is defined the majority/minority is based only on members with that 'role'. + role = "" +} +#//#lease-majority diff --git a/akka-cluster/src/main/scala/akka/cluster/ClusterJmx.scala b/akka-cluster/src/main/scala/akka/cluster/ClusterJmx.scala index 54900b06cd..1d3ad80003 100644 --- a/akka-cluster/src/main/scala/akka/cluster/ClusterJmx.scala +++ b/akka-cluster/src/main/scala/akka/cluster/ClusterJmx.scala @@ -5,7 +5,6 @@ package akka.cluster import java.lang.management.ManagementFactory - import javax.management.InstanceAlreadyExistsException import javax.management.InstanceNotFoundException import javax.management.ObjectName diff --git a/akka-cluster/src/main/scala/akka/cluster/ClusterLogMarker.scala b/akka-cluster/src/main/scala/akka/cluster/ClusterLogMarker.scala index c904589c3d..1a0c3ad784 100644 --- a/akka-cluster/src/main/scala/akka/cluster/ClusterLogMarker.scala +++ b/akka-cluster/src/main/scala/akka/cluster/ClusterLogMarker.scala @@ -7,6 +7,7 @@ package akka.cluster import akka.actor.Address import akka.annotation.ApiMayChange import akka.annotation.InternalApi +import akka.cluster.sbr.DowningStrategy import akka.event.LogMarker /** @@ -22,6 +23,7 @@ object ClusterLogMarker { */ @InternalApi private[akka] object Properties { val MemberStatus = "akkaMemberStatus" + val SbrDecision = "akkaSbrDecision" } /** @@ -91,4 +93,53 @@ object ClusterLogMarker { val singletonTerminated: LogMarker = LogMarker("akkaClusterSingletonTerminated") + /** + * Marker "akkaSbrDowning" of log event when Split Brain Resolver has made a downing decision. Followed + * by [[ClusterLogMarker.sbrDowningNode]] for each node that is downed. + * @param decision The downing decision. Included as property "akkaSbrDecision". + */ + def sbrDowning(decision: DowningStrategy.Decision): LogMarker = + LogMarker("akkaSbrDowning", Map(Properties.SbrDecision -> decision)) + + /** + * Marker "akkaSbrDowningNode" of log event when a member is downed by Split Brain Resolver. + * @param node The address of the node that is downed. Included as property "akkaRemoteAddress" + * and "akkaRemoteAddressUid". + * @param decision The downing decision. Included as property "akkaSbrDecision". + */ + def sbrDowningNode(node: UniqueAddress, decision: DowningStrategy.Decision): LogMarker = + LogMarker( + "akkaSbrDowningNode", + Map( + LogMarker.Properties.RemoteAddress -> node.address, + LogMarker.Properties.RemoteAddressUid -> node.longUid, + Properties.SbrDecision -> decision)) + + /** + * Marker "akkaSbrInstability" of log event when Split Brain Resolver has detected too much instability + * and will down all nodes. + */ + val sbrInstability: LogMarker = + LogMarker("akkaSbrInstability") + + /** + * Marker "akkaSbrLeaseAcquired" of log event when Split Brain Resolver has acquired the lease. + * @param decision The downing decision. Included as property "akkaSbrDecision". + */ + def sbrLeaseAcquired(decision: DowningStrategy.Decision): LogMarker = + LogMarker("akkaSbrLeaseAcquired", Map(Properties.SbrDecision -> decision)) + + /** + * Marker "akkaSbrLeaseDenied" of log event when Split Brain Resolver has acquired the lease. + * @param reverseDecision The (reverse) downing decision. Included as property "akkaSbrDecision". + */ + def sbrLeaseDenied(reverseDecision: DowningStrategy.Decision): LogMarker = + LogMarker("akkaSbrLeaseDenied", Map(Properties.SbrDecision -> reverseDecision)) + + /** + * Marker "akkaSbrLeaseReleased" of log event when Split Brain Resolver has released the lease. + */ + val sbrLeaseReleased: LogMarker = + LogMarker("akkaSbrLeaseReleased") + } diff --git a/akka-cluster/src/main/scala/akka/cluster/ClusterRemoteWatcher.scala b/akka-cluster/src/main/scala/akka/cluster/ClusterRemoteWatcher.scala index 2958e8e172..fc7a7770ca 100644 --- a/akka-cluster/src/main/scala/akka/cluster/ClusterRemoteWatcher.scala +++ b/akka-cluster/src/main/scala/akka/cluster/ClusterRemoteWatcher.scala @@ -70,6 +70,9 @@ private[cluster] class ClusterRemoteWatcher( override val log = Logging(context.system, ActorWithLogClass(this, ClusterLogClass.ClusterCore)) + // allowed to watch even though address not in cluster membership, i.e. remote watch + private val watchPathWhitelist = Set("/system/sharding/") + private var pendingDelayedQuarantine: Set[UniqueAddress] = Set.empty var clusterNodes: Set[Address] = Set.empty @@ -164,7 +167,19 @@ private[cluster] class ClusterRemoteWatcher( if (!clusterNodes(watchee.path.address)) super.watchNode(watchee) override protected def shouldWatch(watchee: InternalActorRef): Boolean = - clusterNodes(watchee.path.address) || super.shouldWatch(watchee) + clusterNodes(watchee.path.address) || super.shouldWatch(watchee) || isWatchOutsideClusterAllowed(watchee) + + /** + * Allowed to watch some paths even though address not in cluster membership, i.e. remote watch. + * Needed for ShardCoordinator that has to watch old incarnations of region ActorRef from the + * recovered state. + */ + private def isWatchOutsideClusterAllowed(watchee: InternalActorRef): Boolean = { + context.system.name == watchee.path.address.system && { + val pathPrefix = watchee.path.elements.take(2).mkString("/", "/", "/") + watchPathWhitelist.contains(pathPrefix) + } + } /** * When a cluster node is added this class takes over the diff --git a/akka-cluster/src/main/scala/akka/cluster/DowningProvider.scala b/akka-cluster/src/main/scala/akka/cluster/DowningProvider.scala index e64c1d581e..f19ad8e3e5 100644 --- a/akka-cluster/src/main/scala/akka/cluster/DowningProvider.scala +++ b/akka-cluster/src/main/scala/akka/cluster/DowningProvider.scala @@ -40,9 +40,7 @@ private[cluster] object DowningProvider { * When implementing a downing provider you should make sure that it will not split the cluster into * several separate clusters in case of network problems or system overload (long GC pauses). This * is much more difficult than it might be perceived at first, so carefully read the concerns and scenarios - * described in - * https://doc.akka.io/docs/akka/current/typed/cluster.html#downing and - * https://doc.akka.io/docs/akka-enhancements/current/split-brain-resolver.html + * described in https://doc.akka.io/docs/akka/current/split-brain-resolver.html */ abstract class DowningProvider { diff --git a/akka-cluster/src/main/scala/akka/cluster/sbr/DowningStrategy.scala b/akka-cluster/src/main/scala/akka/cluster/sbr/DowningStrategy.scala new file mode 100644 index 0000000000..98ff809885 --- /dev/null +++ b/akka-cluster/src/main/scala/akka/cluster/sbr/DowningStrategy.scala @@ -0,0 +1,625 @@ +/* + * Copyright (C) 2009-2020 Lightbend Inc. + */ + +package akka.cluster.sbr + +import scala.collection.immutable +import scala.concurrent.duration.Duration +import scala.concurrent.duration.FiniteDuration + +import akka.actor.Address +import akka.annotation.InternalApi +import akka.cluster.ClusterSettings.DataCenter +import akka.cluster.Member +import akka.cluster.MemberStatus +import akka.cluster.Reachability +import akka.cluster.UniqueAddress +import akka.coordination.lease.scaladsl.Lease + +/** + * INTERNAL API + */ +@InternalApi private[akka] object DowningStrategy { + sealed trait Decision { + def isIndirectlyConnected: Boolean + } + case object DownReachable extends Decision { + override def isIndirectlyConnected = false + } + case object DownUnreachable extends Decision { + override def isIndirectlyConnected = false + } + case object DownAll extends Decision { + override def isIndirectlyConnected = false + } + case object DownIndirectlyConnected extends Decision { + override def isIndirectlyConnected = true + } + sealed trait AcquireLeaseDecision extends Decision { + def acquireDelay: FiniteDuration + } + final case class AcquireLeaseAndDownUnreachable(acquireDelay: FiniteDuration) extends AcquireLeaseDecision { + override def isIndirectlyConnected = false + } + final case class AcquireLeaseAndDownIndirectlyConnected(acquireDelay: FiniteDuration) extends AcquireLeaseDecision { + override def isIndirectlyConnected = true + } + case object ReverseDownIndirectlyConnected extends Decision { + override def isIndirectlyConnected = true + } +} + +/** + * INTERNAL API + */ +@InternalApi private[akka] abstract class DowningStrategy(val selfDc: DataCenter) { + import DowningStrategy._ + + // may contain Joining and WeaklyUp + private var _unreachable: Set[UniqueAddress] = Set.empty[UniqueAddress] + + def unreachable: Set[UniqueAddress] = _unreachable + + def unreachable(m: Member): Boolean = _unreachable(m.uniqueAddress) + + private var _reachability: Reachability = Reachability.empty + + private var _seenBy: Set[Address] = Set.empty + + protected def ordering: Ordering[Member] = Member.ordering + + // all members in self DC, both joining and up. + private var _allMembers: immutable.SortedSet[Member] = immutable.SortedSet.empty(ordering) + + def role: Option[String] + + // all Joining and WeaklyUp members in self DC + def joining: immutable.SortedSet[Member] = + _allMembers.filter(m => m.status == MemberStatus.Joining || m.status == MemberStatus.WeaklyUp) + + // all members in self DC, both joining and up. + def allMembersInDC: immutable.SortedSet[Member] = _allMembers + + /** + * All members in self DC, but doesn't contain Joining, WeaklyUp, Down and Exiting. + */ + def members: immutable.SortedSet[Member] = + members(includingPossiblyUp = false, excludingPossiblyExiting = false) + + /** + * All members in self DC, but doesn't contain Joining, WeaklyUp, Down and Exiting. + * + * When `includingPossiblyUp=true` it also includes Joining and WeaklyUp members that could have been + * changed to Up on the other side of a partition. + * + * When `excludingPossiblyExiting=true` it doesn't include Leaving members that could have been + * changed to Exiting on the other side of the partition. + */ + def members(includingPossiblyUp: Boolean, excludingPossiblyExiting: Boolean): immutable.SortedSet[Member] = + _allMembers.filterNot( + m => + (!includingPossiblyUp && m.status == MemberStatus.Joining) || + (!includingPossiblyUp && m.status == MemberStatus.WeaklyUp) || + (excludingPossiblyExiting && m.status == MemberStatus.Leaving) || + m.status == MemberStatus.Down || + m.status == MemberStatus.Exiting) + + def membersWithRole: immutable.SortedSet[Member] = + membersWithRole(includingPossiblyUp = false, excludingPossiblyExiting = false) + + def membersWithRole(includingPossiblyUp: Boolean, excludingPossiblyExiting: Boolean): immutable.SortedSet[Member] = + role match { + case None => members(includingPossiblyUp, excludingPossiblyExiting) + case Some(r) => members(includingPossiblyUp, excludingPossiblyExiting).filter(_.hasRole(r)) + } + + def reachableMembers: immutable.SortedSet[Member] = + reachableMembers(includingPossiblyUp = false, excludingPossiblyExiting = false) + + def reachableMembers(includingPossiblyUp: Boolean, excludingPossiblyExiting: Boolean): immutable.SortedSet[Member] = { + val mbrs = members(includingPossiblyUp, excludingPossiblyExiting) + if (unreachable.isEmpty) mbrs + else mbrs.filter(m => !unreachable(m)) + } + + def reachableMembersWithRole: immutable.SortedSet[Member] = + reachableMembersWithRole(includingPossiblyUp = false, excludingPossiblyExiting = false) + + def reachableMembersWithRole( + includingPossiblyUp: Boolean, + excludingPossiblyExiting: Boolean): immutable.SortedSet[Member] = + role match { + case None => reachableMembers(includingPossiblyUp, excludingPossiblyExiting) + case Some(r) => reachableMembers(includingPossiblyUp, excludingPossiblyExiting).filter(_.hasRole(r)) + } + + def unreachableMembers: immutable.SortedSet[Member] = + unreachableMembers(includingPossiblyUp = false, excludingPossiblyExiting = false) + + def unreachableMembers( + includingPossiblyUp: Boolean, + excludingPossiblyExiting: Boolean): immutable.SortedSet[Member] = { + if (unreachable.isEmpty) immutable.SortedSet.empty + else members(includingPossiblyUp, excludingPossiblyExiting).filter(unreachable) + } + + def unreachableMembersWithRole: immutable.SortedSet[Member] = + unreachableMembersWithRole(includingPossiblyUp = false, excludingPossiblyExiting = false) + + def unreachableMembersWithRole( + includingPossiblyUp: Boolean, + excludingPossiblyExiting: Boolean): immutable.SortedSet[Member] = + role match { + case None => unreachableMembers(includingPossiblyUp, excludingPossiblyExiting) + case Some(r) => unreachableMembers(includingPossiblyUp, excludingPossiblyExiting).filter(_.hasRole(r)) + } + + def addUnreachable(m: Member): Unit = { + require(m.dataCenter == selfDc) + + add(m) + _unreachable = _unreachable + m.uniqueAddress + } + + def addReachable(m: Member): Unit = { + require(m.dataCenter == selfDc) + + add(m) + _unreachable = _unreachable - m.uniqueAddress + } + + def add(m: Member): Unit = { + require(m.dataCenter == selfDc) + + removeFromAllMembers(m) + _allMembers += m + } + + def remove(m: Member): Unit = { + require(m.dataCenter == selfDc) + + removeFromAllMembers(m) + _unreachable -= m.uniqueAddress + } + + private def removeFromAllMembers(m: Member): Unit = { + if (ordering eq Member.ordering) { + _allMembers -= m + } else { + // must use filterNot for removals/replace in the SortedSet when + // ageOrdering is using upNumber and that will change when Joining -> Up + _allMembers = _allMembers.filterNot(_.uniqueAddress == m.uniqueAddress) + } + } + + def reachability: Reachability = + _reachability + + private def isInSelfDc(node: UniqueAddress): Boolean = { + _allMembers.exists(m => m.uniqueAddress == node && m.dataCenter == selfDc) + } + + /** + * @return true if it was changed + */ + private[sbr] def setReachability(r: Reachability): Boolean = { + // skip records with Reachability.Reachable, and skip records related to other DC + val newReachability = r.filterRecords( + record => + (record.status == Reachability.Unreachable || record.status == Reachability.Terminated) && + isInSelfDc(record.observer) && isInSelfDc(record.subject)) + val oldReachability = _reachability + + val changed = + if (oldReachability.records.size != newReachability.records.size) + true + else + oldReachability.records.map(r => r.observer -> r.subject).toSet != + newReachability.records.map(r => r.observer -> r.subject).toSet + + _reachability = newReachability + changed + } + + def seenBy: Set[Address] = + _seenBy + + def setSeenBy(s: Set[Address]): Unit = + _seenBy = s + + /** + * Nodes that are marked as unreachable but can communicate with gossip via a 3rd party. + * + * Cycle in unreachability graph corresponds to that some node is both + * observing another node as unreachable, and is also observed as unreachable by someone + * else. + * + * Another indication of indirectly connected nodes is if a node is marked as unreachable, + * but it has still marked current gossip state as seen. + * + * Those cases will not happen for clean splits and crashed nodes. + */ + def indirectlyConnected: Set[UniqueAddress] = { + indirectlyConnectedFromIntersectionOfObserversAndSubjects.union(indirectlyConnectedFromSeenCurrentGossip) + } + + private def indirectlyConnectedFromIntersectionOfObserversAndSubjects: Set[UniqueAddress] = { + // cycle in unreachability graph + val observers = reachability.allObservers + observers.intersect(reachability.allUnreachableOrTerminated) + } + + private def indirectlyConnectedFromSeenCurrentGossip: Set[UniqueAddress] = { + reachability.records.flatMap { r => + if (seenBy(r.subject.address)) r.observer :: r.subject :: Nil + else Nil + }.toSet + } + + def hasIndirectlyConnected: Boolean = indirectlyConnected.nonEmpty + + def unreachableButNotIndirectlyConnected: Set[UniqueAddress] = unreachable.diff(indirectlyConnected) + + def nodesToDown(decision: Decision = decide()): Set[UniqueAddress] = { + val downable = members + .union(joining) + .filterNot(m => m.status == MemberStatus.Down || m.status == MemberStatus.Exiting) + .map(_.uniqueAddress) + decision match { + case DownUnreachable | AcquireLeaseAndDownUnreachable(_) => downable.intersect(unreachable) + case DownReachable => downable.diff(unreachable) + case DownAll => downable + case DownIndirectlyConnected | AcquireLeaseAndDownIndirectlyConnected(_) => + // Down nodes that have been marked as unreachable via some network links but they are still indirectly + // connected via other links. It will keep other "normal" nodes. + // If there is a combination of indirectly connected nodes and a clean network partition (or node crashes) + // it will combine the above decision with the ordinary decision, e.g. keep majority, after excluding + // failure detection observations between the indirectly connected nodes. + // Also include nodes that corresponds to the decision without the unreachability observations from + // the indirectly connected nodes + downable.intersect(indirectlyConnected.union(additionalNodesToDownWhenIndirectlyConnected)) + case ReverseDownIndirectlyConnected => + // indirectly connected + all reachable + downable.intersect(indirectlyConnected).union(downable.diff(unreachable)) + } + } + + private def additionalNodesToDownWhenIndirectlyConnected: Set[UniqueAddress] = { + if (unreachableButNotIndirectlyConnected.isEmpty) + Set.empty + else { + val originalUnreachable = _unreachable + val originalReachability = _reachability + try { + val intersectionOfObserversAndSubjects = indirectlyConnectedFromIntersectionOfObserversAndSubjects + val haveSeenCurrentGossip = indirectlyConnectedFromSeenCurrentGossip + // remove records between the indirectly connected + _reachability = reachability.filterRecords( + r => + !((intersectionOfObserversAndSubjects(r.observer) && intersectionOfObserversAndSubjects(r.subject)) || + (haveSeenCurrentGossip(r.observer) && haveSeenCurrentGossip(r.subject)))) + _unreachable = reachability.allUnreachableOrTerminated + val additionalDecision = decide() + + if (additionalDecision.isIndirectlyConnected) + throw new IllegalStateException( + s"SBR double $additionalDecision decision, downing all instead. " + + s"originalReachability: [$originalReachability], filtered reachability [$reachability], " + + s"still indirectlyConnected: [$indirectlyConnected], seenBy: [$seenBy]") + + nodesToDown(additionalDecision) + } finally { + _unreachable = originalUnreachable + _reachability = originalReachability + } + } + } + + def isAllUnreachableDownOrExiting: Boolean = { + _unreachable.isEmpty || + unreachableMembers.forall(m => m.status == MemberStatus.Down || m.status == MemberStatus.Exiting) + } + + def reverseDecision(decision: Decision): Decision = { + decision match { + case DownUnreachable => DownReachable + case AcquireLeaseAndDownUnreachable(_) => DownReachable + case DownReachable => DownUnreachable + case DownAll => DownAll + case DownIndirectlyConnected => ReverseDownIndirectlyConnected + case AcquireLeaseAndDownIndirectlyConnected(_) => ReverseDownIndirectlyConnected + case ReverseDownIndirectlyConnected => DownIndirectlyConnected + } + } + + def decide(): Decision + + def lease: Option[Lease] = None + +} + +/** + * INTERNAL API + * + * Down the unreachable nodes if the number of remaining nodes are greater than or equal to the + * given `quorumSize`. Otherwise down the reachable nodes, i.e. it will shut down that side of the partition. + * In other words, the `quorumSize` defines the minimum number of nodes that the cluster must have to be operational. + * If there are unreachable nodes when starting up the cluster, before reaching this limit, + * the cluster may shutdown itself immediately. This is not an issue if you start all nodes at + * approximately the same time. + * + * Note that you must not add more members to the cluster than `quorumSize * 2 - 1`, because then + * both sides may down each other and thereby form two separate clusters. For example, + * quorum quorumSize configured to 3 in a 6 node cluster may result in a split where each side + * consists of 3 nodes each, i.e. each side thinks it has enough nodes to continue by + * itself. A warning is logged if this recommendation is violated. + * + * If the `role` is defined the decision is based only on members with that `role`. + * + * It is only counting members within the own data center. + */ +@InternalApi private[sbr] final class StaticQuorum( + selfDc: DataCenter, + val quorumSize: Int, + override val role: Option[String]) + extends DowningStrategy(selfDc) { + import DowningStrategy._ + + override def decide(): Decision = { + if (isTooManyMembers) + DownAll + else if (hasIndirectlyConnected) + DownIndirectlyConnected + else if (membersWithRole.size - unreachableMembersWithRole.size >= quorumSize) + DownUnreachable + else + DownReachable + } + + def isTooManyMembers: Boolean = + membersWithRole.size > (quorumSize * 2 - 1) +} + +/** + * INTERNAL API + * + * Down the unreachable nodes if the current node is in the majority part based the last known + * membership information. Otherwise down the reachable nodes, i.e. the own part. If the the + * parts are of equal size the part containing the node with the lowest address is kept. + * + * If the `role` is defined the decision is based only on members with that `role`. + * + * Note that if there are more than two partitions and none is in majority each part + * will shutdown itself, terminating the whole cluster. + * + * It is only counting members within the own data center. + */ +@InternalApi private[sbr] final class KeepMajority(selfDc: DataCenter, override val role: Option[String]) + extends DowningStrategy(selfDc) { + import DowningStrategy._ + + override def decide(): Decision = { + if (hasIndirectlyConnected) + DownIndirectlyConnected + else { + val ms = membersWithRole + if (ms.isEmpty) + DownAll // no node with matching role + else { + val reachableSize = reachableMembersWithRole.size + val unreachableSize = unreachableMembersWithRole.size + + majorityDecision(reachableSize, unreachableSize, ms.head) match { + case DownUnreachable => + majorityDecisionWhenIncludingMembershipChangesEdgeCase() match { + case DownUnreachable => DownUnreachable // same conclusion + case _ => DownAll // different conclusion, safest to DownAll + } + case decision => decision + } + + } + } + } + + private def majorityDecision(thisSide: Int, otherSide: Int, lowest: Member): Decision = { + if (thisSide == otherSide) { + // equal size, keep the side with the lowest address (first in members) + if (unreachable(lowest)) DownReachable else DownUnreachable + } else if (thisSide > otherSide) { + // we are in majority + DownUnreachable + } else { + // we are in minority + DownReachable + } + } + + /** + * Check for edge case when membership change happens at the same time as partition. + * Count Joining and WeaklyUp on other side since those might be Up on other side. + * Don't count Leaving on this side since those might be Exiting on other side. + * Note that the membership changes we are looking for will only be done when all + * members have seen previous state, i.e. when a member is moved to Up everybody + * has seen it joining. + */ + private def majorityDecisionWhenIncludingMembershipChangesEdgeCase(): Decision = { + // for this side we count as few as could be possible (excluding joining, excluding leaving) + val ms = membersWithRole(includingPossiblyUp = false, excludingPossiblyExiting = true) + if (ms.isEmpty) { + DownAll + } else { + val thisSideReachableSize = + reachableMembersWithRole(includingPossiblyUp = false, excludingPossiblyExiting = true).size + // for other side we count as many as could be possible (including joining, including leaving) + val otherSideUnreachableSize = + unreachableMembersWithRole(includingPossiblyUp = true, excludingPossiblyExiting = false).size + majorityDecision(thisSideReachableSize, otherSideUnreachableSize, ms.head) + } + } + +} + +/** + * INTERNAL API + * + * Down the part that does not contain the oldest member (current singleton). + * + * There is one exception to this rule if `downIfAlone` is defined to `true`. + * Then, if the oldest node has partitioned from all other nodes the oldest will + * down itself and keep all other nodes running. The strategy will not down the + * single oldest node when it is the only remaining node in the cluster. + * + * Note that if the oldest node crashes the others will remove it from the cluster + * when `downIfAlone` is `true`, otherwise they will down themselves if the + * oldest node crashes, i.e. shutdown the whole cluster together with the oldest node. + * + * If the `role` is defined the decision is based only on members with that `role`, + * i.e. using the oldest member (singleton) within the nodes with that role. + * + * It is only using members within the own data center, i.e. oldest within the + * data center. + */ +@InternalApi private[sbr] final class KeepOldest( + selfDc: DataCenter, + val downIfAlone: Boolean, + override val role: Option[String]) + extends DowningStrategy(selfDc) { + import DowningStrategy._ + + // sort by age, oldest first + override def ordering: Ordering[Member] = Member.ageOrdering + + override def decide(): Decision = { + if (hasIndirectlyConnected) + DownIndirectlyConnected + else { + val ms = membersWithRole + if (ms.isEmpty) + DownAll // no node with matching role + else { + val oldest = ms.head + val oldestIsReachable = !unreachable(oldest) + val reachableCount = reachableMembersWithRole.size + val unreachableCount = unreachableMembersWithRole.size + + oldestDecision(oldestIsReachable, reachableCount, unreachableCount) match { + case DownUnreachable => + oldestDecisionWhenIncludingMembershipChangesEdgeCase() match { + case DownUnreachable => DownUnreachable // same conclusion + case _ => DownAll // different conclusion, safest to DownAll + } + case decision => decision + } + + } + } + } + + private def oldestDecision(oldestIsOnThisSide: Boolean, thisSide: Int, otherSide: Int): Decision = { + if (oldestIsOnThisSide) { + // if there are only 2 nodes in the cluster it is better to keep the oldest, even though it is alone + // E.g. 2 nodes: thisSide=1, otherSide=1 => DownUnreachable, i.e. keep the oldest + // even though it is alone (because the node on the other side is no better) + // E.g. 3 nodes: thisSide=1, otherSide=2 => DownReachable, i.e. shut down the + // oldest because it is alone + if (downIfAlone && thisSide == 1 && otherSide >= 2) DownReachable + else DownUnreachable + } else { + if (downIfAlone && otherSide == 1 && thisSide >= 2) DownUnreachable + else DownReachable + } + } + + /** + * Check for edge case when membership change happens at the same time as partition. + * Exclude Leaving on this side because those could be Exiting on other side. + * + * When `downIfAlone` also consider Joining and WeaklyUp since those might be Up on other side, + * and thereby flip the alone test. + */ + private def oldestDecisionWhenIncludingMembershipChangesEdgeCase(): Decision = { + val ms = membersWithRole(includingPossiblyUp = false, excludingPossiblyExiting = true) + if (ms.isEmpty) { + DownAll + } else { + val oldest = ms.head + val oldestIsReachable = !unreachable(oldest) + // Joining and WeaklyUp are only relevant when downIfAlone = true + val includingPossiblyUp = downIfAlone + val reachableCount = reachableMembersWithRole(includingPossiblyUp, excludingPossiblyExiting = true).size + val unreachableCount = unreachableMembersWithRole(includingPossiblyUp, excludingPossiblyExiting = true).size + + oldestDecision(oldestIsReachable, reachableCount, unreachableCount) + } + } +} + +/** + * INTERNAL API + * + * Down all nodes unconditionally. + */ +@InternalApi private[sbr] final class DownAllNodes(selfDc: DataCenter) extends DowningStrategy(selfDc) { + import DowningStrategy._ + + override def decide(): Decision = + DownAll + + override def role: Option[String] = None +} + +/** + * INTERNAL API + * + * Keep the part that can acquire the lease, and down the other part. + * + * Best effort is to keep the side that has most nodes, i.e. the majority side. + * This is achieved by adding a delay before trying to acquire the lease on the + * minority side. + * + * If the `role` is defined the majority/minority is based only on members with that `role`. + * It is only counting members within the own data center. + */ +@InternalApi private[sbr] final class LeaseMajority( + selfDc: DataCenter, + override val role: Option[String], + _lease: Lease, + acquireLeaseDelayForMinority: FiniteDuration) + extends DowningStrategy(selfDc) { + import DowningStrategy._ + + override val lease: Option[Lease] = Some(_lease) + + override def decide(): Decision = { + if (hasIndirectlyConnected) + AcquireLeaseAndDownIndirectlyConnected(Duration.Zero) + else + AcquireLeaseAndDownUnreachable(acquireLeaseDelay) + } + + private def acquireLeaseDelay: FiniteDuration = + if (isInMinority) acquireLeaseDelayForMinority else Duration.Zero + + private def isInMinority: Boolean = { + val ms = membersWithRole + if (ms.isEmpty) + false // no node with matching role + else { + val unreachableSize = unreachableMembersWithRole.size + val membersSize = ms.size + + if (unreachableSize * 2 == membersSize) { + // equal size, try to keep the side with the lowest address (first in members) + unreachable(ms.head) + } else if (unreachableSize * 2 < membersSize) { + // we are in majority + false + } else { + // we are in minority + true + } + } + } +} diff --git a/akka-cluster/src/main/scala/akka/cluster/sbr/SplitBrainResolver.scala b/akka-cluster/src/main/scala/akka/cluster/sbr/SplitBrainResolver.scala new file mode 100644 index 0000000000..f70a9530f0 --- /dev/null +++ b/akka-cluster/src/main/scala/akka/cluster/sbr/SplitBrainResolver.scala @@ -0,0 +1,595 @@ +/* + * Copyright (C) 2009-2020 Lightbend Inc. + */ + +package akka.cluster.sbr + +import java.time.Instant +import java.time.temporal.ChronoUnit + +import scala.concurrent.ExecutionContext +import scala.concurrent.duration._ + +import akka.actor.Actor +import akka.actor.Address +import akka.actor.ExtendedActorSystem +import akka.actor.Props +import akka.actor.Stash +import akka.actor.Timers +import akka.annotation.InternalApi +import akka.cluster.Cluster +import akka.cluster.ClusterEvent +import akka.cluster.ClusterEvent._ +import akka.cluster.ClusterLogMarker +import akka.cluster.ClusterSettings.DataCenter +import akka.cluster.Member +import akka.cluster.Reachability +import akka.cluster.UniqueAddress +import akka.cluster.sbr.DowningStrategy.Decision +import akka.event.DiagnosticMarkerBusLoggingAdapter +import akka.event.Logging +import akka.pattern.pipe + +/** + * INTERNAL API + */ +@InternalApi private[sbr] object SplitBrainResolver { + + def props(stableAfter: FiniteDuration, strategy: DowningStrategy): Props = + Props(new SplitBrainResolver(stableAfter, strategy)) + + case object Tick + + /** + * Response (result) of the acquire lease request. + */ + final case class AcquireLeaseResult(holdingLease: Boolean) + + /** + * Response (result) of the release lease request. + */ + final case class ReleaseLeaseResult(released: Boolean) + + /** + * For delayed acquire of the lease. + */ + case object AcquireLease + + sealed trait ReleaseLeaseCondition + object ReleaseLeaseCondition { + case object NoLease extends ReleaseLeaseCondition + final case class WhenMembersRemoved(nodes: Set[UniqueAddress]) extends ReleaseLeaseCondition + final case class WhenTimeElapsed(deadline: Deadline) extends ReleaseLeaseCondition + } + + final case class ReachabilityChangedStats( + firstChangeTimestamp: Long, + latestChangeTimestamp: Long, + changeCount: Long) { + + def isEmpty: Boolean = + changeCount == 0 + + override def toString: String = { + if (isEmpty) + "reachability unchanged" + else { + val now = System.nanoTime() + s"reachability changed $changeCount times since ${(now - firstChangeTimestamp).nanos.toMillis} ms ago, " + + s"latest change was ${(now - latestChangeTimestamp).nanos.toMillis} ms ago" + } + } + } + +} + +/** + * INTERNAL API + * + * Unreachable members will be downed by this actor according to the given strategy. + * It is active on the leader node in the cluster. + * + * The implementation is split into two classes SplitBrainResolver and SplitBrainResolverBase to be + * able to unit test the logic without running cluster. + */ +@InternalApi private[sbr] final class SplitBrainResolver(stableAfter: FiniteDuration, strategy: DowningStrategy) + extends SplitBrainResolverBase(stableAfter, strategy) { + + private val cluster = Cluster(context.system) + + log.info( + "SBR started. Config: stableAfter: {} ms, strategy: {}, selfUniqueAddress: {}, selfDc: {}", + stableAfter.toMillis, + Logging.simpleName(strategy.getClass), + selfUniqueAddress, + selfDc) + + override def selfUniqueAddress: UniqueAddress = cluster.selfUniqueAddress + override def selfDc: DataCenter = cluster.selfDataCenter + + // re-subscribe when restart + override def preStart(): Unit = { + cluster.subscribe(self, ClusterEvent.InitialStateAsEvents, classOf[ClusterDomainEvent]) + super.preStart() + } + override def postStop(): Unit = { + cluster.unsubscribe(self) + super.postStop() + } + + override def down(node: UniqueAddress, decision: Decision): Unit = { + log.info(ClusterLogMarker.sbrDowningNode(node, decision), "SBR is downing [{}]", node) + cluster.down(node.address) + } + +} + +/** + * INTERNAL API + * + * The implementation is split into two classes SplitBrainResolver and SplitBrainResolverBase to be + * able to unit test the logic without running cluster. + */ +@InternalApi private[sbr] abstract class SplitBrainResolverBase(stableAfter: FiniteDuration, strategy: DowningStrategy) + extends Actor + with Stash + with Timers { + + import DowningStrategy._ + import SplitBrainResolver.ReleaseLeaseCondition.NoLease + import SplitBrainResolver._ + + val log: DiagnosticMarkerBusLoggingAdapter = Logging.withMarker(this) + + def selfUniqueAddress: UniqueAddress + + def selfDc: DataCenter + + def down(node: UniqueAddress, decision: Decision): Unit + + // would be better as constructor parameter, but don't want to break Cinnamon instrumentation + private val settings = new SplitBrainResolverSettings(context.system.settings.config) + + def downAllWhenUnstable: FiniteDuration = + settings.DownAllWhenUnstable + + private val releaseLeaseAfter = stableAfter * 2 + + def tickInterval: FiniteDuration = 1.second + + timers.startTimerWithFixedDelay(Tick, Tick, tickInterval) + + var leader = false + var selfMemberAdded = false + + private def internalDispatcher: ExecutionContext = + context.system.asInstanceOf[ExtendedActorSystem].dispatchers.internalDispatcher + + // overridden in tests + protected def newStableDeadline(): Deadline = Deadline.now + stableAfter + var stableDeadline: Deadline = _ + def resetStableDeadline(): Unit = { + stableDeadline = newStableDeadline() + } + + resetStableDeadline() + + private var reachabilityChangedStats: ReachabilityChangedStats = + ReachabilityChangedStats(System.nanoTime(), System.nanoTime(), 0) + + private def resetReachabilityChangedStats(): Unit = { + val now = System.nanoTime() + reachabilityChangedStats = ReachabilityChangedStats(now, now, 0) + } + + private def resetReachabilityChangedStatsIfAllUnreachableDowned(): Unit = { + if (!reachabilityChangedStats.isEmpty && strategy.isAllUnreachableDownOrExiting) { + log.debug("SBR resetting reachability stats, after all unreachable healed, downed or removed") + resetReachabilityChangedStats() + } + } + + private var releaseLeaseCondition: ReleaseLeaseCondition = NoLease + + /** Helper to wrap updates to strategy info with, so that stable-after timer is reset and information is logged about state change */ + def mutateMemberInfo(resetStable: Boolean)(f: () => Unit): Unit = { + val unreachableBefore = strategy.unreachable.size + f() + val unreachableAfter = strategy.unreachable.size + + def earliestTimeOfDecision: String = + Instant.now().plus(stableAfter.toMillis, ChronoUnit.MILLIS).toString + + if (resetStable) { + if (isResponsible) { + if (unreachableBefore == 0 && unreachableAfter > 0) { + log.info( + "SBR found unreachable members, waiting for stable-after = {} ms before taking downing decision. " + + "Now {} unreachable members found. Downing decision will not be made before {}.", + stableAfter.toMillis, + unreachableAfter, + earliestTimeOfDecision) + } else if (unreachableBefore > 0 && unreachableAfter == 0) { + log.info( + "SBR found all unreachable members healed during stable-after period, no downing decision necessary for now.") + } else if (unreachableAfter > 0) { + log.info( + "SBR found unreachable members changed during stable-after period. Resetting timer. " + + "Now {} unreachable members found. Downing decision will not be made before {}.", + unreachableAfter, + earliestTimeOfDecision) + } + // else no unreachable members found but set of members changed + } + + log.debug("SBR reset stable deadline when members/unreachable changed") + resetStableDeadline() + } + } + + /** Helper to wrap updates to `leader` and `selfMemberAdded` to log changes in responsibility status */ + def mutateResponsibilityInfo(f: () => Unit): Unit = { + val responsibleBefore = isResponsible + f() + val responsibleAfter = isResponsible + + if (!responsibleBefore && responsibleAfter) + log.info( + "This node is now the leader responsible for taking SBR decisions among the reachable nodes " + + "(more leaders may exist).") + else if (responsibleBefore && !responsibleAfter) + log.info("This node is not the leader any more and not responsible for taking SBR decisions.") + + if (leader && !selfMemberAdded) + log.debug("This node is leader but !selfMemberAdded.") + } + + private var unreachableDataCenters = Set.empty[DataCenter] + + override def postStop(): Unit = { + if (releaseLeaseCondition != NoLease) { + log.info( + "SBR is stopped and owns the lease. The lease will not be released until after the " + + "lease heartbeat-timeout.") + } + super.postStop() + } + + def receive: Receive = { + case SeenChanged(_, seenBy) => seenChanged(seenBy) + case MemberJoined(m) => addJoining(m) + case MemberWeaklyUp(m) => addWeaklyUp(m) + case MemberUp(m) => addUp(m) + case MemberLeft(m) => leaving(m) + case UnreachableMember(m) => unreachableMember(m) + case MemberDowned(m) => unreachableMember(m) + case MemberExited(m) => unreachableMember(m) + case ReachableMember(m) => reachableMember(m) + case ReachabilityChanged(r) => reachabilityChanged(r) + case MemberRemoved(m, _) => remove(m) + case UnreachableDataCenter(dc) => unreachableDataCenter(dc) + case ReachableDataCenter(dc) => reachableDataCenter(dc) + case LeaderChanged(leaderOption) => leaderChanged(leaderOption) + case ReleaseLeaseResult(released) => releaseLeaseResult(released) + case Tick => tick() + case _: ClusterDomainEvent => // not interested in other events + } + + private def leaderChanged(leaderOption: Option[Address]): Unit = { + mutateResponsibilityInfo { () => + leader = leaderOption.contains(selfUniqueAddress.address) + } + } + + private def tick(): Unit = { + // note the DownAll due to instability is running on all nodes to make that decision as quickly and + // aggressively as possible if time is out + if (reachabilityChangedStats.changeCount > 0) { + val now = System.nanoTime() + val durationSinceLatestChange = (now - reachabilityChangedStats.latestChangeTimestamp).nanos + val durationSinceFirstChange = (now - reachabilityChangedStats.firstChangeTimestamp).nanos + + if (durationSinceLatestChange > (stableAfter * 2)) { + log.debug("SBR no reachability changes within {} ms, resetting stats", (stableAfter * 2).toMillis) + resetReachabilityChangedStats() + } else if (downAllWhenUnstable > Duration.Zero && + durationSinceFirstChange > (stableAfter + downAllWhenUnstable)) { + log.warning( + ClusterLogMarker.sbrInstability, + "SBR detected instability and will down all nodes: {}", + reachabilityChangedStats) + actOnDecision(DownAll) + } + } + + if (isResponsible && strategy.unreachable.nonEmpty && stableDeadline.isOverdue()) { + strategy.decide() match { + case decision: AcquireLeaseDecision => + strategy.lease match { + case Some(lease) => + if (lease.checkLease()) { + log.info( + ClusterLogMarker.sbrLeaseAcquired(decision), + "SBR has acquired lease for decision [{}]", + decision) + actOnDecision(decision) + } else { + if (decision.acquireDelay == Duration.Zero) + acquireLease() // reply message is AcquireLeaseResult + else { + log.debug("SBR delayed attempt to acquire lease for [{} ms]", decision.acquireDelay.toMillis) + timers.startSingleTimer(AcquireLease, AcquireLease, decision.acquireDelay) + } + context.become(waitingForLease(decision)) + } + case None => + throw new IllegalStateException("Unexpected lease decision although lease is not configured") + } + + case decision => + actOnDecision(decision) + } + } + + releaseLeaseCondition match { + case ReleaseLeaseCondition.WhenTimeElapsed(deadline) => + if (deadline.isOverdue()) + releaseLease() // reply message is ReleaseLeaseResult, which will update the releaseLeaseCondition + case _ => + // no lease or first waiting for downed nodes to be removed + } + } + + private def acquireLease(): Unit = { + log.debug("SBR trying to acquire lease") + implicit val ec: ExecutionContext = internalDispatcher + strategy.lease.foreach( + _.acquire() + .recover { + case t => + log.error(t, "SBR acquire of lease failed") + false + } + .map(AcquireLeaseResult) + .pipeTo(self)) + } + + def waitingForLease(decision: Decision): Receive = { + case AcquireLease => + acquireLease() // reply message is LeaseResult + + case AcquireLeaseResult(holdingLease) => + if (holdingLease) { + log.info(ClusterLogMarker.sbrLeaseAcquired(decision), "SBR acquired lease for decision [{}]", decision) + val downedNodes = actOnDecision(decision) + releaseLeaseCondition = releaseLeaseCondition match { + case ReleaseLeaseCondition.WhenMembersRemoved(nodes) => + ReleaseLeaseCondition.WhenMembersRemoved(nodes.union(downedNodes)) + case _ => + if (downedNodes.isEmpty) + ReleaseLeaseCondition.WhenTimeElapsed(Deadline.now + releaseLeaseAfter) + else + ReleaseLeaseCondition.WhenMembersRemoved(downedNodes) + } + } else { + val reverseDecision = strategy.reverseDecision(decision) + log.info( + ClusterLogMarker.sbrLeaseDenied(reverseDecision), + "SBR couldn't acquire lease, reverse decision [{}] to [{}]", + decision, + reverseDecision) + actOnDecision(reverseDecision) + releaseLeaseCondition = NoLease + } + + unstashAll() + context.become(receive) + + case ReleaseLeaseResult(_) => // superseded by new acquire release request + case Tick => // ignore ticks while waiting + case _ => + stash() + } + + private def releaseLeaseResult(released: Boolean): Unit = { + releaseLeaseCondition match { + case ReleaseLeaseCondition.WhenTimeElapsed(deadline) => + if (released && deadline.isOverdue()) { + log.info(ClusterLogMarker.sbrLeaseReleased, "SBR released lease.") + releaseLeaseCondition = NoLease // released successfully + } + case _ => + // no lease or first waiting for downed nodes to be removed + } + } + + /** + * @return the nodes that were downed + */ + def actOnDecision(decision: Decision): Set[UniqueAddress] = { + val nodesToDown = + try { + strategy.nodesToDown(decision) + } catch { + case e: IllegalStateException => + log.warning(e.getMessage) + strategy.nodesToDown(DownAll) + } + + val downMyself = nodesToDown.contains(selfUniqueAddress) + + val indirectlyConnectedLogMessage = + if (decision.isIndirectlyConnected) + s", indirectly connected [${strategy.indirectlyConnected.mkString(", ")}]" + else "" + val unreachableDataCentersLogMessage = + if (unreachableDataCenters.nonEmpty) + s", unreachable DCs [${unreachableDataCenters.mkString(", ")}]" + else "" + + log.warning( + ClusterLogMarker.sbrDowning(decision), + s"SBR took decision $decision and is downing [${nodesToDown.map(_.address).mkString(", ")}]${if (downMyself) " including myself," + else ""}, " + + s"[${strategy.unreachable.size}] unreachable of [${strategy.members.size}] members" + + indirectlyConnectedLogMessage + + s", all members in DC [${strategy.allMembersInDC.mkString(", ")}], full reachability status: ${strategy.reachability}" + + unreachableDataCentersLogMessage) + + if (nodesToDown.nonEmpty) { + // downing is idempotent, and we also avoid calling down on nodes with status Down + // down selfAddress last, since it may shutdown itself if down alone + nodesToDown.foreach(uniqueAddress => if (uniqueAddress != selfUniqueAddress) down(uniqueAddress, decision)) + if (downMyself) + down(selfUniqueAddress, decision) + + resetReachabilityChangedStats() + resetStableDeadline() + } + nodesToDown + } + + def isResponsible: Boolean = leader && selfMemberAdded + + def unreachableMember(m: Member): Unit = { + if (m.uniqueAddress != selfUniqueAddress && m.dataCenter == selfDc) { + log.debug("SBR unreachableMember [{}]", m) + mutateMemberInfo(resetStable = true) { () => + strategy.addUnreachable(m) + resetReachabilityChangedStatsIfAllUnreachableDowned() + } + } + } + + def reachableMember(m: Member): Unit = { + if (m.uniqueAddress != selfUniqueAddress && m.dataCenter == selfDc) { + log.debug("SBR reachableMember [{}]", m) + mutateMemberInfo(resetStable = true) { () => + strategy.addReachable(m) + resetReachabilityChangedStatsIfAllUnreachableDowned() + } + } + } + + private[sbr] def reachabilityChanged(r: Reachability): Unit = { + if (strategy.setReachability(r)) { + // resetStableDeadline is done from unreachableMember/reachableMember + updateReachabilityChangedStats() + // it may also change when members are removed and therefore the reset may be needed + resetReachabilityChangedStatsIfAllUnreachableDowned() + log.debug("SBR noticed {}", reachabilityChangedStats) + } + } + + private def updateReachabilityChangedStats(): Unit = { + val now = System.nanoTime() + if (reachabilityChangedStats.changeCount == 0) + reachabilityChangedStats = ReachabilityChangedStats(now, now, 1) + else + reachabilityChangedStats = reachabilityChangedStats.copy( + latestChangeTimestamp = now, + changeCount = reachabilityChangedStats.changeCount + 1) + } + + def unreachableDataCenter(dc: DataCenter): Unit = { + unreachableDataCenters += dc + log.warning( + "Data center [{}] observed as unreachable. " + + "Note that nodes in other data center will not be downed by SBR in this data center [{}]", + dc, + selfDc) + } + + def reachableDataCenter(dc: DataCenter): Unit = { + unreachableDataCenters -= dc + log.info("Data center [{}] observed as reachable again", dc) + } + + def seenChanged(seenBy: Set[Address]): Unit = { + strategy.setSeenBy(seenBy) + } + + def addUp(m: Member): Unit = { + if (selfDc == m.dataCenter) { + log.debug("SBR add Up [{}]", m) + mutateMemberInfo(resetStable = true) { () => + strategy.add(m) + if (m.uniqueAddress == selfUniqueAddress) mutateResponsibilityInfo { () => + selfMemberAdded = true + } + } + strategy match { + case s: StaticQuorum => + if (s.isTooManyMembers) + log.warning( + "The cluster size is [{}] and static-quorum.quorum-size is [{}]. You should not add " + + "more than [{}] (static-quorum.size * 2 - 1) members to the cluster. If the exceeded cluster size " + + "remains when a SBR decision is needed it will down all nodes.", + s.membersWithRole.size, + s.quorumSize, + s.quorumSize * 2 - 1) + case _ => // ok + } + } + } + + def leaving(m: Member): Unit = { + if (selfDc == m.dataCenter) { + log.debug("SBR leaving [{}]", m) + mutateMemberInfo(resetStable = false) { () => + strategy.add(m) + } + } + } + + def addJoining(m: Member): Unit = { + if (selfDc == m.dataCenter) { + log.debug("SBR add Joining/WeaklyUp [{}]", m) + strategy.add(m) + } + } + + def addWeaklyUp(m: Member): Unit = { + if (m.uniqueAddress == selfUniqueAddress) mutateResponsibilityInfo { () => + selfMemberAdded = true + } + // treat WeaklyUp in same way as joining + addJoining(m) + } + + def remove(m: Member): Unit = { + if (selfDc == m.dataCenter) { + if (m.uniqueAddress == selfUniqueAddress) + context.stop(self) + else + mutateMemberInfo(resetStable = false) { () => + log.debug("SBR remove [{}]", m) + strategy.remove(m) + + resetReachabilityChangedStatsIfAllUnreachableDowned() + + releaseLeaseCondition = releaseLeaseCondition match { + case ReleaseLeaseCondition.WhenMembersRemoved(downedNodes) => + val remainingDownedNodes = downedNodes - m.uniqueAddress + if (remainingDownedNodes.isEmpty) + ReleaseLeaseCondition.WhenTimeElapsed(Deadline.now + releaseLeaseAfter) + else + ReleaseLeaseCondition.WhenMembersRemoved(remainingDownedNodes) + case other => + // no lease or not holding lease + other + } + } + } + } + + private def releaseLease(): Unit = { + implicit val ec: ExecutionContext = internalDispatcher + strategy.lease.foreach { l => + if (releaseLeaseCondition != NoLease) { + log.debug("SBR releasing lease") + l.release().recover { case _ => false }.map(ReleaseLeaseResult.apply).pipeTo(self) + } + } + } +} diff --git a/akka-cluster/src/main/scala/akka/cluster/sbr/SplitBrainResolverProvider.scala b/akka-cluster/src/main/scala/akka/cluster/sbr/SplitBrainResolverProvider.scala new file mode 100644 index 0000000000..24c63cce9c --- /dev/null +++ b/akka-cluster/src/main/scala/akka/cluster/sbr/SplitBrainResolverProvider.scala @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2009-2020 Lightbend Inc. + */ + +package akka.cluster.sbr + +import scala.concurrent.duration.Duration +import scala.concurrent.duration.FiniteDuration + +import akka.actor.ActorSystem +import akka.actor.Props +import akka.cluster.Cluster +import akka.cluster.DowningProvider +import akka.coordination.lease.scaladsl.LeaseProvider + +/** + * See reference documentation: https://doc.akka.io/docs/akka/current/split-brain-resolver.html + * + * Enabled with configuration: + * {{{ + * akka.cluster.downing-provider-class = "akka.cluster.sbr.SplitBrainResolverProvider" + * }}} + */ +final class SplitBrainResolverProvider(system: ActorSystem) extends DowningProvider { + + private val settings = new SplitBrainResolverSettings(system.settings.config) + + override def downRemovalMargin: FiniteDuration = { + // if down-removal-margin is defined we let it trump stable-after to allow + // for two different values for SBR downing and cluster tool stop/start after downing + val drm = Cluster(system).settings.DownRemovalMargin + if (drm != Duration.Zero) drm + else settings.DowningStableAfter + } + + override def downingActorProps: Option[Props] = { + import SplitBrainResolverSettings._ + + val cluster = Cluster(system) + val selfDc = cluster.selfDataCenter + val strategy = + settings.DowningStrategy match { + case KeepMajorityName => + new KeepMajority(selfDc, settings.keepMajorityRole) + case StaticQuorumName => + val s = settings.staticQuorumSettings + new StaticQuorum(selfDc, s.size, s.role) + case KeepOldestName => + val s = settings.keepOldestSettings + new KeepOldest(selfDc, s.downIfAlone, s.role) + case DownAllName => + new DownAllNodes(selfDc) + case LeaseMajorityName => + val s = settings.leaseMajoritySettings + val leaseOwnerName = cluster.selfUniqueAddress.address.hostPort + val lease = LeaseProvider(system).getLease(s"${system.name}-akka-sbr", s.leaseImplementation, leaseOwnerName) + new LeaseMajority(selfDc, s.role, lease, s.acquireLeaseDelayForMinority) + } + + Some(SplitBrainResolver.props(settings.DowningStableAfter, strategy)) + } + +} diff --git a/akka-cluster/src/main/scala/akka/cluster/sbr/SplitBrainResolverSettings.scala b/akka-cluster/src/main/scala/akka/cluster/sbr/SplitBrainResolverSettings.scala new file mode 100644 index 0000000000..723cc61164 --- /dev/null +++ b/akka-cluster/src/main/scala/akka/cluster/sbr/SplitBrainResolverSettings.scala @@ -0,0 +1,128 @@ +/* + * Copyright (C) 2009-2020 Lightbend Inc. + */ + +package akka.cluster.sbr + +import java.util.Locale +import java.util.concurrent.TimeUnit + +import scala.concurrent.duration.Duration +import scala.concurrent.duration.FiniteDuration + +import com.typesafe.config.Config + +import akka.ConfigurationException +import akka.annotation.InternalApi +import akka.util.Helpers +import akka.util.Helpers.Requiring + +/** + * INTERNAL API + */ +@InternalApi private[sbr] object SplitBrainResolverSettings { + final val KeepMajorityName = "keep-majority" + final val LeaseMajorityName = "lease-majority" + final val StaticQuorumName = "static-quorum" + final val KeepOldestName = "keep-oldest" + final val DownAllName = "down-all" + + def allStrategyNames = + Set(KeepMajorityName, LeaseMajorityName, StaticQuorumName, KeepOldestName, DownAllName) +} + +/** + * INTERNAL API + */ +@InternalApi private[sbr] final class SplitBrainResolverSettings(config: Config) { + + import SplitBrainResolverSettings._ + + private val cc = config.getConfig("akka.cluster.split-brain-resolver") + + val DowningStableAfter: FiniteDuration = { + val key = "stable-after" + FiniteDuration(cc.getDuration(key).toMillis, TimeUnit.MILLISECONDS).requiring(_ >= Duration.Zero, key + " >= 0s") + } + + val DowningStrategy: String = + cc.getString("active-strategy").toLowerCase(Locale.ROOT) match { + case strategyName if allStrategyNames(strategyName) => strategyName + case unknown => + throw new ConfigurationException( + s"Unknown downing strategy [$unknown]. Select one of [${allStrategyNames.mkString(",")}]") + } + + val DownAllWhenUnstable: FiniteDuration = { + val key = "down-all-when-unstable" + Helpers.toRootLowerCase(cc.getString("down-all-when-unstable")) match { + case "on" => + // based on stable-after + DowningStableAfter * 3 / 4 + case "off" => + // disabled + Duration.Zero + case _ => + FiniteDuration(cc.getDuration(key).toMillis, TimeUnit.MILLISECONDS) + .requiring(_ > Duration.Zero, key + " > 0s, or 'off' to disable") + } + } + + // the individual sub-configs below should only be called when the strategy has been selected + + def keepMajorityRole: Option[String] = role(strategyConfig(KeepMajorityName)) + + def staticQuorumSettings: StaticQuorumSettings = { + val c = strategyConfig(StaticQuorumName) + val size = c + .getInt("quorum-size") + .requiring(_ >= 1, s"akka.cluster.split-brain-resolver.$StaticQuorumName.quorum-size must be >= 1") + StaticQuorumSettings(size, role(c)) + } + + def keepOldestSettings: KeepOldestSettings = { + val c = strategyConfig(KeepOldestName) + val downIfAlone = c.getBoolean("down-if-alone") + KeepOldestSettings(downIfAlone, role(c)) + } + + def leaseMajoritySettings: LeaseMajoritySettings = { + val c = strategyConfig(LeaseMajorityName) + + val leaseImplementation = c.getString("lease-implementation") + require( + leaseImplementation != "", + s"akka.cluster.split-brain-resolver.$LeaseMajorityName.lease-implementation must be defined") + + val acquireLeaseDelayForMinority = + FiniteDuration(c.getDuration("acquire-lease-delay-for-minority").toMillis, TimeUnit.MILLISECONDS) + + LeaseMajoritySettings(leaseImplementation, acquireLeaseDelayForMinority, role(c)) + } + + private def strategyConfig(strategyName: String): Config = cc.getConfig(strategyName) + + private def role(c: Config): Option[String] = c.getString("role") match { + case "" => None + case r => Some(r) + } + +} + +/** + * INTERNAL API + */ +@InternalApi private[sbr] final case class StaticQuorumSettings(size: Int, role: Option[String]) + +/** + * INTERNAL API + */ +@InternalApi private[sbr] final case class KeepOldestSettings(downIfAlone: Boolean, role: Option[String]) + +/** + * INTERNAL API + */ +@InternalApi private[sbr] final case class LeaseMajoritySettings( + leaseImplementation: String, + acquireLeaseDelayForMinority: FiniteDuration, + role: Option[String]) diff --git a/akka-cluster/src/multi-jvm/scala/akka/cluster/SunnyWeatherSpec.scala b/akka-cluster/src/multi-jvm/scala/akka/cluster/SunnyWeatherSpec.scala index 480817662b..5a68b17bab 100644 --- a/akka-cluster/src/multi-jvm/scala/akka/cluster/SunnyWeatherSpec.scala +++ b/akka-cluster/src/multi-jvm/scala/akka/cluster/SunnyWeatherSpec.scala @@ -31,7 +31,11 @@ object SunnyWeatherMultiJvmSpec extends MultiNodeConfig { loggers = ["akka.testkit.TestEventListener"] loglevel = INFO remote.log-remote-lifecycle-events = off - cluster.failure-detector.monitored-by-nr-of-members = 3 + cluster { + failure-detector.monitored-by-nr-of-members = 3 + downing-provider-class = "akka.cluster.sbr.SplitBrainResolverProvider" + split-brain-resolver.active-strategy = keep-majority + } } """)) diff --git a/akka-cluster/src/multi-jvm/scala/akka/cluster/sbr/DownAllIndirectlyConnected5NodeSpec.scala b/akka-cluster/src/multi-jvm/scala/akka/cluster/sbr/DownAllIndirectlyConnected5NodeSpec.scala new file mode 100644 index 0000000000..a41bf1a553 --- /dev/null +++ b/akka-cluster/src/multi-jvm/scala/akka/cluster/sbr/DownAllIndirectlyConnected5NodeSpec.scala @@ -0,0 +1,128 @@ +/* + * Copyright (C) 2020 Lightbend Inc. + */ + +package akka.cluster.sbr + +import scala.concurrent.duration._ + +import com.typesafe.config.ConfigFactory + +import akka.cluster.Cluster +import akka.cluster.MemberStatus +import akka.cluster.MultiNodeClusterSpec +import akka.remote.testkit.MultiNodeConfig +import akka.remote.testkit.MultiNodeSpec +import akka.remote.transport.ThrottlerTransportAdapter + +object DownAllIndirectlyConnected5NodeSpec extends MultiNodeConfig { + val node1 = role("node1") + val node2 = role("node2") + val node3 = role("node3") + val node4 = role("node4") + val node5 = role("node5") + + commonConfig(ConfigFactory.parseString(""" + akka { + loglevel = INFO + cluster { + downing-provider-class = "akka.cluster.sbr.SplitBrainResolverProvider" + split-brain-resolver.active-strategy = keep-majority + split-brain-resolver.stable-after = 6s + + run-coordinated-shutdown-when-down = off + } + + actor.provider = cluster + + test.filter-leeway = 10s + } + """)) + + testTransport(on = true) +} + +class DownAllIndirectlyConnected5NodeSpecMultiJvmNode1 extends DownAllIndirectlyConnected5NodeSpec +class DownAllIndirectlyConnected5NodeSpecMultiJvmNode2 extends DownAllIndirectlyConnected5NodeSpec +class DownAllIndirectlyConnected5NodeSpecMultiJvmNode3 extends DownAllIndirectlyConnected5NodeSpec +class DownAllIndirectlyConnected5NodeSpecMultiJvmNode4 extends DownAllIndirectlyConnected5NodeSpec +class DownAllIndirectlyConnected5NodeSpecMultiJvmNode5 extends DownAllIndirectlyConnected5NodeSpec + +class DownAllIndirectlyConnected5NodeSpec + extends MultiNodeSpec(DownAllIndirectlyConnected5NodeSpec) + with MultiNodeClusterSpec { + import DownAllIndirectlyConnected5NodeSpec._ + + "A 5-node cluster with keep-one-indirectly-connected = off" should { + "down all when indirectly connected combined with clean partition" in { + val cluster = Cluster(system) + + runOn(node1) { + cluster.join(cluster.selfAddress) + } + enterBarrier("node1 joined") + runOn(node2, node3, node4, node5) { + cluster.join(node(node1).address) + } + within(10.seconds) { + awaitAssert { + cluster.state.members.size should ===(5) + cluster.state.members.foreach { + _.status should ===(MemberStatus.Up) + } + } + } + enterBarrier("Cluster formed") + + runOn(node1) { + for (x <- List(node1, node2, node3); y <- List(node4, node5)) { + testConductor.blackhole(x, y, ThrottlerTransportAdapter.Direction.Both).await + } + } + enterBarrier("blackholed-clean-partition") + + runOn(node1) { + testConductor.blackhole(node2, node3, ThrottlerTransportAdapter.Direction.Both).await + } + enterBarrier("blackholed-indirectly-connected") + + within(10.seconds) { + awaitAssert { + runOn(node1) { + cluster.state.unreachable.map(_.address) should ===(Set(node2, node3, node4, node5).map(node(_).address)) + } + runOn(node2) { + cluster.state.unreachable.map(_.address) should ===(Set(node3, node4, node5).map(node(_).address)) + } + runOn(node3) { + cluster.state.unreachable.map(_.address) should ===(Set(node2, node4, node5).map(node(_).address)) + } + runOn(node4, node5) { + cluster.state.unreachable.map(_.address) should ===(Set(node1, node2, node3).map(node(_).address)) + } + } + } + enterBarrier("unreachable") + + runOn(node1) { + within(15.seconds) { + awaitAssert { + cluster.state.members.map(_.address) should ===(Set(node(node1).address)) + cluster.state.members.foreach { + _.status should ===(MemberStatus.Up) + } + } + } + } + + runOn(node2, node3, node4, node5) { + // downed + awaitCond(cluster.isTerminated, max = 15.seconds) + } + + enterBarrier("done") + } + + } + +} diff --git a/akka-cluster/src/multi-jvm/scala/akka/cluster/sbr/DownAllUnstable5NodeSpec.scala b/akka-cluster/src/multi-jvm/scala/akka/cluster/sbr/DownAllUnstable5NodeSpec.scala new file mode 100644 index 0000000000..e9bb9c7d58 --- /dev/null +++ b/akka-cluster/src/multi-jvm/scala/akka/cluster/sbr/DownAllUnstable5NodeSpec.scala @@ -0,0 +1,133 @@ +/* + * Copyright (C) 2020 Lightbend Inc. + */ + +package akka.cluster.sbr + +import scala.concurrent.duration._ + +import com.typesafe.config.ConfigFactory + +import akka.cluster.Cluster +import akka.cluster.MemberStatus +import akka.cluster.MultiNodeClusterSpec +import akka.remote.testkit.MultiNodeConfig +import akka.remote.testkit.MultiNodeSpec +import akka.remote.transport.ThrottlerTransportAdapter + +object DownAllUnstable5NodeSpec extends MultiNodeConfig { + val node1 = role("node1") + val node2 = role("node2") + val node3 = role("node3") + val node4 = role("node4") + val node5 = role("node5") + + commonConfig(ConfigFactory.parseString(""" + akka { + loglevel = INFO + cluster { + downing-provider-class = "akka.cluster.sbr.SplitBrainResolverProvider" + failure-detector.acceptable-heartbeat-pause = 3s + split-brain-resolver.active-strategy = keep-majority + split-brain-resolver.stable-after = 10s + split-brain-resolver.down-all-when-unstable = 7s + + run-coordinated-shutdown-when-down = off + } + + # quicker reconnect + remote.retry-gate-closed-for = 1s + remote.netty.tcp.connection-timeout = 3 s + + actor.provider = cluster + + test.filter-leeway = 10s + } + """)) + + testTransport(on = true) +} + +class DownAllUnstable5NodeSpecMultiJvmNode1 extends DownAllUnstable5NodeSpec +class DownAllUnstable5NodeSpecMultiJvmNode2 extends DownAllUnstable5NodeSpec +class DownAllUnstable5NodeSpecMultiJvmNode3 extends DownAllUnstable5NodeSpec +class DownAllUnstable5NodeSpecMultiJvmNode4 extends DownAllUnstable5NodeSpec +class DownAllUnstable5NodeSpecMultiJvmNode5 extends DownAllUnstable5NodeSpec + +class DownAllUnstable5NodeSpec extends MultiNodeSpec(DownAllUnstable5NodeSpec) with MultiNodeClusterSpec { + import DownAllUnstable5NodeSpec._ + + "A 5-node cluster with down-all-when-unstable" should { + "down all when instability continues" in { + val cluster = Cluster(system) + + runOn(node1) { + cluster.join(cluster.selfAddress) + } + enterBarrier("node1 joined") + runOn(node2, node3, node4, node5) { + cluster.join(node(node1).address) + } + within(10.seconds) { + awaitAssert { + cluster.state.members.size should ===(5) + cluster.state.members.foreach { + _.status should ===(MemberStatus.Up) + } + } + } + enterBarrier("Cluster formed") + + // acceptable-heartbeat-pause = 3s + // stable-after = 10s + // down-all-when-unstable = 7s + + runOn(node1) { + for (x <- List(node1, node2, node3); y <- List(node4, node5)) { + testConductor.blackhole(x, y, ThrottlerTransportAdapter.Direction.Both).await + } + } + enterBarrier("blackholed-clean-partition") + + within(10.seconds) { + awaitAssert { + runOn(node1, node2, node3) { + cluster.state.unreachable.map(_.address) should ===(Set(node4, node5).map(node(_).address)) + } + runOn(node4, node5) { + cluster.state.unreachable.map(_.address) should ===(Set(node1, node2, node3).map(node(_).address)) + } + } + } + enterBarrier("unreachable-clean-partition") + + // no decision yet + Thread.sleep(2000) + cluster.state.members.size should ===(5) + cluster.state.members.foreach { + _.status should ===(MemberStatus.Up) + } + + runOn(node1) { + testConductor.blackhole(node2, node3, ThrottlerTransportAdapter.Direction.Both).await + } + enterBarrier("blackhole-2") + // then it takes about 5 seconds for failure detector to observe that + Thread.sleep(7000) + + runOn(node1) { + testConductor.passThrough(node2, node3, ThrottlerTransportAdapter.Direction.Both).await + } + enterBarrier("passThrough-2") + + // now it should have been unstable for more than 17 seconds + + // all downed + awaitCond(cluster.isTerminated, max = 15.seconds) + + enterBarrier("done") + } + + } + +} diff --git a/akka-cluster/src/multi-jvm/scala/akka/cluster/sbr/IndirectlyConnected3NodeSpec.scala b/akka-cluster/src/multi-jvm/scala/akka/cluster/sbr/IndirectlyConnected3NodeSpec.scala new file mode 100644 index 0000000000..eebbd67f45 --- /dev/null +++ b/akka-cluster/src/multi-jvm/scala/akka/cluster/sbr/IndirectlyConnected3NodeSpec.scala @@ -0,0 +1,111 @@ +/* + * Copyright (C) 2020 Lightbend Inc. + */ + +package akka.cluster.sbr + +import scala.concurrent.duration._ + +import com.typesafe.config.ConfigFactory + +import akka.cluster.Cluster +import akka.cluster.MemberStatus +import akka.cluster.MultiNodeClusterSpec +import akka.remote.testkit.MultiNodeConfig +import akka.remote.testkit.MultiNodeSpec +import akka.remote.transport.ThrottlerTransportAdapter + +object IndirectlyConnected3NodeSpec extends MultiNodeConfig { + val node1 = role("node1") + val node2 = role("node2") + val node3 = role("node3") + + commonConfig(ConfigFactory.parseString(""" + akka { + loglevel = INFO + cluster { + downing-provider-class = "akka.cluster.sbr.SplitBrainResolverProvider" + split-brain-resolver.active-strategy = keep-majority + split-brain-resolver.stable-after = 6s + + run-coordinated-shutdown-when-down = off + } + + actor.provider = cluster + + test.filter-leeway = 10s + } + """)) + + testTransport(on = true) +} + +class IndirectlyConnected3NodeSpecMultiJvmNode1 extends IndirectlyConnected3NodeSpec +class IndirectlyConnected3NodeSpecMultiJvmNode2 extends IndirectlyConnected3NodeSpec +class IndirectlyConnected3NodeSpecMultiJvmNode3 extends IndirectlyConnected3NodeSpec + +class IndirectlyConnected3NodeSpec extends MultiNodeSpec(IndirectlyConnected3NodeSpec) with MultiNodeClusterSpec { + import IndirectlyConnected3NodeSpec._ + + "A 3-node cluster" should { + "avoid a split brain when two unreachable but can talk via third" in { + val cluster = Cluster(system) + + runOn(node1) { + cluster.join(cluster.selfAddress) + } + enterBarrier("node1 joined") + runOn(node2, node3) { + cluster.join(node(node1).address) + } + within(10.seconds) { + awaitAssert { + cluster.state.members.size should ===(3) + cluster.state.members.foreach { + _.status should ===(MemberStatus.Up) + } + } + } + enterBarrier("Cluster formed") + + runOn(node1) { + testConductor.blackhole(node2, node3, ThrottlerTransportAdapter.Direction.Both).await + } + enterBarrier("Blackholed") + + within(10.seconds) { + awaitAssert { + runOn(node3) { + cluster.state.unreachable.map(_.address) should ===(Set(node(node2).address)) + } + runOn(node2) { + cluster.state.unreachable.map(_.address) should ===(Set(node(node3).address)) + } + runOn(node1) { + cluster.state.unreachable.map(_.address) should ===(Set(node(node3).address, node(node2).address)) + } + } + } + enterBarrier("unreachable") + + runOn(node1) { + within(15.seconds) { + awaitAssert { + cluster.state.members.map(_.address) should ===(Set(node(node1).address)) + cluster.state.members.foreach { + _.status should ===(MemberStatus.Up) + } + } + } + } + + runOn(node2, node3) { + // downed + awaitCond(cluster.isTerminated, max = 15.seconds) + } + + enterBarrier("done") + } + } + +} diff --git a/akka-cluster/src/multi-jvm/scala/akka/cluster/sbr/IndirectlyConnected5NodeSpec.scala b/akka-cluster/src/multi-jvm/scala/akka/cluster/sbr/IndirectlyConnected5NodeSpec.scala new file mode 100644 index 0000000000..97ff66862e --- /dev/null +++ b/akka-cluster/src/multi-jvm/scala/akka/cluster/sbr/IndirectlyConnected5NodeSpec.scala @@ -0,0 +1,125 @@ +/* + * Copyright (C) 2020 Lightbend Inc. + */ + +package akka.cluster.sbr + +import scala.concurrent.duration._ + +import com.typesafe.config.ConfigFactory + +import akka.cluster.Cluster +import akka.cluster.MemberStatus +import akka.cluster.MultiNodeClusterSpec +import akka.remote.testkit.MultiNodeConfig +import akka.remote.testkit.MultiNodeSpec +import akka.remote.transport.ThrottlerTransportAdapter + +object IndirectlyConnected5NodeSpec extends MultiNodeConfig { + val node1 = role("node1") + val node2 = role("node2") + val node3 = role("node3") + val node4 = role("node4") + val node5 = role("node5") + + commonConfig(ConfigFactory.parseString(""" + akka { + loglevel = INFO + cluster { + downing-provider-class = "akka.cluster.sbr.SplitBrainResolverProvider" + split-brain-resolver.active-strategy = keep-majority + split-brain-resolver.stable-after = 6s + + run-coordinated-shutdown-when-down = off + } + + actor.provider = cluster + + test.filter-leeway = 10s + } + """)) + + testTransport(on = true) +} + +class IndirectlyConnected5NodeSpecMultiJvmNode1 extends IndirectlyConnected5NodeSpec +class IndirectlyConnected5NodeSpecMultiJvmNode2 extends IndirectlyConnected5NodeSpec +class IndirectlyConnected5NodeSpecMultiJvmNode3 extends IndirectlyConnected5NodeSpec +class IndirectlyConnected5NodeSpecMultiJvmNode4 extends IndirectlyConnected5NodeSpec +class IndirectlyConnected5NodeSpecMultiJvmNode5 extends IndirectlyConnected5NodeSpec + +class IndirectlyConnected5NodeSpec extends MultiNodeSpec(IndirectlyConnected5NodeSpec) with MultiNodeClusterSpec { + import IndirectlyConnected5NodeSpec._ + + "A 5-node cluster" should { + "avoid a split brain when indirectly connected combined with clean partition" in { + val cluster = Cluster(system) + + runOn(node1) { + cluster.join(cluster.selfAddress) + } + enterBarrier("node1 joined") + runOn(node2, node3, node4, node5) { + cluster.join(node(node1).address) + } + within(10.seconds) { + awaitAssert { + cluster.state.members.size should ===(5) + cluster.state.members.foreach { + _.status should ===(MemberStatus.Up) + } + } + } + enterBarrier("Cluster formed") + + runOn(node1) { + for (x <- List(node1, node2, node3); y <- List(node4, node5)) { + testConductor.blackhole(x, y, ThrottlerTransportAdapter.Direction.Both).await + } + } + enterBarrier("blackholed-clean-partition") + + runOn(node1) { + testConductor.blackhole(node2, node3, ThrottlerTransportAdapter.Direction.Both).await + } + enterBarrier("blackholed-indirectly-connected") + + within(10.seconds) { + awaitAssert { + runOn(node1) { + cluster.state.unreachable.map(_.address) should ===(Set(node2, node3, node4, node5).map(node(_).address)) + } + runOn(node2) { + cluster.state.unreachable.map(_.address) should ===(Set(node3, node4, node5).map(node(_).address)) + } + runOn(node3) { + cluster.state.unreachable.map(_.address) should ===(Set(node2, node4, node5).map(node(_).address)) + } + runOn(node4, node5) { + cluster.state.unreachable.map(_.address) should ===(Set(node1, node2, node3).map(node(_).address)) + } + } + } + enterBarrier("unreachable") + + runOn(node1) { + within(15.seconds) { + awaitAssert { + cluster.state.members.map(_.address) should ===(Set(node(node1).address)) + cluster.state.members.foreach { + _.status should ===(MemberStatus.Up) + } + } + } + } + + runOn(node2, node3, node4, node5) { + // downed + awaitCond(cluster.isTerminated, max = 15.seconds) + } + + enterBarrier("done") + } + } + +} diff --git a/akka-cluster/src/multi-jvm/scala/akka/cluster/sbr/LeaseMajority5NodeSpec.scala b/akka-cluster/src/multi-jvm/scala/akka/cluster/sbr/LeaseMajority5NodeSpec.scala new file mode 100644 index 0000000000..0e1e13a718 --- /dev/null +++ b/akka-cluster/src/multi-jvm/scala/akka/cluster/sbr/LeaseMajority5NodeSpec.scala @@ -0,0 +1,189 @@ +/* + * Copyright (C) 2019-2020 Lightbend Inc. + */ + +package akka.cluster.sbr + +import scala.concurrent.Future +import scala.concurrent.duration._ + +import akka.cluster.MemberStatus +import akka.remote.testconductor.RoleName +import akka.remote.testkit.MultiNodeConfig +import akka.remote.testkit.MultiNodeSpec +import akka.remote.transport.ThrottlerTransportAdapter +import com.typesafe.config.ConfigFactory + +import akka.cluster.MultiNodeClusterSpec +import akka.coordination.lease.TestLease +import akka.coordination.lease.TestLeaseExt + +object LeaseMajority5NodeSpec extends MultiNodeConfig { + val node1 = role("node1") + val node2 = role("node2") + val node3 = role("node3") + val node4 = role("node4") + val node5 = role("node5") + + commonConfig(ConfigFactory.parseString(s""" + akka { + loglevel = INFO + cluster { + downing-provider-class = "akka.cluster.sbr.SplitBrainResolverProvider" + split-brain-resolver { + active-strategy = lease-majority + stable-after = 6s + lease-majority { + lease-implementation = test-lease + acquire-lease-delay-for-minority = 1s + } + } + + run-coordinated-shutdown-when-down = off + } + + actor.provider = cluster + + test.filter-leeway = 10s + } + + test-lease { + lease-class = ${classOf[TestLease].getName} + heartbeat-interval = 1s + heartbeat-timeout = 120s + lease-operation-timeout = 3s + } + """)) + + testTransport(on = true) +} + +class LeaseMajority5NodeSpecMultiJvmNode1 extends LeaseMajority5NodeSpec +class LeaseMajority5NodeSpecMultiJvmNode2 extends LeaseMajority5NodeSpec +class LeaseMajority5NodeSpecMultiJvmNode3 extends LeaseMajority5NodeSpec +class LeaseMajority5NodeSpecMultiJvmNode4 extends LeaseMajority5NodeSpec +class LeaseMajority5NodeSpecMultiJvmNode5 extends LeaseMajority5NodeSpec + +class LeaseMajority5NodeSpec extends MultiNodeSpec(LeaseMajority5NodeSpec) with MultiNodeClusterSpec { + import LeaseMajority5NodeSpec._ + + private val testLeaseName = "LeaseMajority5NodeSpec-akka-sbr" + + def sortByAddress(roles: RoleName*): List[RoleName] = { + + /** + * Sort the roles in the address order used by the cluster node ring. + */ + implicit val clusterOrdering: Ordering[RoleName] = new Ordering[RoleName] { + import akka.cluster.Member.addressOrdering + def compare(x: RoleName, y: RoleName): Int = addressOrdering.compare(node(x).address, node(y).address) + } + roles.toList.sorted + } + + def leader(roles: RoleName*): RoleName = + sortByAddress(roles: _*).head + + "LeaseMajority in a 5-node cluster" should { + "setup cluster" in { + runOn(node1) { + cluster.join(cluster.selfAddress) + } + enterBarrier("node1 joined") + runOn(node2, node3, node4, node5) { + cluster.join(node(node1).address) + } + within(10.seconds) { + awaitAssert { + cluster.state.members.size should ===(5) + cluster.state.members.foreach { + _.status should ===(MemberStatus.Up) + } + } + } + enterBarrier("Cluster formed") + } + + "keep the side that can acquire the lease" in { + val lease = TestLeaseExt(system).getTestLease(testLeaseName) + val leaseProbe = lease.probe + + runOn(node1, node2, node3) { + lease.setNextAcquireResult(Future.successful(true)) + } + runOn(node4, node5) { + lease.setNextAcquireResult(Future.successful(false)) + } + enterBarrier("lease-in-place") + runOn(node1) { + for (x <- List(node1, node2, node3); y <- List(node4, node5)) { + testConductor.blackhole(x, y, ThrottlerTransportAdapter.Direction.Both).await + } + } + enterBarrier("blackholed-clean-partition") + + runOn(node1, node2, node3) { + within(20.seconds) { + awaitAssert { + cluster.state.members.size should ===(3) + } + } + runOn(leader(node1, node2, node3)) { + leaseProbe.expectMsgType[TestLease.AcquireReq] + // after 2 * stable-after + leaseProbe.expectMsgType[TestLease.ReleaseReq](14.seconds) + } + } + runOn(node4, node5) { + within(20.seconds) { + awaitAssert { + cluster.isTerminated should ===(true) + } + runOn(leader(node4, node5)) { + leaseProbe.expectMsgType[TestLease.AcquireReq] + } + } + } + enterBarrier("downed-and-removed") + leaseProbe.expectNoMessage(1.second) + + enterBarrier("done-1") + } + } + + "keep the side that can acquire the lease, round 2" in { + val lease = TestLeaseExt(system).getTestLease(testLeaseName) + + runOn(node1) { + lease.setNextAcquireResult(Future.successful(true)) + } + runOn(node2, node3) { + lease.setNextAcquireResult(Future.successful(false)) + } + enterBarrier("lease-in-place-2") + runOn(node1) { + for (x <- List(node1); y <- List(node2, node3)) { + testConductor.blackhole(x, y, ThrottlerTransportAdapter.Direction.Both).await + } + } + enterBarrier("blackholed-clean-partition-2") + + runOn(node1) { + within(20.seconds) { + awaitAssert { + cluster.state.members.size should ===(1) + } + } + } + runOn(node2, node3) { + within(20.seconds) { + awaitAssert { + cluster.isTerminated should ===(true) + } + } + } + + enterBarrier("done-2") + } + +} diff --git a/akka-cluster/src/test/scala/akka/cluster/ClusterSpec.scala b/akka-cluster/src/test/scala/akka/cluster/ClusterSpec.scala index 616b836d6b..512befffae 100644 --- a/akka-cluster/src/test/scala/akka/cluster/ClusterSpec.scala +++ b/akka-cluster/src/test/scala/akka/cluster/ClusterSpec.scala @@ -5,12 +5,12 @@ package akka.cluster import java.lang.management.ManagementFactory +import javax.management.ObjectName import scala.concurrent.Await import scala.concurrent.duration._ import com.typesafe.config.ConfigFactory -import javax.management.ObjectName import akka.actor.ActorSystem import akka.actor.Address diff --git a/akka-cluster/src/test/scala/akka/cluster/sbr/SplitBrainResolverSpec.scala b/akka-cluster/src/test/scala/akka/cluster/sbr/SplitBrainResolverSpec.scala new file mode 100644 index 0000000000..cae903c8ef --- /dev/null +++ b/akka-cluster/src/test/scala/akka/cluster/sbr/SplitBrainResolverSpec.scala @@ -0,0 +1,1639 @@ +/* + * Copyright (C) 2015-2020 Lightbend Inc. + */ + +package akka.cluster.sbr + +import scala.concurrent.Future +import scala.concurrent.duration._ + +import com.typesafe.config.ConfigFactory +import org.scalatest.concurrent.Eventually + +import akka.actor.ActorRef +import akka.actor.Address +import akka.actor.ExtendedActorSystem +import akka.actor.Props +import akka.cluster.ClusterEvent.LeaderChanged +import akka.cluster.ClusterEvent.MemberRemoved +import akka.cluster.ClusterEvent.MemberUp +import akka.cluster.ClusterEvent.MemberWeaklyUp +import akka.cluster.ClusterEvent.ReachabilityChanged +import akka.cluster.ClusterEvent.ReachableDataCenter +import akka.cluster.ClusterEvent.ReachableMember +import akka.cluster.ClusterEvent.UnreachableDataCenter +import akka.cluster.ClusterEvent.UnreachableMember +import akka.cluster.ClusterSettings.DataCenter +import akka.cluster.MemberStatus._ +import akka.cluster._ +import akka.coordination.lease.LeaseSettings +import akka.coordination.lease.TestLease +import akka.coordination.lease.TimeoutSettings +import akka.testkit.AkkaSpec +import akka.testkit.EventFilter + +object SplitBrainResolverSpec { + + final case class DownCalled(address: Address) + + object DowningTestActor { + def props( + stableAfter: FiniteDuration, + strategy: DowningStrategy, + probe: ActorRef, + selfUniqueAddress: UniqueAddress, + selfDc: DataCenter, + downAllWhenUnstable: FiniteDuration, + tickInterval: FiniteDuration): Props = + Props( + new DowningTestActor( + stableAfter, + strategy, + probe, + selfUniqueAddress, + selfDc, + downAllWhenUnstable, + tickInterval)) + } + + class DowningTestActor( + stableAfter: FiniteDuration, + strategy: DowningStrategy, + probe: ActorRef, + override val selfUniqueAddress: UniqueAddress, + override val selfDc: DataCenter, + override val downAllWhenUnstable: FiniteDuration, + val tick: FiniteDuration) + extends SplitBrainResolverBase(stableAfter, strategy) { + + // manual ticks used in this test + override def tickInterval: FiniteDuration = + if (tick == Duration.Zero) super.tickInterval else tick + + // immediate overdue if Duration.Zero is used + override def newStableDeadline(): Deadline = super.newStableDeadline() - 1.nanos + + var downed = Set.empty[Address] + + override def down(node: UniqueAddress, decision: DowningStrategy.Decision): Unit = { + if (leader && !downed(node.address)) { + downed += node.address + probe ! DownCalled(node.address) + } else if (!leader) + probe ! "down must only be done by leader" + } + } +} + +class SplitBrainResolverSpec + extends AkkaSpec(""" + |akka { + | actor.provider = cluster + | cluster.downing-provider-class = "akka.cluster.sbr.SplitBrainResolverProvider" + | cluster.split-brain-resolver.active-strategy=keep-majority + | remote { + | netty.tcp { + | hostname = "127.0.0.1" + | port = 0 + | } + | } + |} + """.stripMargin) + with Eventually { + + import DowningStrategy._ + import SplitBrainResolverSpec._ + import TestAddresses._ + + private val selfDc = TestAddresses.defaultDataCenter + + private val testLeaseSettings = + new LeaseSettings("akka-sbr", "test", new TimeoutSettings(1.second, 2.minutes, 3.seconds), ConfigFactory.empty) + + def createReachability(unreachability: Seq[(Member, Member)]): Reachability = { + Reachability(unreachability.map { + case (from, to) => Reachability.Record(from.uniqueAddress, to.uniqueAddress, Reachability.Unreachable, 1) + }.toIndexedSeq, unreachability.map { + case (from, _) => from.uniqueAddress -> 1L + }.toMap) + } + + def extSystem: ExtendedActorSystem = system.asInstanceOf[ExtendedActorSystem] + + abstract class StrategySetup { + def createStrategy(): DowningStrategy + + var side1: Set[Member] = Set.empty + var side2: Set[Member] = Set.empty + var side3: Set[Member] = Set.empty + + def side1Nodes: Set[UniqueAddress] = side1.map(_.uniqueAddress) + def side2Nodes: Set[UniqueAddress] = side2.map(_.uniqueAddress) + def side3Nodes: Set[UniqueAddress] = side3.map(_.uniqueAddress) + + var indirectlyConnected: Seq[(Member, Member)] = Nil + + private def initStrategy(): DowningStrategy = { + val strategy = createStrategy() + (side1 ++ side2 ++ side3).foreach(strategy.add) + strategy + } + + def assertDowning(members: Set[Member]): Unit = { + assertDowningSide(side1, members) + assertDowningSide(side2, members) + assertDowningSide(side3, members) + } + + def assertDowningSide(side: Set[Member], members: Set[Member]): Unit = { + if (side.nonEmpty) + strategy(side).nodesToDown() should be(members.map(_.uniqueAddress)) + } + + def strategy(side: Set[Member]): DowningStrategy = { + val others = side1 ++ side2 ++ side3 -- side + (side -- others) should be(side) + + if (side.nonEmpty) { + val strategy = initStrategy() + val unreachability = (indirectlyConnected ++ others.map(o => side.head -> o)).toSet.toList + val r = createReachability(unreachability) + strategy.setReachability(r) + + unreachability.foreach { case (_, to) => strategy.addUnreachable(to) } + + strategy.setSeenBy(side.map(_.address)) + + strategy + } else + createStrategy() + } + + } + + "StaticQuorum" must { + class Setup2(size: Int, role: Option[String]) extends StrategySetup { + override def createStrategy() = + new StaticQuorum(selfDc, size, role) + } + + "down unreachable when enough reachable nodes" in new Setup2(3, None) { + side1 = Set(memberA, memberC, memberE) + side2 = Set(memberB, memberD) + assertDowning(side2) + } + + "down reachable when not enough reachable nodes" in { + val setup = new Setup2(size = 3, None) { + side1 = Set(memberA, memberB) + side2 = Set(memberC, memberD) + } + import setup._ + strategy(side1).decide() should ===(DownReachable) + strategy(side2).decide() should ===(DownReachable) + } + + "down unreachable when enough reachable nodes with role" in new Setup2(2, Some("role3")) { + side1 = Set(memberA, memberB, memberC) + side2 = Set(memberD, memberE) + assertDowning(side2) + } + + "down all if N > static-quorum.size * 2 - 1" in new Setup2(3, None) { + side1 = Set(memberA, memberB, memberC) + side2 = Set(memberD, memberE, memberF) + assertDowning(side1.union(side2)) + } + + "handle joining" in { + val setup = new Setup2(size = 3, None) { + side1 = Set(memberA, memberB, joining(memberC)) + side2 = Set(memberD, memberE, joining(memberF)) + } + import setup._ + // Joining not counted + strategy(side1).decide() should ===(DownReachable) + strategy(side2).decide() should ===(DownReachable) + + // if C becomes Up + side1 = Set(memberA, memberB, memberC) + strategy(side1).decide() should ===(DownUnreachable) + strategy(side2).decide() should ===(DownReachable) + + // if F becomes Up, C still Joining + side1 = Set(memberA, memberB, joining(memberC)) + side2 = Set(memberD, memberE, memberF) + strategy(side1).decide() should ===(DownReachable) + strategy(side2).decide() should ===(DownUnreachable) + + // if both C and F become Up, too many + side1 = Set(memberA, memberB, memberC) + side2 = Set(memberD, memberE, memberF) + strategy(side1).decide() should ===(DownAll) + strategy(side2).decide() should ===(DownAll) + } + + "handle leaving/exiting" in { + val setup = new Setup2(size = 3, None) { + side1 = Set(memberA, memberB, leaving(memberC)) + side2 = Set(memberD, memberE) + } + import setup._ + strategy(side1).decide() should ===(DownUnreachable) + strategy(side2).decide() should ===(DownReachable) + + side1 = Set(memberA, memberB, exiting(memberC)) + strategy(side1).decide() should ===(DownReachable) + strategy(side2).decide() should ===(DownReachable) + } + } + + "KeepMajority" must { + class Setup2(role: Option[String]) extends StrategySetup { + override def createStrategy() = + new KeepMajority(selfDc, role) + } + + "down minority partition: {A, C, E} | {B, D} => {A, C, E}" in new Setup2(role = None) { + side1 = Set(memberA, memberC, memberE) + side2 = Set(memberB, memberD) + assertDowning(side2) + } + + "down minority partition: {A, B} | {C, D, E} => {C, D, E}" in new Setup2(role = None) { + side1 = Set(memberA, memberB) + side2 = Set(memberC, memberD, memberE) + assertDowning(side1) + } + + "down self when alone: {B} | {A, C} => {A, C}" in new Setup2(role = None) { + side1 = Set(memberB) + side2 = Set(memberA, memberC) + assertDowning(side1) + } + + "keep half with lowest address when equal size partition: {A, B} | {C, D} => {A, B}" in new Setup2(role = None) { + side1 = Set(memberA, memberB) + side2 = Set(memberC, memberD) + assertDowning(side2) + } + + "keep node with lowest address in two node cluster: {A} | {B} => {A}" in new Setup2(role = None) { + side1 = Set(memberA) + side2 = Set(memberB) + assertDowning(side2) + } + + "down minority partition with role: {A*, B*} | {C, D*, E} => {A*, B*}" in new Setup2(role = Some("role3")) { + side1 = Set(memberA, memberB) + side2 = Set(memberC, memberD, memberE) + assertDowning(side2) + } + + "keep half with lowest address with role when equal size partition: {A, D*, E} | {B, C*} => {B, C*}" in + new Setup2(role = Some("role2")) { + side1 = Set(memberA, memberD, memberE) + side2 = Set(memberB, memberC) + // memberC is lowest with role2 + assertDowning(side1) + } + + "down all when no node with role: {C} | {E} => {}" in new Setup2(role = Some("role3")) { + side1 = Set(memberC) + side2 = Set(memberE) + assertDowning(side1 ++ side2) + } + + "not count joining node, but down it: {B, D} | {Aj, C} => {B, D}" in new Setup2(role = None) { + side1 = Set(memberB, memberD) + side2 = Set(joining(memberA), memberC) + assertDowning(side2) + } + + "down minority partition and joining node: {A, Bj} | {C, D, E} => {C, D, E}" in new Setup2(role = None) { + side1 = Set(memberA, joining(memberB)) + side2 = Set(memberC, memberD, memberE) + assertDowning(side1) + } + + "down each part when split in 3 too small parts: {A, B} | {C, D} | {E} => {}" in new Setup2(role = None) { + side1 = Set(memberA, memberB) + side2 = Set(memberC, memberD) + side3 = Set(memberE) + assertDowningSide(side1, side1) + assertDowningSide(side2, side2) + assertDowningSide(side3, side3) + } + + "detect edge case of membership change: {A, B, F', G'} | {C, D, E} => {A, B, F, G}" in { + val setup = new Setup2(role = None) { + side1 = Set(memberA, memberB, memberF, memberG) + side2 = Set(memberC, memberD, memberE) + } + import setup._ + val strategy1 = strategy(side1) + val decision1 = strategy1.decide() + decision1 should ===(DownUnreachable) + strategy1.nodesToDown(decision1) should ===(side2Nodes) + + // F and G were moved to Up by side1 at the same time as the partition, and that has not been seen by + // side2 so they are still joining + side1 = Set(memberA, memberB, joining(memberF), joining(memberG)) + val strategy2 = strategy(side2) + val decision2 = strategy2.decide() + decision2 should ===(DownAll) + strategy2.nodesToDown(decision2) should ===(side1Nodes.union(side2Nodes)) + } + + "detect edge case of membership change when equal size: {A, B, F'} | {C, D, E} => {A, B, F}" in { + val setup = new Setup2(role = None) { + side1 = Set(memberA, memberB, memberF) + side2 = Set(memberC, memberD, memberE) + } + import setup._ + val strategy1 = strategy(side1) + val decision1 = strategy1.decide() + // memberA is lowest address + decision1 should ===(DownUnreachable) + strategy1.nodesToDown(decision1) should ===(side2Nodes) + + // F was moved to Up by side1 at the same time as the partition, and that has not been seen by + // side2 so it is still joining + side1 = Set(memberA, memberB, joining(memberF)) + val strategy2 = strategy(side2) + val decision2 = strategy2.decide() + // when counting the joining F it becomes equal size + decision2 should ===(DownAll) + strategy2.nodesToDown(decision2) should ===(side1Nodes.union(side2Nodes)) + } + + "detect safe edge case of membership change: {A, B} | {C, D, E, F'} => {C, D, E, F}" in { + val setup = new Setup2(role = None) { + side1 = Set(memberA, memberB) + side2 = Set(memberC, memberD, memberE, joining(memberF)) + } + import setup._ + val strategy1 = strategy(side1) + val decision1 = strategy1.decide() + decision1 should ===(DownReachable) + strategy1.nodesToDown(decision1) should ===(side1Nodes) + + // F was moved to Up by side2 at the same time as the partition + side2 = Set(memberC, memberD, memberE, memberF) + val strategy2 = strategy(side2) + val decision2 = strategy2.decide() + decision2 should ===(DownUnreachable) + strategy2.nodesToDown(decision2) should ===(side1Nodes) + } + + "detect edge case of leaving/exiting membership change: {A', B} | {C, D} => {C, D}" in { + val setup = new Setup2(role = None) { + side1 = Set(leaving(memberA), memberB, joining(memberE)) + side2 = Set(memberC, memberD) + } + import setup._ + val strategy1 = strategy(side1) + val decision1 = strategy1.decide() + decision1 should ===(DownAll) + strategy1.nodesToDown(decision1) should ===(side1Nodes.union(side2Nodes)) + + // A was moved to Exiting by side2 at the same time as the partition, and that has not been seen by + // side1 so it is still Leaving there + side1 = Set(exiting(memberA), memberB) + val strategy2 = strategy(side2) + val decision2 = strategy2.decide() + decision2 should ===(DownUnreachable) + // A is already Exiting so not downed + strategy2.nodesToDown(decision2) should ===(side1Nodes - memberA.uniqueAddress) + } + + "down indirectly connected: {(A, B)} => {}" in new Setup2(role = None) { + side1 = Set(memberA, memberB) + indirectlyConnected = List(memberA -> memberB, memberB -> memberA) + assertDowning(Set(memberA, memberB)) + } + + "down indirectly connected: {(A, B), C} => {C}" in new Setup2(role = None) { + side1 = Set(memberA, memberB, memberC) + indirectlyConnected = List(memberA -> memberB, memberB -> memberA) + // keep fully connected memberC + assertDowning(Set(memberA, memberB)) + } + + "down indirectly connected: {(A, B, C)} => {}" in new Setup2(role = None) { + side1 = Set(memberA, memberB, memberC) + indirectlyConnected = List(memberA -> memberB, memberB -> memberC, memberC -> memberA) + assertDowning(Set(memberA, memberB, memberC)) + } + + "down indirectly connected: {(A, B, C, D)} => {}" in new Setup2(role = None) { + side1 = Set(memberA, memberB, memberC, memberD) + indirectlyConnected = List(memberA -> memberD, memberD -> memberA, memberB -> memberC, memberC -> memberB) + assertDowning(Set(memberA, memberB, memberC, memberD)) + } + + "down indirectly connected: {(A, B, C), D, E} => {D, E}" in new Setup2(role = None) { + side1 = Set(memberA, memberB, memberC, memberD, memberE) + indirectlyConnected = List(memberA -> memberB, memberB -> memberC, memberC -> memberA) + // keep fully connected memberD, memberE + assertDowning(Set(memberA, memberB, memberC)) + } + + "down indirectly connected{A, (B, C), D, (E, F), G} => {A, D, G}" in new Setup2(role = None) { + side1 = Set(memberA, memberB, memberC, memberD, memberE, memberF, memberG) + // two groups of indirectly connected, 4 in total + indirectlyConnected = List(memberB -> memberC, memberC -> memberB, memberE -> memberF, memberF -> memberE) + // keep fully connected memberA, memberD, memberG + assertDowning(Set(memberB, memberC, memberE, memberF)) + } + + "down indirectly connected, detected via seen: {(A, B, C)} => {}" in new Setup2(role = None) { + side1 = Set(memberA, memberB, memberC) + indirectlyConnected = List(memberA -> memberB, memberA -> memberC) + assertDowning(Set(memberA, memberB, memberC)) + } + + "down indirectly connected, detected via seen: {(A, B, C, D), E} => {E}" in new Setup2(role = None) { + side1 = Set(memberA, memberB, memberC, memberD, memberE) + indirectlyConnected = List(memberB -> memberC, memberC -> memberB, memberA -> memberD) + // keep fully connected memberE + assertDowning(Set(memberA, memberB, memberC, memberD)) + } + + "down indirectly connected when combined with crashed: {(A, B), D, E} | {C} => {D, E}" in new Setup2(role = None) { + side1 = Set(memberA, memberB, memberD, memberE) + side2 = Set(memberC) + indirectlyConnected = List(memberA -> memberB, memberB -> memberA) + // keep fully connected memberD, memberE + // note that crashed memberC is also downed + assertDowningSide(side1, Set(memberA, memberB, memberC)) + } + + "down indirectly connected when combined with clean partition: {A, (B, C)} | {D, E} => {A}" in new Setup2( + role = None) { + side1 = Set(memberA, memberB, memberC) + side2 = Set(memberD, memberE) + indirectlyConnected = List(memberB -> memberC, memberC -> memberB) + + // from side1 of the partition + // keep fully connected memberA + // note that memberD and memberE on the other side of the partition are also downed because side1 + // is majority of clean partition + assertDowningSide(side1, Set(memberB, memberC, memberD, memberE)) + + // from side2 of the partition + // indirectly connected not seen from this side, if clean partition happened first + indirectlyConnected = Nil + // Note that memberC is not downed, as on the other side, because those indirectly connected + // not seen from this side. That outcome is OK. + assertDowningSide(side2, Set(memberD, memberE)) + + // alternative scenario from side2 of the partition + // indirectly connected on side1 happens before the clean partition + indirectlyConnected = List(memberB -> memberC, memberC -> memberB) + assertDowningSide(side2, Set(memberB, memberC, memberD, memberE)) + } + + "down indirectly connected on minority side, when combined with clean partition: {A, (B, C)} | {D, E, F, G} => {D, E, F, G}" in new Setup2( + role = None) { + side1 = Set(memberA, memberB, memberC) + side2 = Set(memberD, memberE, memberF, memberG) + indirectlyConnected = List(memberB -> memberC, memberC -> memberB) + + // from side1 of the partition, minority + assertDowningSide(side1, Set(memberA, memberB, memberC)) + + // from side2 of the partition, majority + // indirectly connected not seen from this side, if clean partition happened first + indirectlyConnected = Nil + assertDowningSide(side2, Set(memberA, memberB, memberC)) + + // alternative scenario from side2 of the partition + // indirectly connected on side1 happens before the clean partition + indirectlyConnected = List(memberB -> memberC, memberC -> memberB) + assertDowningSide(side2, Set(memberA, memberB, memberC)) + } + + "down indirectly connected on majority side, when combined with clean partition: {A, B, C} | {(D, E), F, G} => {F, G}" in new Setup2( + role = None) { + side1 = Set(memberA, memberB, memberC) + side2 = Set(memberD, memberE, memberF, memberG) + + // from side1 of the partition, minority + // indirectly connected not seen from this side, if clean partition happened first + indirectlyConnected = Nil + assertDowningSide(side1, Set(memberA, memberB, memberC)) + + // alternative scenario from side1 of the partition + // indirectly connected on side2 happens before the clean partition + indirectlyConnected = List(memberD -> memberE, memberE -> memberD) + // note that indirectly connected memberD and memberE are also downed + assertDowningSide(side1, Set(memberA, memberB, memberC, memberD, memberE)) + + // from side2 of the partition, majority + indirectlyConnected = List(memberD -> memberE, memberE -> memberD) + assertDowningSide(side2, Set(memberA, memberB, memberC, memberD, memberE)) + } + + "down indirectly connected spanning across a clean partition: {A, (B), C} | {D, (E, F), G} => {D, G}" in new Setup2( + role = None) { + side1 = Set(memberA, memberB, memberC) + side2 = Set(memberD, memberE, memberF, memberG) + indirectlyConnected = List(memberB -> memberE, memberE -> memberF, memberF -> memberB) + + // from side1 of the partition, minority + assertDowningSide(side1, Set(memberA, memberB, memberC, memberE, memberF)) + + // from side2 of the partition, majority + assertDowningSide(side2, Set(memberA, memberB, memberC, memberE, memberF)) + } + + "down indirectly connected, detected via seen, combined with clean partition: {A, B, C} | {(D, E), (F, G)} => {}" in new Setup2( + role = None) { + side1 = Set(memberA, memberB, memberC) + side2 = Set(memberD, memberE, memberF, memberG) + + // from side1 of the partition, minority + assertDowningSide(side1, Set(memberA, memberB, memberC)) + + // from side2 of the partition, majority + indirectlyConnected = List(memberD -> memberE, memberG -> memberF) + assertDowningSide(side2, Set(memberA, memberB, memberC, memberD, memberE, memberF, memberG)) + } + + "double DownIndirectlyConnected when indirectly connected happens before clean partition: {A, B, C} | {(D, E), (F, G)} => {}" in new Setup2( + role = None) { + side1 = Set(memberA, memberB, memberC) + side2 = Set(memberD, memberE, memberF, memberG) + // trouble when indirectly connected happens before clean partition + indirectlyConnected = List(memberD -> memberE, memberG -> memberF) + + // from side1 of the partition, minority + // D and G are observers and marked E and F as unreachable + // A has marked D and G as unreachable + // The records D->E, G->F are not removed in the second decision because they are not detected via seenB + // due to clean partition. That means that the second decision will also be DownIndirectlyConnected. To bail + // out from this situation the strategy will throw IllegalStateException, which is caught and translated to + // DownAll. + intercept[IllegalStateException] { + assertDowningSide(side1, Set(memberA, memberB, memberC)) + } + + // from side2 of the partition, majority + assertDowningSide(side2, Set(memberA, memberB, memberC, memberD, memberE, memberF, memberG)) + } + + } + + "KeepOldest" must { + class Setup2(downIfAlone: Boolean = true, role: Option[String] = None) extends StrategySetup { + override def createStrategy() = new KeepOldest(selfDc, downIfAlone, role) + } + + "keep partition with oldest" in new Setup2 { + // E is the oldest + side1 = Set(memberA, memberE) + side2 = Set(memberB, memberC, memberD) + assertDowning(side2) + } + + "keep partition with oldest with role" in new Setup2(role = Some("role2")) { + // C and D have role2, D is the oldest + side1 = Set(memberA, memberE) + side2 = Set(memberB, memberC, memberD) + assertDowning(side1) + } + + "keep partition with oldest unless alone" in new Setup2(downIfAlone = true) { + side1 = Set(memberE) + side2 = Set(memberA, memberB, memberC, memberD) + assertDowning(side1) + } + + "keep partition with oldest in two nodes cluster" in new Setup2 { + side1 = Set(memberB) + side2 = Set(memberA) + assertDowning(side2) + } + + "keep one single oldest" in new Setup2 { + side1 = Set.empty + side2 = Set(memberA) + assertDowning(side1) + } + + "keep oldest even when alone when downIfAlone = false" in new Setup2(downIfAlone = false) { + side1 = Set(memberE) + side2 = Set(memberA, memberB, memberC, memberD) + assertDowning(side2) + } + + "detect leaving/exiting edge case: keep partition with oldest, scenario 1" in { + val setup = new Setup2(role = None) { + side1 = Set(memberA, memberB, memberD) + side2 = Set(memberC, exiting(memberE)) + } + import setup._ + val strategy1 = strategy(side1) + val decision1 = strategy1.decide() + // E is Exiting so not counted as oldest, D is oldest + decision1 should ===(DownUnreachable) + // side2 is downed, but E is already exiting and therefore not downed + strategy1.nodesToDown(decision1) should ===(side2Nodes - memberE.uniqueAddress) + + // E was changed to Exiting by side1 but that is not seen on side2 due to the partition, so still Leaving + side2 = Set(memberC, leaving(memberE)) + val strategy2 = strategy(side2) + val decision2 = strategy2.decide() + + decision2 should ===(DownAll) + strategy2.nodesToDown(decision2) should ===(side1Nodes.union(side2Nodes)) + } + + "detect leaving/exiting edge case: keep partition with oldest, scenario 2" in { + val setup = new Setup2(role = None) { + side1 = Set(memberA, leaving(memberE)) + side2 = Set(memberB, memberC, memberD) + } + import setup._ + strategy(side1).decide() should ===(DownAll) + strategy(side2).decide() should ===(DownReachable) + } + + "detect leaving/exiting edge case: keep partition with oldest, scenario 3" in new Setup2 { + // E is the oldest + side1 = Set(memberA, memberE) + side2 = Set(leaving(memberB), leaving(memberC), memberD) + assertDowning(side2) + } + + "detect leaving/exiting edge case: keep partition with oldest unless alone, scenario 1" in { + val setup = new Setup2(role = None) { + side1 = Set(leaving(memberD), memberE) + side2 = Set(memberA, memberB, memberC) + } + import setup._ + strategy(side1).decide() should ===(DownAll) + strategy(side2).decide() should ===(DownReachable) + } + + "detect leaving/exiting edge case: keep partition with oldest unless alone, scenario 4" in { + val setup = new Setup2(role = None) { + side1 = Set(memberE) + side2 = Set(memberA, memberB, leaving(memberC)) + } + import setup._ + strategy(side1).decide() should ===(DownReachable) + strategy(side2).decide() should ===(DownUnreachable) + } + + "detect leaving/exiting edge case: keep partition with oldest unless alone, scenario 3" in { + val setup = new Setup2(role = None) { + side1 = Set(memberE) + side2 = Set(memberA, leaving(memberB), leaving(memberC), leaving(memberD)) + } + import setup._ + strategy(side1).decide() should ===(DownReachable) + strategy(side2).decide() should ===(DownAll) + } + + "detect leaving/exiting edge case: DownReachable on both sides when oldest leaving/exiting is alone" in { + val setup = new Setup2(role = None) { + side1 = Set(memberD, exiting(memberE)) + side2 = Set(memberA, memberB, memberC) + } + import setup._ + // E is Exiting so not counted as oldest, D is oldest, but it's alone so keep side2 anyway + strategy(side1).decide() should ===(DownReachable) + + // E was changed to Exiting by side1 but that is not seen on side2 due to the partition, so still Leaving + side1 = Set(memberD, leaving(memberE)) + strategy(side2).decide() should ===(DownReachable) + } + + "detect leaving/exiting edge case: when one single oldest" in { + val setup = new Setup2(role = None) { + side1 = Set(memberA) + side2 = Set(exiting(memberB)) + } + import setup._ + // B is Exiting so not counted as oldest, A is oldest + strategy(side1).decide() should ===(DownUnreachable) + + // B was changed to Exiting by side1 but that is not seen on side2 due to the partition, so still Leaving + side2 = Set(leaving(memberB)) + strategy(side2).decide() should ===(DownAll) + } + + "detect joining/up edge case: keep partition with oldest unless alone, scenario 1" in { + val setup = new Setup2(role = None) { + side1 = Set(joining(memberA), memberE) + side2 = Set(memberB, memberC, memberD) + } + import setup._ + // E alone when not counting joining A + strategy(side1).decide() should ===(DownReachable) + // but A could have been up on other side1 and therefore side2 has to down all + strategy(side2).decide() should ===(DownAll) + } + + "detect joining/up edge case: keep oldest even when alone when downIfAlone = false" in { + val setup = new Setup2(downIfAlone = false) { + side1 = Set(joining(memberA), memberE) + side2 = Set(memberB, memberC, memberD) + } + import setup._ + // joining A shouldn't matter when downIfAlone = false + strategy(side1).decide() should ===(DownUnreachable) + strategy(side2).decide() should ===(DownReachable) + } + + "down indirectly connected: {(A, B), C} => {C}" in new Setup2 { + side1 = Set(memberA, memberB, memberC) + indirectlyConnected = List(memberA -> memberB, memberB -> memberA) + assertDowning(Set(memberA, memberB)) + } + + "down indirectly connected on younger side, when combined with clean partition: {A, (B, C)} | {D, E, F, G} => {D, E, F, G}" in new Setup2 { + side1 = Set(memberA, memberB, memberC) + side2 = Set(memberD, memberE, memberF, memberG) + indirectlyConnected = List(memberB -> memberC, memberC -> memberB) + + // from side1 of the partition, younger + assertDowningSide(side1, Set(memberA, memberB, memberC)) + + // from side2 of the partition, oldest + // indirectly connected not seen from this side, if clean partition happened first + indirectlyConnected = Nil + assertDowningSide(side2, Set(memberA, memberB, memberC)) + + // alternative scenario from side2 of the partition + // indirectly connected on side1 happens before the clean partition + indirectlyConnected = List(memberB -> memberC, memberC -> memberB) + assertDowningSide(side2, Set(memberA, memberB, memberC)) + } + + "down indirectly connected on oldest side, when combined with clean partition: {A, B, C} | {(D, E), F, G} => {F, G}" in new Setup2 { + side1 = Set(memberA, memberB, memberC) + side2 = Set(memberD, memberE, memberF, memberG) + + // from side1 of the partition, younger + // indirectly connected not seen from this side, if clean partition happened first + indirectlyConnected = Nil + assertDowningSide(side1, Set(memberA, memberB, memberC)) + + // alternative scenario from side1 of the partition + // indirectly connected on side2 happens before the clean partition + indirectlyConnected = List(memberD -> memberE, memberE -> memberD) + // note that indirectly connected memberD and memberE are also downed + assertDowningSide(side1, Set(memberA, memberB, memberC, memberD, memberE)) + + // from side2 of the partition, oldest + indirectlyConnected = List(memberD -> memberE, memberE -> memberD) + assertDowningSide(side2, Set(memberA, memberB, memberC, memberD, memberE)) + } + + } + + "DownAllNodes" must { + class Setup2 extends StrategySetup { + override def createStrategy() = new DownAllNodes(selfDc) + } + + "down all" in new Setup2 { + side1 = Set(memberA, memberB, memberC) + side2 = Set(memberD, memberE) + assertDowning(side1.union(side2)) + } + + "down all when any indirectly connected: {(A, B), C} => {}" in new Setup2 { + side1 = Set(memberA, memberB, memberC) + indirectlyConnected = List(memberA -> memberB, memberB -> memberA) + assertDowning(side1) + } + } + + "LeaseMajority" must { + class Setup2(role: Option[String]) extends StrategySetup { + val testLease: TestLease = new TestLease(testLeaseSettings, extSystem) + + val acquireLeaseDelayForMinority: FiniteDuration = 2.seconds + + override def createStrategy() = + new LeaseMajority(selfDc, role, testLease, acquireLeaseDelayForMinority) + } + + "decide AcquireLeaseAndDownUnreachable, and DownReachable as reverse decision" in { + val setup = new Setup2(role = None) { + side1 = Set(memberA, memberC, memberE) + side2 = Set(memberB, memberD) + } + import setup._ + val strategy1 = strategy(side1) + val decision1 = strategy1.decide() + decision1 should ===(AcquireLeaseAndDownUnreachable(Duration.Zero)) + strategy1.nodesToDown(decision1) should ===(side2Nodes) + val reverseDecision1 = strategy1.reverseDecision(decision1) + reverseDecision1 should ===(DownReachable) + strategy1.nodesToDown(reverseDecision1) should ===(side1Nodes) + + val strategy2 = strategy(side2) + val decision2 = strategy2.decide() + decision2 should ===(AcquireLeaseAndDownUnreachable(acquireLeaseDelayForMinority)) + strategy2.nodesToDown(decision2) should ===(side1Nodes) + val reverseDecision2 = strategy2.reverseDecision(decision2) + reverseDecision2 should ===(DownReachable) + strategy2.nodesToDown(reverseDecision2) should ===(side2Nodes) + } + + "try to keep half with lowest address when equal size partition" in { + val setup = new Setup2(role = Some("role2")) { + side1 = Set(memberA, memberD, memberE) + side2 = Set(memberB, memberC) + // memberC is lowest with role2 + } + import setup._ + val strategy1 = strategy(side1) + val decision1 = strategy1.decide() + // delay on side1 because memberC is lowest address with role2 + decision1 should ===(AcquireLeaseAndDownUnreachable(acquireLeaseDelayForMinority)) + strategy1.nodesToDown(decision1) should ===(side2Nodes) + + val strategy2 = strategy(side2) + val decision2 = strategy2.decide() + decision2 should ===(AcquireLeaseAndDownUnreachable(Duration.Zero)) + strategy2.nodesToDown(decision2) should ===(side1Nodes) + } + + "down indirectly connected: {(A, B), C} => {C}" in { + val setup = new Setup2(role = None) { + side1 = Set(memberA, memberB, memberC) + indirectlyConnected = List(memberA -> memberB, memberB -> memberA) + } + import setup._ + val strategy1 = strategy(side1) + val decision1 = strategy1.decide() + decision1 should ===(AcquireLeaseAndDownIndirectlyConnected(Duration.Zero)) + strategy1.nodesToDown(decision1) should ===(Set(memberA.uniqueAddress, memberB.uniqueAddress)) + val reverseDecision1 = strategy1.reverseDecision(decision1) + reverseDecision1 should ===(ReverseDownIndirectlyConnected) + strategy1.nodesToDown(reverseDecision1) should ===(side1Nodes) + } + + "down indirectly connected when combined with clean partition: {A, (B, C)} | {D, E} => {A}" in { + val setup = new Setup2(role = None) { + side1 = Set(memberA, memberB, memberC) + side2 = Set(memberD, memberE) + indirectlyConnected = List(memberB -> memberC, memberC -> memberB) + } + import setup._ + + // from side1 of the partition + // keep fully connected memberA + // note that memberD and memberE on the other side of the partition are also downed + val strategy1 = strategy(side1) + val decision1 = strategy1.decide() + decision1 should ===(AcquireLeaseAndDownIndirectlyConnected(Duration.Zero)) + strategy1.nodesToDown(decision1) should ===(Set(memberB, memberC, memberD, memberE).map(_.uniqueAddress)) + val reverseDecision1 = strategy1.reverseDecision(decision1) + reverseDecision1 should ===(ReverseDownIndirectlyConnected) + strategy1.nodesToDown(reverseDecision1) should ===(side1Nodes) + + // from side2 of the partition + // indirectly connected not seen from this side, if clean partition happened first + indirectlyConnected = Nil + // Note that memberC is not downed, as on the other side, because those indirectly connected + // not seen from this side. That outcome is OK. + val strategy2 = strategy(side2) + val decision2 = strategy2.decide() + decision2 should ===(AcquireLeaseAndDownUnreachable(acquireLeaseDelayForMinority)) + strategy2.nodesToDown(decision2) should ===(side1Nodes) + val reverseDecision2 = strategy2.reverseDecision(decision2) + reverseDecision2 should ===(DownReachable) + strategy2.nodesToDown(reverseDecision2) should ===(side2Nodes) + + // alternative scenario from side2 of the partition + // indirectly connected on side1 happens before the clean partition + indirectlyConnected = List(memberB -> memberC, memberC -> memberB) + val strategy3 = strategy(side2) + val decision3 = strategy3.decide() + decision3 should ===(AcquireLeaseAndDownIndirectlyConnected(Duration.Zero)) + strategy3.nodesToDown(decision3) should ===(side1Nodes) + val reverseDecision3 = strategy3.reverseDecision(decision3) + reverseDecision3 should ===(ReverseDownIndirectlyConnected) + strategy3.nodesToDown(reverseDecision3) should ===(Set(memberB, memberC, memberD, memberE).map(_.uniqueAddress)) + + } + } + + "Strategy" must { + + class MajoritySetup(role: Option[String] = None) extends StrategySetup { + override def createStrategy() = new KeepMajority(selfDc, role) + } + + class OldestSetup(role: Option[String] = None) extends StrategySetup { + override def createStrategy() = new KeepOldest(selfDc, downIfAlone = true, role) + } + + "add and remove members with default Member ordering" in { + val setup = new MajoritySetup(role = None) { + side1 = Set.empty + side2 = Set.empty + } + import setup._ + val strategy1 = strategy(side1) + testAddRemove(strategy1) + } + + "add and remove members with oldest Member ordering" in { + val setup = new OldestSetup(role = None) { + side1 = Set.empty + side2 = Set.empty + } + testAddRemove(setup.strategy(setup.side1)) + } + + def testAddRemove(strategy: DowningStrategy) = { + strategy.add(joining(memberA)) + strategy.add(joining(memberB)) + strategy.allMembersInDC.size should ===(2) + strategy.allMembersInDC.foreach { + _.status should ===(MemberStatus.Joining) + } + strategy.add(memberA) + strategy.add(memberB) + strategy.allMembersInDC.size should ===(2) + strategy.allMembersInDC.foreach { + _.status should ===(MemberStatus.Up) + } + strategy.add(leaving(memberB)) + strategy.allMembersInDC.size should ===(2) + strategy.allMembersInDC.toList.map(_.status).toSet should ===(Set(MemberStatus.Up, MemberStatus.Leaving)) + strategy.add(exiting(memberB)) + strategy.allMembersInDC.size should ===(2) + strategy.allMembersInDC.toList.map(_.status).toSet should ===(Set(MemberStatus.Up, MemberStatus.Exiting)) + strategy.remove(memberA) + strategy.allMembersInDC.size should ===(1) + strategy.allMembersInDC.head.status should ===(MemberStatus.Exiting) + } + + "collect and filter members with default Member ordering" in { + val setup = new MajoritySetup(role = None) { + side1 = Set.empty + side2 = Set.empty + } + + testCollectAndFilter(setup) + } + + "collect and filter members with oldest Member ordering" in { + val setup = new OldestSetup(role = None) { + side1 = Set.empty + side2 = Set.empty + } + + testCollectAndFilter(setup) + } + + def testCollectAndFilter(setup: StrategySetup): Unit = { + import setup._ + + side1 = Set(memberAWeaklyUp, memberB, joining(memberC)) + side2 = Set(memberD, leaving(memberE), downed(memberF), exiting(memberG)) + + val strategy1 = strategy(side1) + + strategy1.membersWithRole should ===(Set(memberB, memberD, leaving(memberE))) + strategy1.membersWithRole(includingPossiblyUp = true, excludingPossiblyExiting = false) should ===( + Set(memberAWeaklyUp, memberB, joining(memberC), memberD, leaving(memberE))) + strategy1.membersWithRole(includingPossiblyUp = false, excludingPossiblyExiting = true) should ===( + Set(memberB, memberD)) + strategy1.membersWithRole(includingPossiblyUp = true, excludingPossiblyExiting = true) should ===( + Set(memberAWeaklyUp, memberB, joining(memberC), memberD)) + + strategy1.reachableMembersWithRole should ===(Set(memberB)) + strategy1.reachableMembersWithRole(includingPossiblyUp = true, excludingPossiblyExiting = false) should ===( + Set(memberAWeaklyUp, memberB, joining(memberC))) + strategy1.reachableMembersWithRole(includingPossiblyUp = false, excludingPossiblyExiting = true) should ===( + Set(memberB)) + strategy1.reachableMembersWithRole(includingPossiblyUp = true, excludingPossiblyExiting = true) should ===( + Set(memberAWeaklyUp, memberB, joining(memberC))) + + strategy1.unreachableMembersWithRole should ===(Set(memberD, leaving(memberE))) + strategy1.unreachableMembers(includingPossiblyUp = true, excludingPossiblyExiting = false) should ===( + Set(memberD, leaving(memberE))) + strategy1.unreachableMembers(includingPossiblyUp = false, excludingPossiblyExiting = true) should ===( + Set(memberD)) + strategy1.unreachableMembers(includingPossiblyUp = true, excludingPossiblyExiting = true) should ===(Set(memberD)) + + strategy1.unreachable(memberAWeaklyUp) should ===(false) + strategy1.unreachable(memberB) should ===(false) + strategy1.unreachable(memberD) should ===(true) + strategy1.unreachable(leaving(memberE)) should ===(true) + strategy1.unreachable(downed(memberF)) should ===(true) + strategy1.joining should ===(Set(memberAWeaklyUp, joining(memberC))) + + val strategy2 = strategy(side2) + + strategy2.membersWithRole should ===(Set(memberB, memberD, leaving(memberE))) + strategy2.membersWithRole(includingPossiblyUp = true, excludingPossiblyExiting = false) should ===( + Set(memberAWeaklyUp, memberB, joining(memberC), memberD, leaving(memberE))) + strategy2.membersWithRole(includingPossiblyUp = false, excludingPossiblyExiting = true) should ===( + Set(memberB, memberD)) + strategy2.membersWithRole(includingPossiblyUp = true, excludingPossiblyExiting = true) should ===( + Set(memberAWeaklyUp, memberB, joining(memberC), memberD)) + + strategy2.unreachableMembersWithRole should ===(Set(memberB)) + strategy2.unreachableMembersWithRole(includingPossiblyUp = true, excludingPossiblyExiting = false) should ===( + Set(memberAWeaklyUp, memberB, joining(memberC))) + strategy2.unreachableMembersWithRole(includingPossiblyUp = false, excludingPossiblyExiting = true) should ===( + Set(memberB)) + strategy2.unreachableMembersWithRole(includingPossiblyUp = true, excludingPossiblyExiting = true) should ===( + Set(memberAWeaklyUp, memberB, joining(memberC))) + + strategy2.reachableMembersWithRole should ===(Set(memberD, leaving(memberE))) + strategy2.reachableMembers(includingPossiblyUp = true, excludingPossiblyExiting = false) should ===( + Set(memberD, leaving(memberE))) + strategy2.reachableMembers(includingPossiblyUp = false, excludingPossiblyExiting = true) should ===(Set(memberD)) + strategy2.reachableMembers(includingPossiblyUp = true, excludingPossiblyExiting = true) should ===(Set(memberD)) + + strategy2.unreachable(memberAWeaklyUp) should ===(true) + strategy2.unreachable(memberB) should ===(true) + strategy2.unreachable(memberD) should ===(false) + strategy2.unreachable(leaving(memberE)) should ===(false) + strategy2.unreachable(downed(memberF)) should ===(false) + strategy2.joining should ===(Set(memberAWeaklyUp, joining(memberC))) + } + } + + "Split Brain Resolver" must { + + class SetupKeepMajority( + stableAfter: FiniteDuration, + selfUniqueAddress: UniqueAddress, + role: Option[String], + downAllWhenUnstable: FiniteDuration = Duration.Zero, + tickInterval: FiniteDuration = Duration.Zero) + extends Setup(stableAfter, new KeepMajority(selfDc, role), selfUniqueAddress, downAllWhenUnstable, tickInterval) + + class SetupKeepOldest( + stableAfter: FiniteDuration, + selfUniqueAddress: UniqueAddress, + downIfAlone: Boolean, + role: Option[String]) + extends Setup(stableAfter, new KeepOldest(selfDc, downIfAlone, role), selfUniqueAddress) + + class SetupStaticQuorum( + stableAfter: FiniteDuration, + selfUniqueAddress: UniqueAddress, + size: Int, + role: Option[String]) + extends Setup(stableAfter, new StaticQuorum(selfDc, size, role), selfUniqueAddress) + + class SetupDownAllNodes(stableAfter: FiniteDuration, selfUniqueAddress: UniqueAddress) + extends Setup(stableAfter, new DownAllNodes(selfDc), selfUniqueAddress) + + class SetupLeaseMajority( + stableAfter: FiniteDuration, + selfUniqueAddress: UniqueAddress, + role: Option[String], + val testLease: TestLease, + downAllWhenUnstable: FiniteDuration = Duration.Zero, + tickInterval: FiniteDuration = Duration.Zero) + extends Setup( + stableAfter, + new LeaseMajority(selfDc, role, testLease, acquireLeaseDelayForMinority = 20.millis), + selfUniqueAddress, + downAllWhenUnstable, + tickInterval) + + abstract class Setup( + stableAfter: FiniteDuration, + strategy: DowningStrategy, + selfUniqueAddress: UniqueAddress, + downAllWhenUnstable: FiniteDuration = Duration.Zero, + tickInterval: FiniteDuration = Duration.Zero) { + + val a = system.actorOf( + DowningTestActor + .props(stableAfter, strategy, testActor, selfUniqueAddress, selfDc, downAllWhenUnstable, tickInterval)) + + def memberUp(members: Member*): Unit = + members.foreach(m => a ! MemberUp(m)) + + def memberWeaklyUp(members: Member*): Unit = + members.foreach(m => a ! MemberWeaklyUp(m)) + + def leader(member: Member): Unit = + a ! LeaderChanged(Some(member.address)) + + def unreachable(members: Member*): Unit = + members.foreach(m => a ! UnreachableMember(m)) + + def reachabilityChanged(unreachability: (Member, Member)*): Unit = { + unreachable(unreachability.map { case (_, to) => to }: _*) + + val r = createReachability(unreachability) + a ! ReachabilityChanged(r) + } + + def remove(members: Member*): Unit = + members.foreach(m => a ! MemberRemoved(m.copy(Removed), previousStatus = Exiting)) + + def dcUnreachable(members: Member*): Unit = + members.map(_.dataCenter).toSet[DataCenter].foreach(dc => a ! UnreachableDataCenter(dc)) + + def dcReachable(members: Member*): Unit = + members.map(_.dataCenter).toSet[DataCenter].foreach(dc => a ! ReachableDataCenter(dc)) + + def reachable(members: Member*): Unit = + members.foreach(m => a ! ReachableMember(m)) + + def tick(): Unit = a ! SplitBrainResolver.Tick + + def expectDownCalled(members: Member*): Unit = + receiveN(members.length).toSet should be(members.map(m => DownCalled(m.address)).toSet) + + def expectNoDecision(max: FiniteDuration): Unit = + expectNoMessage(max) + + def stop(): Unit = { + system.stop(a) + expectNoMessage(100.millis) + } + } + + "have downRemovalMargin equal to stable-after" in { + val cluster = Cluster(system) + val sbrSettings = new SplitBrainResolverSettings(system.settings.config) + cluster.downingProvider.downRemovalMargin should be(sbrSettings.DowningStableAfter) + } + + "down unreachable when leader" in new SetupKeepMajority(Duration.Zero, memberA.uniqueAddress, role = None) { + memberUp(memberA, memberB, memberC) + leader(memberA) + unreachable(memberB) + tick() + expectDownCalled(memberB) + stop() + } + + "not down unreachable when not leader" in new SetupKeepMajority(Duration.Zero, memberB.uniqueAddress, role = None) { + memberUp(memberA, memberB, memberC) + leader(memberA) + unreachable(memberC) + tick() + expectNoMessage(500.millis) + stop() + } + + "down unreachable when becoming leader" in new SetupKeepMajority( + stableAfter = Duration.Zero, + memberA.uniqueAddress, + role = None) { + memberUp(memberA, memberB, memberC) + leader(memberB) + unreachable(memberC) + leader(memberA) + tick() + expectDownCalled(memberC) + stop() + } + + "down unreachable after specified duration" in new SetupKeepMajority( + stableAfter = 2.seconds, + memberA.uniqueAddress, + role = None) { + memberUp(memberA, memberB, memberC) + leader(memberA) + unreachable(memberB) + expectNoMessage(1.second) + expectDownCalled(memberB) + stop() + } + + "down unreachable when becoming leader inbetween detection and specified duration" in new SetupKeepMajority( + stableAfter = 2.seconds, + memberA.uniqueAddress, + role = None) { + memberUp(memberA, memberB, memberC) + leader(memberB) + unreachable(memberC) + leader(memberA) + tick() + expectNoMessage(1.second) + expectDownCalled(memberC) + stop() + } + + "not down unreachable when loosing leadership inbetween detection and specified duration" in new SetupKeepMajority( + stableAfter = 1.seconds, + memberA.uniqueAddress, + role = None) { + memberUp(memberA, memberB, memberC) + leader(memberA) + unreachable(memberC) + leader(memberB) + tick() + expectNoMessage(1500.millis) + stop() + } + + // reproducer of issue #436 + "down when becoming Weakly-Up leader" in new SetupKeepMajority( + stableAfter = Duration.Zero, + memberAWeaklyUp.uniqueAddress, + role = None) { + memberUp(memberC) + memberWeaklyUp(memberAWeaklyUp, memberBWeaklyUp) + unreachable(memberC) + leader(memberAWeaklyUp) + tick() + expectDownCalled(memberAWeaklyUp, memberBWeaklyUp) + stop() + } + + "not down when unreachable become reachable inbetween detection and specified duration" in new SetupKeepMajority( + stableAfter = 1.seconds, + memberA.uniqueAddress, + role = None) { + memberUp(memberA, memberB, memberC) + leader(memberA) + unreachable(memberB) + reachable(memberB) + tick() + expectNoMessage(1500.millis) + stop() + } + + "not down when unreachable is removed inbetween detection and specified duration" in new SetupKeepMajority( + stableAfter = 1.seconds, + memberA.uniqueAddress, + role = None) { + memberUp(memberA, memberB, memberC) + leader(memberA) + unreachable(memberB) + a ! MemberRemoved(memberB.copy(Removed), previousStatus = Exiting) + tick() + expectNoMessage(1500.millis) + stop() + } + + "not down when unreachable is already Down" in new SetupKeepMajority( + stableAfter = Duration.Zero, + memberA.uniqueAddress, + role = None) { + memberUp(memberA, memberB, memberC) + leader(memberA) + unreachable(memberB.copy(Down)) + tick() + expectNoMessage(1000.millis) + stop() + } + + "down minority partition" in new SetupKeepMajority(stableAfter = Duration.Zero, memberA.uniqueAddress, role = None) { + memberUp(memberA, memberB, memberC, memberD, memberE) + leader(memberA) + reachabilityChanged(memberA -> memberB, memberC -> memberD) + tick() + expectDownCalled(memberB, memberD) + stop() + } + + "keep partition with oldest" in new SetupKeepOldest( + stableAfter = Duration.Zero, + memberA.uniqueAddress, + downIfAlone = true, + role = None) { + memberUp(memberA, memberB, memberC, memberD, memberE) + leader(memberA) + reachabilityChanged(memberA -> memberB, memberA -> memberC, memberE -> memberD) + tick() + expectDownCalled(memberB, memberC, memberD) + stop() + } + + "log warning if N > static-quorum.size * 2 - 1" in new SetupStaticQuorum( + stableAfter = Duration.Zero, + memberA.uniqueAddress, + size = 2, + role = None) { + EventFilter.warning(pattern = "cluster size is \\[4\\].*not add more than \\[3\\]", occurrences = 1).intercept { + memberUp(memberA, memberB, memberC, memberD) + } + leader(memberA) + unreachable(memberC, memberD) + tick() + // down all + expectDownCalled(memberA, memberB, memberC, memberD) + stop() + } + + "not care about partition across data centers" in new SetupKeepMajority( + stableAfter = Duration.Zero, + memberA.uniqueAddress, + role = None) { + memberUp(dataCenter(selfDc, memberA, memberB, memberC).toList: _*) + memberUp(dataCenter("other", memberD, memberE).toList: _*) + leader(memberA) + unreachable(memberB) + dcUnreachable(dataCenter("other", memberD, memberE).toList: _*) + tick() + expectDownCalled(memberB) + stop() + } + + "not count members from other data centers" in new SetupKeepMajority( + stableAfter = Duration.Zero, + memberA.uniqueAddress, + role = None) { + memberUp(dataCenter(selfDc, memberA, memberB, memberC).toList: _*) + memberUp(dataCenter("other", memberD, memberE).toList: _*) + leader(memberA) + unreachable(memberB, memberC) + tick() + // if memberD and memberE would be counted then memberA would be in majority side + expectDownCalled(memberA) + stop() + } + + "keep oldest in self data centers" in new SetupKeepOldest( + // note that this is now on B + stableAfter = Duration.Zero, + memberB.uniqueAddress, + downIfAlone = true, + role = None) { + memberUp(dataCenter(selfDc, memberA, memberB, memberC).toList: _*) + // D and E have lower upNumber (older), but not in self DC + memberUp(dataCenter("other", memberD, memberE).toList: _*) + leader(memberB) + unreachable(memberA, memberC) + tick() + // if memberD and memberE would be counted then memberC would not oldest + // C is oldest in selfDc, so keep C and B and down self + expectDownCalled(memberB) + stop() + } + + "log warning for data center unreachability" in new SetupKeepMajority( + stableAfter = Duration.Zero, + memberA.uniqueAddress, + role = None) { + memberUp(dataCenter(selfDc, memberA, memberB, memberC).toList: _*) + memberUp(dataCenter("other", memberD, memberE).toList: _*) + leader(memberA) + EventFilter.warning(start = "Data center [other] observed as unreachable", occurrences = 1).intercept { + dcUnreachable(dataCenter("other", memberD, memberE).toList: _*) + } + stop() + } + + "down indirectly connected: {(A, B), C} => {C}" in new SetupKeepMajority( + stableAfter = Duration.Zero, + memberA.uniqueAddress, + role = None) { + memberUp(memberA, memberB, memberC) + leader(memberA) + reachabilityChanged(memberA -> memberB, memberB -> memberA) + tick() + // keep fully connected memberC + expectDownCalled(memberA, memberB) + stop() + } + + "down indirectly connected when combined with crashed: {(A, B), D, E} | {C} => {D, E}" in new SetupKeepMajority( + stableAfter = Duration.Zero, + memberA.uniqueAddress, + role = None) { + memberUp(memberA, memberB, memberC, memberD, memberE) + leader(memberA) + reachabilityChanged(memberA -> memberB, memberB -> memberA, memberB -> memberC) + tick() + // keep fully connected memberD, memberE + // note that crashed memberC is also downed + expectDownCalled(memberA, memberB, memberC) + stop() + } + + "down indirectly connected when combined with clean partition: {A, (B, C)} | {D, E} => {A}" in { + // from left side of the partition, memberA, memberB, memberC + new SetupKeepMajority(stableAfter = Duration.Zero, memberA.uniqueAddress, role = None) { + memberUp(memberA, memberB, memberC, memberD, memberE) + leader(memberA) + // indirectly connected: memberB, memberC + // clean partition: memberA, memberB, memberC | memeberD, memberE + reachabilityChanged( + memberB -> memberC, + memberC -> memberB, + memberA -> memberD, + memberB -> memberD, + memberB -> memberE, + memberC -> memberE) + tick() + // keep fully connected memberA + // note that memberD and memberE on the other side of the partition are also downed + expectDownCalled(memberB, memberC, memberD, memberE) + stop() + } + + // from right side of the partition, memberD, memberE + new SetupKeepMajority(stableAfter = Duration.Zero, memberD.uniqueAddress, role = None) { + memberUp(memberA, memberB, memberC, memberD, memberE) + leader(memberD) + // indirectly connected not seen from this side + // clean partition: memberA, memberB, memberC | memeberD, memberE + reachabilityChanged(memberD -> memberA, memberD -> memberB, memberE -> memberB, memberE -> memberC) + tick() + // Note that memberC is not downed, as on the other side, because those indirectly connected + // not seen from this side. That outcome is OK. + expectDownCalled(memberD, memberE) + stop() + } + + } + + "down all in self data centers" in new SetupDownAllNodes(stableAfter = Duration.Zero, memberA.uniqueAddress) { + memberUp(dataCenter(selfDc, memberA, memberB, memberC).toList: _*) + // D and E not in self DC + memberUp(dataCenter("other", memberD, memberE).toList: _*) + leader(memberA) + unreachable(memberA, memberC) + tick() + expectDownCalled(memberA, memberB, memberC) + stop() + } + + "down all when unstable" in new SetupKeepMajority( + stableAfter = 2.seconds, + downAllWhenUnstable = 1.second, + selfUniqueAddress = memberA.uniqueAddress, + role = None, + tickInterval = 100.seconds) { + memberUp(memberA, memberB, memberC, memberD, memberE) + leader(memberA) + reachabilityChanged(memberB -> memberD, memberB -> memberE) + tick() + expectNoDecision(100.millis) + + Thread.sleep(1000) + reachabilityChanged(memberB -> memberD) + reachable(memberE) + tick() + expectNoDecision(100.millis) + + Thread.sleep(1000) + reachabilityChanged(memberB -> memberD, memberB -> memberE) + tick() + expectNoDecision(100.millis) + + Thread.sleep(1000) + reachabilityChanged(memberB -> memberD) + reachable(memberE) + tick() + expectDownCalled(memberA, memberB, memberC, memberD, memberE) + } + + "not down all when becoming stable again" in new SetupKeepMajority( + stableAfter = 2.seconds, + downAllWhenUnstable = 1.second, + selfUniqueAddress = memberA.uniqueAddress, + role = None, + tickInterval = 100.seconds) { + memberUp(memberA, memberB, memberC, memberD, memberE) + leader(memberA) + reachabilityChanged(memberB -> memberD, memberB -> memberE) + tick() + expectNoDecision(100.millis) + + Thread.sleep(1000) + reachabilityChanged(memberB -> memberD) + reachable(memberE) + tick() + expectNoDecision(100.millis) + + // wait longer than stableAfter + Thread.sleep(500) + tick() + expectNoDecision(100.millis) + reachabilityChanged() + reachable(memberD) + Thread.sleep(500) + tick() + expectNoDecision(100.millis) + + Thread.sleep(3000) + tick() + expectNoDecision(100.millis) + } + + "down other side when lease can be acquired" in new SetupLeaseMajority( + Duration.Zero, + memberA.uniqueAddress, + role = None, + new TestLease(testLeaseSettings, extSystem)) { + memberUp(memberA, memberB, memberC) + leader(memberA) + unreachable(memberB) + testLease.setNextAcquireResult(Future.successful(true)) + tick() + expectDownCalled(memberB) + stop() + } + + "down own side when lease cannot be acquired" in new SetupLeaseMajority( + Duration.Zero, + memberA.uniqueAddress, + role = None, + new TestLease(testLeaseSettings, extSystem)) { + memberUp(memberA, memberB, memberC) + leader(memberA) + unreachable(memberB) + testLease.setNextAcquireResult(Future.successful(false)) + tick() + expectDownCalled(memberA, memberC) + stop() + } + + "down indirectly connected when lease can be acquired: {(A, B), C} => {C}" in new SetupLeaseMajority( + stableAfter = Duration.Zero, + memberA.uniqueAddress, + role = None, + new TestLease(testLeaseSettings, extSystem)) { + memberUp(memberA, memberB, memberC) + leader(memberA) + reachabilityChanged(memberA -> memberB, memberB -> memberA) + testLease.setNextAcquireResult(Future.successful(true)) + tick() + // keep fully connected memberC + expectDownCalled(memberA, memberB) + stop() + } + + "down indirectly connected when lease cannot be acquired: {(A, B), C} => {C}" in new SetupLeaseMajority( + stableAfter = Duration.Zero, + memberA.uniqueAddress, + role = None, + new TestLease(testLeaseSettings, extSystem)) { + memberUp(memberA, memberB, memberC) + leader(memberA) + reachabilityChanged(memberA -> memberB, memberB -> memberA) + testLease.setNextAcquireResult(Future.successful(false)) + tick() + // all reachable + all indirectly connected + expectDownCalled(memberA, memberB, memberC) + stop() + } + + } + + "Split Brain Resolver downing provider" must { + + "be loadable through the cluster extension" in { + Cluster(system).downingProvider shouldBe a[SplitBrainResolverProvider] + } + } + + "Reachability changes" must { + val strategy = new KeepMajority(defaultDataCenter, None) + strategy.add(memberA) + strategy.add(memberB) + strategy.add(memberC) + + val memberDInOtherDC = dcMember("otherDC", memberD) + val memberEInOtherDC = dcMember("otherDC", memberE) + + "be noticed when records added" in { + strategy.setReachability(createReachability(List(memberA -> memberB))) + strategy.setReachability(createReachability(List(memberA -> memberB, memberA -> memberC))) should ===(true) + } + + "be noticed when records removed" in { + strategy.setReachability(createReachability(List(memberA -> memberB, memberA -> memberC))) + strategy.setReachability(createReachability(List(memberA -> memberB))) should ===(true) + strategy.setReachability(Reachability.empty) should ===(true) + } + + "be noticed when records change to Reachable" in { + val r = createReachability(List(memberA -> memberB, memberA -> memberC)) + strategy.setReachability(r) + strategy.setReachability(r.reachable(memberA.uniqueAddress, memberC.uniqueAddress)) should ===(true) + } + + "be noticed when records added and removed" in { + strategy.setReachability(createReachability(List(memberA -> memberB))) + strategy.setReachability(createReachability(List(memberC -> memberB))) should ===(true) + } + + "be ignored when records for other DC added" in { + strategy.setReachability(createReachability(List(memberA -> memberB))) + strategy.setReachability(createReachability(List(memberA -> memberB, memberA -> memberDInOtherDC))) should ===( + false) + strategy.setReachability(createReachability(List(memberA -> memberB, memberDInOtherDC -> memberB))) should ===( + false) + strategy.setReachability(createReachability(List(memberA -> memberB, memberDInOtherDC -> memberEInOtherDC))) should ===( + false) + } + } + +} diff --git a/akka-cluster/src/test/scala/akka/cluster/sbr/TestAddresses.scala b/akka-cluster/src/test/scala/akka/cluster/sbr/TestAddresses.scala new file mode 100644 index 0000000000..3ac75dc830 --- /dev/null +++ b/akka-cluster/src/test/scala/akka/cluster/sbr/TestAddresses.scala @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2016-2020 Lightbend Inc. + */ + +package akka.cluster.sbr + +import akka.actor.Address +import akka.cluster.ClusterSettings +import akka.cluster.Member +import akka.cluster.MemberStatus +import akka.cluster.MemberStatus.Up +import akka.cluster.MemberStatus.WeaklyUp +import akka.cluster.UniqueAddress + +/** + * Needed since the Member constructor is akka private + */ +object TestAddresses { + private def dcRole(dc: ClusterSettings.DataCenter): String = + ClusterSettings.DcRolePrefix + dc + val defaultDataCenter = ClusterSettings.DefaultDataCenter + private def defaultDcRole = dcRole(defaultDataCenter) + + val addressA = Address("akka.tcp", "sys", "a", 2552) + val memberA = new Member(UniqueAddress(addressA, 0L), 5, Up, Set("role3", defaultDcRole)) + val memberB = + new Member(UniqueAddress(addressA.copy(host = Some("b")), 0L), 4, Up, Set("role1", "role3", defaultDcRole)) + val memberC = new Member(UniqueAddress(addressA.copy(host = Some("c")), 0L), 3, Up, Set("role2", defaultDcRole)) + val memberD = + new Member(UniqueAddress(addressA.copy(host = Some("d")), 0L), 2, Up, Set("role1", "role2", "role3", defaultDcRole)) + val memberE = new Member(UniqueAddress(addressA.copy(host = Some("e")), 0L), 1, Up, Set(defaultDcRole)) + val memberF = new Member(UniqueAddress(addressA.copy(host = Some("f")), 0L), 5, Up, Set(defaultDcRole)) + val memberG = new Member(UniqueAddress(addressA.copy(host = Some("g")), 0L), 6, Up, Set(defaultDcRole)) + + val memberAWeaklyUp = new Member(memberA.uniqueAddress, Int.MaxValue, WeaklyUp, memberA.roles) + val memberBWeaklyUp = new Member(memberB.uniqueAddress, Int.MaxValue, WeaklyUp, memberB.roles) + + def dcMember(dc: ClusterSettings.DataCenter, m: Member): Member = + new Member( + m.uniqueAddress, + m.upNumber, + m.status, + m.roles.filterNot(_.startsWith(ClusterSettings.DcRolePrefix)) + dcRole(dc)) + + def dataCenter(dc: ClusterSettings.DataCenter, members: Member*): Set[Member] = + members.toSet[Member].map(m => dcMember(dc, m)) + + def joining(m: Member): Member = Member(m.uniqueAddress, m.roles) + + def leaving(m: Member): Member = m.copy(MemberStatus.Leaving) + + def exiting(m: Member): Member = leaving(m).copy(MemberStatus.Exiting) + + def downed(m: Member): Member = m.copy(MemberStatus.Down) +} diff --git a/akka-cluster-tools/src/test/scala/akka/cluster/TestLease.scala b/akka-coordination/src/test/scala/akka/coordination/lease/TestLease.scala similarity index 88% rename from akka-cluster-tools/src/test/scala/akka/cluster/TestLease.scala rename to akka-coordination/src/test/scala/akka/coordination/lease/TestLease.scala index ebdfbd6930..bc9462524e 100644 --- a/akka-cluster-tools/src/test/scala/akka/cluster/TestLease.scala +++ b/akka-coordination/src/test/scala/akka/coordination/lease/TestLease.scala @@ -2,18 +2,22 @@ * Copyright (C) 2019-2020 Lightbend Inc. */ -package akka.cluster +package akka.coordination.lease import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.atomic.AtomicReference -import scala.concurrent.{ Future, Promise } +import scala.concurrent.Future +import scala.concurrent.Promise import com.typesafe.config.ConfigFactory -import akka.actor.{ ActorSystem, ExtendedActorSystem, Extension, ExtensionId, ExtensionIdProvider } +import akka.actor.ActorSystem import akka.actor.ClassicActorSystemProvider -import akka.coordination.lease.LeaseSettings +import akka.actor.ExtendedActorSystem +import akka.actor.Extension +import akka.actor.ExtensionId +import akka.actor.ExtensionIdProvider import akka.coordination.lease.scaladsl.Lease import akka.event.Logging import akka.testkit.TestProbe @@ -47,9 +51,9 @@ object TestLease { final case class AcquireReq(owner: String) final case class ReleaseReq(owner: String) - val config = ConfigFactory.parseString(""" + val config = ConfigFactory.parseString(s""" test-lease { - lease-class = akka.cluster.TestLease + lease-class = ${classOf[TestLease].getName} } """.stripMargin) } diff --git a/akka-cluster-tools/src/multi-jvm/scala/akka/cluster/TestLeaseActor.scala b/akka-coordination/src/test/scala/akka/coordination/lease/TestLeaseActor.scala similarity index 96% rename from akka-cluster-tools/src/multi-jvm/scala/akka/cluster/TestLeaseActor.scala rename to akka-coordination/src/test/scala/akka/coordination/lease/TestLeaseActor.scala index 81c21791aa..091027acac 100644 --- a/akka-cluster-tools/src/multi-jvm/scala/akka/cluster/TestLeaseActor.scala +++ b/akka-coordination/src/test/scala/akka/coordination/lease/TestLeaseActor.scala @@ -2,7 +2,7 @@ * Copyright (C) 2019-2020 Lightbend Inc. */ -package akka.cluster +package akka.coordination.lease import java.util.concurrent.atomic.AtomicReference @@ -19,8 +19,6 @@ import akka.actor.Extension import akka.actor.ExtensionId import akka.actor.ExtensionIdProvider import akka.actor.Props -import akka.cluster.TestLeaseActor.{ Acquire, Create, Release } -import akka.coordination.lease.LeaseSettings import akka.coordination.lease.scaladsl.Lease import akka.event.Logging import akka.pattern.ask @@ -96,6 +94,9 @@ class TestLeaseActorClientExt(val system: ExtendedActorSystem) extends Extension } class TestLeaseActorClient(settings: LeaseSettings, system: ExtendedActorSystem) extends Lease(settings) { + import TestLeaseActor.Acquire + import TestLeaseActor.Create + import TestLeaseActor.Release private val log = Logging(system, getClass) val leaseActor = TestLeaseActorClientExt(system).getLeaseActor() diff --git a/akka-distributed-data/src/main/mima-filters/2.6.5.backwards.excludes/pr-29041-immutable-cancellable.excludes b/akka-distributed-data/src/main/mima-filters/2.6.5.backwards.excludes/pr-29041-immutable-cancellable.excludes new file mode 100644 index 0000000000..ffa12fe001 --- /dev/null +++ b/akka-distributed-data/src/main/mima-filters/2.6.5.backwards.excludes/pr-29041-immutable-cancellable.excludes @@ -0,0 +1,6 @@ +# Change internal methods of the Replicator actor +ProblemFilters.exclude[DirectMissingMethodProblem]("akka.cluster.ddata.ReadWriteAggregator.sendToSecondarySchedule") +ProblemFilters.exclude[DirectMissingMethodProblem]("akka.cluster.ddata.ReadWriteAggregator.sendToSecondarySchedule_=") +ProblemFilters.exclude[DirectMissingMethodProblem]("akka.cluster.ddata.ReadWriteAggregator.timeoutSchedule") +ProblemFilters.exclude[DirectMissingMethodProblem]("akka.cluster.ddata.ReadWriteAggregator.timeoutSchedule_=") + diff --git a/akka-distributed-data/src/main/scala/akka/cluster/ddata/Replicator.scala b/akka-distributed-data/src/main/scala/akka/cluster/ddata/Replicator.scala index 21879dc47a..8cbfa04501 100644 --- a/akka-distributed-data/src/main/scala/akka/cluster/ddata/Replicator.scala +++ b/akka-distributed-data/src/main/scala/akka/cluster/ddata/Replicator.scala @@ -2369,8 +2369,8 @@ final class Replicator(settings: ReplicatorSettings) extends Actor with ActorLog def shuffle: Boolean import context.dispatcher - var sendToSecondarySchedule = context.system.scheduler.scheduleOnce(timeout / 5, self, SendToSecondary) - var timeoutSchedule = context.system.scheduler.scheduleOnce(timeout, self, ReceiveTimeout) + private val sendToSecondarySchedule = context.system.scheduler.scheduleOnce(timeout / 5, self, SendToSecondary) + private val timeoutSchedule = context.system.scheduler.scheduleOnce(timeout, self, ReceiveTimeout) var remaining = nodes.iterator.map(_.address).toSet diff --git a/akka-docs/src/main/paradox/actors.md b/akka-docs/src/main/paradox/actors.md index 0e4c4cd997..2952150f13 100644 --- a/akka-docs/src/main/paradox/actors.md +++ b/akka-docs/src/main/paradox/actors.md @@ -8,7 +8,7 @@ To use Classic Actors, add the following dependency in your project: @@dependency[sbt,Maven,Gradle] { group="com.typesafe.akka" - artifact="akka-actor_$scala.binary_version$" + artifact="akka-actor_$scala.binary.version$" version="$akka.version$" } @@ -16,7 +16,7 @@ To use Classic Actors, add the following dependency in your project: ## Introduction -The [Actor Model](http://en.wikipedia.org/wiki/Actor_model) provides a higher level of abstraction for writing concurrent +The [Actor Model](https://en.wikipedia.org/wiki/Actor_model) provides a higher level of abstraction for writing concurrent and distributed systems. It alleviates the developer from having to deal with explicit locking and thread management, making it easier to write correct concurrent and parallel systems. Actors were defined in the 1973 paper by Carl @@ -294,7 +294,7 @@ singleton scope. Techniques for dependency injection and integration with dependency injection frameworks are described in more depth in the -[Using Akka with Dependency Injection](http://letitcrash.com/post/55958814293/akka-dependency-injection) +[Using Akka with Dependency Injection](https://letitcrash.com/post/55958814293/akka-dependency-injection) guideline and the [Akka Java Spring](https://github.com/typesafehub/activator-akka-java-spring) tutorial. ## Actor API @@ -832,7 +832,7 @@ That has benefits such as: The `Receive` can be implemented in other ways than using the `ReceiveBuilder` since it in the end is just a wrapper around a Scala `PartialFunction`. In Java, you can implement `PartialFunction` by extending `AbstractPartialFunction`. For example, one could implement an adapter -to [Vavr Pattern Matching DSL](http://www.vavr.io/vavr-docs/#_pattern_matching). See the [Akka Vavr sample project](https://github.com/akka/akka-samples/tree/2.5/akka-sample-vavr) for more details. +to [Vavr Pattern Matching DSL](https://www.vavr.io/vavr-docs/#_pattern_matching). See the [Akka Vavr sample project](https://github.com/akka/akka-samples/tree/2.5/akka-sample-vavr) for more details. If the validation of the `ReceiveBuilder` match logic turns out to be a bottleneck for some of your actors you can consider to implement it at lower level by extending `UntypedAbstractActor` instead diff --git a/akka-docs/src/main/paradox/additional/books.md b/akka-docs/src/main/paradox/additional/books.md index 6b40f1fe7d..bd1311e9ca 100644 --- a/akka-docs/src/main/paradox/additional/books.md +++ b/akka-docs/src/main/paradox/additional/books.md @@ -4,16 +4,16 @@ ### Recommended reads * [Reactive Design Patterns](https://www.reactivedesignpatterns.com/), by Roland Kuhn with Jamie Allen and Brian Hanafee, Manning Publications Co., ISBN 9781617291807, Feb 2017 - * [Akka in Action](http://www.lightbend.com/resources/e-book/akka-in-action), by Raymond Roestenburg and Rob Bakker, Manning Publications Co., ISBN: 9781617291012, September 2016 + * [Akka in Action](https://www.lightbend.com/resources/e-book/akka-in-action), by Raymond Roestenburg and Rob Bakker, Manning Publications Co., ISBN: 9781617291012, September 2016 ### Other reads about Akka and the Actor model * [Akka Cookbook](https://www.packtpub.com/application-development/akka-cookbook), by Héctor Veiga Ortiz & Piyush Mishra, PACKT Publishing, ISBN: 9781785288180, May 2017 * [Mastering Akka](https://www.packtpub.com/application-development/mastering-akka), by Christian Baxter, PACKT Publishing, ISBN: 9781786465023, October 2016 * [Learning Akka](https://www.packtpub.com/application-development/learning-akka), by Jason Goodwin, PACKT Publishing, ISBN: 9781784393007, December 2015 - * [Reactive Messaging Patterns with the Actor Model](http://www.informit.com/store/reactive-messaging-patterns-with-the-actor-model-applications-9780133846836), by Vaughn Vernon, Addison-Wesley Professional, ISBN: 0133846830, August 2015 - * [Developing an Akka Edge](http://bleedingedgepress.com/our-books/developing-an-akka-edge/), by Thomas Lockney and Raymond Tay, Bleeding Edge Press, ISBN: 9781939902054, April 2014 - * [Effective Akka](http://shop.oreilly.com/product/0636920028789.do), by Jamie Allen, O'Reilly Media, ISBN: 1449360076, August 2013 - * [Akka Concurrency](http://www.artima.com/shop/akka_concurrency), by Derek Wyatt, artima developer, ISBN: 0981531660, May 2013 + * [Reactive Messaging Patterns with the Actor Model](https://www.informit.com/store/reactive-messaging-patterns-with-the-actor-model-applications-9780133846836), by Vaughn Vernon, Addison-Wesley Professional, ISBN: 0133846830, August 2015 + * [Developing an Akka Edge](https://bleedingedgepress.com/developing-an-akka-edge/), by Thomas Lockney and Raymond Tay, Bleeding Edge Press, ISBN: 9781939902054, April 2014 + * [Effective Akka](https://shop.oreilly.com/product/0636920028789.do), by Jamie Allen, O'Reilly Media, ISBN: 1449360076, August 2013 + * [Akka Concurrency](https://www.artima.com/shop/akka_concurrency), by Derek Wyatt, artima developer, ISBN: 0981531660, May 2013 * [Akka Essentials](https://www.packtpub.com/application-development/akka-essentials), by Munish K. Gupta, PACKT Publishing, ISBN: 1849518289, October 2012 * [Start Building RESTful Microservices using Akka HTTP with Scala](https://www.amazon.com/dp/1976762545/), by Ayush Kumar Mishra, Knoldus Software LLP, ISBN: 9781976762543, December 2017 @@ -23,3 +23,7 @@ * [Zen of Akka](https://www.youtube.com/watch?v=vgFoKOxrTzg) - an overview of good and bad practices in Akka, by Konrad Malawski, ScalaDays New York, June 2016 * [Learning Akka Videos](https://www.packtpub.com/application-development/learning-akka-video), by Salma Khater, PACKT Publishing, ISBN: 9781784391836, January 2016 * [Building Microservice with AKKA HTTP (Video)](https://www.packtpub.com/application-development/building-microservice-akka-http-video), by Tomasz Lelek, PACKT Publishing, ISBN: 9781788298582, March 2017 + +## Blogs + +A list of [blogs and presentations](https://akka.io/blog/external-archive.html) curated by the Akka team. diff --git a/akka-docs/src/main/paradox/additional/faq.md b/akka-docs/src/main/paradox/additional/faq.md index c995f27a1a..e783a66a70 100644 --- a/akka-docs/src/main/paradox/additional/faq.md +++ b/akka-docs/src/main/paradox/additional/faq.md @@ -4,7 +4,7 @@ ### Where does the name Akka come from? -It is the name of a beautiful Swedish [mountain](https://lh4.googleusercontent.com/-z28mTALX90E/UCOsd249TdI/AAAAAAAAAB0/zGyNNZla-zY/w442-h331/akka-beautiful-panorama.jpg) +It is the name of a beautiful Swedish [mountain](https://en.wikipedia.org/wiki/%C3%81hkk%C3%A1) up in the northern part of Sweden called Laponia. The mountain is also sometimes called 'The Queen of Laponia'. @@ -16,9 +16,9 @@ Also, the name AKKA is a palindrome of the letters A and K as in Actor Kernel. Akka is also: - * the name of the goose that Nils traveled across Sweden on in [The Wonderful Adventures of Nils](http://en.wikipedia.org/wiki/The_Wonderful_Adventures_of_Nils) by the Swedish writer Selma Lagerlöf. + * the name of the goose that Nils traveled across Sweden on in [The Wonderful Adventures of Nils](https://en.wikipedia.org/wiki/The_Wonderful_Adventures_of_Nils) by the Swedish writer Selma Lagerlöf. * the Finnish word for 'nasty elderly woman' and the word for 'elder sister' in the Indian languages Tamil, Telugu, Kannada and Marathi. - * a [font](http://www.dafont.com/akka.font) + * a [font](https://www.dafont.com/akka.font) * a town in Morocco * a near-earth asteroid diff --git a/akka-docs/src/main/paradox/additional/operations.md b/akka-docs/src/main/paradox/additional/operations.md index 4adc231285..0f29afbf0b 100644 --- a/akka-docs/src/main/paradox/additional/operations.md +++ b/akka-docs/src/main/paradox/additional/operations.md @@ -14,7 +14,7 @@ When starting clusters on cloud systems such as Kubernetes, AWS, Google Cloud, A you may want to automate the discovery of nodes for the cluster joining process, using your cloud providers, cluster orchestrator, or other form of service discovery (such as managed DNS). -The open source Akka Management library includes the [Cluster Bootstrap](https://doc.akka.io/docs/akka-management/current/bootstrap/index.html) +The open source Akka Management library includes the @extref:[Cluster Bootstrap](akka-management:bootstrap/index.html) module which handles just that. Please refer to its documentation for more details. @@@ note @@ -32,13 +32,13 @@ See @ref:[Rolling Updates, Cluster Shutdown and Coordinated Shutdown](../additio There are several management tools for the cluster. Complete information on running and managing Akka applications can be found in -the [Akka Management](https://doc.akka.io/docs/akka-management/current/) project documentation. +the @exref:[Akka Management](akka-management:) project documentation. ### HTTP Information and management of the cluster is available with a HTTP API. -See documentation of [Akka Management](http://developer.lightbend.com/docs/akka-management/current/). +See documentation of @extref:[Akka Management](akka-management:). ### JMX @@ -60,6 +60,6 @@ Member nodes are identified by their address, in format *`akka://actor-system-na ## Monitoring and Observability Aside from log monitoring and the monitoring provided by your APM or platform provider, [Lightbend Telemetry](https://developer.lightbend.com/docs/telemetry/current/instrumentations/akka/akka.html), -available through a [Lightbend Platform Subscription](https://www.lightbend.com/lightbend-platform-subscription), +available through a [Lightbend Subscription](https://www.lightbend.com/lightbend-subscription), can provide additional insights in the run-time characteristics of your application, including metrics, events, and distributed tracing for Akka Actors, Cluster, HTTP, and more. diff --git a/akka-docs/src/main/paradox/additional/osgi.md b/akka-docs/src/main/paradox/additional/osgi.md index 55abb960d5..01e7aaaca5 100644 --- a/akka-docs/src/main/paradox/additional/osgi.md +++ b/akka-docs/src/main/paradox/additional/osgi.md @@ -6,13 +6,13 @@ To use Akka in OSGi, you must add the following dependency in your project: @@dependency[sbt,Maven,Gradle] { group=com.typesafe.akka - artifact=akka-osgi_$scala.binary_version$ + artifact=akka-osgi_$scala.binary.version$ version=$akka.version$ } ## Background -[OSGi](http://www.osgi.org/developer) is a mature packaging and deployment standard for component-based systems. It +[OSGi](https://www.osgi.org/developer/where-to-start/) is a mature packaging and deployment standard for component-based systems. It has similar capabilities as Project Jigsaw (originally scheduled for JDK 1.8), but has far stronger facilities to support legacy Java code. This is to say that while Jigsaw-ready modules require significant changes to most source files and on occasion to the structure of the overall application, OSGi can be used to modularize almost any Java code as far diff --git a/akka-docs/src/main/paradox/additional/packaging.md b/akka-docs/src/main/paradox/additional/packaging.md index b57a962f3e..9bb4e8c360 100644 --- a/akka-docs/src/main/paradox/additional/packaging.md +++ b/akka-docs/src/main/paradox/additional/packaging.md @@ -33,12 +33,12 @@ Add [sbt-native-packager](https://github.com/sbt/sbt-native-packager) in `projec addSbtPlugin("com.typesafe.sbt" % "sbt-native-packager" % "1.1.5") ``` -Follow the instructions for the `JavaAppPackaging` in the [sbt-native-packager plugin documentation](http://sbt-native-packager.readthedocs.io/en/latest/archetypes/java_app/index.html). +Follow the instructions for the `JavaAppPackaging` in the [sbt-native-packager plugin documentation](https://sbt-native-packager.readthedocs.io/en/latest/archetypes/java_app/index.html). ## Maven: jarjar, onejar or assembly -You can use the [Apache Maven Shade Plugin](http://maven.apache.org/plugins/maven-shade-plugin) -support for [Resource Transformers](http://maven.apache.org/plugins/maven-shade-plugin/examples/resource-transformers.html#AppendingTransformer) +You can use the [Apache Maven Shade Plugin](https://maven.apache.org/plugins/maven-shade-plugin/) +support for [Resource Transformers](https://maven.apache.org/plugins/maven-shade-plugin/examples/resource-transformers.html#AppendingTransformer) to merge all the reference.confs on the build classpath into one. The plugin configuration might look like this: diff --git a/akka-docs/src/main/paradox/additional/rolling-updates.md b/akka-docs/src/main/paradox/additional/rolling-updates.md index 3f50c6b81b..ee52862ac4 100644 --- a/akka-docs/src/main/paradox/additional/rolling-updates.md +++ b/akka-docs/src/main/paradox/additional/rolling-updates.md @@ -67,7 +67,7 @@ Environments such as Kubernetes send a SIGTERM, however if the JVM is wrapped wi In case of network failures it may still be necessary to set the node's status to Down in order to complete the removal. @ref:[Cluster Downing](../typed/cluster.md#downing) details downing nodes and downing providers. -[Split Brain Resolver](https://doc.akka.io/docs/akka-enhancements/current/split-brain-resolver.html) can be used to ensure +@ref:[Split Brain Resolver](../split-brain-resolver.md) can be used to ensure the cluster continues to function during network partitions and node failures. For example if there is an unreachability problem Split Brain Resolver would make a decision based on the configured downing strategy. diff --git a/akka-docs/src/main/paradox/cluster-client.md b/akka-docs/src/main/paradox/cluster-client.md index 37d737f167..2c00ea9de8 100644 --- a/akka-docs/src/main/paradox/cluster-client.md +++ b/akka-docs/src/main/paradox/cluster-client.md @@ -15,7 +15,7 @@ To use Cluster Client, you must add the following dependency in your project: @@dependency[sbt,Maven,Gradle] { group=com.typesafe.akka - artifact=akka-cluster-tools_$scala.binary_version$ + artifact=akka-cluster-tools_$scala.binary.version$ version=$akka.version$ } @@ -231,7 +231,7 @@ contacts can be fetched and a new cluster client started. ## Migration to Akka gRPC Cluster Client is deprecated and it is not advised to build new applications with it. -As a replacement we recommend using [Akka gRPC](https://doc.akka.io/docs/akka-grpc/current/index.html) +As a replacement we recommend using [Akka gRPC](https://doc.akka.io/docs/akka-grpc/current/) with an application-specific protocol. The benefits of this approach are: * Improved security by using TLS for gRPC (HTTP/2) versus exposing Akka Remoting outside the Akka Cluster @@ -244,7 +244,7 @@ with an application-specific protocol. The benefits of this approach are: ### Migrating directly Existing users of Cluster Client may migrate directly to Akka gRPC and use it -as documented in [its documentation](https://doc.akka.io/docs/akka-grpc/current). +as documented in [its documentation](https://doc.akka.io/docs/akka-grpc/current/). ### Migrating gradually diff --git a/akka-docs/src/main/paradox/cluster-metrics.md b/akka-docs/src/main/paradox/cluster-metrics.md index 4efabc446d..7bec024ae6 100644 --- a/akka-docs/src/main/paradox/cluster-metrics.md +++ b/akka-docs/src/main/paradox/cluster-metrics.md @@ -6,7 +6,7 @@ To use Cluster Metrics Extension, you must add the following dependency in your @@dependency[sbt,Maven,Gradle] { group=com.typesafe.akka - artifact=akka-cluster-metrics_$scala.binary_version$ + artifact=akka-cluster-metrics_$scala.binary.version$ version=$akka.version$ } @@ -112,7 +112,7 @@ To enable usage of Sigar you can add the following dependency to the user projec version="$sigar_loader.version$" } -You can download Kamon sigar-loader from [Maven Central](http://search.maven.org/#search%7Cga%7C1%7Csigar-loader) +You can download Kamon sigar-loader from [Maven Central](https://search.maven.org/search?q=sigar-loader) ## Adaptive Load Balancing @@ -126,7 +126,7 @@ It can be configured to use a specific MetricsSelector to produce the probabilit * `mix` / `MixMetricsSelector` - Combines heap, cpu and load. Weights based on mean of remaining capacity of the combined selectors. * Any custom implementation of `akka.cluster.metrics.MetricsSelector` -The collected metrics values are smoothed with [exponential weighted moving average](http://en.wikipedia.org/wiki/Moving_average#Exponential_moving_average). In the @ref:[Cluster configuration](cluster-usage.md#cluster-configuration) you can adjust how quickly past data is decayed compared to new data. +The collected metrics values are smoothed with [exponential weighted moving average](https://en.wikipedia.org/wiki/Moving_average#Exponential_moving_average). In the @ref:[Cluster configuration](cluster-usage.md#cluster-configuration) you can adjust how quickly past data is decayed compared to new data. Let's take a look at this router in action. What can be more demanding than calculating factorials? diff --git a/akka-docs/src/main/paradox/cluster-routing.md b/akka-docs/src/main/paradox/cluster-routing.md index 1b1c515c3b..6dc7e56f45 100644 --- a/akka-docs/src/main/paradox/cluster-routing.md +++ b/akka-docs/src/main/paradox/cluster-routing.md @@ -33,7 +33,7 @@ To use Cluster aware routers, you must add the following dependency in your proj @@dependency[sbt,Maven,Gradle] { group="com.typesafe.akka" - artifact="akka-cluster_$scala.binary_version$" + artifact="akka-cluster_$scala.binary.version$" version="$akka.version$" } diff --git a/akka-docs/src/main/paradox/cluster-sharding.md b/akka-docs/src/main/paradox/cluster-sharding.md index 351808691a..a908bd7585 100644 --- a/akka-docs/src/main/paradox/cluster-sharding.md +++ b/akka-docs/src/main/paradox/cluster-sharding.md @@ -9,7 +9,7 @@ To use Cluster Sharding, you must add the following dependency in your project: @@dependency[sbt,Maven,Gradle] { group=com.typesafe.akka - artifact=akka-cluster-sharding_$scala.binary_version$ + artifact=akka-cluster-sharding_$scala.binary.version$ version=$akka.version$ } diff --git a/akka-docs/src/main/paradox/cluster-singleton.md b/akka-docs/src/main/paradox/cluster-singleton.md index 2b49211696..94fc0bfa65 100644 --- a/akka-docs/src/main/paradox/cluster-singleton.md +++ b/akka-docs/src/main/paradox/cluster-singleton.md @@ -9,7 +9,7 @@ To use Cluster Singleton, you must add the following dependency in your project: @@dependency[sbt,Maven,Gradle] { group=com.typesafe.akka - artifact=akka-cluster-tools_$scala.binary_version$ + artifact=akka-cluster-tools_$scala.binary.version$ version=$akka.version$ } @@ -107,7 +107,7 @@ There are two actors that could potentially be supervised. For the `consumer` si The Cluster singleton manager actor should not have its supervision strategy changed as it should always be running. However it is sometimes useful to add supervision for the user actor. To accomplish this add a parent supervisor actor which will be used to create the 'real' singleton instance. -Below is an example implementation (credit to [this StackOverflow answer](https://stackoverflow.com/a/36716708/779513)) +Below is an example implementation (credit to [this StackOverflow answer](https://stackoverflow.com/questions/36701898/how-to-supervise-cluster-singleton-in-akka/36716708#36716708)) Scala : @@snip [ClusterSingletonSupervision.scala](/akka-docs/src/test/scala/docs/cluster/singleton/ClusterSingletonSupervision.scala) { #singleton-supervisor-actor } diff --git a/akka-docs/src/main/paradox/cluster-usage.md b/akka-docs/src/main/paradox/cluster-usage.md index d45c049209..51467fd5b4 100644 --- a/akka-docs/src/main/paradox/cluster-usage.md +++ b/akka-docs/src/main/paradox/cluster-usage.md @@ -25,7 +25,7 @@ To use Akka Cluster add the following dependency in your project: @@dependency[sbt,Maven,Gradle] { group="com.typesafe.akka" - artifact="akka-cluster_$scala.binary_version$" + artifact="akka-cluster_$scala.binary.version$" version="$akka.version$" } @@ -414,7 +414,7 @@ Examples: ./akka-cluster localhost 9999 is-available ``` To be able to use the script you must enable remote monitoring and management when starting the JVMs of the cluster nodes, -as described in [Monitoring and Management Using JMX Technology](http://docs.oracle.com/javase/8/docs/technotes/guides/management/agent.html). +as described in [Monitoring and Management Using JMX Technology](https://docs.oracle.com/javase/8/docs/technotes/guides/management/agent.html). Make sure you understand the security implications of enabling remote monitoring and management. diff --git a/akka-docs/src/main/paradox/common/other-modules.md b/akka-docs/src/main/paradox/common/other-modules.md index 04680bb242..ed3e1046ed 100644 --- a/akka-docs/src/main/paradox/common/other-modules.md +++ b/akka-docs/src/main/paradox/common/other-modules.md @@ -8,7 +8,7 @@ A full server- and client-side HTTP stack on top of akka-actor and akka-stream. Alpakka is a Reactive Enterprise Integration library for Java and Scala, based on Reactive Streams and Akka. -## [Alpakka Kafka Connector](https://doc.akka.io/docs/akka-stream-kafka/current/) +## [Alpakka Kafka Connector](https://doc.akka.io/docs/alpakka-kafka/current/) The Alpakka Kafka Connector connects Apache Kafka with Akka Streams. @@ -26,6 +26,7 @@ An Akka Persistence journal and snapshot store backed by Couchbase. * [Akka Cluster Bootstrap](https://doc.akka.io/docs/akka-management/current/bootstrap/) helps bootstrapping an Akka cluster using Akka Discovery. * [Akka Management Cluster HTTP](https://doc.akka.io/docs/akka-management/current/cluster-http-management.html) provides HTTP endpoints for introspecting and managing Akka clusters. * [Akka Discovery for Kubernetes, Consul, Marathon, and AWS](https://doc.akka.io/docs/akka-management/current/discovery/) +* [Kubernetes Lease](https://doc.akka.io/docs/akka-management/current/kubernetes-lease.html) ## [Akka gRPC](https://doc.akka.io/docs/akka-grpc/current/) @@ -33,8 +34,6 @@ Akka gRPC provides support for building streaming gRPC servers and clients on to ## Akka Resilience Enhancements -* [Akka Split Brain Resolver](https://doc.akka.io/docs/akka-enhancements/current/split-brain-resolver.html) -* [Kubernetes Lease](https://doc.akka.io/docs/akka-enhancements/current/kubernetes-lease.html) * [Akka Thread Starvation Detector](https://doc.akka.io/docs/akka-enhancements/current/starvation-detector.html) * [Akka Configuration Checker](https://doc.akka.io/docs/akka-enhancements/current/config-checker.html) * [Akka Diagnostics Recorder](https://doc.akka.io/docs/akka-enhancements/current/diagnostics-recorder.html) @@ -44,7 +43,6 @@ Akka gRPC provides support for building streaming gRPC servers and clients on to * [Akka Multi-DC Persistence](https://doc.akka.io/docs/akka-enhancements/current/persistence-dc/index.html) * [Akka GDPR for Persistence](https://doc.akka.io/docs/akka-enhancements/current/gdpr/index.html) - ## Community Projects Akka has a vibrant and passionate user community, the members of which have created many independent projects using Akka as well as extensions to it. See [Community Projects](https://akka.io/community/). diff --git a/akka-docs/src/main/paradox/coordinated-shutdown.md b/akka-docs/src/main/paradox/coordinated-shutdown.md index 6fb5e5808d..28b366275b 100644 --- a/akka-docs/src/main/paradox/coordinated-shutdown.md +++ b/akka-docs/src/main/paradox/coordinated-shutdown.md @@ -1,37 +1,39 @@ # Coordinated Shutdown -Under normal conditions when `ActorSystem` is terminated or the JVM process is shut down certain +Under normal conditions, when an `ActorSystem` is terminated or the JVM process is shut down, certain actors and services will be stopped in a specific order. -This is handled by an extension named `CoordinatedShutdown`. It will run the registered tasks -during the shutdown process. The order of the shutdown phases is defined in configuration `akka.coordinated-shutdown.phases`. -The default phases are defined as: +The @apidoc[CoordinatedShutdown$] extension registers internal and user-defined tasks to be executed during the shutdown process. The tasks are grouped in configuration-defined "phases" which define the shutdown order. -@@snip [reference.conf](/akka-actor/src/main/resources/reference.conf) { #coordinated-shutdown-phases } - -More phases can be added in the application's configuration if needed by overriding a phase with an -additional `depends-on`. Especially the phases `before-service-unbind`, `before-cluster-shutdown` and +Especially the phases `before-service-unbind`, `before-cluster-shutdown` and `before-actor-system-terminate` are intended for application specific phases or tasks. +The order of the shutdown phases is defined in configuration `akka.coordinated-shutdown.phases`. See the default phases in the `reference.conf` tab: + +Most relevant default phases +: | Phase | Description | +|-------------|----------------------------------------------| +| before-service-unbind | The first pre-defined phase during shutdown. | +| before-cluster-shutdown | Phase for custom application tasks that are to be run after service shutdown and before cluster shutdown. | +| before-actor-system-terminate | Phase for custom application tasks that are to be run after cluster shutdown and before `ActorSystem` termination. | + +reference.conf (HOCON) +: @@snip [reference.conf](/akka-actor/src/main/resources/reference.conf) { #coordinated-shutdown-phases } + +More phases can be added in the application's `application.conf` if needed by overriding a phase with an +additional `depends-on`. + The default phases are defined in a single linear order, but the phases can be ordered as a directed acyclic graph (DAG) by defining the dependencies between the phases. The phases are ordered with [topological](https://en.wikipedia.org/wiki/Topological_sorting) sort of the DAG. -Tasks can be added to a phase with: +Tasks can be added to a phase like in this example which allows a certain actor to react before termination starts: Scala -: @@snip [ActorDocSpec.scala](/akka-docs/src/test/scala/docs/actor/ActorDocSpec.scala) { #coordinated-shutdown-addTask } +: @@snip [snip](/akka-docs/src/test/scala/docs/actor/typed/CoordinatedActorShutdownSpec.scala) { #coordinated-shutdown-addTask } Java -: @@snip [ActorDocTest.java](/akka-docs/src/test/java/jdocs/actor/ActorDocTest.java) { #coordinated-shutdown-addTask } - -If cancellation of previously added tasks is required: - -Scala -: @@snip [ActorDocSpec.scala](/akka-docs/src/test/scala/docs/actor/ActorDocSpec.scala) { #coordinated-shutdown-cancellable } - -Java -: @@snip [ActorDocTest.java](/akka-docs/src/test/java/jdocs/actor/ActorDocTest.java) { #coordinated-shutdown-cancellable } +: @@snip [snip](/akka-docs/src/test/java/jdocs/actor/typed/CoordinatedActorShutdownTest.java) { #coordinated-shutdown-addTask } The returned @scala[`Future[Done]`] @java[`CompletionStage`] should be completed when the task is completed. The task name parameter is only used for debugging/logging. @@ -43,9 +45,17 @@ If tasks are not completed within a configured timeout (see @ref:[reference.conf the next phase will be started anyway. It is possible to configure `recover=off` for a phase to abort the rest of the shutdown process if a task fails or is not completed within the timeout. +If cancellation of previously added tasks is required: + +Scala +: @@snip [snip](/akka-docs/src/test/scala/docs/actor/typed/CoordinatedActorShutdownSpec.scala) { #coordinated-shutdown-cancellable } + +Java +: @@snip [snip](/akka-docs/src/test/java/jdocs/actor/typed/CoordinatedActorShutdownTest.java) { #coordinated-shutdown-cancellable } + In the above example, it may be more convenient to simply stop the actor when it's done shutting down, rather than send back a done message, and for the shutdown task to not complete until the actor is terminated. A convenience method is provided that adds a task that sends -a message to the actor and then watches its termination: +a message to the actor and then watches its termination (there is currently no corresponding functionality for the new actors API @github[see #29056](#29056)): Scala : @@snip [ActorDocSpec.scala](/akka-docs/src/test/scala/docs/actor/ActorDocSpec.scala) { #coordinated-shutdown-addActorTerminationTask } @@ -57,14 +67,14 @@ Tasks should typically be registered as early as possible after system startup. the coordinated shutdown tasks that have been registered will be performed but tasks that are added too late will not be run. -To start the coordinated shutdown process you can invoke @scala[`run`] @java[`runAll`] on the `CoordinatedShutdown` -extension: +To start the coordinated shutdown process you can either invoke `terminate()` on the `ActorSystem`, or @scala[`run`] @java[`runAll`] on the `CoordinatedShutdown` +extension and pass it a class implementing @apidoc[CoordinatedShutdown.Reason] for informational purposes: Scala -: @@snip [ActorDocSpec.scala](/akka-docs/src/test/scala/docs/actor/ActorDocSpec.scala) { #coordinated-shutdown-run } +: @@snip [snip](/akka-docs/src/test/scala/docs/actor/typed/CoordinatedActorShutdownSpec.scala) { #coordinated-shutdown-run } Java -: @@snip [ActorDocTest.java](/akka-docs/src/test/java/jdocs/actor/ActorDocTest.java) { #coordinated-shutdown-run } +: @@snip [snip](/akka-docs/src/test/java/jdocs/actor/typed/CoordinatedActorShutdownTest.java) { #coordinated-shutdown-run } It's safe to call the @scala[`run`] @java[`runAll`] method multiple times. It will only run once. @@ -76,7 +86,7 @@ To enable a hard `System.exit` as a final action you can configure: akka.coordinated-shutdown.exit-jvm = on ``` -The coordinated shutdown process can also be started by calling `ActorSystem.terminate()`. +The coordinated shutdown process is also started once the actor system's root actor is stopped. When using @ref:[Akka Cluster](cluster-usage.md) the `CoordinatedShutdown` will automatically run when the cluster node sees itself as `Exiting`, i.e. leaving from another node will trigger @@ -96,10 +106,10 @@ If you have application specific JVM shutdown hooks it's recommended that you re those shutting down Akka Remoting (Artery). Scala -: @@snip [ActorDocSpec.scala](/akka-docs/src/test/scala/docs/actor/ActorDocSpec.scala) { #coordinated-shutdown-jvm-hook } +: @@snip [snip](/akka-docs/src/test/scala/docs/actor/typed/CoordinatedActorShutdownSpec.scala) { #coordinated-shutdown-jvm-hook } Java -: @@snip [ActorDocTest.java](/akka-docs/src/test/java/jdocs/actor/ActorDocTest.java) { #coordinated-shutdown-jvm-hook } +: @@snip [snip](/akka-docs/src/test/java/jdocs/actor/typed/CoordinatedActorShutdownTest.java) { #coordinated-shutdown-jvm-hook } For some tests it might be undesired to terminate the `ActorSystem` via `CoordinatedShutdown`. You can disable that by adding the following to the configuration of the `ActorSystem` that is diff --git a/akka-docs/src/main/paradox/coordination.md b/akka-docs/src/main/paradox/coordination.md index d1dcfb5685..e2266ad2ab 100644 --- a/akka-docs/src/main/paradox/coordination.md +++ b/akka-docs/src/main/paradox/coordination.md @@ -9,7 +9,7 @@ Akka Coordination is a set of tools for distributed coordination. @@dependency[sbt,Gradle,Maven] { group="com.typesafe.akka" - artifact="akka-coordination_$scala.binary_version$" + artifact="akka-coordination_$scala.binary.version$" version="$akka.version$" } @@ -35,10 +35,10 @@ Any lease implementation should provide the following guarantees: To acquire a lease: Scala -: @@snip [LeaseDocSpec.scala](/akka-coordination/src/test/scala/docs/akka/coordination/LeaseDocSpec.scala) { #lease-usage } +: @@snip [LeaseDocSpec.scala](/akka-docs/src/test/scala/docs/coordination/LeaseDocSpec.scala) { #lease-usage } Java -: @@snip [LeaseDocTest.java](/akka-coordination/src/test/java/jdocs/akka/coordination/lease/LeaseDocTest.java) { #lease-usage } +: @@snip [LeaseDocTest.java](/akka-docs/src/test/java/jdocs/coordination/LeaseDocTest.java) { #lease-usage } Acquiring a lease returns a @scala[Future]@java[CompletionStage] as lease implementations typically are implemented via a third party system such as the Kubernetes API server or Zookeeper. @@ -53,10 +53,10 @@ It is important to pick a lease name that will be unique for your use case. If a in a Cluster the cluster host port can be use: Scala -: @@snip [LeaseDocSpec.scala](/akka-coordination/src/test/scala/docs/akka/coordination/LeaseDocSpec.scala) { #cluster-owner } +: @@snip [LeaseDocSpec.scala](/akka-docs/src/test/scala/docs/coordination/LeaseDocSpec.scala) { #cluster-owner } Java -: @@snip [LeaseDocTest.scala](/akka-coordination/src/test/java/jdocs/akka/coordination/lease/LeaseDocTest.java) { #cluster-owner } +: @@snip [LeaseDocTest.scala](/akka-docs/src/test/java/jdocs/coordination/LeaseDocTest.java) { #cluster-owner } For use cases where multiple different leases on the same node then something unique must be added to the name. For example a lease can be used with Cluster Sharding and in this case the shard Id is included in the lease name for each shard. @@ -77,7 +77,7 @@ Leases can be used for @ref[Cluster Singletons](cluster-singleton.md#lease) and ## Lease implementations -* [Kubernetes API](https://doc.akka.io/docs/akka-enhancements/current/kubernetes-lease.html) +* [Kubernetes API](https://doc.akka.io/docs/akka-management/current/kubernetes-lease.html) ## Implementing a lease @@ -85,10 +85,10 @@ Implementations should extend the @scala[`akka.coordination.lease.scaladsl.Lease`]@java[`akka.coordination.lease.javadsl.Lease`] Scala -: @@snip [LeaseDocSpec.scala](/akka-coordination/src/test/scala/docs/akka/coordination/LeaseDocSpec.scala) { #lease-example } +: @@snip [LeaseDocSpec.scala](/akka-docs/src/test/scala/docs/coordination/LeaseDocSpec.scala) { #lease-example } Java -: @@snip [LeaseDocTest.scala](/akka-coordination/src/test/java/jdocs/akka/coordination/lease/LeaseDocTest.java) { #lease-example } +: @@snip [LeaseDocTest.java](/akka-docs/src/test/java/jdocs/coordination/LeaseDocTest.java) { #lease-example } The methods should provide the following guarantees: @@ -109,10 +109,10 @@ The lease implementation should have support for the following properties where This configuration location is passed into `getLease`. Scala -: @@snip [LeaseDocSpec.scala](/akka-coordination/src/test/scala/docs/akka/coordination/LeaseDocSpec.scala) { #lease-config } +: @@snip [LeaseDocSpec.scala](/akka-docs/src/test/scala/docs/coordination/LeaseDocSpec.scala) { #lease-config } Java -: @@snip [LeaseDocSpec.scala](/akka-coordination/src/test/scala/docs/akka/coordination/LeaseDocSpec.scala) { #lease-config } +: @@snip [LeaseDocSpec.scala](/akka-docs/src/test/scala/docs/coordination/LeaseDocSpec.scala) { #lease-config } diff --git a/akka-docs/src/main/paradox/discovery/index.md b/akka-docs/src/main/paradox/discovery/index.md index 5fb4cf93b8..51c13594f4 100644 --- a/akka-docs/src/main/paradox/discovery/index.md +++ b/akka-docs/src/main/paradox/discovery/index.md @@ -35,7 +35,7 @@ See @ref:[Migration hints](#migrating-from-akka-management-discovery-before-1-0- @@dependency[sbt,Gradle,Maven] { group="com.typesafe.akka" - artifact="akka-discovery_$scala.binary_version$" + artifact="akka-discovery_$scala.binary.version$" version="$akka.version$" } diff --git a/akka-docs/src/main/paradox/dispatchers.md b/akka-docs/src/main/paradox/dispatchers.md index 8146f0e7e3..5e33a5e9bb 100644 --- a/akka-docs/src/main/paradox/dispatchers.md +++ b/akka-docs/src/main/paradox/dispatchers.md @@ -9,7 +9,7 @@ Dispatchers are part of core Akka, which means that they are part of the akka-ac @@dependency[sbt,Maven,Gradle] { group="com.typesafe.akka" - artifact="akka-actor_$scala.binary_version$" + artifact="akka-actor_$scala.binary.version$" version="$akka.version$" } diff --git a/akka-docs/src/main/paradox/distributed-data.md b/akka-docs/src/main/paradox/distributed-data.md index f3594bd4a7..c28563c09d 100644 --- a/akka-docs/src/main/paradox/distributed-data.md +++ b/akka-docs/src/main/paradox/distributed-data.md @@ -9,7 +9,7 @@ To use Akka Distributed Data, you must add the following dependency in your proj @@dependency[sbt,Maven,Gradle] { group="com.typesafe.akka" - artifact="akka-distributed-data_$scala.binary_version$" + artifact="akka-distributed-data_$scala.binary.version$" version="$akka.version$" } @@ -268,11 +268,9 @@ For the full documentation of this feature and for new projects see @ref:[Limita ## Learn More about CRDTs - * [Eventually Consistent Data Structures](https://vimeo.com/43903960) -talk by Sean Cribbs * [Strong Eventual Consistency and Conflict-free Replicated Data Types (video)](https://www.youtube.com/watch?v=oyUHd894w18&feature=youtu.be) talk by Mark Shapiro - * [A comprehensive study of Convergent and Commutative Replicated Data Types](http://hal.upmc.fr/file/index/docid/555588/filename/techreport.pdf) + * [A comprehensive study of Convergent and Commutative Replicated Data Types](https://hal.inria.fr/file/index/docid/555588/filename/techreport.pdf) paper by Mark Shapiro et. al. ## Configuration diff --git a/akka-docs/src/main/paradox/distributed-pub-sub.md b/akka-docs/src/main/paradox/distributed-pub-sub.md index 2f1d8167ea..fd998e8fde 100644 --- a/akka-docs/src/main/paradox/distributed-pub-sub.md +++ b/akka-docs/src/main/paradox/distributed-pub-sub.md @@ -9,7 +9,7 @@ To use Distributed Publish Subscribe you must add the following dependency in yo @@dependency[sbt,Maven,Gradle] { group="com.typesafe.akka" - artifact="akka-cluster-tools_$scala.binary_version$" + artifact="akka-cluster-tools_$scala.binary.version$" version="$akka.version$" } diff --git a/akka-docs/src/main/paradox/fault-tolerance.md b/akka-docs/src/main/paradox/fault-tolerance.md index a80c246d20..8c75abcb6c 100644 --- a/akka-docs/src/main/paradox/fault-tolerance.md +++ b/akka-docs/src/main/paradox/fault-tolerance.md @@ -9,7 +9,7 @@ The concept of fault tolerance relates to actors, so in order to use these make @@dependency[sbt,Maven,Gradle] { group="com.typesafe.akka" - artifact="akka-actor_$scala.binary_version$" + artifact="akka-actor_$scala.binary.version$" version="$akka.version$" } diff --git a/akka-docs/src/main/paradox/fsm.md b/akka-docs/src/main/paradox/fsm.md index 7902a16b4d..01c0bfc0a3 100644 --- a/akka-docs/src/main/paradox/fsm.md +++ b/akka-docs/src/main/paradox/fsm.md @@ -9,14 +9,14 @@ To use Finite State Machine actors, you must add the following dependency in you @@dependency[sbt,Maven,Gradle] { group="com.typesafe.akka" - artifact="akka-actor_$scala.binary_version$" + artifact="akka-actor_$scala.binary.version$" version="$akka.version$" } ## Overview The FSM (Finite State Machine) is available as @scala[a mixin for the] @java[an abstract base class that implements an] Akka Actor and -is best described in the [Erlang design principles](http://www.erlang.org/documentation/doc-4.8.2/doc/design_principles/fsm.html) +is best described in the [Erlang design principles](https://www.erlang.org/documentation/doc-4.8.2/doc/design_principles/fsm.html) A FSM can be described as a set of relations of the form: diff --git a/akka-docs/src/main/paradox/general/actors.md b/akka-docs/src/main/paradox/general/actors.md index 386d47710e..89ef1d0d61 100644 --- a/akka-docs/src/main/paradox/general/actors.md +++ b/akka-docs/src/main/paradox/general/actors.md @@ -9,7 +9,7 @@ section looks at one such actor in isolation, explaining the concepts you encounter while implementing it. For a more in depth reference with all the details please refer to @ref:[Introduction to Actors](../typed/actors.md). -The [Actor Model](http://en.wikipedia.org/wiki/Actor_model) as defined by +The [Actor Model](https://en.wikipedia.org/wiki/Actor_model) as defined by Hewitt, Bishop and Steiger in 1973 is a computational model that expresses exactly what it means for computation to be distributed. The processing units—Actors—can only communicate by exchanging messages and upon reception of a diff --git a/akka-docs/src/main/paradox/general/message-delivery-reliability.md b/akka-docs/src/main/paradox/general/message-delivery-reliability.md index 5e54816157..825d906697 100644 --- a/akka-docs/src/main/paradox/general/message-delivery-reliability.md +++ b/akka-docs/src/main/paradox/general/message-delivery-reliability.md @@ -87,7 +87,7 @@ mailbox would interact with the third point, or even what it would mean to decide upon the “successfully” part of point five. Along those same lines goes the reasoning in [Nobody Needs Reliable -Messaging](http://www.infoq.com/articles/no-reliable-messaging). The only meaningful way for a sender to know whether an +Messaging](https://www.infoq.com/articles/no-reliable-messaging/). The only meaningful way for a sender to know whether an interaction was successful is by receiving a business-level acknowledgement message, which is not something Akka could make up on its own (neither are we writing a “do what I mean” framework nor would you want us to). @@ -96,7 +96,7 @@ Akka embraces distributed computing and makes the fallibility of communication explicit through message passing, therefore it does not try to lie and emulate a leaky abstraction. This is a model that has been used with great success in Erlang and requires the users to design their applications around it. You can -read more about this approach in the [Erlang documentation](http://www.erlang.org/faq/academic.html) (section 10.9 and +read more about this approach in the [Erlang documentation](https://erlang.org/faq/academic.html) (section 10.9 and 10.10), Akka follows it closely. Another angle on this issue is that by providing only basic guarantees those diff --git a/akka-docs/src/main/paradox/general/stream/stream-design.md b/akka-docs/src/main/paradox/general/stream/stream-design.md index f7c5e9301f..45416068c8 100644 --- a/akka-docs/src/main/paradox/general/stream/stream-design.md +++ b/akka-docs/src/main/paradox/general/stream/stream-design.md @@ -104,7 +104,7 @@ A source that emits a stream of streams is still a normal Source, the kind of el ## The difference between Error and Failure -The starting point for this discussion is the [definition given by the Reactive Manifesto](http://www.reactivemanifesto.org/glossary#Failure). Translated to streams this means that an error is accessible within the stream as a normal data element, while a failure means that the stream itself has failed and is collapsing. In concrete terms, on the Reactive Streams interface level data elements (including errors) are signaled via `onNext` while failures raise the `onError` signal. +The starting point for this discussion is the [definition given by the Reactive Manifesto](https://www.reactivemanifesto.org/glossary#Failure). Translated to streams this means that an error is accessible within the stream as a normal data element, while a failure means that the stream itself has failed and is collapsing. In concrete terms, on the Reactive Streams interface level data elements (including errors) are signaled via `onNext` while failures raise the `onError` signal. @@@ note diff --git a/akka-docs/src/main/paradox/includes/cluster.md b/akka-docs/src/main/paradox/includes/cluster.md index 242b956d87..58e61c8418 100644 --- a/akka-docs/src/main/paradox/includes/cluster.md +++ b/akka-docs/src/main/paradox/includes/cluster.md @@ -33,6 +33,14 @@ i.e. the sender does not have to know on which node the destination actor is run + +### Cluster aware routers + +Distribute messages to actors on different nodes in the cluster with routing strategies +like round-robin and consistent hashing. + + + ### Cluster across multiple data centers diff --git a/akka-docs/src/main/paradox/index-actors.md b/akka-docs/src/main/paradox/index-actors.md index f5327d4c94..1b6d49aa4b 100644 --- a/akka-docs/src/main/paradox/index-actors.md +++ b/akka-docs/src/main/paradox/index-actors.md @@ -8,7 +8,7 @@ To use Classic Akka Actors, you must add the following dependency in your projec @@dependency[sbt,Maven,Gradle] { group="com.typesafe.akka" - artifact="akka-actor_$scala.binary_version$" + artifact="akka-actor_$scala.binary.version$" version="$akka.version$" } diff --git a/akka-docs/src/main/paradox/index-utilities-classic.md b/akka-docs/src/main/paradox/index-utilities-classic.md index f6b98d9699..76d88a281d 100644 --- a/akka-docs/src/main/paradox/index-utilities-classic.md +++ b/akka-docs/src/main/paradox/index-utilities-classic.md @@ -6,7 +6,7 @@ To use Utilities, you must add the following dependency in your project: @@dependency[sbt,Maven,Gradle] { group="com.typesafe.akka" - artifact="akka-actor_$scala.binary_version$" + artifact="akka-actor_$scala.binary.version$" version="$akka.version$" } diff --git a/akka-docs/src/main/paradox/io-tcp.md b/akka-docs/src/main/paradox/io-tcp.md index 25e04221a7..56a132e032 100644 --- a/akka-docs/src/main/paradox/io-tcp.md +++ b/akka-docs/src/main/paradox/io-tcp.md @@ -9,7 +9,7 @@ To use TCP, you must add the following dependency in your project: @@dependency[sbt,Maven,Gradle] { group="com.typesafe.akka" - artifact="akka-actor_$scala.binary_version$" + artifact="akka-actor_$scala.binary.version$" version="$akka.version$" } diff --git a/akka-docs/src/main/paradox/io-udp.md b/akka-docs/src/main/paradox/io-udp.md index ee0c125349..f9ea24ab1a 100644 --- a/akka-docs/src/main/paradox/io-udp.md +++ b/akka-docs/src/main/paradox/io-udp.md @@ -9,7 +9,7 @@ To use UDP, you must add the following dependency in your project: @@dependency[sbt,Maven,Gradle] { group="com.typesafe.akka" - artifact="akka-actor_$scala.binary_version$" + artifact="akka-actor_$scala.binary.version$" version="$akka.version$" } diff --git a/akka-docs/src/main/paradox/io.md b/akka-docs/src/main/paradox/io.md index cfb509cdda..6eced33888 100644 --- a/akka-docs/src/main/paradox/io.md +++ b/akka-docs/src/main/paradox/io.md @@ -6,7 +6,7 @@ To use I/O, you must add the following dependency in your project: @@dependency[sbt,Maven,Gradle] { group="com.typesafe.akka" - artifact="akka-actor_$scala.binary_version$" + artifact="akka-actor_$scala.binary.version$" version="$akka.version$" } @@ -93,7 +93,7 @@ To maintain isolation, actors should communicate with immutable objects only. `B immutable container for bytes. It is used by Akka's I/O system as an efficient, immutable alternative the traditional byte containers used for I/O on the JVM, such as @scala[`Array[Byte]`]@java[`byte[]`] and `ByteBuffer`. -`ByteString` is a [rope-like](http://en.wikipedia.org/wiki/Rope_\(computer_science\)) data structure that is immutable +`ByteString` is a [rope-like](https://en.wikipedia.org/wiki/Rope_\(computer_science\)) data structure that is immutable and provides fast concatenation and slicing operations (perfect for I/O). When two `ByteString`s are concatenated together they are both stored within the resulting `ByteString` instead of copying both to a new @scala[`Array`]@java[array]. Operations such as `drop` and `take` return `ByteString`s that still reference the original @scala[`Array`]@java[array], but just change the diff --git a/akka-docs/src/main/paradox/logging.md b/akka-docs/src/main/paradox/logging.md index e9d788019f..1b63bb1b86 100644 --- a/akka-docs/src/main/paradox/logging.md +++ b/akka-docs/src/main/paradox/logging.md @@ -9,7 +9,7 @@ To use Logging, you must at least use the Akka actors dependency in your project @@dependency[sbt,Maven,Gradle] { group="com.typesafe.akka" - artifact="akka-actor_$scala.binary_version$" + artifact="akka-actor_$scala.binary.version$" version="$akka.version$" } @@ -339,7 +339,7 @@ It has a single dependency: the slf4j-api jar. In your runtime, you also need a @@dependency[sbt,Maven,Gradle] { group="com.typesafe.akka" - artifact="akka-slf4j_$scala.binary_version$" + artifact="akka-slf4j_$scala.binary.version$" version="$akka.version$" group2="ch.qos.logback" artifact2="logback-classic" diff --git a/akka-docs/src/main/paradox/mailboxes.md b/akka-docs/src/main/paradox/mailboxes.md index 53267dbd55..e1b110d59b 100644 --- a/akka-docs/src/main/paradox/mailboxes.md +++ b/akka-docs/src/main/paradox/mailboxes.md @@ -9,7 +9,7 @@ To use Mailboxes, you must add the following dependency in your project: @@dependency[sbt,Maven,Gradle] { group="com.typesafe.akka" - artifact="akka-actor_$scala.binary_version$" + artifact="akka-actor_$scala.binary.version$" version="$akka.version$" } diff --git a/akka-docs/src/main/paradox/multi-node-testing.md b/akka-docs/src/main/paradox/multi-node-testing.md index ec165631da..db6a786303 100644 --- a/akka-docs/src/main/paradox/multi-node-testing.md +++ b/akka-docs/src/main/paradox/multi-node-testing.md @@ -9,7 +9,7 @@ To use Multi Node Testing, you must add the following dependency in your project @@dependency[sbt,Maven,Gradle] { group=com.typesafe.akka - artifact=akka-multi-node-testkit_$scala.binary_version$ + artifact=akka-multi-node-testkit_$scala.binary.version$ version=$akka.version$ } diff --git a/akka-docs/src/main/paradox/persistence-fsm.md b/akka-docs/src/main/paradox/persistence-fsm.md index 073cc6cbf9..a675054a8a 100644 --- a/akka-docs/src/main/paradox/persistence-fsm.md +++ b/akka-docs/src/main/paradox/persistence-fsm.md @@ -8,7 +8,7 @@ Persistent FSMs are part of Akka persistence, you must add the following depende @@dependency[sbt,Maven,Gradle] { group="com.typesafe.akka" - artifact="akka-persistence_$scala.binary_version$" + artifact="akka-persistence_$scala.binary.version$" version="$akka.version$" } diff --git a/akka-docs/src/main/paradox/persistence-journals.md b/akka-docs/src/main/paradox/persistence-journals.md index b7411052f9..c73fe34874 100644 --- a/akka-docs/src/main/paradox/persistence-journals.md +++ b/akka-docs/src/main/paradox/persistence-journals.md @@ -97,13 +97,13 @@ Don't run snapshot store tasks/futures on the system default dispatcher, since t ## Plugin TCK -In order to help developers build correct and high quality storage plugins, we provide a Technology Compatibility Kit ([TCK](http://en.wikipedia.org/wiki/Technology_Compatibility_Kit) for short). +In order to help developers build correct and high quality storage plugins, we provide a Technology Compatibility Kit ([TCK](https://en.wikipedia.org/wiki/Technology_Compatibility_Kit) for short). The TCK is usable from Java as well as Scala projects. To test your implementation (independently of language) you need to include the akka-persistence-tck dependency: @@dependency[sbt,Maven,Gradle] { group="com.typesafe.akka" - artifact="akka-persistence-tck_$scala.binary_version$" + artifact="akka-persistence-tck_$scala.binary.version$" version="$akka.version$" } diff --git a/akka-docs/src/main/paradox/persistence-query-leveldb.md b/akka-docs/src/main/paradox/persistence-query-leveldb.md index 6066b5c87e..e7b9e0a9a5 100644 --- a/akka-docs/src/main/paradox/persistence-query-leveldb.md +++ b/akka-docs/src/main/paradox/persistence-query-leveldb.md @@ -6,7 +6,7 @@ To use Persistence Query, you must add the following dependency in your project: @@dependency[sbt,Maven,Gradle] { group=com.typesafe.akka - artifact=akka-persistence-query_$scala.binary_version$ + artifact=akka-persistence-query_$scala.binary.version$ version=$akka.version$ } diff --git a/akka-docs/src/main/paradox/persistence-query.md b/akka-docs/src/main/paradox/persistence-query.md index d29d9d09ac..fc86fe8cf5 100644 --- a/akka-docs/src/main/paradox/persistence-query.md +++ b/akka-docs/src/main/paradox/persistence-query.md @@ -9,7 +9,7 @@ To use Persistence Query, you must add the following dependency in your project: @@dependency[sbt,Maven,Gradle] { group=com.typesafe.akka - artifact=akka-persistence-query_$scala.binary_version$ + artifact=akka-persistence-query_$scala.binary.version$ version=$akka.version$ } @@ -197,7 +197,7 @@ Java ## Performance and denormalization -When building systems using @ref:[Event sourcing](typed/persistence.md#event-sourcing-concepts) and CQRS ([Command & Query Responsibility Segregation](https://msdn.microsoft.com/en-us/library/jj554200.aspx)) techniques +When building systems using @ref:[Event sourcing](typed/persistence.md#event-sourcing-concepts) and CQRS ([Command & Query Responsibility Segregation](https://docs.microsoft.com/en-us/previous-versions/msp-n-p/jj554200(v=pandp.10)?redirectedfrom=MSDN)) techniques it is tremendously important to realise that the write-side has completely different needs from the read-side, and separating those concerns into datastores that are optimised for either side makes it possible to offer the best experience for the write and read sides independently. @@ -221,7 +221,7 @@ it may be more efficient or interesting to query it (instead of the source event ### Materialize view to Reactive Streams compatible datastore -If the read datastore exposes a [Reactive Streams](http://reactive-streams.org) interface then implementing a simple projection +If the read datastore exposes a [Reactive Streams](https://www.reactive-streams.org) interface then implementing a simple projection is as simple as, using the read-journal and feeding it into the databases driver interface, for example like so: Scala diff --git a/akka-docs/src/main/paradox/persistence-schema-evolution.md b/akka-docs/src/main/paradox/persistence-schema-evolution.md index edf9b73356..bef012ac34 100644 --- a/akka-docs/src/main/paradox/persistence-schema-evolution.md +++ b/akka-docs/src/main/paradox/persistence-schema-evolution.md @@ -6,7 +6,7 @@ This documentation page touches upon @ref[Akka Persistence](persistence.md), so @@dependency[sbt,Maven,Gradle] { group="com.typesafe.akka" - artifact="akka-persistence_$scala.binary_version$" + artifact="akka-persistence_$scala.binary.version$" version="$akka.version$" } diff --git a/akka-docs/src/main/paradox/persistence.md b/akka-docs/src/main/paradox/persistence.md index d586f61f3f..e3abde3dd8 100644 --- a/akka-docs/src/main/paradox/persistence.md +++ b/akka-docs/src/main/paradox/persistence.md @@ -12,7 +12,7 @@ To use Akka Persistence, you must add the following dependency in your project: @@dependency[sbt,Maven,Gradle] { group="com.typesafe.akka" - artifact="akka-persistence_$scala.binary_version$" + artifact="akka-persistence_$scala.binary.version$" version="$akka.version$" } diff --git a/akka-docs/src/main/paradox/project/examples.md b/akka-docs/src/main/paradox/project/examples.md index 966ffe5182..af9e879ffe 100644 --- a/akka-docs/src/main/paradox/project/examples.md +++ b/akka-docs/src/main/paradox/project/examples.md @@ -5,8 +5,8 @@ of how to run. ## Quickstart -@scala[[Quickstart Guide](https://developer.lightbend.com/guides/akka-quickstart-scala)] -@java[[Quickstart Guide](https://developer.lightbend.com/guides/akka-quickstart-java)] +@scala[[Quickstart Guide](https://developer.lightbend.com/guides/akka-quickstart-scala/)] +@java[[Quickstart Guide](https://developer.lightbend.com/guides/akka-quickstart-java/)] The *Quickstart* guide walks you through example code that introduces how to define actor systems, actors, and messages as well as how to use the test module and logging. diff --git a/akka-docs/src/main/paradox/project/immutable.md b/akka-docs/src/main/paradox/project/immutable.md index 8ad4668755..957ef91f31 100644 --- a/akka-docs/src/main/paradox/project/immutable.md +++ b/akka-docs/src/main/paradox/project/immutable.md @@ -83,5 +83,5 @@ getter, toString, hashCode, equals. ### Integrating Lombok with an IDE Lombok integrates with popular IDEs: -* To use Lombok in IntelliJ IDEA you'll need the [Lombok Plugin for IntelliJ IDEA](https://plugins.jetbrains.com/idea/plugin/6317-lombok-plugin) and you'll also need to enable Annotation Processing (`Settings / Build,Execution,Deployment / Compiler / Annotation Processors` and tick `Enable annotation processing`) +* To use Lombok in IntelliJ IDEA you'll need the [Lombok Plugin for IntelliJ IDEA](https://plugins.jetbrains.com/plugin/6317-lombok) and you'll also need to enable Annotation Processing (`Settings / Build,Execution,Deployment / Compiler / Annotation Processors` and tick `Enable annotation processing`) * To Use Lombok in Eclipse, run `java -jar lombok.jar` (see the video at [Project Lombok](https://projectlombok.org/)). diff --git a/akka-docs/src/main/paradox/project/licenses.md b/akka-docs/src/main/paradox/project/licenses.md index 5f1c69df4c..5d18ca3f50 100644 --- a/akka-docs/src/main/paradox/project/licenses.md +++ b/akka-docs/src/main/paradox/project/licenses.md @@ -22,10 +22,10 @@ the License. ## Akka Committer License Agreement -All committers have signed this [CLA](http://www.lightbend.com/contribute/current-cla). -It can be [signed online](http://www.lightbend.com/contribute/cla). +All committers have signed this [CLA](https://www.lightbend.com/contribute/current-cla). +It can be [signed online](https://www.lightbend.com/contribute/cla). ## Licenses for Dependency Libraries Each dependency and its license can be seen in the project build file (the comment on the side of each dependency): -@extref[AkkaBuild.scala](github:project/AkkaBuild.scala#L1054) \ No newline at end of file +@extref[AkkaBuild.scala](github:project/AkkaBuild.scala#L1054) diff --git a/akka-docs/src/main/paradox/project/links.md b/akka-docs/src/main/paradox/project/links.md index 1c2ce078a5..d7f450ea91 100644 --- a/akka-docs/src/main/paradox/project/links.md +++ b/akka-docs/src/main/paradox/project/links.md @@ -2,18 +2,18 @@ ## Commercial Support -Commercial support is provided by [Lightbend](http://www.lightbend.com). -Akka is part of the [Lightbend Platform](http://www.lightbend.com/platform). +Commercial support is provided by [Lightbend](https://www.lightbend.com). +Akka is part of the [Akka Platform](https://www.lightbend.com/akka-platform). ## Sponsors **Lightbend** is the company behind the Akka Project, Scala Programming Language, Play Web Framework, Lagom, sbt and many other open source projects. -It also provides the Lightbend Reactive Platform, which is powered by an open source core and commercial Enterprise Suite for building scalable Reactive systems on the JVM. Learn more at [lightbend.com](http://www.lightbend.com). +It also provides the Lightbend Reactive Platform, which is powered by an open source core and commercial Enterprise Suite for building scalable Reactive systems on the JVM. Learn more at [lightbend.com](https://www.lightbend.com). ## Akka Discuss Forums -[Akka Discuss Forums](http://discuss.akka.io) +[Akka Discuss Forums](https://discuss.akka.io) ## Gitter @@ -28,7 +28,7 @@ Akka uses Git and is hosted at [Github akka/akka](https://github.com/akka/akka). ## Releases Repository All Akka releases are published via Sonatype to Maven Central, see -[search.maven.org](http://search.maven.org/#search%7Cga%7C1%7Cg%3A%22com.typesafe.akka%22) +[search.maven.org](https://search.maven.org/search?q=g:com.typesafe.akka) ## Snapshots Repository @@ -36,7 +36,7 @@ Nightly builds are available in [https://repo.akka.io/snapshots](https://repo.ak timestamped versions. For timestamped versions, pick a timestamp from -[https://repo.akka.io/snapshots/com/typesafe/akka](https://repo.akka.io/snapshots/com/typesafe/akka). +[https://repo.akka.io/snapshots/com/typesafe/akka/](https://repo.akka.io/snapshots/com/typesafe/akka/). All Akka modules that belong to the same build have the same timestamp. @@@ warning @@ -57,7 +57,7 @@ Define the library dependencies with the timestamp as version. For example: @@@vars ``` -libraryDependencies += "com.typesafe.akka" % "akka-remote_$scala.binary_version$" % "2.5-20170510-230859" +libraryDependencies += "com.typesafe.akka" % "akka-remote_$scala.binary.version$" % "2.5-20170510-230859" ``` @@@ @@ -83,7 +83,7 @@ Define the library dependencies with the timestamp as version. For example: com.typesafe.akka - akka-remote_$scala.binary_version$ + akka-remote_$scala.binary.version$ 2.5-20170510-230859 diff --git a/akka-docs/src/main/paradox/project/migration-guide-2.5.x-2.6.x.md b/akka-docs/src/main/paradox/project/migration-guide-2.5.x-2.6.x.md index ed07987267..6413f37f0f 100644 --- a/akka-docs/src/main/paradox/project/migration-guide-2.5.x-2.6.x.md +++ b/akka-docs/src/main/paradox/project/migration-guide-2.5.x-2.6.x.md @@ -280,7 +280,7 @@ Explicitly disable Artery by setting property `akka.remote.artery.enabled` to `f specific to classic remoting needs to be moved to `akka.remote.classic`. To see which configuration options are specific to classic search for them in: @ref:[`akka-remote/reference.conf`](../general/configuration-reference.md#config-akka-remote). -If you have a [Lightbend Platform Subscription](https://www.lightbend.com/lightbend-platform-subscription) you can use our [Config Checker](https://doc.akka.io/docs/akka-enhancements/current/config-checker.html) enhancement to flag any settings that have not been properly migrated. +If you have a [Lightbend Subscription](https://www.lightbend.com/lightbend-subscription) you can use our [Config Checker](https://doc.akka.io/docs/akka-enhancements/current/config-checker.html) enhancement to flag any settings that have not been properly migrated. ### Persistent mode for Cluster Sharding diff --git a/akka-docs/src/main/paradox/project/migration-guide-old.md b/akka-docs/src/main/paradox/project/migration-guide-old.md index 2d1e45e4fd..884d66179d 100644 --- a/akka-docs/src/main/paradox/project/migration-guide-old.md +++ b/akka-docs/src/main/paradox/project/migration-guide-old.md @@ -3,7 +3,7 @@ Migration from old versions: * [2.3.x to 2.4.x](https://doc.akka.io/docs/akka/2.4/project/migration-guide-2.3.x-2.4.x.html) -* [2.2.x to 2.3.x](https://doc.akka.io/docs/akka/2.3.12/project/migration-guide-2.2.x-2.3.x.html) -* [2.1.x to 2.2.x](https://doc.akka.io/docs/akka/2.2.3/project/migration-guide-2.1.x-2.2.x.html) -* [2.0.x to 2.1.x](https://doc.akka.io/docs/akka/2.1.4/project/migration-guide-2.0.x-2.1.x.html) +* [2.2.x to 2.3.x](https://doc.akka.io/docs/akka/2.3/project/migration-guide-2.2.x-2.3.x.html) +* [2.1.x to 2.2.x](https://doc.akka.io/docs/akka/2.2/project/migration-guide-2.1.x-2.2.x.html) +* [2.0.x to 2.1.x](https://doc.akka.io/docs/akka/2.1/project/migration-guide-2.0.x-2.1.x.html) * [1.3.x to 2.0.x](https://doc.akka.io/docs/akka/2.0.5/project/migration-guide-1.3.x-2.0.x.html). diff --git a/akka-docs/src/main/paradox/remoting-artery.md b/akka-docs/src/main/paradox/remoting-artery.md index 683d71e95c..ef3a800c13 100644 --- a/akka-docs/src/main/paradox/remoting-artery.md +++ b/akka-docs/src/main/paradox/remoting-artery.md @@ -22,7 +22,7 @@ To use Artery Remoting, you must add the following dependency in your project: @@dependency[sbt,Maven,Gradle] { group=com.typesafe.akka - artifact=akka-remote_$scala.binary_version$ + artifact=akka-remote_$scala.binary.version$ version=$akka.version$ } @@ -311,13 +311,13 @@ According to [RFC 7525](https://tools.ietf.org/html/rfc7525) the recommended alg You should always check the latest information about security and algorithm recommendations though before you configure your system. Creating and working with keystores and certificates is well documented in the -[Generating X.509 Certificates](http://lightbend.github.io/ssl-config/CertificateGeneration.html#using-keytool) +[Generating X.509 Certificates](https://lightbend.github.io/ssl-config/CertificateGeneration.html#using-keytool) section of Lightbend's SSL-Config library. Since an Akka remoting is inherently @ref:[peer-to-peer](general/remoting.md#symmetric-communication) both the key-store as well as trust-store need to be configured on each remoting node participating in the cluster. -The official [Java Secure Socket Extension documentation](http://docs.oracle.com/javase/7/docs/technotes/guides/security/jsse/JSSERefGuide.html) +The official [Java Secure Socket Extension documentation](https://docs.oracle.com/javase/8/docs/technotes/guides/security/jsse/JSSERefGuide.html) as well as the [Oracle documentation on creating KeyStore and TrustStores](https://docs.oracle.com/cd/E19509-01/820-3503/6nf1il6er/index.html) are both great resources to research when setting up security on the JVM. Please consult those resources when troubleshooting and configuring SSL. @@ -717,7 +717,7 @@ The needed classpath: Agrona-0.5.4.jar:aeron-driver-1.0.1.jar:aeron-client-1.0.1.jar ``` -You find those jar files on [Maven Central](http://search.maven.org/), or you can create a +You find those jar files on [Maven Central](https://search.maven.org/), or you can create a package with your preferred build tool. You can pass [Aeron properties](https://github.com/real-logic/Aeron/wiki/Configuration-Options) as diff --git a/akka-docs/src/main/paradox/remoting.md b/akka-docs/src/main/paradox/remoting.md index fcc7174fe1..4e3597c17a 100644 --- a/akka-docs/src/main/paradox/remoting.md +++ b/akka-docs/src/main/paradox/remoting.md @@ -26,7 +26,7 @@ To use Akka Remoting, you must add the following dependency in your project: @@dependency[sbt,Maven,Gradle] { group=com.typesafe.akka - artifact=akka-remote_$scala.binary_version$ + artifact=akka-remote_$scala.binary.version$ version=$akka.version$ } @@ -486,13 +486,13 @@ According to [RFC 7525](https://tools.ietf.org/html/rfc7525) the recommended alg You should always check the latest information about security and algorithm recommendations though before you configure your system. Creating and working with keystores and certificates is well documented in the -[Generating X.509 Certificates](http://lightbend.github.io/ssl-config/CertificateGeneration.html#using-keytool) +[Generating X.509 Certificates](https://lightbend.github.io/ssl-config/CertificateGeneration.html#using-keytool) section of Lightbend's SSL-Config library. Since an Akka remoting is inherently @ref:[peer-to-peer](general/remoting.md#symmetric-communication) both the key-store as well as trust-store need to be configured on each remoting node participating in the cluster. -The official [Java Secure Socket Extension documentation](http://docs.oracle.com/javase/7/docs/technotes/guides/security/jsse/JSSERefGuide.html) +The official [Java Secure Socket Extension documentation](https://docs.oracle.com/javase/8/docs/technotes/guides/security/jsse/JSSERefGuide.html) as well as the [Oracle documentation on creating KeyStore and TrustStores](https://docs.oracle.com/cd/E19509-01/820-3503/6nf1il6er/index.html) are both great resources to research when setting up security on the JVM. Please consult those resources when troubleshooting and configuring SSL. diff --git a/akka-docs/src/main/paradox/routing.md b/akka-docs/src/main/paradox/routing.md index ddd3d01f76..feb311203d 100644 --- a/akka-docs/src/main/paradox/routing.md +++ b/akka-docs/src/main/paradox/routing.md @@ -9,7 +9,7 @@ To use Routing, you must add the following dependency in your project: @@dependency[sbt,Maven,Gradle] { group="com.typesafe.akka" - artifact="akka-actor_$scala.binary_version$" + artifact="akka-actor_$scala.binary.version$" version="$akka.version$" } @@ -257,7 +257,7 @@ Java ### RoundRobinPool and RoundRobinGroup -Routes in a [round-robin](http://en.wikipedia.org/wiki/Round-robin) fashion to its routees. +Routes in a [round-robin](https://en.wikipedia.org/wiki/Round-robin) fashion to its routees. RoundRobinPool defined in configuration: @@ -598,7 +598,7 @@ Java ### ConsistentHashingPool and ConsistentHashingGroup -The ConsistentHashingPool uses [consistent hashing](http://en.wikipedia.org/wiki/Consistent_hashing) +The ConsistentHashingPool uses [consistent hashing](https://en.wikipedia.org/wiki/Consistent_hashing) to select a routee based on the sent message. This [article](http://www.tom-e-white.com/2007/11/consistent-hashing.html) gives good insight into how consistent hashing is implemented. diff --git a/akka-docs/src/main/paradox/scheduler.md b/akka-docs/src/main/paradox/scheduler.md index 3225e40b4f..600edeae22 100644 --- a/akka-docs/src/main/paradox/scheduler.md +++ b/akka-docs/src/main/paradox/scheduler.md @@ -12,7 +12,7 @@ To use Scheduler, you must add the following dependency in your project: @@dependency[sbt,Maven,Gradle] { group="com.typesafe.akka" - artifact="akka-actor_$scala.binary_version$" + artifact="akka-actor_$scala.binary.version$" version="$akka.version$" } diff --git a/akka-docs/src/main/paradox/security/2017-02-10-java-serialization.md b/akka-docs/src/main/paradox/security/2017-02-10-java-serialization.md index 4fa590288f..82e40e849f 100644 --- a/akka-docs/src/main/paradox/security/2017-02-10-java-serialization.md +++ b/akka-docs/src/main/paradox/security/2017-02-10-java-serialization.md @@ -27,7 +27,7 @@ Please subscribe to the [akka-security](https://groups.google.com/forum/#!forum/ ### Severity -The [CVSS](https://en.wikipedia.org/wiki/CVSS) score of this vulnerability is 6.8 (Medium), based on vector [AV:A/AC:M/Au:N/C:C/I:C/A:C/E:F/RL:TF/RC:C](https://nvd.nist.gov/cvss.cfm?calculator&version=2&vector=\(AV:A/AC:M/Au:N/C:C/I:C/A:C/E:F/RL:TF/RC:C\)). +The [CVSS](https://en.wikipedia.org/wiki/CVSS) score of this vulnerability is 6.8 (Medium), based on vector [AV:A/AC:M/Au:N/C:C/I:C/A:C/E:F/RL:TF/RC:C](https://nvd.nist.gov/vuln-metrics/cvss/v2-calculator?calculator&version=2&vector=%5C(AV:A/AC:M/Au:N/C:C/I:C/A:C/E:F/RL:TF/RC:C%5C)). Rationale for the score: diff --git a/akka-docs/src/main/paradox/serialization-classic.md b/akka-docs/src/main/paradox/serialization-classic.md index 01debcb51e..03f324b707 100644 --- a/akka-docs/src/main/paradox/serialization-classic.md +++ b/akka-docs/src/main/paradox/serialization-classic.md @@ -11,7 +11,7 @@ To use Serialization, you must add the following dependency in your project: @@dependency[sbt,Maven,Gradle] { group="com.typesafe.akka" - artifact="akka-actor_$scala.binary_version$" + artifact="akka-actor_$scala.binary.version$" version="$akka.version$" } diff --git a/akka-docs/src/main/paradox/serialization-jackson.md b/akka-docs/src/main/paradox/serialization-jackson.md index 7db9903329..775344ab92 100644 --- a/akka-docs/src/main/paradox/serialization-jackson.md +++ b/akka-docs/src/main/paradox/serialization-jackson.md @@ -9,7 +9,7 @@ To use Jackson Serialization, you must add the following dependency in your proj @@dependency[sbt,Maven,Gradle] { group="com.typesafe.akka" - artifact="akka-serialization-jackson_$scala.binary_version$" + artifact="akka-serialization-jackson_$scala.binary.version$" version="$akka.version$" } @@ -377,7 +377,9 @@ the `jackson-json` binding the default configuration is: @@snip [reference.conf](/akka-serialization-jackson/src/main/resources/reference.conf) { #compression } -Messages larger than the `compress-larger-than` property are compressed with GZIP. +Supported compression algorithms are: gzip, lz4. Use 'off' to disable compression. +Gzip is generally slower than lz4. +Messages larger than the `compress-larger-than` property are compressed. Compression can be disabled by setting the `algorithm` property to `off`. It will still be able to decompress payloads that were compressed when serialized, e.g. if this configuration is changed. diff --git a/akka-docs/src/main/paradox/serialization.md b/akka-docs/src/main/paradox/serialization.md index 718fed94ab..8970b19d1f 100644 --- a/akka-docs/src/main/paradox/serialization.md +++ b/akka-docs/src/main/paradox/serialization.md @@ -9,7 +9,7 @@ To use Serialization, you must add the following dependency in your project: @@dependency[sbt,Maven,Gradle] { group="com.typesafe.akka" - artifact="akka-actor_$scala.binary_version$" + artifact="akka-actor_$scala.binary.version$" version="$akka.version$" } @@ -59,7 +59,7 @@ you would need to reference it as `Wrapper$Message` instead of `Wrapper.Message` @@@ -Akka provides serializers for several primitive types and [protobuf](http://code.google.com/p/protobuf/) +Akka provides serializers for several primitive types and [protobuf](https://github.com/protocolbuffers/protobuf) `com.google.protobuf.GeneratedMessage` (protobuf2) and `com.google.protobuf.GeneratedMessageV3` (protobuf3) by default (the latter only if depending on the akka-remote module), so normally you don't need to add configuration for that if you send raw protobuf messages as actor messages. @@ -82,6 +82,7 @@ Scala Java : @@snip [SerializationDocTest.java](/akka-docs/src/test/java/jdocs/serialization/SerializationDocTest.java) { #programmatic } + The manifest is a type hint so that the same serializer can be used for different classes. Note that when deserializing from bytes the manifest and the identifier of the serializer are needed. @@ -119,6 +120,21 @@ Scala Java : @@snip [SerializationDocTest.java](/akka-docs/src/test/java/jdocs/serialization/SerializationDocTest.java) { #my-own-serializer } +The `identifier` must be unique. The identifier is used when selecting which serializer to use for deserialization. +If you have accidentally configured several serializers with the same identifier that will be detected and prevent +the `ActorSystem` from being started. It can be a hardcoded value because it must remain the same value to support +rolling updates. + +@@@ div { .group-scala } + +If you prefer to define the identifier in cofiguration that is supported by the `BaseSerializer` trait, which +implements the `def identifier` by reading it from configuration based on the serializer's class name: + +Scala +: @@snip [SerializationDocSpec.scala](/akka-docs/src/test/scala/docs/serialization/SerializationDocSpec.scala) { #serialization-identifiers-config } + +@@@ + The manifest is a type hint so that the same serializer can be used for different classes. The manifest parameter in @scala[`fromBinary`]@java[`fromBinaryJava`] is the class of the object that was serialized. In `fromBinary` you can match on the class and deserialize the diff --git a/akka-docs/src/main/paradox/split-brain-resolver.md b/akka-docs/src/main/paradox/split-brain-resolver.md new file mode 100644 index 0000000000..e6886d22c6 --- /dev/null +++ b/akka-docs/src/main/paradox/split-brain-resolver.md @@ -0,0 +1,481 @@ +# Split Brain Resolver + +When operating an Akka cluster you must consider how to handle +[network partitions](http://en.wikipedia.org/wiki/Network_partition) (a.k.a. split brain scenarios) +and machine crashes (including JVM and hardware failures). This is crucial for correct behavior if +you use @ref:[Cluster Singleton](typed/cluster-singleton.md) or @ref:[Cluster Sharding](typed/cluster-sharding.md), +especially together with Akka Persistence. + +## Module info + +To use Akka Split Brain Resolver is part of `akka-cluster` and you probably already have that +dependency included. Otherwise, add the following dependency in your project: + +@@dependency[sbt,Maven,Gradle] { + group=com.typesafe.akka + artifact=akka-cluster_$scala.binary.version$ + version=$akka.version$ +} + +@@project-info{ projectId="akka-cluster" } + +## Enable the Split Brain Resolver + +You need to enable the Split Brain Resolver by configuring it as downing provider in the configuration of +the `ActorSystem` (`application.conf`): + +``` +akka.cluster.downing-provider-class = "akka.cluster.sbr.SplitBrainResolverProvider" +``` + +You should also consider the different available @ref:[downing strategies](#strategies). + +## The Problem + +A fundamental problem in distributed systems is that network partitions (split brain scenarios) and +machine crashes are indistinguishable for the observer, i.e. a node can observe that there is a problem +with another node, but it cannot tell if it has crashed and will never be available again or if there is +a network issue that might or might not heal again after a while. Temporary and permanent failures are +indistinguishable because decisions must be made in finite time, and there always exists a temporary +failure that lasts longer than the time limit for the decision. + +A third type of problem is if a process is unresponsive, e.g. because of overload, CPU starvation or +long garbage collection pauses. This is also indistinguishable from network partitions and crashes. +The only signal we have for decision is "no reply in given time for heartbeats" and this means that +phenomena causing delays or lost heartbeats are indistinguishable from each other and must be +handled in the same way. + +When there is a crash, we would like to remove the affected node immediately from the cluster membership. +When there is a network partition or unresponsive process we would like to wait for a while in the hope +that it is a transient problem that will heal again, but at some point, we must give up and continue with +the nodes on one side of the partition and shut down nodes on the other side. Also, certain features are +not fully available during partitions so it might not matter that the partition is transient or not if +it just takes too long. Those two goals are in conflict with each other and there is a trade-off +between how quickly we can remove a crashed node and premature action on transient network partitions. + +This is a difficult problem to solve given that the nodes on the different sides of the network partition +cannot communicate with each other. We must ensure that both sides can make this decision by themselves and +that they take the same decision about which part will keep running and which part will shut itself down. + +Another type of problem that makes it difficult to see the "right" picture is when some nodes are not fully +connected and cannot communicate directly to each other but information can be disseminated between them via +other nodes. + +The Akka cluster has a failure detector that will notice network partitions and machine crashes (but it +cannot distinguish the two). It uses periodic heartbeat messages to check if other nodes are available +and healthy. These observations by the failure detector are referred to as a node being *unreachable* +and it may become *reachable* again if the failure detector observes that it can communicate with it again. + +The failure detector in itself is not enough for making the right decision in all situations. +The naive approach is to remove an unreachable node from the cluster membership after a timeout. +This works great for crashes and short transient network partitions, but not for long network +partitions. Both sides of the network partition will see the other side as unreachable and +after a while remove it from its cluster membership. Since this happens on both sides the result +is that two separate disconnected clusters have been created. +This approach is provided by the opt-in (off by default) auto-down feature in the OSS version of +Akka Cluster. + +If you use the timeout based auto-down feature in combination with Cluster Singleton or Cluster Sharding +that would mean that two singleton instances or two sharded entities with the same identifier would be running. +One would be running: one in each cluster. +For example when used together with Akka Persistence that could result in that two instances of a +persistent actor with the same `persistenceId` are running and writing concurrently to the +same stream of persistent events, which will have fatal consequences when replaying these events. + +The default setting in Akka Cluster is to not remove unreachable nodes automatically and +the recommendation is that the decision of what to +do should be taken by a human operator or an external monitoring system. This is a valid solution, +but not very convenient if you do not have this staff or external system for other reasons. + +If the unreachable nodes are not downed at all they will still be part of the cluster membership. +Meaning that Cluster Singleton and Cluster Sharding will not failover to another node. While there +are unreachable nodes new nodes that are joining the cluster will not be promoted to full worthy +members (with status Up). Similarly, leaving members will not be removed until all unreachable +nodes have been resolved. In other words, keeping unreachable members for an unbounded time is +undesirable. + +With that introduction of the problem domain, it is time to look at the provided strategies for +handling network partition, unresponsive nodes and crashed nodes. + +## Strategies + +By default the @ref:[Keep Majority](#keep-majority) strategy will be used because it works well for +most systems. However, it's wort considering the other available strategies and pick a strategy that fits +the characteristics of your system. For example, in a Kubernetes environment the @ref:[Lease](#lease) strategy +can be a good choice. + +Every strategy has a failure scenario where it makes a "wrong" decision. This section describes the different +strategies and guidelines of when to use what. + +When there is uncertainty it selects to down more nodes than necessary, or even downing of all nodes. +Therefore Split Brain Resolver should always be combined with a mechanism to automatically start up nodes that +have been shutdown, and join them to the existing cluster or form a new cluster again. + +You enable a strategy with the configuration property `akka.cluster.split-brain-resolver.active-strategy`. + +### Stable after + +All strategies are inactive until the cluster membership and the information about unreachable nodes +have been stable for a certain time period. Continuously adding more nodes while there is a network +partition does not influence this timeout, since the status of those nodes will not be changed to Up +while there are unreachable nodes. Joining nodes are not counted in the logic of the strategies. + +@@snip [reference.conf](/akka-cluster/src/main/resources/reference.conf) { #split-brain-resolver } + +Set `akka.cluster.split-brain-resolver.stable-after` to a shorter duration to have quicker removal of crashed nodes, +at the price of risking too early action on transient network partitions that otherwise would have healed. Do not +set this to a shorter duration than the membership dissemination time in the cluster, which depends +on the cluster size. Recommended minimum duration for different cluster sizes: + +|cluster size | stable-after| +|-------------|-------------| +|5 | 7 s | +|10 | 10 s| +|20 | 13 s| +|50 | 17 s| +|100 | 20 s| +|1000 | 30 s| + +The different strategies may have additional settings that are described below. + +@@@ note + +It is important that you use the same configuration on all nodes. + +@@@ + +The side of the split that decides to shut itself down will use the cluster *down* command +to initiate the removal of a cluster member. When that has been spread among the reachable nodes +it will be removed from the cluster membership. + +It's good to terminate the `ActorSystem` and exit the JVM when the node is removed from the cluster. + +That is handled by @ref:[Coordinated Shutdown](coordinated-shutdown.md) +but to exit the JVM it's recommended that you enable: + +``` +akka.coordinated-shutdown.exit-jvm = on +``` + +@@@ note + +Some legacy containers may block calls to System.exit(..) and you may have to find an alternate +way to shut the app down. For example, when running Akka on top of a Spring / Tomcat setup, you +could replace the call to `System.exit(..)` with a call to Spring's ApplicationContext .close() method +(or with a HTTP call to Tomcat Manager's API to un-deploy the app). + +@@@ + +### Keep Majority + +The strategy named `keep-majority` will down the unreachable nodes if the current node is in +the majority part based on the last known membership information. Otherwise down the reachable nodes, +i.e. the own part. If the parts are of equal size the part containing the node with the lowest +address is kept. + +This strategy is a good choice when the number of nodes in the cluster change dynamically and you can +therefore not use `static-quorum`. + +This strategy also handles the edge case that may occur when there are membership changes at the same +time as the network partition occurs. For example, the status of two members are changed to `Up` +on one side but that information is not disseminated to the other side before the connection is broken. +Then one side sees two more nodes and both sides might consider themselves having a majority. It will +detect this situation and make the safe decision to down all nodes on the side that could be in minority +if the joining nodes were changed to `Up` on the other side. Note that this has the drawback that +if the joining nodes were not changed to `Up` and becoming a majority on the other side then each part +will shut down itself, terminating the whole cluster. + +Note that if there are more than two partitions and none is in majority each part will shut down +itself, terminating the whole cluster. + +If more than half of the nodes crash at the same time the other running nodes will down themselves +because they think that they are not in majority, and thereby the whole cluster is terminated. + +The decision can be based on nodes with a configured `role` instead of all nodes in the cluster. +This can be useful when some types of nodes are more valuable than others. You might for example +have some nodes responsible for persistent data and some nodes with stateless worker services. +Then it probably more important to keep as many persistent data nodes as possible even though +it means shutting down more worker nodes. + +Configuration: + +``` +akka.cluster.split-brain-resolver.active-strategy=keep-majority +``` + +@@snip [reference.conf](/akka-cluster/src/main/resources/reference.conf) { #keep-majority } + +### Static Quorum + +The strategy named `static-quorum` will down the unreachable nodes if the number of remaining +nodes are greater than or equal to a configured `quorum-size`. Otherwise, it will down the reachable nodes, +i.e. it will shut down that side of the partition. In other words, the `quorum-size` defines the minimum +number of nodes that the cluster must have to be operational. + +This strategy is a good choice when you have a fixed number of nodes in the cluster, or when you can +define a fixed number of nodes with a certain role. + +For example, in a 9 node cluster you will configure the `quorum-size` to 5. If there is a network split +of 4 and 5 nodes the side with 5 nodes will survive and the other 4 nodes will be downed. After that, +in the 5 node cluster, no more failures can be handled, because the remaining cluster size would be +less than 5. In the case of another failure in that 5 node cluster all nodes will be downed. + +Therefore it is important that you join new nodes when old nodes have been removed. + +Another consequence of this is that if there are unreachable nodes when starting up the cluster, +before reaching this limit, the cluster may shut itself down immediately. This is not an issue +if you start all nodes at approximately the same time or use the `akka.cluster.min-nr-of-members` +to define required number of members before the leader changes member status of 'Joining' members to 'Up' +You can tune the timeout after which downing decisions are made using the `stable-after` setting. + +You should not add more members to the cluster than **quorum-size * 2 - 1**. A warning is logged +if this recommendation is violated. If the exceeded cluster size remains when a SBR decision is +needed it will down all nodes because otherwise there is a risk that both sides may down each +other and thereby form two separate clusters. + +For rolling updates it's best to leave the cluster gracefully via +@ref:[Coordinated Shutdown](coordinated-shutdown.md) (SIGTERM). +For successful leaving SBR will not be used (no downing) but if there is an unreachability problem +at the same time as the rolling update is in progress there could be an SBR decision. To avoid that +the total number of members limit is not exceeded during the rolling update it's recommended to +leave and fully remove one node before adding a new one, when using `static-quorum`. + +If the cluster is split into 3 (or more) parts each part that is smaller than then configured `quorum-size` +will down itself and possibly shutdown the whole cluster. + +If more nodes than the configured `quorum-size` crash at the same time the other running nodes +will down themselves because they think that they are not in the majority, and thereby the whole +cluster is terminated. + +The decision can be based on nodes with a configured `role` instead of all nodes in the cluster. +This can be useful when some types of nodes are more valuable than others. You might, for example, +have some nodes responsible for persistent data and some nodes with stateless worker services. +Then it probably more important to keep as many persistent data nodes as possible even though +it means shutting down more worker nodes. + +There is another use of the `role` as well. By defining a `role` for a few (e.g. 7) stable +nodes in the cluster and using that in the configuration of `static-quorum` you will be able +to dynamically add and remove other nodes without this role and still have good decisions of what +nodes to keep running and what nodes to shut down in the case of network partitions. The advantage +of this approach compared to `keep-majority` (described below) is that you *do not* risk splitting +the cluster into two separate clusters, i.e. *a split brain**. You must still obey the rule of not +starting too many nodes with this `role` as described above. It also suffers the risk of shutting +down all nodes if there is a failure when there are not enough nodes with this `role` remaining +in the cluster, as described above. + +Configuration: + +``` +akka.cluster.split-brain-resolver.active-strategy=static-quorum +``` + +@@snip [reference.conf](/akka-cluster/src/main/resources/reference.conf) { #static-quorum } + +### Keep Oldest + +The strategy named `keep-oldest` will down the part that does not contain the oldest +member. The oldest member is interesting because the active Cluster Singleton instance +is running on the oldest member. + +There is one exception to this rule if `down-if-alone` is configured to `on`. +Then, if the oldest node has partitioned from all other nodes the oldest will down itself +and keep all other nodes running. The strategy will not down the single oldest node when +it is the only remaining node in the cluster. + +Note that if the oldest node crashes the others will remove it from the cluster +when `down-if-alone` is `on`, otherwise they will down themselves if the +oldest node crashes, i.e. shut down the whole cluster together with the oldest node. + +This strategy is good to use if you use Cluster Singleton and do not want to shut down the node +where the singleton instance runs. If the oldest node crashes a new singleton instance will be +started on the next oldest node. The drawback is that the strategy may keep only a few nodes +in a large cluster. For example, if one part with the oldest consists of 2 nodes and the +other part consists of 98 nodes then it will keep 2 nodes and shut down 98 nodes. + +This strategy also handles the edge case that may occur when there are membership changes at the same +time as the network partition occurs. For example, the status of the oldest member is changed to `Exiting` +on one side but that information is not disseminated to the other side before the connection is broken. +It will detect this situation and make the safe decision to down all nodes on the side that sees the oldest as +`Leaving`. Note that this has the drawback that if the oldest was `Leaving` and not changed to `Exiting` then +each part will shut down itself, terminating the whole cluster. + +The decision can be based on nodes with a configured `role` instead of all nodes in the cluster, +i.e. using the oldest member (singleton) within the nodes with that role. + +Configuration: + +``` +akka.cluster.split-brain-resolver.active-strategy=keep-oldest +``` + +@@snip [reference.conf](/akka-cluster/src/main/resources/reference.conf) { #keep-oldest } + +### Down All + +The strategy named `down-all` will down all nodes. + +This strategy can be a safe alternative if the network environment is highly unstable with unreachability observations +that can't be fully trusted, and including frequent occurrences of @ref:[indirectly connected nodes](#indirectly-connected-nodes). +Due to the instability there is an increased risk of different information on different sides of partitions and +therefore the other strategies may result in conflicting decisions. In such environments it can be better to shutdown +all nodes and start up a new fresh cluster. + +Shutting down all nodes means that the system will be completely unavailable until nodes have been restarted and +formed a new cluster. This strategy is not recommended for large clusters (> 10 nodes) because any minor problem +will shutdown all nodes, and that is more likely to happen in larger clusters since there are more nodes that +may fail. + +See also @ref[Down all when unstable](#down-all-when-unstable) and @ref:[indirectly connected nodes](#indirectly-connected-nodes). + +### Lease + +The strategy named `lease-majority` is using a distributed lease (lock) to decide what nodes that are allowed to +survive. Only one SBR instance can acquire the lease make the decision to remain up. The other side will +not be able to aquire the lease and will therefore down itself. + +Best effort is to keep the side that has most nodes, i.e. the majority side. This is achieved by adding a delay +before trying to acquire the lease on the minority side. + +There is currently one supported implementation of the lease which is backed by a +[Custom Resource Definition (CRD)](https://kubernetes.io/docs/concepts/extend-kubernetes/api-extension/custom-resources/) +in Kubernetes. It is described in the [Kubernetes Lease](https://doc.akka.io/docs/akka-management/current/kubernetes-lease.html) +documentation. + +This strategy is very safe since coordination is added by an external arbiter. The trade-off compared to other +strategies is that it requires additional infrastructure for implementing the lease and it reduces the availability +of a decision to that of the system backing the lease store. + +Similar to other strategies it is important that decisions are not deferred for too because the nodes that couldn't +acquire the lease must decide to down themselves, see @ref[Down all when unstable](#down-all-when-unstable). + +In some cases the lease will be unavailable when needed for a decision from all SBR instances, e.g. because it is +on another side of a network partition, and then all nodes will be downed. + +Configuration: + +``` +akka { + cluster { + downing-provider-class = "akka.cluster.sbr.SplitBrainResolverProvider" + split-brain-resolver { + active-strategy = "lease-majority" + lease-majority { + lease-implementation = "akka.lease.kubernetes" + } + } + } +} +``` + +@@snip [reference.conf](/akka-cluster/src/main/resources/reference.conf) { #lease-majority } + +See also configuration and additional dependency in [Kubernetes Lease](https://doc.akka.io/docs/akka-management/current/kubernetes-lease.html) + +## Indirectly connected nodes + +In a malfunctional network there can be situations where nodes are observed as unreachable via some network +links but they are still indirectly connected via other nodes, i.e. it's not a clean network partition (or node crash). + +When this situation is detected the Split Brain Resolvers will keep fully connected nodes and down all the indirectly +connected nodes. + +If there is a combination of indirectly connected nodes and a clean network partition it will combine the +above decision with the ordinary decision, e.g. keep majority, after excluding suspicious failure detection +observations. + +## Down all when unstable + +When reachability observations by the failure detector are changed the SBR decisions +are deferred until there are no changes within the `stable-after` duration. +If this continues for too long it might be an indication of an unstable system/network +and it could result in delayed or conflicting decisions on separate sides of a network +partition. + +As a precaution for that scenario all nodes are downed if no decision is made within +`stable-after + down-all-when-unstable` from the first unreachability event. +The measurement is reset if all unreachable have been healed, downed or removed, or +if there are no changes within `stable-after * 2`. + +This is enabled by default for all strategies and by default the duration is derived to +be 3/4 of `stable-after`. + +The below property can be defined as a duration of for how long the changes are acceptable to +continue after the `stable-after` or it can be set to `off` to disable this feature. + + +``` +akka.cluster.split-brain-resolver { + down-all-when-unstable = 15s + stable-after = 20s +} +``` + +@@@ warning + +It is recommended to keep `down-all-when-unstable` enabled and not set it to a longer duration than `stable-after` +(`down-removal-margin`) because that can result in delayed decisions on the side that should have been downed, e.g. +in the case of a clean network partition followed by continued instability on the side that should be downed. +That could result in that members are removed from one side but are still running on the other side. + +@@@ + +## Multiple data centers + +Akka Cluster has @ref:[support for multiple data centers](cluster-dc.md), where the cluster +membership is managed by each data center separately and independently of network partitions across different +data centers. The Split Brain Resolver is embracing that strategy and will not count nodes or down nodes in +another data center. + +When there is a network partition across data centers the typical solution is to wait the partition out until it heals, i.e. +do nothing. Other decisions should be performed by an external monitoring tool or human operator. + +## Cluster Singleton and Cluster Sharding + +The purpose of Cluster Singleton and Cluster Sharding is to run at most one instance +of a given actor at any point in time. When such an instance is shut down a new instance +is supposed to be started elsewhere in the cluster. It is important that the new instance is +not started before the old instance has been stopped. This is especially important when the +singleton or the sharded instance is persistent, since there must only be one active +writer of the journaled events of a persistent actor instance. + +Since the strategies on different sides of a network partition cannot communicate with each other +and they may take the decision at slightly different points in time there must be a time based +margin that makes sure that the new instance is not started before the old has been stopped. + +You would like to configure this to a short duration to have quick failover, but that will increase the +risk of having multiple singleton/sharded instances running at the same time and it may take a different +amount of time to act on the decision (dissemination of the down/removal). The duration is by default +the same as the `stable-after` property (see @ref:[Stable after](#stable-after) above). It is recommended to +leave this value as is, but it can also be separately overriden with the `akka.cluster.down-removal-margin` property. + +Another concern for setting this `stable-after`/`akka.cluster.down-removal-margin` is dealing with JVM pauses e.g. +garbage collection. When a node is unresponsive it is not known if it is due to a pause, overload, a crash or a +network partition. If it is pause that lasts longer than `stable-after` * 2 it gives time for SBR to down the node +and for singletons and shards to be started on other nodes. When the node un-pauses there will be a short time before +it sees its self as down where singletons and sharded actors are still running. It is therefore important to understand +the max pause time your application is likely to incur and make sure it is smaller than `stable-margin`. + +If you choose to set a separate value for `down-removal-margin`, the recommended minimum duration for different cluster sizes are: + +|cluster size | down-removal-margin| +|-------------|--------------------| +|5 | 7 s | +|10 | 10 s| +|20 | 13 s| +|50 | 17 s| +|100 | 20 s| +|1000 | 30 s| + +### Expected Failover Time + +As you have seen, there are several configured timeouts that add to the total failover latency. +With default configuration those are: + + * failure detection 5 seconds + * stable-after 20 seconds + * down-removal-margin (by default the same as stable-after) 20 seconds + +In total, you can expect the failover time of a singleton or sharded instance to be around 45 seconds +with default configuration. The default configuration is sized for a cluster of 100 nodes. If you have +around 10 nodes you can reduce the `stable-after` to around 10 seconds, +resulting in an expected failover time of around 25 seconds. diff --git a/akka-docs/src/main/paradox/stream/actor-interop.md b/akka-docs/src/main/paradox/stream/actor-interop.md index 2e8386e4b6..6b52523b43 100644 --- a/akka-docs/src/main/paradox/stream/actor-interop.md +++ b/akka-docs/src/main/paradox/stream/actor-interop.md @@ -6,7 +6,7 @@ To use Akka Streams, add the module to your project: @@dependency[sbt,Maven,Gradle] { group="com.typesafe.akka" - artifact="akka-stream_$scala.binary_version$" + artifact="akka-stream_$scala.binary.version$" version="$akka.version$" } diff --git a/akka-docs/src/main/paradox/stream/futures-interop.md b/akka-docs/src/main/paradox/stream/futures-interop.md index 9ec22c8dd9..4901ec5422 100644 --- a/akka-docs/src/main/paradox/stream/futures-interop.md +++ b/akka-docs/src/main/paradox/stream/futures-interop.md @@ -6,7 +6,7 @@ To use Akka Streams, add the module to your project: @@dependency[sbt,Maven,Gradle] { group="com.typesafe.akka" - artifact="akka-stream_$scala.binary_version$" + artifact="akka-stream_$scala.binary.version$" version="$akka.version$" } diff --git a/akka-docs/src/main/paradox/stream/index.md b/akka-docs/src/main/paradox/stream/index.md index 3d51561f0c..1dfce673b4 100644 --- a/akka-docs/src/main/paradox/stream/index.md +++ b/akka-docs/src/main/paradox/stream/index.md @@ -9,7 +9,7 @@ To use Akka Streams, add the module to your project: @@dependency[sbt,Maven,Gradle] { group="com.typesafe.akka" - artifact="akka-stream_$scala.binary_version$" + artifact="akka-stream_$scala.binary.version$" version="$akka.version$" } diff --git a/akka-docs/src/main/paradox/stream/operators/ActorFlow/ask.md b/akka-docs/src/main/paradox/stream/operators/ActorFlow/ask.md index 8f65f6aa7f..0eb9276624 100644 --- a/akka-docs/src/main/paradox/stream/operators/ActorFlow/ask.md +++ b/akka-docs/src/main/paradox/stream/operators/ActorFlow/ask.md @@ -10,7 +10,7 @@ This operator is included in: @@dependency[sbt,Maven,Gradle] { group="com.typesafe.akka" - artifact="akka-stream-typed_$scala.binary_version$" + artifact="akka-stream-typed_$scala.binary.version$" version="$akka.version$" } diff --git a/akka-docs/src/main/paradox/stream/operators/ActorSink/actorRef.md b/akka-docs/src/main/paradox/stream/operators/ActorSink/actorRef.md index 924f1f1ccf..f7b84e3cfb 100644 --- a/akka-docs/src/main/paradox/stream/operators/ActorSink/actorRef.md +++ b/akka-docs/src/main/paradox/stream/operators/ActorSink/actorRef.md @@ -10,17 +10,13 @@ This operator is included in: @@dependency[sbt,Maven,Gradle] { group="com.typesafe.akka" - artifact="akka-stream-typed_$scala.binary_version$" + artifact="akka-stream-typed_$scala.binary.version$" version="$akka.version$" } -@@@div { .group-scala } - ## Signature -@@signature [ActorSink.scala](/akka-stream-typed/src/main/scala/akka/stream/typed/scaladsl/ActorSink.scala) { #actorRef } - -@@@ +@apidoc[ActorSink.actorRef](ActorSink$) { scala="#actorRef[T](ref:akka.actor.typed.ActorRef[T],onCompleteMessage:T,onFailureMessage:Throwable=>T):akka.stream.scaladsl.Sink[T,akka.NotUsed]" java="#actorRef(akka.actor.typed.ActorRef,java.lang.Object,akka.japi.function.Function)" } ## Description diff --git a/akka-docs/src/main/paradox/stream/operators/ActorSink/actorRefWithBackpressure.md b/akka-docs/src/main/paradox/stream/operators/ActorSink/actorRefWithBackpressure.md index 8f7c828fc0..0b601a8f1f 100644 --- a/akka-docs/src/main/paradox/stream/operators/ActorSink/actorRefWithBackpressure.md +++ b/akka-docs/src/main/paradox/stream/operators/ActorSink/actorRefWithBackpressure.md @@ -10,17 +10,13 @@ This operator is included in: @@dependency[sbt,Maven,Gradle] { group="com.typesafe.akka" - artifact="akka-stream-typed_$scala.binary_version$" + artifact="akka-stream-typed_$scala.binary.version$" version="$akka.version$" } -@@@div { .group-scala } - ## Signature -@@signature [ActorSink.scala](/akka-stream-typed/src/main/scala/akka/stream/typed/scaladsl/ActorSink.scala) { #actorRefWithBackpressure } - -@@@ +@apidoc[ActorSink.actorRefWithBackpressure](ActorSink$) { scala="#actorRefWithBackpressure[T,M,A](ref:akka.actor.typed.ActorRef[M],messageAdapter:(akka.actor.typed.ActorRef[A],T)=>M,onInitMessage:akka.actor.typed.ActorRef[A]=>M,ackMessage:A,onCompleteMessage:M,onFailureMessage:Throwable=>M):akka.stream.scaladsl.Sink[T,akka.NotUsed]" java="#actorRefWithBackpressure(akka.actor.typed.ActorRef,akka.japi.function.Function2,akka.japi.function.Function,java.lang.Object,java.lang.Object,akka.japi.function.Function)" } ## Description diff --git a/akka-docs/src/main/paradox/stream/operators/ActorSource/actorRef.md b/akka-docs/src/main/paradox/stream/operators/ActorSource/actorRef.md index ed16e84aee..c08e569dd7 100644 --- a/akka-docs/src/main/paradox/stream/operators/ActorSource/actorRef.md +++ b/akka-docs/src/main/paradox/stream/operators/ActorSource/actorRef.md @@ -10,17 +10,13 @@ This operator is included in: @@dependency[sbt,Maven,Gradle] { group="com.typesafe.akka" - artifact="akka-stream-typed_$scala.binary_version$" + artifact="akka-stream-typed_$scala.binary.version$" version="$akka.version$" } -@@@div { .group-scala } - ## Signature -@@signature [ActorSource.scala](/akka-stream-typed/src/main/scala/akka/stream/typed/scaladsl/ActorSource.scala) { #actorRef } - -@@@ +@apidoc[ActorSource.actorRef](ActorSource$) { scala="#actorRef[T](completionMatcher:PartialFunction[T,Unit],failureMatcher:PartialFunction[T,Throwable],bufferSize:Int,overflowStrategy:akka.stream.OverflowStrategy):akka.stream.scaladsl.Source[T,akka.actor.typed.ActorRef[T]]" java="#actorRef(java.util.function.Predicate,akka.japi.function.Function,int,akka.stream.OverflowStrategy)" } ## Description diff --git a/akka-docs/src/main/paradox/stream/operators/ActorSource/actorRefWithBackpressure.md b/akka-docs/src/main/paradox/stream/operators/ActorSource/actorRefWithBackpressure.md index b2c52d4eb6..e6988389ad 100644 --- a/akka-docs/src/main/paradox/stream/operators/ActorSource/actorRefWithBackpressure.md +++ b/akka-docs/src/main/paradox/stream/operators/ActorSource/actorRefWithBackpressure.md @@ -10,16 +10,13 @@ This operator is included in: @@dependency[sbt,Maven,Gradle] { group="com.typesafe.akka" - artifact="akka-stream-typed_$scala.binary_version$" + artifact="akka-stream-typed_$scala.binary.version$" version="$akka.version$" } -@@@div { .group-scala } - ## Signature -@@signature [ActorSource.scala](/akka-stream-typed/src/main/scala/akka/stream/typed/scaladsl/ActorSource.scala) { #actorRefWithBackpressure } -@@@ +@apidoc[ActorSource.actorRefWithBackpressure](ActorSource$) { scala="#actorRefWithBackpressure[T,Ack](ackTo:akka.actor.typed.ActorRef[Ack],ackMessage:Ack,completionMatcher:PartialFunction[T,akka.stream.CompletionStrategy],failureMatcher:PartialFunction[T,Throwable]):akka.stream.scaladsl.Source[T,akka.actor.typed.ActorRef[T]]" java="#actorRefWithBackpressure(akka.actor.typed.ActorRef,java.lang.Object,akka.japi.function.Function,akka.japi.function.Function)" } ## Description diff --git a/akka-docs/src/main/paradox/stream/operators/Flow/fromSinkAndSource.md b/akka-docs/src/main/paradox/stream/operators/Flow/fromSinkAndSource.md index b7d08ed73b..aae3771eaf 100644 --- a/akka-docs/src/main/paradox/stream/operators/Flow/fromSinkAndSource.md +++ b/akka-docs/src/main/paradox/stream/operators/Flow/fromSinkAndSource.md @@ -34,7 +34,7 @@ Java With this server running you could use `telnet 127.0.0.1 9999` to see a stream of timestamps being printed, one every second. -The following sample is a little bit more advanced and uses the @apidoc[MergeHub] to dynamically merge incoming messages to a single stream which is then fed into a @apidoc[BroadcastHub] which emits elements over a dynamic set of downstreams allowing us to create a simplistic little TCP chat server in which a text entered from one client is emitted to all connected clients. +The following sample is a little bit more advanced and uses the @apidoc[MergeHub$] to dynamically merge incoming messages to a single stream which is then fed into a @apidoc[BroadcastHub$] which emits elements over a dynamic set of downstreams allowing us to create a simplistic little TCP chat server in which a text entered from one client is emitted to all connected clients. Scala : @@snip [FromSinkAndSource.scala](/akka-docs/src/test/scala/docs/stream/operators/flow/FromSinkAndSource.scala) { #chat } diff --git a/akka-docs/src/main/paradox/stream/operators/Flow/futureFlow.md b/akka-docs/src/main/paradox/stream/operators/Flow/futureFlow.md index f9f30da3f2..55a2ba4b5f 100644 --- a/akka-docs/src/main/paradox/stream/operators/Flow/futureFlow.md +++ b/akka-docs/src/main/paradox/stream/operators/Flow/futureFlow.md @@ -36,5 +36,9 @@ Scala **completes** when upstream completes and all futures have been completed and all elements have been emitted +**cancels** when downstream cancels (keep reading) + The operator's default behaviour in case of downstream cancellation before nested flow materialization (future completion) is to cancel immediately. + This behaviour can be controlled by setting the [[akka.stream.Attributes.NestedMaterializationCancellationPolicy.PropagateToNested]] attribute, + this will delay downstream cancellation until nested flow's materialization which is then immediately cancelled (with the original cancellation cause). @@@ diff --git a/akka-docs/src/main/paradox/stream/operators/Flow/lazyFlow.md b/akka-docs/src/main/paradox/stream/operators/Flow/lazyFlow.md index 02c995ea39..950ac5d625 100644 --- a/akka-docs/src/main/paradox/stream/operators/Flow/lazyFlow.md +++ b/akka-docs/src/main/paradox/stream/operators/Flow/lazyFlow.md @@ -11,11 +11,45 @@ Defers creation and materialization of a `Flow` until there is a first element. ## Description -When the first element comes from upstream the actual `Flow` is created and materialized. -The internal `Flow` will not be created if there are no elements on completion or failure of up or downstream. +Defers `Flow` creation and materialization until when the first element arrives at the `lazyFlow` from upstream. After +that the stream behaves as if the nested flow replaced the `lazyFlow`. +The nested `Flow` will not be created if the outer flow completes or fails before any elements arrive. -The materialized value of the `Flow` will be the materialized value of the created internal flow if it is materialized -and failed with a `akka.stream.NeverMaterializedException` if the stream fails or completes without the flow being materialized. +Note that asynchronous boundaries and many other operators in the stream may do pre-fetching or trigger demand and thereby making an early element come throught the stream leading to creation of the inner flow earlier than you would expect. + +The materialized value of the `Flow` is a @scala[`Future`]@java[`CompletionStage`] that is completed with the +materialized value of the nested flow once that is constructed. + +See also: + + * @ref:[flatMapPrefix](../Source-or-Flow/flatMapPrefix.md) + * @ref:[Flow.lazyFutureFlow](lazyFutureFlow.md) and @ref:[Flow.lazyCompletionStageFlow](lazyCompletionStageFlow.md) + * @ref:[Source.lazySource](../Source/lazySource.md) + * @ref:[Sink.lazySink](../Sink/lazySink.md) + +## Examples + +In this sample we produce a short sequence of numbers, mostly to side effect and write to standard out to see in which +order things happen. Note how producing the first value in the `Source` happens before the creation of the flow: + +Scala +: @@snip [Lazy.scala](/akka-docs/src/test/scala/docs/stream/operators/flow/Lazy.scala) { #simple-example } + +Java +: @@snip [Lazy.java](/akka-docs/src/test/java/jdocs/stream/operators/flow/Lazy.java) { #simple-example } + +Since the factory is called once per stream materialization it can be used to safely construct a mutable object to +use with the actual deferred `Flow`. In this example we fold elements into an `ArrayList` created inside the lazy +flow factory: + +Scala +: @@snip [Lazy.scala](/akka-docs/src/test/scala/docs/stream/operators/flow/Lazy.scala) { #mutable-example } + +Java +: @@snip [Lazy.java](/akka-docs/src/test/java/jdocs/stream/operators/flow/Lazy.java) { #mutable-example } + +If we instead had used `fold` directly with an `ArrayList` we would have shared the same list across +all materialization and what is even worse, unsafely across threads. ## Reactive Streams semantics @@ -29,5 +63,8 @@ and failed with a `akka.stream.NeverMaterializedException` if the stream fails o **completes** when upstream completes and all futures have been completed and all elements have been emitted +**cancels** when downstream cancels (keep reading) + The operator's default behaviour in case of downstream cancellation before nested flow materialization (future completion) is to cancel immediately. + This behaviour can be controlled by setting the [[akka.stream.Attributes.NestedMaterializationCancellationPolicy.PropagateToNested]] attribute, + this will delay downstream cancellation until nested flow's materialization which is then immediately cancelled (with the original cancellation cause). @@@ - diff --git a/akka-docs/src/main/paradox/stream/operators/Flow/lazyFutureFlow.md b/akka-docs/src/main/paradox/stream/operators/Flow/lazyFutureFlow.md index e383b116c9..fe8841464f 100644 --- a/akka-docs/src/main/paradox/stream/operators/Flow/lazyFutureFlow.md +++ b/akka-docs/src/main/paradox/stream/operators/Flow/lazyFutureFlow.md @@ -35,5 +35,9 @@ See @ref:[lazyFlow](lazyFlow.md) for sample. **completes** when upstream completes and all futures have been completed and all elements have been emitted +**cancels** when downstream cancels (keep reading) + The operator's default behaviour in case of downstream cancellation before nested flow materialization (future completion) is to cancel immediately. + This behaviour can be controlled by setting the [[akka.stream.Attributes.NestedMaterializationCancellationPolicy.PropagateToNested]] attribute, + this will delay downstream cancellation until nested flow's materialization which is then immediately cancelled (with the original cancellation cause). @@@ diff --git a/akka-docs/src/main/paradox/stream/operators/Flow/lazyInitAsync.md b/akka-docs/src/main/paradox/stream/operators/Flow/lazyInitAsync.md index 5e147c4c35..2dd60ca08a 100644 --- a/akka-docs/src/main/paradox/stream/operators/Flow/lazyInitAsync.md +++ b/akka-docs/src/main/paradox/stream/operators/Flow/lazyInitAsync.md @@ -26,5 +26,9 @@ Defers creation until a first element arrives. **completes** when upstream completes and all futures have been completed and all elements have been emitted +**cancels** when downstream cancels (keep reading) + The operator's default behaviour in case of downstream cancellation before nested flow materialization (future completion) is to cancel immediately. + This behaviour can be controlled by setting the [[akka.stream.Attributes.NestedMaterializationCancellationPolicy.PropagateToNested]] attribute, + this will delay downstream cancellation until nested flow's materialization which is then immediately cancelled (with the original cancellation cause). @@@ diff --git a/akka-docs/src/main/paradox/stream/operators/Sink/actorRef.md b/akka-docs/src/main/paradox/stream/operators/Sink/actorRef.md index 127ffae75e..9aacf693d8 100644 --- a/akka-docs/src/main/paradox/stream/operators/Sink/actorRef.md +++ b/akka-docs/src/main/paradox/stream/operators/Sink/actorRef.md @@ -4,12 +4,9 @@ Send the elements from the stream to an `ActorRef`. @ref[Sink operators](../index.md#sink-operators) -@@@div { .group-scala } ## Signature -@@signature [Sink.scala](/akka-stream/src/main/scala/akka/stream/scaladsl/Sink.scala) { #actorRef } - -@@@ +@apidoc[Sink.actorRef](Sink$) { scala="#actorRef[T](ref:akka.actor.ActorRef,onCompleteMessage:Any,onFailureMessage:Throwable=>Any):akka.stream.scaladsl.Sink[T,akka.NotUsed]" java="#actorRef(akka.actor.ActorRef,java.lang.Object)" } ## Description diff --git a/akka-docs/src/main/paradox/stream/operators/Sink/actorRefWithBackpressure.md b/akka-docs/src/main/paradox/stream/operators/Sink/actorRefWithBackpressure.md index 6f9d26da68..b243a03d5b 100644 --- a/akka-docs/src/main/paradox/stream/operators/Sink/actorRefWithBackpressure.md +++ b/akka-docs/src/main/paradox/stream/operators/Sink/actorRefWithBackpressure.md @@ -4,6 +4,10 @@ Send the elements from the stream to an `ActorRef` which must then acknowledge r @ref[Sink operators](../index.md#sink-operators) +## Signature + +@apidoc[Sink.actorRefWithBackpressure](Sink$) { scala="#actorRefWithBackpressure[T](ref:akka.actor.ActorRef,onInitMessage:Any,ackMessage:Any,onCompleteMessage:Any,onFailureMessage:Throwable=>Any):akka.stream.scaladsl.Sink[T,akka.NotUsed]" java="#actorRefWithBackpressure(akka.actor.ActorRef,java.lang.Object,java.lang.Object,java.lang.Object,akka.japi.function.Function)" } + ## Description Send the elements from the stream to an `ActorRef` which must then acknowledge reception after completing a message, diff --git a/akka-docs/src/main/paradox/stream/operators/Sink/fromMaterializer.md b/akka-docs/src/main/paradox/stream/operators/Sink/fromMaterializer.md index 22a8786a9b..7485e657b8 100644 --- a/akka-docs/src/main/paradox/stream/operators/Sink/fromMaterializer.md +++ b/akka-docs/src/main/paradox/stream/operators/Sink/fromMaterializer.md @@ -4,15 +4,11 @@ Defer the creation of a `Sink` until materialization and access `Materializer` a @ref[Sink operators](../index.md#sink-operators) -@@@ div { .group-scala } - ## Signature -@@signature [Sink.scala](/akka-stream/src/main/scala/akka/stream/scaladsl/Sink.scala) { #fromMaterializer } - -@@@ +@apidoc[Sink.fromMaterializer](Sink$) { scala="#fromMaterializer[T,M](factory:(akka.stream.Materializer,akka.stream.Attributes)=>akka.stream.scaladsl.Sink[T,M]):akka.stream.scaladsl.Sink[T,scala.concurrent.Future[M]]" java="#fromMaterializer(java.util.function.BiFunction)" } ## Description Typically used when access to materializer is needed to run a different stream during the construction of a sink. -Can also be used to access the underlying `ActorSystem` from `Materializer`. \ No newline at end of file +Can also be used to access the underlying `ActorSystem` from `Materializer`. diff --git a/akka-docs/src/main/paradox/stream/operators/Sink/lazySink.md b/akka-docs/src/main/paradox/stream/operators/Sink/lazySink.md index b3bd22bd43..7639b3c675 100644 --- a/akka-docs/src/main/paradox/stream/operators/Sink/lazySink.md +++ b/akka-docs/src/main/paradox/stream/operators/Sink/lazySink.md @@ -11,15 +11,33 @@ Defers creation and materialization of a `Sink` until there is a first element. ## Description -When the first element comes from upstream the actual `Sink` is created and materialized. -The internal `Sink` will not be created if the stream completes of fails before any element got through. +Defers `Sink` creation and materialization until when the first element arrives from upstream to the `lazySink`. After +that the stream behaves as if the nested sink replaced the `lazySink`. +The nested `Sink` will not be created if upstream completes or fails without any elements arriving at the sink. -The materialized value of the `Sink` will be the materialized value of the created internal flow if it is materialized -and failed with a `akka.stream.NeverMaterializedException` if the stream fails or completes without the flow being materialized. +The materialized value of the `Sink` is a @scala[`Future`]@java[`CompletionStage`] that is completed with the +materialized value of the nested sink once that is constructed. Can be combined with @ref[prefixAndTail](../Source-or-Flow/prefixAndTail.md) to base the sink on the first element. -See also @ref:[lazyFutureSink](lazyFutureSink.md) and @ref:[lazyCompletionStageSink](lazyCompletionStageSink.md). +See also: + + * @ref:[Sink.lazyFutureSink](lazyFutureSink.md) and @ref:[lazyCompletionStageSink](lazyCompletionStageSink.md). + * @ref:[Source.lazySource](../Source/lazySource.md) + * @ref:[Flow.lazyFlow](../Flow/lazyFlow.md) + +## Examples + +In this example we side effect from `Flow.map`, the sink factory and `Sink.foreach` so that the order becomes visible, +the nested sink is only created once the element has passed `map`: + +Scala +: @@snip [Lazy.scala](/akka-docs/src/test/scala/docs/stream/operators/sink/Lazy.scala) { #simple-example } + +Java +: @@snip [Lazy.java](/akka-docs/src/test/java/jdocs/stream/operators/sink/Lazy.java) { #simple-example } + + ## Reactive Streams semantics diff --git a/akka-docs/src/main/paradox/stream/operators/Source-or-Flow/ask.md b/akka-docs/src/main/paradox/stream/operators/Source-or-Flow/ask.md index d549d37b23..abdf060481 100644 --- a/akka-docs/src/main/paradox/stream/operators/Source-or-Flow/ask.md +++ b/akka-docs/src/main/paradox/stream/operators/Source-or-Flow/ask.md @@ -6,6 +6,7 @@ Use the "Ask Pattern" to send a request-reply message to the target `ref` actor ## Signature +@apidoc[Source.ask](Source) {scala="#ask[S](ref:akka.actor.ActorRef)(implicittimeout:akka.util.Timeout,implicittag:scala.reflect.ClassTag[S]):FlowOps.this.Repr[S]" java="#ask(akka.actor.ActorRef,java.lang.Class,akka.util.Timeout)" } @apidoc[Flow.ask](Flow$) { scala="#ask%5BS](ref:akka.actor.ActorRef)(implicittimeout:akka.util.Timeout,implicittag:scala.reflect.ClassTag%5BS]):FlowOps.this.Repr%5BS]" java="#ask(akka.actor.ActorRef,java.lang.Class,akka.util.Timeout)" } ## Description @@ -15,7 +16,7 @@ If any of the asks times out it will fail the stream with a @apidoc[AskTimeoutEx The @java[`mapTo` class]@scala[`S` generic] parameter is used to cast the responses from the actor to the expected outgoing flow type. -Similar to the plain ask pattern, the target actor is allowed to reply with @apidoc[akka.actor.Status]. +Similar to the plain ask pattern, the target actor is allowed to reply with @apidoc[akka.actor.Status$]. An @apidoc[akka.actor.Status.Failure] will cause the operator to fail with the cause carried in the `Failure` message. Adheres to the @apidoc[ActorAttributes.SupervisionStrategy] attribute. diff --git a/akka-docs/src/main/paradox/stream/operators/Source-or-Flow/fromMaterializer.md b/akka-docs/src/main/paradox/stream/operators/Source-or-Flow/fromMaterializer.md index 7ec3be8adb..ecfdeeee3e 100644 --- a/akka-docs/src/main/paradox/stream/operators/Source-or-Flow/fromMaterializer.md +++ b/akka-docs/src/main/paradox/stream/operators/Source-or-Flow/fromMaterializer.md @@ -4,14 +4,11 @@ Defer the creation of a `Source/Flow` until materialization and access `Material @ref[Simple operators](../index.md#simple-operators) -@@@ div { .group-scala } - ## Signature -@@signature [Source.scala](/akka-stream/src/main/scala/akka/stream/scaladsl/Source.scala) { #fromMaterializer } -@@signature [Flow.scala](/akka-stream/src/main/scala/akka/stream/scaladsl/Flow.scala) { #fromMaterializer } +@apidoc[Source.fromMaterializer](Source$) { scala="#fromMaterializer[T,M](factory:(akka.stream.Materializer,akka.stream.Attributes)=>akka.stream.scaladsl.Source[T,M]):akka.stream.scaladsl.Source[T,scala.concurrent.Future[M]]" java="#fromMaterializer(java.util.function.BiFunction)" } +@apidoc[Flow.fromMaterializer](Flow$) { scala="#fromMaterializer[T,U,M](factory:(akka.stream.Materializer,akka.stream.Attributes)=>akka.stream.scaladsl.Flow[T,U,M]):akka.stream.scaladsl.Flow[T,U,scala.concurrent.Future[M]]" java="#fromMaterializer(java.util.function.BiFunction)" } -@@@ ## Description diff --git a/akka-docs/src/main/paradox/stream/operators/Source-or-Flow/map.md b/akka-docs/src/main/paradox/stream/operators/Source-or-Flow/map.md index 49fa7e3ccc..d1001d8ab7 100644 --- a/akka-docs/src/main/paradox/stream/operators/Source-or-Flow/map.md +++ b/akka-docs/src/main/paradox/stream/operators/Source-or-Flow/map.md @@ -4,13 +4,10 @@ Transform each element in the stream by calling a mapping function with it and p @ref[Simple operators](../index.md#simple-operators) -@@@div { .group-scala } - ## Signature -@@signature [Flow.scala](/akka-stream/src/main/scala/akka/stream/scaladsl/Flow.scala) { #map } - -@@@ +@apidoc[Source.map](Source) { scala="#map[T](f:Out=>T):FlowOps.this.Repr[T]" java="#map(akka.japi.function.Function)" } +@apidoc[Flow.map](Flow) { scala="#map[T](f:Out=>T):FlowOps.this.Repr[T]" java="#map(akka.japi.function.Function)" } ## Description diff --git a/akka-docs/src/main/paradox/stream/operators/Source/actorRefWithBackpressure.md b/akka-docs/src/main/paradox/stream/operators/Source/actorRefWithBackpressure.md index 96004b0ab0..4806a22fe1 100644 --- a/akka-docs/src/main/paradox/stream/operators/Source/actorRefWithBackpressure.md +++ b/akka-docs/src/main/paradox/stream/operators/Source/actorRefWithBackpressure.md @@ -4,11 +4,9 @@ Materialize an `ActorRef`; sending messages to it will emit them on the stream. @ref[Source operators](../index.md#source-operators) -@@@ div { .group-scala } ## Signature -@@signature [Source.scala](/akka-stream/src/main/scala/akka/stream/scaladsl/Source.scala) { #actorRefWithBackpressure } -@@@ +@apidoc[Source.actorRefWithBackpressure](Source$) { scala="#actorRefWithBackpressure[T](ackMessage:Any,completionMatcher:PartialFunction[Any,akka.stream.CompletionStrategy],failureMatcher:PartialFunction[Any,Throwable]):akka.stream.scaladsl.Source[T,akka.actor.ActorRef]" java="#actorRefWithBackpressure(java.lang.Object,akka.japi.function.Function,akka.japi.function.Function)" } ## Description diff --git a/akka-docs/src/main/paradox/stream/operators/Source/empty.md b/akka-docs/src/main/paradox/stream/operators/Source/empty.md index 0cddd1ef71..59f3c643cb 100644 --- a/akka-docs/src/main/paradox/stream/operators/Source/empty.md +++ b/akka-docs/src/main/paradox/stream/operators/Source/empty.md @@ -4,6 +4,8 @@ Complete right away without ever emitting any elements. @ref[Source operators](../index.md#source-operators) +@ref:[`Source.never`](never.md) a source which emits nothing and never completes. + ## Signature @apidoc[Source.empty](Source$) { scala="#empty[T]:akka.stream.scaladsl.Source[T,akka.NotUsed]" java="#empty()" java="#empty(java.lang.Class)" } diff --git a/akka-docs/src/main/paradox/stream/operators/Source/from.md b/akka-docs/src/main/paradox/stream/operators/Source/from.md index 9d65c06edd..b546e1b70a 100644 --- a/akka-docs/src/main/paradox/stream/operators/Source/from.md +++ b/akka-docs/src/main/paradox/stream/operators/Source/from.md @@ -4,14 +4,19 @@ Stream the values of an @scala[`immutable.Seq`]@java[`Iterable`]. @ref[Source operators](../index.md#source-operators) +## Signature @@@div { .group-scala } -## Signature +@apidoc[Source.apply](Source$) { scala="#apply[T](iterable:scala.collection.immutable.Iterable[T]):akka.stream.scaladsl.Source[T,akka.NotUsed]" } -@@signature [Source.scala](/akka-stream/src/main/scala/akka/stream/scaladsl/Source.scala) { #apply } +@@@ -@@@ +@@@div { .group-java } + +@apidoc[Source.from](Source$) { java="#from(java.lang.Iterable)" } + +@@@ ## Description diff --git a/akka-docs/src/main/paradox/stream/operators/Source/lazySource.md b/akka-docs/src/main/paradox/stream/operators/Source/lazySource.md index 7b3e429a56..42585bb5a3 100644 --- a/akka-docs/src/main/paradox/stream/operators/Source/lazySource.md +++ b/akka-docs/src/main/paradox/stream/operators/Source/lazySource.md @@ -12,12 +12,50 @@ Defers creation and materialization of a `Source` until there is demand. ## Description Defers creation and materialization of a `Source` until there is demand, then emits the elements from the source -downstream just like if it had been created up front. +downstream just like if it had been created up front. If the stream fails or cancels before there is demand the factory will not be invoked. -See also @ref:[lazyFutureSource](lazyFutureSource.md). +Note that asynchronous boundaries and many other operators in the stream may do pre-fetching or trigger demand earlier +than you would expect. -Note that asynchronous boundaries (and other operators) in the stream may do pre-fetching which counter acts -the laziness and will trigger the factory immediately. +The materialized value of the `lazy` is a @scala[`Future`]@java[`CompletionStage`] that is completed with the +materialized value of the nested source once that is constructed. + +See also: + + * @ref:[Source.lazyFutureSource](lazyFutureSource.md) and @ref:[Source.lazyCompletionStageSource](lazyCompletionStageSource.md) + * @ref:[Flow.lazyFlow](../Flow/lazyFlow.md) + * @ref:[Sink.lazySink](../Sink/lazySink.md) + +## Example + +In this example you might expect this sample to not construct the expensive source until `.pull` is called. However, +since `Sink.queue` has a buffer and will ask for that immediately on materialization the expensive source is in created +quickly after the stream has been materialized: + +Scala +: @@snip [Lazy.scala](/akka-docs/src/test/scala/docs/stream/operators/source/Lazy.scala) { #not-a-good-example } + +Java +: @@snip [Lazy.java](/akka-docs/src/test/java/jdocs/stream/operators/source/Lazy.java) { #not-a-good-example } + +Instead the most useful aspect of the operator is that the factory is called once per stream materialization +which means that it can be used to safely construct a mutable object to use with the actual deferred source. + +In this example we make use of that by unfolding a mutable object that works like an iterator with a method to say if +there are more elements and one that produces the next and moves to the next element. + +If the `IteratorLikeThing` was used directly in a `Source.unfold` the same instance would end up being unsafely shared +across all three materializations of the stream, but wrapping it with `Source.lazy` ensures we create a separate instance +for each of the started streams: + +Scala +: @@snip [Lazy.scala](/akka-docs/src/test/scala/docs/stream/operators/source/Lazy.scala) { #one-per-materialization } + +Java +: @@snip [Lazy.java](/akka-docs/src/test/java/jdocs/stream/operators/source/Lazy.java) { #one-per-materialization } + +Note though that you can often also achieve the same using @ref:[unfoldResource](unfoldResource.md). If you have an actual `Iterator` +you should prefer @ref:[fromIterator](fromIterator.md). ## Reactive Streams semantics diff --git a/akka-docs/src/main/paradox/stream/operators/Source/never.md b/akka-docs/src/main/paradox/stream/operators/Source/never.md new file mode 100644 index 0000000000..4b07141417 --- /dev/null +++ b/akka-docs/src/main/paradox/stream/operators/Source/never.md @@ -0,0 +1,27 @@ +# never + +Never emit any elements, never complete and never fail. + +@ref[Source operators](../index.md#source-operators) + +@ref:[`Source.empty`](empty.md), a source which emits nothing and completes immediately. + +## Signature + +@@signature [Source.scala](/akka-stream/src/main/scala/akka/stream/scaladsl/Source.scala) { #never } + +@@@ + +## Description + +Create a source which never emits any elements, never completes and never failes. Useful for tests. + +## Reactive Streams semantics + +@@@div { .callout } + +**emits** never + +**completes** never + +@@@ diff --git a/akka-docs/src/main/paradox/stream/operators/Source/range.md b/akka-docs/src/main/paradox/stream/operators/Source/range.md index 573f41dd2d..412ecc1f82 100644 --- a/akka-docs/src/main/paradox/stream/operators/Source/range.md +++ b/akka-docs/src/main/paradox/stream/operators/Source/range.md @@ -8,7 +8,7 @@ Emit each integer in a range, with an option to take bigger steps than 1. @@dependency[sbt,Maven,Gradle] { group="com.typesafe.akka" - artifact="akka-stream_$scala.binary_version$" + artifact="akka-stream_$scala.binary.version$" version="$akka.version$" } diff --git a/akka-docs/src/main/paradox/stream/operators/index.md b/akka-docs/src/main/paradox/stream/operators/index.md index 1153e85ccd..649b321b9d 100644 --- a/akka-docs/src/main/paradox/stream/operators/index.md +++ b/akka-docs/src/main/paradox/stream/operators/index.md @@ -36,6 +36,7 @@ These built-in sources are available from @scala[`akka.stream.scaladsl.Source`] |Source|@ref[lazySingle](Source/lazySingle.md)|Defers creation of a single element source until there is demand.| |Source|@ref[lazySource](Source/lazySource.md)|Defers creation and materialization of a `Source` until there is demand.| |Source|@ref[maybe](Source/maybe.md)|Create a source that emits once the materialized @scala[`Promise`] @java[`CompletableFuture`] is completed with a value.| +|Source|@ref[never](Source/never.md)|Never emit any elements, never complete and never fail.| |Source|@ref[queue](Source/queue.md)|Materialize a `SourceQueue` onto which elements can be pushed for emitting from the source. | |Source|@ref[range](Source/range.md)|Emit each integer in a range, with an option to take bigger steps than 1.| |Source|@ref[repeat](Source/repeat.md)|Stream a single object repeatedly.| @@ -484,6 +485,7 @@ For more background see the @ref[Error Handling in Streams](../stream-error.md) * [mergePrioritized](Source-or-Flow/mergePrioritized.md) * [mergeSorted](Source-or-Flow/mergeSorted.md) * [monitor](Source-or-Flow/monitor.md) +* [never](Source/never.md) * [onComplete](Sink/onComplete.md) * [onFailuresWithBackoff](RestartSource/onFailuresWithBackoff.md) * [onFailuresWithBackoff](RestartFlow/onFailuresWithBackoff.md) diff --git a/akka-docs/src/main/paradox/stream/reactive-streams-interop.md b/akka-docs/src/main/paradox/stream/reactive-streams-interop.md index 55ae578240..0afebd4ac6 100644 --- a/akka-docs/src/main/paradox/stream/reactive-streams-interop.md +++ b/akka-docs/src/main/paradox/stream/reactive-streams-interop.md @@ -6,14 +6,14 @@ To use Akka Streams, add the module to your project: @@dependency[sbt,Maven,Gradle] { group="com.typesafe.akka" - artifact="akka-stream_$scala.binary_version$" + artifact="akka-stream_$scala.binary.version$" version="$akka.version$" } ## Overview -Akka Streams implements the [Reactive Streams](http://reactive-streams.org/) standard for asynchronous stream processing with non-blocking +Akka Streams implements the [Reactive Streams](https://www.reactive-streams.org/) standard for asynchronous stream processing with non-blocking back pressure. Since Java 9 the APIs of Reactive Streams has been included in the Java Standard library, under the `java.util.concurrent.Flow` @@ -133,5 +133,5 @@ An incomplete list of other implementations: * [Reactor (1.1+)](https://github.com/reactor/reactor) * [RxJava](https://github.com/ReactiveX/RxJavaReactiveStreams) - * [Ratpack](http://www.ratpack.io/manual/current/streams.html) - * [Slick](http://slick.lightbend.com) + * [Ratpack](https://www.ratpack.io/manual/current/streams.html) + * [Slick](https://scala-slick.org/) diff --git a/akka-docs/src/main/paradox/stream/stream-composition.md b/akka-docs/src/main/paradox/stream/stream-composition.md index daba8c933d..07148745db 100644 --- a/akka-docs/src/main/paradox/stream/stream-composition.md +++ b/akka-docs/src/main/paradox/stream/stream-composition.md @@ -6,7 +6,7 @@ To use Akka Streams, add the module to your project: @@dependency[sbt,Maven,Gradle] { group="com.typesafe.akka" - artifact="akka-stream_$scala.binary_version$" + artifact="akka-stream_$scala.binary.version$" version="$akka.version$" } diff --git a/akka-docs/src/main/paradox/stream/stream-cookbook.md b/akka-docs/src/main/paradox/stream/stream-cookbook.md index 39c2a53973..e1f9693968 100644 --- a/akka-docs/src/main/paradox/stream/stream-cookbook.md +++ b/akka-docs/src/main/paradox/stream/stream-cookbook.md @@ -6,7 +6,7 @@ To use Akka Streams, add the module to your project: @@dependency[sbt,Maven,Gradle] { group="com.typesafe.akka" - artifact="akka-stream_$scala.binary_version$" + artifact="akka-stream_$scala.binary.version$" version="$akka.version$" } diff --git a/akka-docs/src/main/paradox/stream/stream-customize.md b/akka-docs/src/main/paradox/stream/stream-customize.md index 9381c4571c..c06caec49b 100644 --- a/akka-docs/src/main/paradox/stream/stream-customize.md +++ b/akka-docs/src/main/paradox/stream/stream-customize.md @@ -6,7 +6,7 @@ To use Akka Streams, add the module to your project: @@dependency[sbt,Maven,Gradle] { group="com.typesafe.akka" - artifact="akka-stream_$scala.binary_version$" + artifact="akka-stream_$scala.binary.version$" version="$akka.version$" } @@ -35,7 +35,7 @@ smaller ones, and allows state to be maintained inside it in a safe way. As a first motivating example, we will build a new `Source` that will emit numbers from 1 until it is cancelled. To start, we need to define the "interface" of our operator, which is called *shape* in Akka Streams terminology -(this is explained in more detail in the section @ref:[Modularity, Composition and Hierarchy](stream-composition.md)). This is how this looks like: +(this is explained in more detail in the section @ref:[Modularity, Composition and Hierarchy](stream-composition.md)). This is how it looks: Scala : @@snip [GraphStageDocSpec.scala](/akka-docs/src/test/scala/docs/stream/GraphStageDocSpec.scala) { #boilerplate-example } @@ -516,7 +516,7 @@ allow nicer syntax. The short answer is that Scala 2 does not support this in a that it is impossible to abstract over the kind of stream that is being extended because `Source`, `Flow` and `SubFlow` differ in the number and kind of their type parameters. While it would be possible to write an implicit class that enriches them generically, this class would require explicit instantiation with all type -parameters due to [SI-2712](https://issues.scala-lang.org/browse/SI-2712). For a partial workaround that unifies +parameters due to [SI-2712](https://github.com/scala/bug/issues/2712). For a partial workaround that unifies extensions to `Source` and `Flow` see [this sketch by R. Kuhn](https://gist.github.com/rkuhn/2870fcee4937dda2cad5). A lot simpler is the task of adding an extension method to `Source` as shown below: diff --git a/akka-docs/src/main/paradox/stream/stream-dynamic.md b/akka-docs/src/main/paradox/stream/stream-dynamic.md index b1aa256251..3d0693554d 100644 --- a/akka-docs/src/main/paradox/stream/stream-dynamic.md +++ b/akka-docs/src/main/paradox/stream/stream-dynamic.md @@ -6,7 +6,7 @@ To use Akka Streams, add the module to your project: @@dependency[sbt,Maven,Gradle] { group="com.typesafe.akka" - artifact="akka-stream_$scala.binary_version$" + artifact="akka-stream_$scala.binary.version$" version="$akka.version$" } diff --git a/akka-docs/src/main/paradox/stream/stream-error.md b/akka-docs/src/main/paradox/stream/stream-error.md index 589379b5ff..dc250b2984 100644 --- a/akka-docs/src/main/paradox/stream/stream-error.md +++ b/akka-docs/src/main/paradox/stream/stream-error.md @@ -6,7 +6,7 @@ To use Akka Streams, add the module to your project: @@dependency[sbt,Maven,Gradle] { group="com.typesafe.akka" - artifact="akka-stream_$scala.binary_version$" + artifact="akka-stream_$scala.binary.version$" version="$akka.version$" } diff --git a/akka-docs/src/main/paradox/stream/stream-flows-and-basics.md b/akka-docs/src/main/paradox/stream/stream-flows-and-basics.md index 34eb239e0b..b8c50ecbfb 100644 --- a/akka-docs/src/main/paradox/stream/stream-flows-and-basics.md +++ b/akka-docs/src/main/paradox/stream/stream-flows-and-basics.md @@ -6,7 +6,7 @@ To use Akka Streams, add the module to your project: @@dependency[sbt,Maven,Gradle] { group="com.typesafe.akka" - artifact="akka-stream_$scala.binary_version$" + artifact="akka-stream_$scala.binary.version$" version="$akka.version$" } @@ -190,7 +190,7 @@ of absence of a value we recommend using @scala[`scala.Option` or `scala.util.Ei ## Back-pressure explained -Akka Streams implement an asynchronous non-blocking back-pressure protocol standardised by the [Reactive Streams](http://reactive-streams.org/) +Akka Streams implement an asynchronous non-blocking back-pressure protocol standardised by the [Reactive Streams](https://www.reactive-streams.org/) specification, which Akka is a founding member of. The user of the library does not have to write any explicit back-pressure handling code — it is built in diff --git a/akka-docs/src/main/paradox/stream/stream-graphs.md b/akka-docs/src/main/paradox/stream/stream-graphs.md index e8d7497459..979e7da9bf 100644 --- a/akka-docs/src/main/paradox/stream/stream-graphs.md +++ b/akka-docs/src/main/paradox/stream/stream-graphs.md @@ -6,7 +6,7 @@ To use Akka Streams, add the module to your project: @@dependency[sbt,Maven,Gradle] { group="com.typesafe.akka" - artifact="akka-stream_$scala.binary_version$" + artifact="akka-stream_$scala.binary.version$" version="$akka.version$" } diff --git a/akka-docs/src/main/paradox/stream/stream-introduction.md b/akka-docs/src/main/paradox/stream/stream-introduction.md index 312c6b63a3..9e4ea40388 100644 --- a/akka-docs/src/main/paradox/stream/stream-introduction.md +++ b/akka-docs/src/main/paradox/stream/stream-introduction.md @@ -28,7 +28,7 @@ efficiently and with bounded resource usage—no more OutOfMemoryErrors. In orde to achieve this our streams need to be able to limit the buffering that they employ, they need to be able to slow down producers if the consumers cannot keep up. This feature is called back-pressure and is at the core of the -[Reactive Streams](http://reactive-streams.org/) initiative of which Akka is a +[Reactive Streams](https://www.reactive-streams.org/) initiative of which Akka is a founding member. For you this means that the hard problem of propagating and reacting to back-pressure has been incorporated in the design of Akka Streams already, so you have one less thing to worry about; it also means that Akka diff --git a/akka-docs/src/main/paradox/stream/stream-io.md b/akka-docs/src/main/paradox/stream/stream-io.md index 8032611783..88cc3df83f 100644 --- a/akka-docs/src/main/paradox/stream/stream-io.md +++ b/akka-docs/src/main/paradox/stream/stream-io.md @@ -6,7 +6,7 @@ To use Akka Streams, add the module to your project: @@dependency[sbt,Maven,Gradle] { group="com.typesafe.akka" - artifact="akka-stream_$scala.binary_version$" + artifact="akka-stream_$scala.binary.version$" version="$akka.version$" } diff --git a/akka-docs/src/main/paradox/stream/stream-parallelism.md b/akka-docs/src/main/paradox/stream/stream-parallelism.md index 5b44054e72..2f6e78de06 100644 --- a/akka-docs/src/main/paradox/stream/stream-parallelism.md +++ b/akka-docs/src/main/paradox/stream/stream-parallelism.md @@ -6,7 +6,7 @@ To use Akka Streams, add the module to your project: @@dependency[sbt,Maven,Gradle] { group="com.typesafe.akka" - artifact="akka-stream_$scala.binary_version$" + artifact="akka-stream_$scala.binary.version$" version="$akka.version$" } diff --git a/akka-docs/src/main/paradox/stream/stream-quickstart.md b/akka-docs/src/main/paradox/stream/stream-quickstart.md index bd8d400769..f06453d3f2 100644 --- a/akka-docs/src/main/paradox/stream/stream-quickstart.md +++ b/akka-docs/src/main/paradox/stream/stream-quickstart.md @@ -6,7 +6,7 @@ To use Akka Streams, add the module to your project: @@dependency[sbt,Maven,Gradle] { group="com.typesafe.akka" - artifact="akka-stream_$scala.binary_version$" + artifact="akka-stream_$scala.binary.version$" version="$akka.version$" } diff --git a/akka-docs/src/main/paradox/stream/stream-rate.md b/akka-docs/src/main/paradox/stream/stream-rate.md index 5482fdf30b..67846c17d0 100644 --- a/akka-docs/src/main/paradox/stream/stream-rate.md +++ b/akka-docs/src/main/paradox/stream/stream-rate.md @@ -6,7 +6,7 @@ To use Akka Streams, add the module to your project: @@dependency[sbt,Maven,Gradle] { group="com.typesafe.akka" - artifact="akka-stream_$scala.binary_version$" + artifact="akka-stream_$scala.binary.version$" version="$akka.version$" } @@ -261,4 +261,4 @@ This makes `expand` able to transform or even filter out (by providing an empty Regardless, since we provide a non-empty `Iterator` in both examples, this means that the output of this flow is going to report a drift of zero if the producer is fast enough - or a larger drift otherwise. -See also @ref:[`extrapolate`](operators/Source-or-Flow/extrapolate.md) and @ref:[`expand`](operators/Source-or-Flow/expand.md) for more information and examples. \ No newline at end of file +See also @ref:[`extrapolate`](operators/Source-or-Flow/extrapolate.md) and @ref:[`expand`](operators/Source-or-Flow/expand.md) for more information and examples. diff --git a/akka-docs/src/main/paradox/stream/stream-refs.md b/akka-docs/src/main/paradox/stream/stream-refs.md index c9d6009afb..c13ba1f08b 100644 --- a/akka-docs/src/main/paradox/stream/stream-refs.md +++ b/akka-docs/src/main/paradox/stream/stream-refs.md @@ -6,7 +6,7 @@ To use Akka Streams, add the module to your project: @@dependency[sbt,Maven,Gradle] { group="com.typesafe.akka" - artifact="akka-stream_$scala.binary_version$" + artifact="akka-stream_$scala.binary.version$" version="$akka.version$" } @@ -29,7 +29,7 @@ distributed processing framework or to introduce such capabilities in specific p Stream refs are trivial to use in existing clustered Akka applications and require no additional configuration or setup. They automatically maintain flow-control / back-pressure over the network and employ Akka's failure detection mechanisms to fail-fast ("let it crash!") in the case of failures of remote nodes. They can be seen as an implementation -of the [Work Pulling Pattern](http://www.michaelpollmeier.com/akka-work-pulling-pattern), which one would otherwise +of the [Work Pulling Pattern](https://www.michaelpollmeier.com/akka-work-pulling-pattern), which one would otherwise implement manually. @@@ note diff --git a/akka-docs/src/main/paradox/stream/stream-substream.md b/akka-docs/src/main/paradox/stream/stream-substream.md index bfdc093fda..13026497b8 100644 --- a/akka-docs/src/main/paradox/stream/stream-substream.md +++ b/akka-docs/src/main/paradox/stream/stream-substream.md @@ -6,7 +6,7 @@ To use Akka Streams, add the module to your project: @@dependency[sbt,Maven,Gradle] { group="com.typesafe.akka" - artifact="akka-stream_$scala.binary_version$" + artifact="akka-stream_$scala.binary.version$" version="$akka.version$" } diff --git a/akka-docs/src/main/paradox/stream/stream-testkit.md b/akka-docs/src/main/paradox/stream/stream-testkit.md index a8115bfd73..2b132c7359 100644 --- a/akka-docs/src/main/paradox/stream/stream-testkit.md +++ b/akka-docs/src/main/paradox/stream/stream-testkit.md @@ -6,7 +6,7 @@ To use Akka Stream TestKit, add the module to your project: @@dependency[sbt,Maven,Gradle] { group="com.typesafe.akka" - artifact="akka-stream-testkit_$scala.binary_version$" + artifact="akka-stream-testkit_$scala.binary.version$" version="$akka.version$" scope="test" } diff --git a/akka-docs/src/main/paradox/testing.md b/akka-docs/src/main/paradox/testing.md index 712d53db5c..8d1aeb979b 100644 --- a/akka-docs/src/main/paradox/testing.md +++ b/akka-docs/src/main/paradox/testing.md @@ -9,7 +9,7 @@ To use Akka Testkit, you must add the following dependency in your project: @@dependency[sbt,Maven,Gradle] { group="com.typesafe.akka" - artifact="akka-testkit_$scala.binary_version$" + artifact="akka-testkit_$scala.binary.version$" version="$akka.version$" scope="test" } @@ -758,7 +758,7 @@ akka { ## Different Testing Frameworks -Akka’s own test suite is written using [ScalaTest](http://scalatest.org), +Akka’s own test suite is written using [ScalaTest](http://www.scalatest.org), which also shines through in documentation examples. However, the TestKit and its facilities do not depend on that framework, you can essentially use whichever suits your development style best. @@ -783,7 +783,7 @@ backwards compatibility in the future, use at own risk. ### Specs2 -Some [Specs2](http://specs2.org) users have contributed examples of how to work around some clashes which may arise: +Some [Specs2](https://etorreborre.github.io/specs2/) users have contributed examples of how to work around some clashes which may arise: * Mixing TestKit into `org.specs2.mutable.Specification` results in a name clash involving the `end` method (which is a private variable in diff --git a/akka-docs/src/main/paradox/typed/actor-discovery.md b/akka-docs/src/main/paradox/typed/actor-discovery.md index 902939d803..6a50e3986f 100644 --- a/akka-docs/src/main/paradox/typed/actor-discovery.md +++ b/akka-docs/src/main/paradox/typed/actor-discovery.md @@ -1,6 +1,6 @@ # Actor discovery -For the Akka Classic documentation of this feature see @ref:[Classic Actors](../actors.md#actorselection). +You are viewing the documentation for the new actor APIs, to view the Akka Classic documentation, see @ref:[Classic Actors](../actors.md#actorselection). ## Dependency @@ -8,7 +8,7 @@ To use Akka Actor Typed, you must add the following dependency in your project: @@dependency[sbt,Maven,Gradle] { group=com.typesafe.akka - artifact=akka-actor-typed_$scala.binary_version$ + artifact=akka-actor-typed_$scala.binary.version$ version=$akka.version$ } @@ -36,7 +36,7 @@ registered to the same key. The registry is dynamic. New actors can be registered during the lifecycle of the system. Entries are removed when registered actors are stopped, manually deregistered or the node they live on is removed from the @ref:[Cluster](cluster.md). To facilitate this dynamic aspect you can also subscribe to changes with the `Receptionist.Subscribe` message. It will send -`Listing` messages to the subscriber when entries for a key are changed. +`Listing` messages to the subscriber, first with the set of entries upon subscription, then whenever the entries for a key are changed. These imports are used in the following example: diff --git a/akka-docs/src/main/paradox/typed/actor-lifecycle.md b/akka-docs/src/main/paradox/typed/actor-lifecycle.md index 23a5af3b6a..7835178972 100644 --- a/akka-docs/src/main/paradox/typed/actor-lifecycle.md +++ b/akka-docs/src/main/paradox/typed/actor-lifecycle.md @@ -3,7 +3,7 @@ project.description: The Akka Actor lifecycle. --- # Actor lifecycle -For the Akka Classic documentation of this feature see @ref:[Classic Actors](../actors.md). +You are viewing the documentation for the new actor APIs, to view the Akka Classic documentation, see @ref:[Classic Actors](../actors.md). ## Dependency @@ -11,7 +11,7 @@ To use Akka Actor Typed, you must add the following dependency in your project: @@dependency[sbt,Maven,Gradle] { group=com.typesafe.akka - artifact=akka-actor-typed_$scala.binary_version$ + artifact=akka-actor-typed_$scala.binary.version$ version=$akka.version$ } diff --git a/akka-docs/src/main/paradox/typed/actors.md b/akka-docs/src/main/paradox/typed/actors.md index 2c3d16b9e0..6e323ff8b0 100644 --- a/akka-docs/src/main/paradox/typed/actors.md +++ b/akka-docs/src/main/paradox/typed/actors.md @@ -3,7 +3,7 @@ project.description: The Actor model, managing internal state and changing behav --- # Introduction to Actors -For the Akka Classic documentation of this feature see @ref:[Classic Actors](../actors.md). +You are viewing the documentation for the new actor APIs, to view the Akka Classic documentation, see @ref:[Classic Actors](../actors.md). ## Module info @@ -11,7 +11,7 @@ To use Akka Actors, add the following dependency in your project: @@dependency[sbt,Maven,Gradle] { group=com.typesafe.akka - artifact=akka-actor-typed_$scala.binary_version$ + artifact=akka-actor-typed_$scala.binary.version$ version=$akka.version$ } @@ -23,7 +23,7 @@ imports when working in Scala, or viceversa. See @ref:[IDE Tips](../additional/i ## Akka Actors -The [Actor Model](http://en.wikipedia.org/wiki/Actor_model) provides a higher level of abstraction for writing concurrent +The [Actor Model](https://en.wikipedia.org/wiki/Actor_model) provides a higher level of abstraction for writing concurrent and distributed systems. It alleviates the developer from having to deal with explicit locking and thread management, making it easier to write correct concurrent and parallel systems. Actors were defined in the 1973 paper by Carl diff --git a/akka-docs/src/main/paradox/typed/choosing-cluster.md b/akka-docs/src/main/paradox/typed/choosing-cluster.md index 5a594cb274..f56b200c9f 100644 --- a/akka-docs/src/main/paradox/typed/choosing-cluster.md +++ b/akka-docs/src/main/paradox/typed/choosing-cluster.md @@ -10,7 +10,7 @@ Microservices has many attractive properties, such as the independent nature of multiple smaller and more focused teams that can deliver new functionality more frequently and can respond quicker to business opportunities. Reactive Microservices should be isolated, autonomous, and have a single responsibility as identified by Jonas Bonér in the book -[Reactive Microsystems: The Evolution of Microservices at Scale](https://info.lightbend.com/ebook-reactive-microservices-the-evolution-of-microservices-at-scale-register.html). +[Reactive Microsystems: The Evolution of Microservices at Scale](https://www.lightbend.com/ebooks/reactive-microsystems-evolution-of-microservices-scalability-oreilly). In a microservices architecture, you should consider communication within a service and between services. @@ -29,9 +29,9 @@ during a rolling deployment, but deployment of the entire set has a single point intra-service communication can take advantage of Akka Cluster, failure management and actor messaging, which is convenient to use and has great performance. -Between different services [Akka HTTP](https://doc.akka.io/docs/akka-http/current) or +Between different services [Akka HTTP](https://doc.akka.io/docs/akka-http/current/) or [Akka gRPC](https://doc.akka.io/docs/akka-grpc/current/) can be used for synchronous (yet non-blocking) -communication and [Akka Streams Kafka](https://doc.akka.io/docs/akka-stream-kafka/current/home.html) or other +communication and [Akka Streams Kafka](https://doc.akka.io/docs/alpakka-kafka/current/) or other [Alpakka](https://doc.akka.io/docs/alpakka/current/) connectors for integration asynchronous communication. All those communication mechanisms work well with streaming of messages with end-to-end back-pressure, and the synchronous communication tools can also be used for single request response interactions. It is also important diff --git a/akka-docs/src/main/paradox/typed/cluster-concepts.md b/akka-docs/src/main/paradox/typed/cluster-concepts.md index a142898987..1153f782ab 100644 --- a/akka-docs/src/main/paradox/typed/cluster-concepts.md +++ b/akka-docs/src/main/paradox/typed/cluster-concepts.md @@ -30,15 +30,15 @@ and membership state transitions. ### Gossip -The cluster membership used in Akka is based on Amazon's [Dynamo](http://www.allthingsdistributed.com/files/amazon-dynamo-sosp2007.pdf) system and -particularly the approach taken in Basho's' [Riak](http://basho.com/technology/architecture/) distributed database. -Cluster membership is communicated using a [Gossip Protocol](http://en.wikipedia.org/wiki/Gossip_protocol), where the current +The cluster membership used in Akka is based on Amazon's [Dynamo](https://www.allthingsdistributed.com/files/amazon-dynamo-sosp2007.pdf) system and +particularly the approach taken in Basho's' [Riak](https://riak.com/technology/architecture/) distributed database. +Cluster membership is communicated using a [Gossip Protocol](https://en.wikipedia.org/wiki/Gossip_protocol), where the current state of the cluster is gossiped randomly through the cluster, with preference to members that have not seen the latest version. #### Vector Clocks -[Vector clocks](http://en.wikipedia.org/wiki/Vector_clock) are a type of data structure and algorithm for generating a partial +[Vector clocks](https://en.wikipedia.org/wiki/Vector_clock) are a type of data structure and algorithm for generating a partial ordering of events in a distributed system and detecting causality violations. We use vector clocks to reconcile and merge differences in cluster state @@ -48,7 +48,7 @@ to the cluster state has an accompanying update to the vector clock. #### Gossip Convergence Information about the cluster converges locally at a node at certain points in time. -This is when a node can prove that the cluster state he is observing has been observed +This is when a node can prove that the cluster state it is observing has been observed by all other nodes in the cluster. Convergence is implemented by passing a set of nodes that have seen current state version during gossip. This information is referred to as the seen set in the gossip overview. When all nodes are included in the seen set there is @@ -175,5 +175,5 @@ The periodic nature of the gossip has a nice batching effect of state changes, e.g. joining several nodes quickly after each other to one node will result in only one state change to be spread to other members in the cluster. -The gossip messages are serialized with [protobuf](https://code.google.com/p/protobuf/) and also gzipped to reduce payload +The gossip messages are serialized with [protobuf](https://github.com/protocolbuffers/protobuf) and also gzipped to reduce payload size. diff --git a/akka-docs/src/main/paradox/typed/cluster-dc.md b/akka-docs/src/main/paradox/typed/cluster-dc.md index 315c1e4c8c..544bfd3ebd 100644 --- a/akka-docs/src/main/paradox/typed/cluster-dc.md +++ b/akka-docs/src/main/paradox/typed/cluster-dc.md @@ -1,6 +1,6 @@ # Multi-DC Cluster -For the Akka Classic documentation of this feature see @ref:[Classic Multi-DC Cluster](../cluster-dc.md) +You are viewing the documentation for the new actor APIs, to view the Akka Classic documentation, see @ref:[Classic Multi-DC Cluster](../cluster-dc.md) This chapter describes how @ref[Akka Cluster](cluster.md) can be used across multiple data centers, availability zones or regions. @@ -20,7 +20,7 @@ To use Akka Cluster add the following dependency in your project: @@dependency[sbt,Maven,Gradle] { group=com.typesafe.akka - artifact=akka-cluster-typed_$scala.binary_version$ + artifact=akka-cluster-typed_$scala.binary.version$ version=$akka.version$ } diff --git a/akka-docs/src/main/paradox/typed/cluster-membership.md b/akka-docs/src/main/paradox/typed/cluster-membership.md index 327408916e..dbd53512cc 100644 --- a/akka-docs/src/main/paradox/typed/cluster-membership.md +++ b/akka-docs/src/main/paradox/typed/cluster-membership.md @@ -29,7 +29,7 @@ UID. ## Member States -The cluster membership state is a specialized [CRDT](http://hal.upmc.fr/docs/00/55/55/88/PDF/techreport.pdf), which means that it has a monotonic +The cluster membership state is a specialized [CRDT](https://hal.inria.fr/file/index/docid/555588/filename/techreport.pdf), which means that it has a monotonic merge function. When concurrent changes occur on different nodes the updates can always be merged and converge to the same end result. @@ -108,8 +108,7 @@ performs in such a case must be designed in a way that all concurrent leaders wo might be impossible in general and only feasible under additional constraints). The most important case of that kind is a split brain scenario where nodes need to be downed, either manually or automatically, to bring the cluster back to convergence. -See the [Lightbend Split Brain Resolver](https://doc.akka.io/docs/akka-enhancements/current/split-brain-resolver.html) -for an implementation of that. +The @ref:[Split Brain Resolver](../split-brain-resolver.md) is the built-in implementation of that. Another transition that is possible without convergence is marking members as `WeaklyUp` as described in the next section. @@ -140,7 +139,7 @@ startup if a node to join have been specified in the configuration * **leave** - tell a node to leave the cluster gracefully, normally triggered by ActorSystem or JVM shutdown through @ref[coordinated shutdown](../coordinated-shutdown.md) - * **down** - mark a node as down. This action is required to remove crashed nodes (that did not 'leave') from the cluster. It can be triggered manually, through [Cluster HTTP Management](https://doc.akka.io/docs/akka-management/current/cluster-http-management.html#put-cluster-members-address-responses), or automatically by a @ref[downing provider](cluster.md#downing) like [Split Brain Resolver](https://doc.akka.io/docs/akka-enhancements/current/split-brain-resolver.html) + * **down** - mark a node as down. This action is required to remove crashed nodes (that did not 'leave') from the cluster. It can be triggered manually, through [Cluster HTTP Management](https://doc.akka.io/docs/akka-management/current/cluster-http-management.html#put-cluster-members-address-responses), or automatically by a @ref[downing provider](cluster.md#downing) like @ref:[Split Brain Resolver](../split-brain-resolver.md) #### Leader Actions diff --git a/akka-docs/src/main/paradox/typed/cluster-sharded-daemon-process.md b/akka-docs/src/main/paradox/typed/cluster-sharded-daemon-process.md index 9ed36d620f..1d145ab8bd 100644 --- a/akka-docs/src/main/paradox/typed/cluster-sharded-daemon-process.md +++ b/akka-docs/src/main/paradox/typed/cluster-sharded-daemon-process.md @@ -15,7 +15,7 @@ To use Akka Sharded Daemon Process, you must add the following dependency in you @@dependency[sbt,Maven,Gradle] { group=com.typesafe.akka - artifact=akka-cluster-sharding-typed_$scala.binary_version$ + artifact=akka-cluster-sharding-typed_$scala.binary.version$ version=$akka.version$ } @@ -55,4 +55,4 @@ either with a single `ServiceKey` which all daemon process actors register theme ## Scalability This cluster tool is intended for small numbers of consumers and will not scale well to a large set. In large clusters -it is recommended to limit the nodes the sharded daemon process will run on using a role. \ No newline at end of file +it is recommended to limit the nodes the sharded daemon process will run on using a role. diff --git a/akka-docs/src/main/paradox/typed/cluster-sharding.md b/akka-docs/src/main/paradox/typed/cluster-sharding.md index 655a1e6fb8..587022fe74 100644 --- a/akka-docs/src/main/paradox/typed/cluster-sharding.md +++ b/akka-docs/src/main/paradox/typed/cluster-sharding.md @@ -3,7 +3,7 @@ project.description: Shard a clustered compute process across the network with l --- # Cluster Sharding -For the Akka Classic documentation of this feature see @ref:[Classic Cluster Sharding](../cluster-sharding.md) +You are viewing the documentation for the new actor APIs, to view the Akka Classic documentation, see @ref:[Classic Cluster Sharding](../cluster-sharding.md) ## Module info @@ -11,7 +11,7 @@ To use Akka Cluster Sharding, you must add the following dependency in your proj @@dependency[sbt,Maven,Gradle] { group=com.typesafe.akka - artifact=akka-cluster-sharding-typed_$scala.binary_version$ + artifact=akka-cluster-sharding-typed_$scala.binary.version$ version=$akka.version$ } diff --git a/akka-docs/src/main/paradox/typed/cluster-singleton.md b/akka-docs/src/main/paradox/typed/cluster-singleton.md index 65c004743d..0ac134bfaa 100644 --- a/akka-docs/src/main/paradox/typed/cluster-singleton.md +++ b/akka-docs/src/main/paradox/typed/cluster-singleton.md @@ -1,6 +1,6 @@ # Cluster Singleton -For the Akka Classic documentation of this feature see @ref:[Classic Cluster Singleton](../cluster-singleton.md). +You are viewing the documentation for the new actor APIs, to view the Akka Classic documentation, see @ref:[Classic Cluster Singleton](../cluster-singleton.md). ## Module info @@ -8,7 +8,7 @@ To use Cluster Singleton, you must add the following dependency in your project: @@dependency[sbt,Maven,Gradle] { group=com.typesafe.akka - artifact=akka-cluster-typed_$scala.binary_version$ + artifact=akka-cluster-typed_$scala.binary.version$ version=$akka.version$ } diff --git a/akka-docs/src/main/paradox/typed/cluster.md b/akka-docs/src/main/paradox/typed/cluster.md index c17bf83d15..fa007ae31e 100644 --- a/akka-docs/src/main/paradox/typed/cluster.md +++ b/akka-docs/src/main/paradox/typed/cluster.md @@ -13,7 +13,7 @@ For specific documentation topics see: * @ref:[Rolling Updates](../additional/rolling-updates.md) * @ref:[Operating, Managing, Observability](../additional/operations.md) -For the Akka Classic documentation of this feature see @ref:[Classic Cluster](../cluster-usage.md). +You are viewing the documentation for the new actor APIs, to view the Akka Classic documentation, see @ref:[Classic Cluster](../cluster-usage.md). You have to enable @ref:[serialization](../serialization.md) to send messages between ActorSystems (nodes) in the Cluster. @ref:[Serialization with Jackson](../serialization-jackson.md) is a good choice in many cases, and our @@ -25,7 +25,7 @@ To use Akka Cluster add the following dependency in your project: @@dependency[sbt,Maven,Gradle] { group=com.typesafe.akka - artifact=akka-cluster-typed_$scala.binary_version$ + artifact=akka-cluster-typed_$scala.binary.version$ version=$akka.version$ } @@ -275,23 +275,22 @@ new joining members to 'Up'. The node must first become `reachable` again, or th status of the unreachable member must be changed to `Down`. Changing status to `Down` can be performed automatically or manually. -By default, downing must be performed manually using @ref:[HTTP](../additional/operations.md#http) or @ref:[JMX](../additional/operations.md#jmx). +We recommend that you enable the @ref:[Split Brain Resolver](../split-brain-resolver.md) that is part of the +Akka Cluster module. You enable it with configuration: + +``` +akka.cluster.downing-provider-class = "akka.cluster.sbr.SplitBrainResolverProvider" +``` + +You should also consider the different available @ref:[downing strategies](../split-brain-resolver.md#strategies). + +If a downing provider is not configured downing must be performed manually using +@ref:[HTTP](../additional/operations.md#http) or @ref:[JMX](../additional/operations.md#jmx). Note that @ref:[Cluster Singleton](cluster-singleton.md) or @ref:[Cluster Sharding entities](cluster-sharding.md) that are running on a crashed (unreachable) node will not be started on another node until the previous node has been removed from the Cluster. Removal of crashed (unreachable) nodes is performed after a downing decision. -A production solution for downing is provided by -[Split Brain Resolver](https://doc.akka.io/docs/akka-enhancements/current/split-brain-resolver.html), -which is part of the [Lightbend Platform](http://www.lightbend.com/platform). -If you don’t have a Lightbend Platform Subscription, you should still carefully read the -[documentation](https://doc.akka.io/docs/akka-enhancements/current/split-brain-resolver.html) -of the Split Brain Resolver and make sure that the solution you are using handles the concerns and scenarios -described there. - -A custom downing strategy can be implemented with a @apidoc[akka.cluster.DowningProvider] and enabled with -configuration `akka.cluster.downing-provider-class`. - Downing can also be performed programmatically with @scala[`Cluster(system).manager ! Down(address)`]@java[`Cluster.get(system).manager().tell(Down(address))`], but that is mostly useful from tests and when implementing a `DowningProvider`. @@ -439,7 +438,11 @@ See @ref:[Cluster Sharding](cluster-sharding.md). @@include[cluster.md](../includes/cluster.md) { #cluster-ddata } See @ref:[Distributed Data](distributed-data.md). -@@include[cluster.md](../includes/cluster.md) { #cluster-pubsub } +@@include[cluster.md](../includes/cluster.md) { #cluster-pubsub } +See @ref:[Distributed Publish Subscribe](distributed-pub-sub.md). + +@@include[cluster.md](../includes/cluster.md) { #cluster-router } +See @ref:[Group Routers](routers.md#group-router). @@include[cluster.md](../includes/cluster.md) { #cluster-multidc } See @ref:[Cluster Multi-DC](cluster-dc.md). diff --git a/akka-docs/src/main/paradox/typed/coexisting.md b/akka-docs/src/main/paradox/typed/coexisting.md index 3922313bfe..c79716e6ff 100644 --- a/akka-docs/src/main/paradox/typed/coexisting.md +++ b/akka-docs/src/main/paradox/typed/coexisting.md @@ -6,7 +6,7 @@ To use Akka Actor Typed, you must add the following dependency in your project: @@dependency[sbt,Maven,Gradle] { group=com.typesafe.akka - artifact=akka-actor-typed_$scala.binary_version$ + artifact=akka-actor-typed_$scala.binary.version$ version=$akka.version$ } diff --git a/akka-docs/src/main/paradox/typed/dispatchers.md b/akka-docs/src/main/paradox/typed/dispatchers.md index 1f3fc1d615..8d825cd214 100644 --- a/akka-docs/src/main/paradox/typed/dispatchers.md +++ b/akka-docs/src/main/paradox/typed/dispatchers.md @@ -3,7 +3,7 @@ project.description: Akka dispatchers and how to choose the right ones. --- # Dispatchers -For the Akka Classic documentation of this feature see @ref:[Classic Dispatchers](../dispatchers.md). +You are viewing the documentation for the new actor APIs, to view the Akka Classic documentation, see @ref:[Classic Dispatchers](../dispatchers.md). ## Dependency @@ -12,7 +12,7 @@ page describes how to use dispatchers with `akka-actor-typed`, which has depende @@dependency[sbt,Maven,Gradle] { group="com.typesafe.akka" - artifact="akka-actor-typed_$scala.binary_version$" + artifact="akka-actor-typed_$scala.binary.version$" version="$akka.version$" } diff --git a/akka-docs/src/main/paradox/typed/distributed-data.md b/akka-docs/src/main/paradox/typed/distributed-data.md index 573f289816..510c6f0e0e 100644 --- a/akka-docs/src/main/paradox/typed/distributed-data.md +++ b/akka-docs/src/main/paradox/typed/distributed-data.md @@ -3,7 +3,7 @@ project.description: Share data between nodes and perform updates without coordi --- # Distributed Data -For the Akka Classic documentation of this feature see @ref:[Classic Distributed Data](../distributed-data.md). +You are viewing the documentation for the new actor APIs, to view the Akka Classic documentation, see @ref:[Classic Distributed Data](../distributed-data.md). ## Module info @@ -11,7 +11,7 @@ To use Akka Cluster Distributed Data, you must add the following dependency in y @@dependency[sbt,Maven,Gradle] { group=com.typesafe.akka - artifact=akka-cluster-typed_$scala.binary_version$ + artifact=akka-cluster-typed_$scala.binary.version$ version=$akka.version$ } @@ -270,8 +270,8 @@ better safety for small clusters. #### Consistency and response types When using `ReadLocal`, you will never receive a `GetFailure` response, since the local replica is always available to -local readers. `WriteLocal` however may still reply with `UpdateFailure` messages, in the event that the `modify` function -threw an exception, or, if using @ref:[durable storage](#durable-storage), if storing failed. +local readers. `WriteLocal` however may still reply with `UpdateFailure` messages if the `modify` function +throws an exception, or if it fails to persist to @ref:[durable storage](#durable-storage). #### Examples @@ -534,7 +534,7 @@ akka.cluster.distributed-data.prefer-oldest = on ### Delta-CRDT -[Delta State Replicated Data Types](http://arxiv.org/abs/1603.01529) +[Delta State Replicated Data Types](https://arxiv.org/abs/1603.01529) are supported. A delta-CRDT is a way to reduce the need for sending the full state for updates. For example adding element `'c'` and `'d'` to set `{'a', 'b'}` would result in sending the delta `{'c', 'd'}` and merge that with the state on the @@ -665,7 +665,7 @@ All entries can be made durable by specifying: akka.cluster.distributed-data.durable.keys = ["*"] ``` -@scala[[LMDB](https://symas.com/products/lightning-memory-mapped-database/)]@java[[LMDB](https://github.com/lmdbjava/lmdbjava/)] is the default storage implementation. It is +@scala[[LMDB](https://symas.com/lmdb/technical/)]@java[[LMDB](https://github.com/lmdbjava/lmdbjava/)] is the default storage implementation. It is possible to replace that with another implementation by implementing the actor protocol described in `akka.cluster.ddata.DurableStore` and defining the `akka.cluster.distributed-data.durable.store-actor-class` property for the new implementation. @@ -761,11 +761,9 @@ API documentation of the `Replicator` for details. ## Learn More about CRDTs - * [Eventually Consistent Data Structures](https://vimeo.com/43903960) -talk by Sean Cribbs * [Strong Eventual Consistency and Conflict-free Replicated Data Types (video)](https://www.youtube.com/watch?v=oyUHd894w18&feature=youtu.be) talk by Mark Shapiro - * [A comprehensive study of Convergent and Commutative Replicated Data Types](http://hal.upmc.fr/file/index/docid/555588/filename/techreport.pdf) + * [A comprehensive study of Convergent and Commutative Replicated Data Types](https://hal.inria.fr/file/index/docid/555588/filename/techreport.pdf) paper by Mark Shapiro et. al. ## Configuration diff --git a/akka-docs/src/main/paradox/typed/distributed-pub-sub.md b/akka-docs/src/main/paradox/typed/distributed-pub-sub.md index 813dfe8593..6e2f20cd27 100644 --- a/akka-docs/src/main/paradox/typed/distributed-pub-sub.md +++ b/akka-docs/src/main/paradox/typed/distributed-pub-sub.md @@ -1,6 +1,6 @@ # Distributed Publish Subscribe in Cluster -For the Akka Classic documentation of this feature see @ref:[Classic Distributed Publish Subscribe](../distributed-pub-sub.md). +You are viewing the documentation for the new actor APIs, to view the Akka Classic documentation, see @ref:[Classic Distributed Publish Subscribe](../distributed-pub-sub.md). ## Module info @@ -9,7 +9,7 @@ when used in a clustered application: @@dependency[sbt,Maven,Gradle] { group="com.typesafe.akka" - artifact="akka-cluster-typed_$scala.binary_version$" + artifact="akka-cluster-typed_$scala.binary.version$" version="$akka.version$" } diff --git a/akka-docs/src/main/paradox/typed/fault-tolerance.md b/akka-docs/src/main/paradox/typed/fault-tolerance.md index 0fa90388bf..cccd7db2a8 100644 --- a/akka-docs/src/main/paradox/typed/fault-tolerance.md +++ b/akka-docs/src/main/paradox/typed/fault-tolerance.md @@ -1,6 +1,6 @@ # Fault Tolerance -For the Akka Classic documentation of this feature see @ref:[Classic Fault Tolerance](../fault-tolerance.md). +You are viewing the documentation for the new actor APIs, to view the Akka Classic documentation, see @ref:[Classic Fault Tolerance](../fault-tolerance.md). When an actor throws an unexpected exception, a failure, while processing a message or during initialization, the actor will by default be stopped. diff --git a/akka-docs/src/main/paradox/typed/from-classic.md b/akka-docs/src/main/paradox/typed/from-classic.md index 81dd3d05f6..844db9d0d1 100644 --- a/akka-docs/src/main/paradox/typed/from-classic.md +++ b/akka-docs/src/main/paradox/typed/from-classic.md @@ -27,7 +27,7 @@ For example `akka-cluster-typed`: @@dependency[sbt,Maven,Gradle] { group=com.typesafe.akka - artifact=akka-cluster-typed_$scala.binary_version$ + artifact=akka-cluster-typed_$scala.binary.version$ version=$akka.version$ } diff --git a/akka-docs/src/main/paradox/typed/fsm.md b/akka-docs/src/main/paradox/typed/fsm.md index a9d28486c1..cc954e223d 100644 --- a/akka-docs/src/main/paradox/typed/fsm.md +++ b/akka-docs/src/main/paradox/typed/fsm.md @@ -3,7 +3,7 @@ project.description: Finite State Machines (FSM) with Akka Actors. --- # Behaviors as finite state machines -For the Akka Classic documentation of this feature see @ref:[Classic FSM](../fsm.md). +You are viewing the documentation for the new actor APIs, to view the Akka Classic documentation, see @ref:[Classic FSM](../fsm.md). An actor can be used to model a Finite State Machine (FSM). diff --git a/akka-docs/src/main/paradox/typed/guide/introduction.md b/akka-docs/src/main/paradox/typed/guide/introduction.md index ab2580b737..86576d8271 100644 --- a/akka-docs/src/main/paradox/typed/guide/introduction.md +++ b/akka-docs/src/main/paradox/typed/guide/introduction.md @@ -31,7 +31,7 @@ efficiently. ## How to get started If this is your first experience with Akka, we recommend that you start by -running a simple Hello World project. See the @scala[[Quickstart Guide](https://developer.lightbend.com/guides/akka-quickstart-scala)] @java[[Quickstart Guide](https://developer.lightbend.com/guides/akka-quickstart-java)] for +running a simple Hello World project. See the @scala[[Quickstart Guide](https://developer.lightbend.com/guides/akka-quickstart-scala/)] @java[[Quickstart Guide](https://developer.lightbend.com/guides/akka-quickstart-java/)] for instructions on downloading and running the Hello World example. The *Quickstart* guide walks you through example code that introduces how to define actor systems, actors, and messages as well as how to use the test module and logging. Within 30 minutes, you should be able to run the Hello World example and learn how it is constructed. This *Getting Started* guide provides the next level of information. It covers why the actor model fits the needs of modern distributed systems and includes a tutorial that will help further your knowledge of Akka. Topics include: diff --git a/akka-docs/src/main/paradox/typed/guide/modules.md b/akka-docs/src/main/paradox/typed/guide/modules.md index bc7effcf0d..dd626f07c6 100644 --- a/akka-docs/src/main/paradox/typed/guide/modules.md +++ b/akka-docs/src/main/paradox/typed/guide/modules.md @@ -1,6 +1,6 @@ # Overview of Akka libraries and modules -Before delving into some best practices for writing actors, it will be helpful to preview the most commonly used Akka libraries. This will help you start thinking about the functionality you want to use in your system. All core Akka functionality is available as Open Source Software (OSS). Lightbend sponsors Akka development but can also help you with [commercial offerings ](https://www.lightbend.com/platform/subscription) such as training, consulting, support, and [Enterprise Suite](https://www.lightbend.com/platform/production) — a comprehensive set of tools for managing Akka systems. +Before delving into some best practices for writing actors, it will be helpful to preview the most commonly used Akka libraries. This will help you start thinking about the functionality you want to use in your system. All core Akka functionality is available as Open Source Software (OSS). Lightbend sponsors Akka development but can also help you with [commercial offerings ](https://www.lightbend.com/lightbend-subscription) such as training, consulting, support, and [Enterprise capabilities](https://www.lightbend.com/why-lightbend#enterprise-capabilities) — a comprehensive set of tools for managing Akka systems. The following capabilities are included with Akka OSS and are introduced later on this page: @@ -14,11 +14,10 @@ The following capabilities are included with Akka OSS and are introduced later o * @ref:[Streams](#streams) * @ref:[HTTP](#http) -With a [Lightbend Platform Subscription](https://www.lightbend.com/platform/subscription), you can use [Akka Enhancements](https://doc.akka.io/docs/akka-enhancements/current/) that includes: +With a [Lightbend Platform Subscription](https://www.lightbend.com/lightbend-subscription), you can use [Akka Enhancements](https://doc.akka.io/docs/akka-enhancements/current/) that includes: [Akka Resilience Enhancements](https://doc.akka.io/docs/akka-enhancements/current/akka-resilience-enhancements.html): -* [Split Brain Resolver](https://doc.akka.io/docs/akka-enhancements/current/split-brain-resolver.html) — Detects and recovers from network partitions, eliminating data inconsistencies and possible downtime. * [Configuration Checker](https://doc.akka.io/docs/akka-enhancements/current/config-checker.html) — Checks for potential configuration issues and logs suggestions. * [Diagnostics Recorder](https://doc.akka.io/docs/akka-enhancements/current/diagnostics-recorder.html) — Captures configuration and system information in a format that makes it easy to troubleshoot issues during development and production. * [Thread Starvation Detector](https://doc.akka.io/docs/akka-enhancements/current/starvation-detector.html) — Monitors an Akka system dispatcher and logs warnings if it becomes unresponsive. @@ -36,7 +35,7 @@ This page does not list all available modules, but overviews the main functional @@dependency[sbt,Maven,Gradle] { group=com.typesafe.akka - artifact=akka-actor-typed_$scala.binary_version$ + artifact=akka-actor-typed_$scala.binary.version$ version=$akka.version$ } @@ -61,7 +60,7 @@ Challenges that actors solve include the following: @@dependency[sbt,Maven,Gradle] { group=com.typesafe.akka - artifact=akka-remote_$scala.binary_version$ + artifact=akka-remote_$scala.binary.version$ version=$akka.version$ } @@ -84,7 +83,7 @@ Challenges Remoting solves include the following: @@dependency[sbt,Maven,Gradle] { group=com.typesafe.akka - artifact=akka-cluster-typed_$scala.binary_version$ + artifact=akka-cluster-typed_$scala.binary.version$ version=$akka.version$ } @@ -107,7 +106,7 @@ Challenges the Cluster module solves include the following: @@dependency[sbt,Maven,Gradle] { group=com.typesafe.akka - artifact=akka-cluster-sharding-typed_$scala.binary_version$ + artifact=akka-cluster-sharding-typed_$scala.binary.version$ version=$akka.version$ } @@ -126,7 +125,7 @@ Challenges that Sharding solves include the following: @@dependency[sbt,Maven,Gradle] { group=com.typesafe.akka - artifact=akka-cluster-singleton_$scala.binary_version$ + artifact=akka-cluster-singleton_$scala.binary.version$ version=$akka.version$ } @@ -147,7 +146,7 @@ The Singleton module can be used to solve these challenges: @@dependency[sbt,Maven,Gradle] { group=com.typesafe.akka - artifact=akka-persistence-typed_$scala.binary_version$ + artifact=akka-persistence-typed_$scala.binary.version$ version=$akka.version$ } @@ -160,7 +159,7 @@ cluster for example) or alternate views (like reports). Persistence tackles the following challenges: * How to restore the state of an entity/actor when system restarts or crashes. -* How to implement a [CQRS system](https://msdn.microsoft.com/en-us/library/jj591573.aspx). +* How to implement a [CQRS system](https://docs.microsoft.com/en-us/previous-versions/msp-n-p/jj591573(v=pandp.10)?redirectedfrom=MSDN). * How to ensure reliable delivery of messages in face of network errors and system crashes. * How to introspect domain events that have led an entity to its current state. * How to leverage [Event Sourcing](https://martinfowler.com/eaaDev/EventSourcing.html) in your application to support long-running processes while the project continues to evolve. @@ -169,7 +168,7 @@ Persistence tackles the following challenges: @@dependency[sbt,Maven,Gradle] { group=com.typesafe.akka - artifact=akka-cluster-typed_$scala.binary_version$ + artifact=akka-cluster-typed_$scala.binary.version$ version=$akka.version$ } @@ -188,7 +187,7 @@ Distributed Data is intended to solve the following challenges: @@dependency[sbt,Maven,Gradle] { group=com.typesafe.akka - artifact=akka-stream-typed_$scala.binary_version$ + artifact=akka-stream-typed_$scala.binary.version$ version=$akka.version$ } @@ -198,7 +197,7 @@ process a potentially large, or infinite, stream of sequential events and proper faster processing stages do not overwhelm slower ones in the chain or graph. Streams provide a higher-level abstraction on top of actors that simplifies writing such processing networks, handling all the fine details in the background and providing a safe, typed, composable programming model. Streams is also an implementation -of the [Reactive Streams standard](http://www.reactive-streams.org) which enables integration with all third +of the [Reactive Streams standard](https://www.reactive-streams.org) which enables integration with all third party implementations of that standard. Streams solve the following challenges: @@ -210,7 +209,7 @@ Streams solve the following challenges: ### HTTP -[Akka HTTP](https://doc.akka.io/docs/akka-http/current) is a separate module from Akka. +[Akka HTTP](https://doc.akka.io/docs/akka-http/current/) is a separate module from Akka. The de facto standard for providing APIs remotely, internal or external, is [HTTP](https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol). Akka provides a library to construct or consume such HTTP services by giving a set of tools to create HTTP services (and serve them) and a client that can be used to consume other services. These tools are particularly suited to streaming in and out a large set of data or real-time events by leveraging the underlying model of Akka Streams. diff --git a/akka-docs/src/main/paradox/typed/guide/tutorial_1.md b/akka-docs/src/main/paradox/typed/guide/tutorial_1.md index 2db15c4aea..17e8251d54 100644 --- a/akka-docs/src/main/paradox/typed/guide/tutorial_1.md +++ b/akka-docs/src/main/paradox/typed/guide/tutorial_1.md @@ -6,7 +6,7 @@ Add the following dependency in your project: @@dependency[sbt,Maven,Gradle] { group="com.typesafe.akka" - artifact="akka-actor-typed_$scala.binary_version$" + artifact="akka-actor-typed_$scala.binary.version$" version="$akka.version$" } diff --git a/akka-docs/src/main/paradox/typed/guide/tutorial_5.md b/akka-docs/src/main/paradox/typed/guide/tutorial_5.md index 0d2bfe38ab..0dd8d8ed06 100644 --- a/akka-docs/src/main/paradox/typed/guide/tutorial_5.md +++ b/akka-docs/src/main/paradox/typed/guide/tutorial_5.md @@ -228,7 +228,7 @@ In the context of the IoT system, this guide introduced the following concepts, To continue your journey with Akka, we recommend: -* Start building your own applications with Akka, make sure you [get involved in our amazing community](https://akka.io/get-involved) for help if you get stuck. +* Start building your own applications with Akka, make sure you [get involved in our amazing community](https://akka.io/get-involved/) for help if you get stuck. * If you’d like some additional background, and detail, read the rest of the @ref:[reference documentation](../actors.md) and check out some of the @ref:[books and videos](../../additional/books.md) on Akka. * If you are interested in functional programming, read how actors can be defined in a @ref:[functional style](../actors.md#functional-style). In this guide the object-oriented style was used, but you can mix both as you like. diff --git a/akka-docs/src/main/paradox/typed/index-cluster.md b/akka-docs/src/main/paradox/typed/index-cluster.md index a17e0dca73..4712692cac 100644 --- a/akka-docs/src/main/paradox/typed/index-cluster.md +++ b/akka-docs/src/main/paradox/typed/index-cluster.md @@ -25,6 +25,7 @@ project.description: Akka Cluster concepts, node membership service, CRDT Distri * [multi-node-testing](../multi-node-testing.md) * [remoting-artery](../remoting-artery.md) * [remoting](../remoting.md) +* [split-brain-resolver](../split-brain-resolver.md) * [coordination](../coordination.md) * [choosing-cluster](choosing-cluster.md) diff --git a/akka-docs/src/main/paradox/typed/interaction-patterns.md b/akka-docs/src/main/paradox/typed/interaction-patterns.md index 661fae4fd1..41e79c98d2 100644 --- a/akka-docs/src/main/paradox/typed/interaction-patterns.md +++ b/akka-docs/src/main/paradox/typed/interaction-patterns.md @@ -1,6 +1,6 @@ # Interaction Patterns -For the Akka Classic documentation of this feature see @ref:[Classic Actors](../actors.md). +You are viewing the documentation for the new actor APIs, to view the Akka Classic documentation, see @ref:[Classic Actors](../actors.md). ## Dependency @@ -8,7 +8,7 @@ To use Akka Actor Typed, you must add the following dependency in your project: @@dependency[sbt,Maven,Gradle] { group=com.typesafe.akka - artifact=akka-actor-typed_$scala.binary_version$ + artifact=akka-actor-typed_$scala.binary.version$ version=$akka.version$ } diff --git a/akka-docs/src/main/paradox/typed/logging.md b/akka-docs/src/main/paradox/typed/logging.md index d4b407dea3..c6b376df45 100644 --- a/akka-docs/src/main/paradox/typed/logging.md +++ b/akka-docs/src/main/paradox/typed/logging.md @@ -3,7 +3,7 @@ project.description: Logging options with Akka. --- # Logging -For the Akka Classic documentation of this feature see @ref:[Classic Logging](../logging.md). +You are viewing the documentation for the new actor APIs, to view the Akka Classic documentation, see @ref:[Classic Logging](../logging.md). ## Dependency @@ -12,7 +12,7 @@ via the SLF4J backend, such as Logback configuration. @@dependency[sbt,Maven,Gradle] { group="com.typesafe.akka" - artifact="akka-actor-typed_$scala.binary_version$" + artifact="akka-actor-typed_$scala.binary.version$" version="$akka.version$" } diff --git a/akka-docs/src/main/paradox/typed/mailboxes.md b/akka-docs/src/main/paradox/typed/mailboxes.md index c95c13347b..3b73270591 100644 --- a/akka-docs/src/main/paradox/typed/mailboxes.md +++ b/akka-docs/src/main/paradox/typed/mailboxes.md @@ -1,6 +1,6 @@ # Mailboxes -For the Akka Classic documentation of this feature see @ref:[Classic Mailboxes](../mailboxes.md). +You are viewing the documentation for the new actor APIs, to view the Akka Classic documentation, see @ref:[Classic Mailboxes](../mailboxes.md). ## Dependency @@ -9,7 +9,7 @@ page describes how to use mailboxes with `akka-actor-typed`, which has dependenc @@dependency[sbt,Maven,Gradle] { group="com.typesafe.akka" - artifact="akka-actor-typed_$scala.binary_version$" + artifact="akka-actor-typed_$scala.binary.version$" version="$akka.version$" } diff --git a/akka-docs/src/main/paradox/typed/persistence-snapshot.md b/akka-docs/src/main/paradox/typed/persistence-snapshot.md index 6bdba2c68c..227609871c 100644 --- a/akka-docs/src/main/paradox/typed/persistence-snapshot.md +++ b/akka-docs/src/main/paradox/typed/persistence-snapshot.md @@ -3,7 +3,7 @@ project.description: Append only event logs, snapshots and recovery with Akka ev --- # Snapshotting -For the Akka Classic documentation of this feature see @ref:[Classic Akka Persistence](../persistence.md). +You are viewing the documentation for the new actor APIs, to view the Akka Classic documentation, see @ref:[Classic Akka Persistence](../persistence.md). ## Snapshots diff --git a/akka-docs/src/main/paradox/typed/persistence-testing.md b/akka-docs/src/main/paradox/typed/persistence-testing.md index 30bac9073d..3f471dd46a 100644 --- a/akka-docs/src/main/paradox/typed/persistence-testing.md +++ b/akka-docs/src/main/paradox/typed/persistence-testing.md @@ -6,10 +6,10 @@ To use Akka Persistence TestKit, add the module to your project: @@dependency[sbt,Maven,Gradle] { group1=com.typesafe.akka - artifact1=akka-persistence-typed_$scala.binary_version$ + artifact1=akka-persistence-typed_$scala.binary.version$ version1=$akka.version$ group2=com.typesafe.akka - artifact2=akka-persistence-testkit_$scala.binary_version$ + artifact2=akka-persistence-testkit_$scala.binary.version$ version2=$akka.version$ scope2=test } @@ -61,7 +61,7 @@ To use the testkit you need to add the following dependency in your project: @@dependency[sbt,Maven,Gradle] { group="com.typesafe.akka" - artifact="akka-persistence-testkit_$scala.binary_version$" + artifact="akka-persistence-testkit_$scala.binary.version$" version="$akka.version$" } @@ -195,7 +195,7 @@ the plugins at the same time. To coordinate initialization you can use the `Pers @@dependency[sbt,Maven,Gradle] { group="com.typesafe.akka" - artifact="akka-persistence-testkit_$scala.binary_version$" + artifact="akka-persistence-testkit_$scala.binary.version$" version="$akka.version$" } diff --git a/akka-docs/src/main/paradox/typed/persistence.md b/akka-docs/src/main/paradox/typed/persistence.md index 19c98de0fa..0b2e2dd5ae 100644 --- a/akka-docs/src/main/paradox/typed/persistence.md +++ b/akka-docs/src/main/paradox/typed/persistence.md @@ -3,7 +3,7 @@ project.description: Event Sourcing with Akka Persistence enables actors to pers --- # Event Sourcing -For the Akka Classic documentation of this feature see @ref:[Classic Akka Persistence](../persistence.md). +You are viewing the documentation for the new actor APIs, to view the Akka Classic documentation, see @ref:[Classic Akka Persistence](../persistence.md). ## Module info @@ -11,7 +11,7 @@ To use Akka Persistence, add the module to your project: @@dependency[sbt,Maven,Gradle] { group=com.typesafe.akka - artifact=akka-persistence-typed_$scala.binary_version$ + artifact=akka-persistence-typed_$scala.binary.version$ version=$akka.version$ } @@ -47,7 +47,7 @@ provides tools to facilitate in building GDPR capable systems. ### Event sourcing concepts -See an [introduction to EventSourcing](https://msdn.microsoft.com/en-us/library/jj591559.aspx) at MSDN. +See an [introduction to EventSourcing](https://docs.microsoft.com/en-us/previous-versions/msp-n-p/jj591559(v=pandp.10)?redirectedfrom=MSDN) at MSDN. Another excellent article about "thinking in Events" is [Events As First-Class Citizens](https://hackernoon.com/events-as-first-class-citizens-8633e8479493) by Randy Shoup. It is a short and recommended read if you're starting developing Events based applications. diff --git a/akka-docs/src/main/paradox/typed/reliable-delivery.md b/akka-docs/src/main/paradox/typed/reliable-delivery.md index d9a8768001..3f263ec9a0 100644 --- a/akka-docs/src/main/paradox/typed/reliable-delivery.md +++ b/akka-docs/src/main/paradox/typed/reliable-delivery.md @@ -3,7 +3,7 @@ project.description: Reliable delivery and flow control of messages between acto --- # Reliable delivery -For the Akka Classic documentation of this feature see @ref:[Classic At-Least-Once Delivery](../persistence.md#at-least-once-delivery). +You are viewing the documentation for the new actor APIs, to view the Akka Classic documentation, see @ref:[Classic At-Least-Once Delivery](../persistence.md#at-least-once-delivery). @@@ warning @@ -19,7 +19,7 @@ To use reliable delivery, add the module to your project: @@dependency[sbt,Maven,Gradle] { group=com.typesafe.akka - artifact=akka-actor-typed_$scala.binary_version$ + artifact=akka-actor-typed_$scala.binary.version$ version=$akka.version$ } @@ -53,7 +53,7 @@ There are 3 supported patterns, which are described in the following sections: This pattern implements point-to-point reliable delivery between a single producer actor sending messages and a single consumer actor receiving the messages. -Messages are sent from the producer to @apidoc[ProducerController] and via @apidoc[ConsumerController] actors, which +Messages are sent from the producer to @apidoc[ProducerController$] and via @apidoc[ConsumerController$] actors, which handle the delivery and confirmation of the processing in the destination consumer actor. ![delivery-p2p-1.png](./images/delivery-p2p-1.png) @@ -156,7 +156,7 @@ One important property is that the order of the messages should not matter, beca message is routed randomly to one of the workers with demand. In other words, two subsequent messages may be routed to two different workers and processed independent of each other. -Messages are sent from the producer to @apidoc[WorkPullingProducerController] and via @apidoc[ConsumerController] +Messages are sent from the producer to @apidoc[WorkPullingProducerController$] and via @apidoc[ConsumerController$] actors, which handle the delivery and confirmation of the processing in the destination worker (consumer) actor. ![delivery-work-pulling-1.png](./images/delivery-work-pulling-1.png) @@ -249,7 +249,7 @@ To use reliable delivery with Cluster Sharding, add the following module to your @@dependency[sbt,Maven,Gradle] { group=com.typesafe.akka - artifact=akka-cluster-sharding-typed_$scala.binary_version$ + artifact=akka-cluster-sharding-typed_$scala.binary.version$ version=$akka.version$ } @@ -266,7 +266,7 @@ and sending from another producer (different node) ![delivery-work-sharding-3.png](./images/delivery-sharding-3.png) -The @apidoc[ShardingProducerController] should be used together with @apidoc[ShardingConsumerController]. +The @apidoc[ShardingProducerController$] should be used together with @apidoc[ShardingConsumerController$]. A producer can send messages via a `ShardingProducerController` to any `ShardingConsumerController` identified by an `entityId`. A single `ShardingProducerController` per `ActorSystem` (node) can be @@ -344,7 +344,7 @@ some of these may already have been processed by the previous consumer. Until sent messages have been confirmed the producer side keeps them in memory to be able to resend them. If the JVM of the producer side crashes those unconfirmed messages are lost. -To make sure the messages can be delivered also in that scenario a @apidoc[DurableProducerQueue] can be used. +To make sure the messages can be delivered also in that scenario a @apidoc[DurableProducerQueue$] can be used. Then the unconfirmed messages are stored in a durable way so that they can be redelivered when the producer is started again. An implementation of the `DurableProducerQueue` is provided by @apidoc[EventSourcedProducerQueue] in `akka-persistence-typed`. @@ -355,7 +355,7 @@ When using the `EventSourcedProducerQueue` the following dependency is needed: @@dependency[sbt,Maven,Gradle] { group=com.typesafe.akka - artifact=akka-persistence-typed_$scala.binary_version$ + artifact=akka-persistence-typed_$scala.binary.version$ version=$akka.version$ } diff --git a/akka-docs/src/main/paradox/typed/routers.md b/akka-docs/src/main/paradox/typed/routers.md index 989d12cb7a..d6302afa30 100644 --- a/akka-docs/src/main/paradox/typed/routers.md +++ b/akka-docs/src/main/paradox/typed/routers.md @@ -1,6 +1,6 @@ # Routers -For the Akka Classic documentation of this feature see @ref:[Classic Routing](../routing.md). +You are viewing the documentation for the new actor APIs, to view the Akka Classic documentation, see @ref:[Classic Routing](../routing.md). ## Dependency @@ -8,7 +8,7 @@ To use Akka Actor Typed, you must add the following dependency in your project: @@dependency[sbt,Maven,Gradle] { group=com.typesafe.akka - artifact=akka-actor-typed_$scala.binary_version$ + artifact=akka-actor-typed_$scala.binary.version$ version=$akka.version$ } @@ -73,7 +73,7 @@ Java ## Routing strategies -There are two different strategies for selecting what routee a message is forwarded to that can be selected +There are three different strategies for selecting which routee a message is forwarded to that can be selected from the router before spawning it: Scala @@ -105,13 +105,18 @@ An optional parameter `preferLocalRoutees` can be used for this strategy. Router ### Consistent Hashing -Uses [consistent hashing](http://en.wikipedia.org/wiki/Consistent_hashing) to select a routee based +Uses [consistent hashing](https://en.wikipedia.org/wiki/Consistent_hashing) to select a routee based on the sent message. This [article](http://www.tom-e-white.com/2007/11/consistent-hashing.html) gives good insight into how consistent hashing is implemented. Currently you have to define hashMapping of the router to map incoming messages to their consistent hash key. This makes the decision transparent for the sender. +Consistent hashing makes messages with the same hash routee to the same routee as long as the set of routees stays the same. +When the set of routees changes, consistent hashing tries to make sure, but does not guarantee, that messages with the same hash are routed to the same routee. + +See also @ref[Akka Cluster Sharding](cluster-sharding.md) which provides stable routing and rebalancing of the routee actors. + ## Routers and performance Note that if the routees are sharing a resource, the resource will determine if increasing the number of diff --git a/akka-docs/src/main/paradox/typed/stash.md b/akka-docs/src/main/paradox/typed/stash.md index 2a7b77f7f8..d43675ffe0 100644 --- a/akka-docs/src/main/paradox/typed/stash.md +++ b/akka-docs/src/main/paradox/typed/stash.md @@ -1,6 +1,6 @@ # Stash -For the Akka Classic documentation of this feature see @ref:[Classic Actors](../actors.md#stash). +You are viewing the documentation for the new actor APIs, to view the Akka Classic documentation, see @ref:[Classic Actors](../actors.md#stash). ## Dependency @@ -8,7 +8,7 @@ To use Akka Actor Typed, you must add the following dependency in your project: @@dependency[sbt,Maven,Gradle] { group=com.typesafe.akka - artifact=akka-actor-typed_$scala.binary_version$ + artifact=akka-actor-typed_$scala.binary.version$ version=$akka.version$ } diff --git a/akka-docs/src/main/paradox/typed/style-guide.md b/akka-docs/src/main/paradox/typed/style-guide.md index 49a600268c..ac42c38477 100644 --- a/akka-docs/src/main/paradox/typed/style-guide.md +++ b/akka-docs/src/main/paradox/typed/style-guide.md @@ -446,8 +446,8 @@ be good to know that it's optional in case you would prefer a different approach * direct processing because there is only one message type * if or switch statements * annotation processor -* [Vavr Pattern Matching DSL](http://www.vavr.io/vavr-docs/#_pattern_matching) -* future pattern matching in Java ([JEP 305](http://openjdk.java.net/jeps/305)) +* [Vavr Pattern Matching DSL](https://www.vavr.io/vavr-docs/#_pattern_matching) +* pattern matching since JDK 14 ([JEP 305](https://openjdk.java.net/jeps/305)) In `Behaviors` there are `receive`, `receiveMessage` and `receiveSignal` factory methods that takes functions instead of using the `ReceiveBuilder`, which is the `receive` with the class parameter. diff --git a/akka-docs/src/main/paradox/typed/testing-async.md b/akka-docs/src/main/paradox/typed/testing-async.md index 1213c5efab..d8b24596b6 100644 --- a/akka-docs/src/main/paradox/typed/testing-async.md +++ b/akka-docs/src/main/paradox/typed/testing-async.md @@ -1,6 +1,6 @@ ## Asynchronous testing -For the Akka Classic documentation of this feature see @ref:[Classic Testing](../testing.md). +You are viewing the documentation for the new actor APIs, to view the Akka Classic documentation, see @ref:[Classic Testing](../testing.md). Asynchronous testing uses a real @apidoc[akka.actor.typed.ActorSystem] that allows you to test your Actors in a more realistic environment. diff --git a/akka-docs/src/main/paradox/typed/testing-sync.md b/akka-docs/src/main/paradox/typed/testing-sync.md index ae9fb96f68..f5459fcfe3 100644 --- a/akka-docs/src/main/paradox/typed/testing-sync.md +++ b/akka-docs/src/main/paradox/typed/testing-sync.md @@ -1,6 +1,6 @@ ## Synchronous behavior testing -For the Akka Classic documentation of this feature see @ref:[Classic Testing](../testing.md). +You are viewing the documentation for the new actor APIs, to view the Akka Classic documentation, see @ref:[Classic Testing](../testing.md). The `BehaviorTestKit` provides a very nice way of unit testing a `Behavior` in a deterministic way, but it has some limitations to be aware of. diff --git a/akka-docs/src/main/paradox/typed/testing.md b/akka-docs/src/main/paradox/typed/testing.md index 17c2770d49..2e60e08de2 100644 --- a/akka-docs/src/main/paradox/typed/testing.md +++ b/akka-docs/src/main/paradox/typed/testing.md @@ -1,6 +1,6 @@ # Testing -For the Akka Classic documentation of this feature see @ref:[Classic Testing](../testing.md). +You are viewing the documentation for the new actor APIs, to view the Akka Classic documentation, see @ref:[Classic Testing](../testing.md). ## Module info @@ -8,7 +8,7 @@ To use Actor TestKit add the module to your project: @@dependency[sbt,Maven,Gradle] { group=com.typesafe.akka - artifact=akka-actor-testkit-typed_$scala.binary_version$ + artifact=akka-actor-testkit-typed_$scala.binary.version$ version=$akka.version$ scope=test } @@ -19,7 +19,7 @@ We recommend using Akka TestKit with ScalaTest: @@dependency[sbt,Maven,Gradle] { group=org.scalatest - artifact=scalatest_$scala.binary_version$ + artifact=scalatest_$scala.binary.version$ version=$scalatest.version$ scope=test } diff --git a/akka-docs/src/test/java/jdocs/actor/ActorDocTest.java b/akka-docs/src/test/java/jdocs/actor/ActorDocTest.java index c31b1d191a..ebe238eefc 100644 --- a/akka-docs/src/test/java/jdocs/actor/ActorDocTest.java +++ b/akka-docs/src/test/java/jdocs/actor/ActorDocTest.java @@ -847,51 +847,11 @@ public class ActorDocTest extends AbstractJavaTest { }; } - private CompletionStage cleanup() { - return null; - } - - @Test - public void coordinatedShutdown() { - final ActorRef someActor = system.actorOf(Props.create(FirstActor.class)); - // #coordinated-shutdown-addTask - CoordinatedShutdown.get(system) - .addTask( - CoordinatedShutdown.PhaseBeforeServiceUnbind(), - "someTaskName", - () -> { - return akka.pattern.Patterns.ask(someActor, "stop", Duration.ofSeconds(5)) - .thenApply(reply -> Done.getInstance()); - }); - // #coordinated-shutdown-addTask - - // #coordinated-shutdown-cancellable - Cancellable cancellable = - CoordinatedShutdown.get(system) - .addCancellableTask( - CoordinatedShutdown.PhaseBeforeServiceUnbind(), "someTaskCleanup", () -> cleanup()); - // much later... - cancellable.cancel(); - // #coordinated-shutdown-cancellable - - // #coordinated-shutdown-jvm-hook - CoordinatedShutdown.get(system) - .addJvmShutdownHook(() -> System.out.println("custom JVM shutdown hook...")); - // #coordinated-shutdown-jvm-hook - - // don't run this - if (false) { - // #coordinated-shutdown-run - CompletionStage done = - CoordinatedShutdown.get(system).runAll(CoordinatedShutdown.unknownReason()); - // #coordinated-shutdown-run - } - } - @Test public void coordinatedShutdownActorTermination() { ActorRef someActor = system.actorOf(Props.create(FirstActor.class)); someActor.tell(PoisonPill.getInstance(), ActorRef.noSender()); + // https://github.com/akka/akka/issues/29056 // #coordinated-shutdown-addActorTerminationTask CoordinatedShutdown.get(system) .addActorTerminationTask( diff --git a/akka-docs/src/test/java/jdocs/actor/typed/CoordinatedActorShutdownTest.java b/akka-docs/src/test/java/jdocs/actor/typed/CoordinatedActorShutdownTest.java new file mode 100644 index 0000000000..c4690305eb --- /dev/null +++ b/akka-docs/src/test/java/jdocs/actor/typed/CoordinatedActorShutdownTest.java @@ -0,0 +1,133 @@ +/* + * Copyright (C) 2020 Lightbend Inc. + */ + +package jdocs.actor.typed; + +import akka.Done; +import akka.actor.Cancellable; +import akka.actor.CoordinatedShutdown; +import akka.actor.typed.ActorRef; +import akka.actor.typed.ActorSystem; +import akka.actor.typed.Behavior; +import akka.actor.typed.javadsl.*; +// #coordinated-shutdown-addTask +import static akka.actor.typed.javadsl.AskPattern.ask; + +// #coordinated-shutdown-addTask + +import java.time.Duration; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; + +public class CoordinatedActorShutdownTest { + + // #coordinated-shutdown-addTask + public static class MyActor extends AbstractBehavior { + interface Messages {} + + // ... + + static final class Stop implements Messages { + final ActorRef replyTo; + + Stop(ActorRef replyTo) { + this.replyTo = replyTo; + } + } + // #coordinated-shutdown-addTask + + public static Behavior create() { + return Behaviors.setup(MyActor::new); + } + + private MyActor(ActorContext context) { + super(context); + } + + // #coordinated-shutdown-addTask + @Override + public Receive createReceive() { + return newReceiveBuilder().onMessage(Stop.class, this::stop).build(); + } + + private Behavior stop(Stop stop) { + // shut down the actor internal + // ... + stop.replyTo.tell(Done.done()); + return Behaviors.stopped(); + } + } + + // #coordinated-shutdown-addTask + + public static class Root extends AbstractBehavior { + + public static Behavior create() { + return Behaviors.setup( + context -> { + ActorRef myActor = context.spawn(MyActor.create(), "my-actor"); + ActorSystem system = context.getSystem(); + // #coordinated-shutdown-addTask + CoordinatedShutdown.get(system) + .addTask( + CoordinatedShutdown.PhaseBeforeServiceUnbind(), + "someTaskName", + () -> + ask(myActor, MyActor.Stop::new, Duration.ofSeconds(5), system.scheduler())); + // #coordinated-shutdown-addTask + return Behaviors.empty(); + }); + } + + private Root(ActorContext context) { + super(context); + } + + @Override + public Receive createReceive() { + return newReceiveBuilder().build(); + } + } + + private CompletionStage cleanup() { + return CompletableFuture.completedFuture(Done.done()); + } + + public void mount() { + ActorSystem system = ActorSystem.create(Root.create(), "main"); + + // #coordinated-shutdown-cancellable + Cancellable cancellable = + CoordinatedShutdown.get(system) + .addCancellableTask( + CoordinatedShutdown.PhaseBeforeServiceUnbind(), "someTaskCleanup", () -> cleanup()); + // much later... + cancellable.cancel(); + // #coordinated-shutdown-cancellable + + // #coordinated-shutdown-jvm-hook + CoordinatedShutdown.get(system) + .addJvmShutdownHook(() -> System.out.println("custom JVM shutdown hook...")); + // #coordinated-shutdown-jvm-hook + + // don't run this + if (false) { + // #coordinated-shutdown-run + // shut down with `ActorSystemTerminateReason` + system.terminate(); + + // or define a specific reason + class UserInitiatedShutdown implements CoordinatedShutdown.Reason { + @Override + public String toString() { + return "UserInitiatedShutdown"; + } + } + + CompletionStage done = + CoordinatedShutdown.get(system).runAll(new UserInitiatedShutdown()); + // #coordinated-shutdown-run + } + } +} diff --git a/akka-coordination/src/test/java/jdocs/akka/coordination/lease/LeaseDocTest.java b/akka-docs/src/test/java/jdocs/coordination/LeaseDocTest.java similarity index 97% rename from akka-coordination/src/test/java/jdocs/akka/coordination/lease/LeaseDocTest.java rename to akka-docs/src/test/java/jdocs/coordination/LeaseDocTest.java index c1c2d53b22..49ede28cae 100644 --- a/akka-coordination/src/test/java/jdocs/akka/coordination/lease/LeaseDocTest.java +++ b/akka-docs/src/test/java/jdocs/coordination/LeaseDocTest.java @@ -2,14 +2,14 @@ * Copyright (C) 2019-2020 Lightbend Inc. */ -package jdocs.akka.coordination.lease; +package jdocs.coordination; import akka.actor.ActorSystem; import akka.coordination.lease.LeaseSettings; import akka.coordination.lease.javadsl.Lease; import akka.coordination.lease.javadsl.LeaseProvider; import akka.testkit.javadsl.TestKit; -import docs.akka.coordination.LeaseDocSpec; +import docs.coordination.LeaseDocSpec; import org.junit.AfterClass; import org.junit.BeforeClass; import org.junit.Test; diff --git a/akka-docs/src/test/java/jdocs/stream/operators/flow/Lazy.java b/akka-docs/src/test/java/jdocs/stream/operators/flow/Lazy.java new file mode 100644 index 0000000000..b503475e5c --- /dev/null +++ b/akka-docs/src/test/java/jdocs/stream/operators/flow/Lazy.java @@ -0,0 +1,87 @@ +/* + * Copyright (C) 2020 Lightbend Inc. + */ + +package jdocs.stream.operators.flow; +/* + * Copyright (C) 2009-2020 Lightbend Inc. + */ + +import akka.NotUsed; +import akka.actor.ActorSystem; +import akka.japi.Pair; +import akka.stream.javadsl.Flow; +import akka.stream.javadsl.RunnableGraph; +import akka.stream.javadsl.Sink; +import akka.stream.javadsl.Source; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.CompletionStage; + +public class Lazy { + private ActorSystem system = null; + + void example() { + // #simple-example + Source numbers = + Source.unfold( + 0, + n -> { + int next = n + 1; + System.out.println("Source producing " + next); + return Optional.of(Pair.create(next, next)); + }) + .take(3); + + Flow> flow = + Flow.lazyFlow( + () -> { + System.out.println("Creating the actual flow"); + return Flow.fromFunction( + element -> { + System.out.println("Actual flow mapped " + element); + return element; + }); + }); + + numbers.via(flow).run(system); + // prints: + // Source producing 1 + // Creating the actual flow + // Actual flow mapped 1 + // Source producing 2 + // Actual flow mapped 2 + // #simple-example + } + + void statefulMap() { + // #mutable-example + Flow, CompletionStage> mutableFold = + Flow.lazyFlow( + () -> { + List zero = new ArrayList<>(); + + return Flow.of(Integer.class) + .fold( + zero, + (list, element) -> { + list.add(element); + return list; + }); + }); + + RunnableGraph stream = + Source.range(1, 3).via(mutableFold).to(Sink.foreach(System.out::println)); + + stream.run(system); + stream.run(system); + stream.run(system); + // prints: + // [1, 2, 3] + // [1, 2, 3] + // [1, 2, 3] + // #mutable-example + } +} diff --git a/akka-docs/src/test/java/jdocs/stream/operators/sink/Lazy.java b/akka-docs/src/test/java/jdocs/stream/operators/sink/Lazy.java new file mode 100644 index 0000000000..f3b30f0f17 --- /dev/null +++ b/akka-docs/src/test/java/jdocs/stream/operators/sink/Lazy.java @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2020 Lightbend Inc. + */ + +package jdocs.stream.operators.sink; +/* + * Copyright (C) 2009-2020 Lightbend Inc. + */ + +import akka.actor.ActorSystem; +import akka.stream.javadsl.Keep; +import akka.stream.javadsl.Sink; +import akka.stream.javadsl.Source; + +import java.util.Optional; +import java.util.concurrent.CompletionStage; + +public class Lazy { + + private ActorSystem system = null; + + void example() { + // #simple-example + CompletionStage> matVal = + Source.maybe() + .map( + element -> { + System.out.println("mapped " + element); + return element; + }) + .toMat( + Sink.lazySink( + () -> { + System.out.println("Sink created"); + return Sink.foreach(elem -> System.out.println("foreach " + elem)); + }), + Keep.left()) + .run(system); + + // some time passes + // nothing has been printed + matVal.toCompletableFuture().complete(Optional.of("one")); + // now prints: + // mapped one + // Sink created + // foreach one + + // #simple-example + } +} diff --git a/akka-docs/src/test/java/jdocs/stream/operators/source/Lazy.java b/akka-docs/src/test/java/jdocs/stream/operators/source/Lazy.java new file mode 100644 index 0000000000..b8695b102f --- /dev/null +++ b/akka-docs/src/test/java/jdocs/stream/operators/source/Lazy.java @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2009-2020 Lightbend Inc. + */ + +package jdocs.stream.operators.source; + +import akka.Done; +import akka.NotUsed; +import akka.actor.ActorSystem; +import akka.japi.Pair; +import akka.stream.javadsl.RunnableGraph; +import akka.stream.javadsl.Sink; +import akka.stream.javadsl.SinkQueueWithCancel; +import akka.stream.javadsl.Source; + +import java.util.Optional; +import java.util.concurrent.CompletionStage; + +public class Lazy { + + private ActorSystem system = null; + + private Source createExpensiveSource() { + throw new UnsupportedOperationException("Not implemented in sample"); + } + + void notReallyThatLazy() { + // #not-a-good-example + Source> source = + Source.lazySource( + () -> { + System.out.println("Creating the actual source"); + return createExpensiveSource(); + }); + + SinkQueueWithCancel queue = source.runWith(Sink.queue(), system); + + // ... time passes ... + // at some point in time we pull the first time + // but the source creation may already have been triggered + queue.pull(); + // #not-a-good-example + } + + static class IteratorLikeThing { + boolean thereAreMore() { + throw new UnsupportedOperationException("Not implemented in sample"); + } + + String extractNext() { + throw new UnsupportedOperationException("Not implemented in sample"); + } + } + + void safeMutableSource() { + // #one-per-materialization + RunnableGraph> stream = + Source.lazySource( + () -> { + IteratorLikeThing instance = new IteratorLikeThing(); + return Source.unfold( + instance, + sameInstance -> { + if (sameInstance.thereAreMore()) + return Optional.of(Pair.create(sameInstance, sameInstance.extractNext())); + else return Optional.empty(); + }); + }) + .to(Sink.foreach(System.out::println)); + + // each of the three materializations will have their own instance of IteratorLikeThing + stream.run(system); + stream.run(system); + stream.run(system); + // #one-per-materialization + } +} diff --git a/akka-docs/src/test/scala/docs/actor/ActorDocSpec.scala b/akka-docs/src/test/scala/docs/actor/ActorDocSpec.scala index 49f8176e9b..8362b3f01f 100644 --- a/akka-docs/src/test/scala/docs/actor/ActorDocSpec.scala +++ b/akka-docs/src/test/scala/docs/actor/ActorDocSpec.scala @@ -724,34 +724,8 @@ class ActorDocSpec extends AkkaSpec(""" } "using CoordinatedShutdown" in { - val someActor = system.actorOf(Props(classOf[Replier], this)) - //#coordinated-shutdown-addTask - CoordinatedShutdown(system).addTask(CoordinatedShutdown.PhaseBeforeServiceUnbind, "someTaskName") { () => - import akka.pattern.ask - import system.dispatcher - implicit val timeout = Timeout(5.seconds) - (someActor ? "stop").map(_ => Done) - } - //#coordinated-shutdown-addTask - - { - def cleanup(): Unit = {} - import system.dispatcher - //#coordinated-shutdown-cancellable - val c = CoordinatedShutdown(system).addCancellableTask(CoordinatedShutdown.PhaseBeforeServiceUnbind, "cleanup") { - () => - Future { - cleanup() - Done - } - } - - // much later... - c.cancel() - //#coordinated-shutdown-cancellable - } - - { + // other snippets moved to docs.actor.typed.CoordinatedActorShutdownSpec + { // https://github.com/akka/akka/issues/29056 val someActor = system.actorOf(Props(classOf[Replier], this)) someActor ! PoisonPill //#coordinated-shutdown-addActorTerminationTask @@ -762,19 +736,6 @@ class ActorDocSpec extends AkkaSpec(""" Some("stop")) //#coordinated-shutdown-addActorTerminationTask } - - //#coordinated-shutdown-jvm-hook - CoordinatedShutdown(system).addJvmShutdownHook { - println("custom JVM shutdown hook...") - } - //#coordinated-shutdown-jvm-hook - - // don't run this - def dummy(): Unit = { - //#coordinated-shutdown-run - val done: Future[Done] = CoordinatedShutdown(system).run(CoordinatedShutdown.UnknownReason) - //#coordinated-shutdown-run - } } } diff --git a/akka-docs/src/test/scala/docs/actor/typed/CoordinatedActorShutdownSpec.scala b/akka-docs/src/test/scala/docs/actor/typed/CoordinatedActorShutdownSpec.scala new file mode 100644 index 0000000000..c8728c27ed --- /dev/null +++ b/akka-docs/src/test/scala/docs/actor/typed/CoordinatedActorShutdownSpec.scala @@ -0,0 +1,91 @@ +/* + * Copyright (C) 2020 Lightbend Inc. + */ + +package docs.actor.typed + +import akka.Done +import akka.actor.{ Cancellable, CoordinatedShutdown } +import akka.actor.typed.{ ActorRef, ActorSystem, Behavior } +import akka.actor.typed.scaladsl.Behaviors +import akka.actor.typed.scaladsl.AskPattern._ +import akka.util.Timeout + +import scala.concurrent.Future +import scala.concurrent.duration._ + +class CoordinatedActorShutdownSpec { + + //#coordinated-shutdown-addTask + object MyActor { + + trait Messages + case class Stop(replyTo: ActorRef[Done]) extends Messages + + def behavior: Behavior[Messages] = + Behaviors.receiveMessage { + // ... + case Stop(replyTo) => + // shut down the actor internals + // .. + replyTo.tell(Done) + Behaviors.stopped + } + } + + //#coordinated-shutdown-addTask + + trait Message + + def root: Behavior[Message] = Behaviors.setup[Message] { context => + implicit val system = context.system + val myActor = context.spawn(MyActor.behavior, "my-actor") + //#coordinated-shutdown-addTask + CoordinatedShutdown(context.system).addTask(CoordinatedShutdown.PhaseBeforeServiceUnbind, "someTaskName") { () => + implicit val timeout = Timeout(5.seconds) + myActor.ask(MyActor.Stop) + } + //#coordinated-shutdown-addTask + + Behaviors.empty + + } + + def showCancel: Unit = { + val system = ActorSystem(root, "main") + + def cleanup(): Unit = {} + import system.executionContext + //#coordinated-shutdown-cancellable + val c: Cancellable = + CoordinatedShutdown(system).addCancellableTask(CoordinatedShutdown.PhaseBeforeServiceUnbind, "cleanup") { () => + Future { + cleanup() + Done + } + } + + // much later... + c.cancel() + //#coordinated-shutdown-cancellable + + //#coordinated-shutdown-jvm-hook + CoordinatedShutdown(system).addJvmShutdownHook { + println("custom JVM shutdown hook...") + } + //#coordinated-shutdown-jvm-hook + + // don't run this + def dummy(): Unit = { + //#coordinated-shutdown-run + // shut down with `ActorSystemTerminateReason` + system.terminate() + + // or define a specific reason + case object UserInitiatedShutdown extends CoordinatedShutdown.Reason + + val done: Future[Done] = CoordinatedShutdown(system).run(UserInitiatedShutdown) + //#coordinated-shutdown-run + } + } +} diff --git a/akka-coordination/src/test/scala/docs/akka/coordination/LeaseDocSpec.scala b/akka-docs/src/test/scala/docs/coordination/LeaseDocSpec.scala similarity index 90% rename from akka-coordination/src/test/scala/docs/akka/coordination/LeaseDocSpec.scala rename to akka-docs/src/test/scala/docs/coordination/LeaseDocSpec.scala index 4e6fa168f4..f1f2116081 100644 --- a/akka-coordination/src/test/scala/docs/akka/coordination/LeaseDocSpec.scala +++ b/akka-docs/src/test/scala/docs/coordination/LeaseDocSpec.scala @@ -2,15 +2,17 @@ * Copyright (C) 2019-2020 Lightbend Inc. */ -package docs.akka.coordination +package docs.coordination + +import scala.concurrent.Future + +import com.typesafe.config.ConfigFactory import akka.cluster.Cluster import akka.coordination.lease.LeaseSettings -import akka.coordination.lease.scaladsl.{ Lease, LeaseProvider } +import akka.coordination.lease.scaladsl.Lease +import akka.coordination.lease.scaladsl.LeaseProvider import akka.testkit.AkkaSpec -import com.typesafe.config.ConfigFactory - -import scala.concurrent.Future //#lease-example class SampleLease(settings: LeaseSettings) extends Lease(settings) { @@ -37,11 +39,11 @@ object LeaseDocSpec { def config() = ConfigFactory.parseString(""" - jdocs-lease.lease-class = "jdocs.akka.coordination.lease.LeaseDocTest$SampleLease" + jdocs-lease.lease-class = "jdocs.coordination.LeaseDocTest$SampleLease" #lease-config akka.actor.provider = cluster docs-lease { - lease-class = "docs.akka.coordination.SampleLease" + lease-class = "docs.coordination.SampleLease" heartbeat-timeout = 100s heartbeat-interval = 1s lease-operation-timeout = 1s diff --git a/akka-docs/src/test/scala/docs/serialization/SerializationDocSpec.scala b/akka-docs/src/test/scala/docs/serialization/SerializationDocSpec.scala index 2a7a90ab2f..81c304af47 100644 --- a/akka-docs/src/test/scala/docs/serialization/SerializationDocSpec.scala +++ b/akka-docs/src/test/scala/docs/serialization/SerializationDocSpec.scala @@ -109,6 +109,21 @@ package docs.serialization { */ trait JsonSerializable + object SerializerIdConfig { + val config = + """ + #//#serialization-identifiers-config + akka { + actor { + serialization-identifiers { + "docs.serialization.MyOwnSerializer" = 1234567 + } + } + } + #//#serialization-identifiers-config + """ + } + class SerializationDocSpec extends AkkaSpec { "demonstrate configuration of serialize messages" in { val config = ConfigFactory.parseString(""" diff --git a/akka-docs/src/test/scala/docs/stream/operators/flow/Lazy.scala b/akka-docs/src/test/scala/docs/stream/operators/flow/Lazy.scala new file mode 100644 index 0000000000..a619b03418 --- /dev/null +++ b/akka-docs/src/test/scala/docs/stream/operators/flow/Lazy.scala @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2009-2020 Lightbend Inc. + */ + +package docs.stream.operators.flow + +import java.util + +import akka.actor.ActorSystem +import akka.stream.scaladsl.Flow +import akka.stream.scaladsl.Sink +import akka.stream.scaladsl.Source + +object Lazy { + + implicit val system: ActorSystem = ??? + + def example(): Unit = { + // #simple-example + val numbers = Source + .unfold(0) { n => + val next = n + 1 + println(s"Source producing $next") + Some((next, next)) + } + .take(3) + + val flow = Flow.lazyFlow { () => + println("Creating the actual flow") + Flow[Int].map { element => + println(s"Actual flow mapped $element") + element + } + } + + numbers.via(flow).run() + // prints: + // Source producing 1 + // Creating the actual flow + // Actual flow mapped 1 + // Source producing 2 + // Actual flow mapped 2 + // #simple-example + } + + def statefulMap(): Unit = { + // #mutable-example + val mutableFold = Flow.lazyFlow { () => + val zero = new util.ArrayList[Int]() + Flow[Int].fold(zero) { (list, element) => + list.add(element) + list + } + } + val stream = + Source(1 to 3).via(mutableFold).to(Sink.foreach(println)) + + stream.run() + stream.run() + stream.run() + // prints: + // [1, 2, 3] + // [1, 2, 3] + // [1, 2, 3] + + // #mutable-example + } + +} diff --git a/akka-docs/src/test/scala/docs/stream/operators/sink/Lazy.scala b/akka-docs/src/test/scala/docs/stream/operators/sink/Lazy.scala new file mode 100644 index 0000000000..6587325cbc --- /dev/null +++ b/akka-docs/src/test/scala/docs/stream/operators/sink/Lazy.scala @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2009-2020 Lightbend Inc. + */ + +package docs.stream.operators.sink + +import akka.actor.ActorSystem +import akka.stream.scaladsl.Keep +import akka.stream.scaladsl.Sink +import akka.stream.scaladsl.Source + +object Lazy { + + implicit val system: ActorSystem = ??? + + def example(): Unit = { + // #simple-example + val matVal = + Source + .maybe[String] + .map { element => + println(s"mapped $element") + element + } + .toMat(Sink.lazySink { () => + println("Sink created") + Sink.foreach(elem => println(s"foreach $elem")) + })(Keep.left) + .run() + + // some time passes + // nothing has been printed + matVal.success(Some("one")) + // now prints: + // mapped one + // Sink created + // foreach one + + // #simple-example + } +} diff --git a/akka-docs/src/test/scala/docs/stream/operators/source/Lazy.scala b/akka-docs/src/test/scala/docs/stream/operators/source/Lazy.scala new file mode 100644 index 0000000000..568ce3829e --- /dev/null +++ b/akka-docs/src/test/scala/docs/stream/operators/source/Lazy.scala @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2009-2020 Lightbend Inc. + */ + +package docs.stream.operators.source + +import akka.NotUsed +import akka.actor.ActorSystem +import akka.stream.scaladsl.Sink +import akka.stream.scaladsl.Source + +object Lazy { + + implicit val system: ActorSystem = ??? + + def createExpensiveSource(): Source[String, NotUsed] = ??? + + def notReallyThatLazy(): Unit = { + // #not-a-good-example + val source = Source.lazySource { () => + println("Creating the actual source") + createExpensiveSource() + } + + val queue = source.runWith(Sink.queue()) + + // ... time passes ... + // at some point in time we pull the first time + // but the source creation may already have been triggered + queue.pull() + // #not-a-good-example + } + + class IteratorLikeThing { + def thereAreMore: Boolean = ??? + def extractNext: String = ??? + } + def safeMutableSource(): Unit = { + // #one-per-materialization + val stream = Source + .lazySource { () => + val iteratorLike = new IteratorLikeThing + Source.unfold(iteratorLike) { iteratorLike => + if (iteratorLike.thereAreMore) Some((iteratorLike, iteratorLike.extractNext)) + else None + } + } + .to(Sink.foreach(println)) + + // each of the three materializations will have their own instance of IteratorLikeThing + stream.run() + stream.run() + stream.run() + // #one-per-materialization + } +} diff --git a/akka-multi-node-testkit/src/main/java/akka/remote/testconductor/TestConductorProtocol.java b/akka-multi-node-testkit/src/main/java/akka/remote/testconductor/TestConductorProtocol.java index ddda2348b8..b9825acb94 100644 --- a/akka-multi-node-testkit/src/main/java/akka/remote/testconductor/TestConductorProtocol.java +++ b/akka-multi-node-testkit/src/main/java/akka/remote/testconductor/TestConductorProtocol.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2019-2020 Lightbend Inc. + * Copyright (C) 2020 Lightbend Inc. */ // Generated by the protocol buffer compiler. DO NOT EDIT! diff --git a/akka-persistence-testkit/src/main/scala/akka/persistence/testkit/javadsl/EventSourcedBehaviorTestKit.scala b/akka-persistence-testkit/src/main/scala/akka/persistence/testkit/javadsl/EventSourcedBehaviorTestKit.scala index 42b1330e00..3446225e76 100644 --- a/akka-persistence-testkit/src/main/scala/akka/persistence/testkit/javadsl/EventSourcedBehaviorTestKit.scala +++ b/akka-persistence-testkit/src/main/scala/akka/persistence/testkit/javadsl/EventSourcedBehaviorTestKit.scala @@ -4,8 +4,8 @@ package akka.persistence.testkit.javadsl -import java.util.function.{ Function => JFunction } import java.util.{ List => JList } +import java.util.function.{ Function => JFunction } import scala.reflect.ClassTag diff --git a/akka-persistence-typed/src/main/scala/akka/persistence/typed/internal/EventSourcedBehaviorImpl.scala b/akka-persistence-typed/src/main/scala/akka/persistence/typed/internal/EventSourcedBehaviorImpl.scala index 5a4a926f3b..e989af5bca 100644 --- a/akka-persistence-typed/src/main/scala/akka/persistence/typed/internal/EventSourcedBehaviorImpl.scala +++ b/akka-persistence-typed/src/main/scala/akka/persistence/typed/internal/EventSourcedBehaviorImpl.scala @@ -31,13 +31,13 @@ import akka.persistence.typed.DeleteSnapshotsFailed import akka.persistence.typed.DeletionTarget import akka.persistence.typed.EventAdapter import akka.persistence.typed.NoOpEventAdapter -import akka.persistence.typed.scaladsl.{ Recovery => TypedRecovery } import akka.persistence.typed.PersistenceId import akka.persistence.typed.SnapshotAdapter import akka.persistence.typed.SnapshotCompleted import akka.persistence.typed.SnapshotFailed import akka.persistence.typed.SnapshotSelectionCriteria import akka.persistence.typed.scaladsl._ +import akka.persistence.typed.scaladsl.{ Recovery => TypedRecovery } import akka.persistence.typed.scaladsl.RetentionCriteria import akka.util.ConstantFun import akka.util.unused diff --git a/akka-persistence-typed/src/main/scala/akka/persistence/typed/internal/ExternalInteractions.scala b/akka-persistence-typed/src/main/scala/akka/persistence/typed/internal/ExternalInteractions.scala index abe744434d..4b8298f366 100644 --- a/akka-persistence-typed/src/main/scala/akka/persistence/typed/internal/ExternalInteractions.scala +++ b/akka-persistence-typed/src/main/scala/akka/persistence/typed/internal/ExternalInteractions.scala @@ -99,7 +99,7 @@ private[akka] trait JournalInteractions[C, E, S] { @unused repr: immutable.Seq[PersistentRepr]): Unit = () protected def replayEvents(fromSeqNr: Long, toSeqNr: Long): Unit = { - setup.log.debug2("Replaying messages: from: {}, to: {}", fromSeqNr, toSeqNr) + setup.log.debug2("Replaying events: from: {}, to: {}", fromSeqNr, toSeqNr) setup.journal.tell( ReplayMessages(fromSeqNr, toSeqNr, setup.recovery.replayMax, setup.persistenceId.id, setup.selfClassic), setup.selfClassic) diff --git a/akka-persistence-typed/src/main/scala/akka/persistence/typed/internal/ReplayingEvents.scala b/akka-persistence-typed/src/main/scala/akka/persistence/typed/internal/ReplayingEvents.scala index da06fb1294..c61da7ce27 100644 --- a/akka-persistence-typed/src/main/scala/akka/persistence/typed/internal/ReplayingEvents.scala +++ b/akka-persistence-typed/src/main/scala/akka/persistence/typed/internal/ReplayingEvents.scala @@ -116,10 +116,8 @@ private[akka] final class ReplayingEvents[C, E, S]( def handleEvent(event: E): Unit = { eventForErrorReporting = OptionVal.Some(event) - state = state.copy( - seqNr = repr.sequenceNr, - state = setup.eventHandler(state.state, event), - eventSeenInInterval = true) + state = state.copy(seqNr = repr.sequenceNr) + state = state.copy(state = setup.eventHandler(state.state, event), eventSeenInInterval = true) } eventSeq match { @@ -247,5 +245,6 @@ private[akka] final class ReplayingEvents[C, E, S]( setup.cancelRecoveryTimer() } - override def currentSequenceNumber: Long = state.seqNr + override def currentSequenceNumber: Long = + state.seqNr } diff --git a/akka-persistence-typed/src/main/scala/akka/persistence/typed/internal/Running.scala b/akka-persistence-typed/src/main/scala/akka/persistence/typed/internal/Running.scala index a9174e5440..c1f4173801 100644 --- a/akka-persistence-typed/src/main/scala/akka/persistence/typed/internal/Running.scala +++ b/akka-persistence-typed/src/main/scala/akka/persistence/typed/internal/Running.scala @@ -97,10 +97,15 @@ private[akka] object Running { import InternalProtocol._ import Running.RunningState + // Needed for WithSeqNrAccessible, when unstashing + private var _currentSequenceNumber = 0L + final class HandlingCommands(state: RunningState[S]) extends AbstractBehavior[InternalProtocol](setup.context) with WithSeqNrAccessible { + _currentSequenceNumber = state.seqNr + def onMessage(msg: InternalProtocol): Behavior[InternalProtocol] = msg match { case IncomingCommand(c: C @unchecked) => onCommand(state, c) case JournalResponse(r) => onDeleteEventsJournalResponse(r, state.state) @@ -150,6 +155,7 @@ private[akka] object Running { // apply the event before persist so that validation exception is handled before persisting // the invalid event, in case such validation is implemented in the event handler. // also, ensure that there is an event handler for each single event + _currentSequenceNumber = state.seqNr + 1 val newState = state.applyEvent(setup, event) val eventToPersist = adaptEvent(event) @@ -166,12 +172,13 @@ private[akka] object Running { // apply the event before persist so that validation exception is handled before persisting // the invalid event, in case such validation is implemented in the event handler. // also, ensure that there is an event handler for each single event - var seqNr = state.seqNr + _currentSequenceNumber = state.seqNr val (newState, shouldSnapshotAfterPersist) = events.foldLeft((state, NoSnapshot: SnapshotAfterPersist)) { case ((currentState, snapshot), event) => - seqNr += 1 + _currentSequenceNumber += 1 val shouldSnapshot = - if (snapshot == NoSnapshot) setup.shouldSnapshot(currentState.state, event, seqNr) else snapshot + if (snapshot == NoSnapshot) setup.shouldSnapshot(currentState.state, event, _currentSequenceNumber) + else snapshot (currentState.applyEvent(setup, event), shouldSnapshot) } @@ -212,7 +219,8 @@ private[akka] object Running { setup.setMdcPhase(PersistenceMdc.RunningCmds) - override def currentSequenceNumber: Long = state.seqNr + override def currentSequenceNumber: Long = + _currentSequenceNumber } // =============================================== @@ -335,7 +343,9 @@ private[akka] object Running { else Behaviors.unhandled } - override def currentSequenceNumber: Long = visibleState.seqNr + override def currentSequenceNumber: Long = { + _currentSequenceNumber + } } // =============================================== @@ -430,7 +440,8 @@ private[akka] object Running { Behaviors.unhandled } - override def currentSequenceNumber: Long = state.seqNr + override def currentSequenceNumber: Long = + _currentSequenceNumber } // -------------------------- diff --git a/akka-persistence-typed/src/test/java/akka/persistence/typed/javadsl/PersistentActorJavaDslTest.java b/akka-persistence-typed/src/test/java/akka/persistence/typed/javadsl/PersistentActorJavaDslTest.java index 45a57a3df4..57e28b85ff 100644 --- a/akka-persistence-typed/src/test/java/akka/persistence/typed/javadsl/PersistentActorJavaDslTest.java +++ b/akka-persistence-typed/src/test/java/akka/persistence/typed/javadsl/PersistentActorJavaDslTest.java @@ -773,7 +773,7 @@ public class PersistentActorJavaDslTest extends JUnitSuite { probe.expectMessage("0 onRecoveryCompleted"); ref.tell("cmd"); probe.expectMessage("0 onCommand"); - probe.expectMessage("0 applyEvent"); + probe.expectMessage("1 applyEvent"); probe.expectMessage("1 thenRun"); } } diff --git a/akka-persistence-typed/src/test/scala/akka/persistence/typed/scaladsl/EventSourcedBehaviorSpec.scala b/akka-persistence-typed/src/test/scala/akka/persistence/typed/scaladsl/EventSourcedBehaviorSpec.scala index 4acb871860..25f4f9042c 100644 --- a/akka-persistence-typed/src/test/scala/akka/persistence/typed/scaladsl/EventSourcedBehaviorSpec.scala +++ b/akka-persistence-typed/src/test/scala/akka/persistence/typed/scaladsl/EventSourcedBehaviorSpec.scala @@ -32,7 +32,6 @@ import akka.actor.typed.scaladsl.Behaviors import akka.persistence.{ SnapshotMetadata => ClassicSnapshotMetadata } import akka.persistence.{ SnapshotSelectionCriteria => ClassicSnapshotSelectionCriteria } import akka.persistence.SelectedSnapshot -import akka.persistence.typed.SnapshotSelectionCriteria import akka.persistence.journal.inmem.InmemJournal import akka.persistence.query.EventEnvelope import akka.persistence.query.PersistenceQuery @@ -44,6 +43,7 @@ import akka.persistence.typed.RecoveryCompleted import akka.persistence.typed.SnapshotCompleted import akka.persistence.typed.SnapshotFailed import akka.persistence.typed.SnapshotMetadata +import akka.persistence.typed.SnapshotSelectionCriteria import akka.serialization.jackson.CborSerializable import akka.stream.scaladsl.Sink diff --git a/akka-persistence-typed/src/test/scala/akka/persistence/typed/scaladsl/EventSourcedSequenceNumberSpec.scala b/akka-persistence-typed/src/test/scala/akka/persistence/typed/scaladsl/EventSourcedSequenceNumberSpec.scala index 3c3947ab30..222f25fe97 100644 --- a/akka-persistence-typed/src/test/scala/akka/persistence/typed/scaladsl/EventSourcedSequenceNumberSpec.scala +++ b/akka-persistence-typed/src/test/scala/akka/persistence/typed/scaladsl/EventSourcedSequenceNumberSpec.scala @@ -49,7 +49,12 @@ class EventSourcedSequenceNumberSpec case "cmd" => probe ! s"${EventSourcedBehavior.lastSequenceNumber(ctx)} onCommand" Effect - .persist(command) + .persist("evt") + .thenRun(_ => probe ! s"${EventSourcedBehavior.lastSequenceNumber(ctx)} thenRun") + case "cmd3" => + probe ! s"${EventSourcedBehavior.lastSequenceNumber(ctx)} onCommand" + Effect + .persist("evt1", "evt2", "evt3") .thenRun(_ => probe ! s"${EventSourcedBehavior.lastSequenceNumber(ctx)} thenRun") case "stash" => probe ! s"${EventSourcedBehavior.lastSequenceNumber(ctx)} stash" @@ -59,7 +64,7 @@ class EventSourcedSequenceNumberSpec } } }, { (_, evt) => - probe ! s"${EventSourcedBehavior.lastSequenceNumber(ctx)} eventHandler" + probe ! s"${EventSourcedBehavior.lastSequenceNumber(ctx)} eventHandler $evt" evt }).snapshotWhen((_, event, _) => event == "snapshot").receiveSignal { case (_, RecoveryCompleted) => @@ -75,11 +80,40 @@ class EventSourcedSequenceNumberSpec ref ! "cmd" probe.expectMessage("0 onCommand") - probe.expectMessage("0 eventHandler") + probe.expectMessage("1 eventHandler evt") probe.expectMessage("1 thenRun") + + ref ! "cmd" + probe.expectMessage("1 onCommand") + probe.expectMessage("2 eventHandler evt") + probe.expectMessage("2 thenRun") + + ref ! "cmd3" + probe.expectMessage("2 onCommand") + probe.expectMessage("3 eventHandler evt1") + probe.expectMessage("4 eventHandler evt2") + probe.expectMessage("5 eventHandler evt3") + probe.expectMessage("5 thenRun") + + testKit.stop(ref) + probe.expectTerminated(ref) + + // and during replay + val ref2 = spawn(behavior(PersistenceId.ofUniqueId("ess-1"), probe.ref)) + probe.expectMessage("1 eventHandler evt") + probe.expectMessage("2 eventHandler evt") + probe.expectMessage("3 eventHandler evt1") + probe.expectMessage("4 eventHandler evt2") + probe.expectMessage("5 eventHandler evt3") + probe.expectMessage("5 onRecoveryComplete") + + ref2 ! "cmd" + probe.expectMessage("5 onCommand") + probe.expectMessage("6 eventHandler evt") + probe.expectMessage("6 thenRun") } - "be available while replaying stash" in { + "be available while unstashing" in { val probe = TestProbe[String]() val ref = spawn(behavior(PersistenceId.ofUniqueId("ess-2"), probe.ref)) probe.expectMessage("0 onRecoveryComplete") @@ -87,18 +121,23 @@ class EventSourcedSequenceNumberSpec ref ! "stash" ref ! "cmd" ref ! "cmd" - ref ! "cmd" + ref ! "cmd3" ref ! "unstash" probe.expectMessage("0 stash") - probe.expectMessage("0 eventHandler") + probe.expectMessage("1 eventHandler stashing") probe.expectMessage("1 unstash") - probe.expectMessage("1 eventHandler") + probe.expectMessage("2 eventHandler normal") probe.expectMessage("2 onCommand") - probe.expectMessage("2 eventHandler") + probe.expectMessage("3 eventHandler evt") probe.expectMessage("3 thenRun") probe.expectMessage("3 onCommand") - probe.expectMessage("3 eventHandler") + probe.expectMessage("4 eventHandler evt") probe.expectMessage("4 thenRun") + probe.expectMessage("4 onCommand") // cmd3 + probe.expectMessage("5 eventHandler evt1") + probe.expectMessage("6 eventHandler evt2") + probe.expectMessage("7 eventHandler evt3") + probe.expectMessage("7 thenRun") } // reproducer for #27935 @@ -112,11 +151,11 @@ class EventSourcedSequenceNumberSpec ref ! "cmd" probe.expectMessage("0 onCommand") // first command - probe.expectMessage("0 eventHandler") + probe.expectMessage("1 eventHandler evt") probe.expectMessage("1 thenRun") - probe.expectMessage("1 eventHandler") // snapshot + probe.expectMessage("2 eventHandler snapshot") probe.expectMessage("2 onCommand") // second command - probe.expectMessage("2 eventHandler") + probe.expectMessage("3 eventHandler evt") probe.expectMessage("3 thenRun") } } diff --git a/akka-persistence/src/main/java/akka/persistence/serialization/MessageFormats.java b/akka-persistence/src/main/java/akka/persistence/serialization/MessageFormats.java index 027ddb6b6f..9cb54077c0 100644 --- a/akka-persistence/src/main/java/akka/persistence/serialization/MessageFormats.java +++ b/akka-persistence/src/main/java/akka/persistence/serialization/MessageFormats.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2019-2020 Lightbend Inc. + * Copyright (C) 2020 Lightbend Inc. */ // Generated by the protocol buffer compiler. DO NOT EDIT! diff --git a/akka-pki/src/main/scala/akka/pki/pem/DERPrivateKeyLoader.scala b/akka-pki/src/main/scala/akka/pki/pem/DERPrivateKeyLoader.scala new file mode 100644 index 0000000000..68f3f58b3e --- /dev/null +++ b/akka-pki/src/main/scala/akka/pki/pem/DERPrivateKeyLoader.scala @@ -0,0 +1,130 @@ +/* + * Copyright (C) 2020 Lightbend Inc. + */ + +package akka.pki.pem + +import java.math.BigInteger +import java.security.KeyFactory +import java.security.PrivateKey +import java.security.spec.PKCS8EncodedKeySpec +import java.security.spec.RSAMultiPrimePrivateCrtKeySpec +import java.security.spec.RSAOtherPrimeInfo +import java.security.spec.RSAPrivateCrtKeySpec + +import akka.annotation.ApiMayChange +import akka.pki.pem.PEMDecoder.DERData +import com.hierynomus.asn1.ASN1InputStream +import com.hierynomus.asn1.encodingrules.der.DERDecoder +import com.hierynomus.asn1.types.constructed.ASN1Sequence +import com.hierynomus.asn1.types.primitive.ASN1Integer + +final class PEMLoadingException(message: String, cause: Throwable) extends RuntimeException(message, cause) { + def this(msg: String) = this(msg, null) +} + +object DERPrivateKeyLoader { + + /** + * Converts the DER payload in [[PEMDecoder.DERData]] into a [[java.security.PrivateKey]]. The received DER + * data must be a valid PKCS#1 (identified in PEM as "RSA PRIVATE KEY") or non-ecnrypted PKCS#8 (identified + * in PEM as "PRIVATE KEY"). + * @throws PEMLoadingException when the `derData` is for an unsupported format + */ + @ApiMayChange + @throws[PEMLoadingException]("when the `derData` is for an unsupported format") + def load(derData: DERData): PrivateKey = { + derData.label match { + case "RSA PRIVATE KEY" => + loadPkcs1PrivateKey(derData.bytes) + case "PRIVATE KEY" => + loadPkcs8PrivateKey(derData.bytes) + case unknown => + throw new PEMLoadingException(s"Don't know how to read a private key from PEM data with label [$unknown]") + } + } + + private def loadPkcs1PrivateKey(bytes: Array[Byte]) = { + val derInputStream = new ASN1InputStream(new DERDecoder, bytes) + // Here's the specification: https://tools.ietf.org/html/rfc3447#appendix-A.1.2 + val sequence = { + try { + derInputStream.readObject[ASN1Sequence]() + } finally { + derInputStream.close() + } + } + val version = getInteger(sequence, 0, "version").intValueExact() + if (version < 0 || version > 1) { + throw new IllegalArgumentException(s"Unsupported PKCS1 version: $version") + } + val modulus = getInteger(sequence, 1, "modulus") + val publicExponent = getInteger(sequence, 2, "publicExponent") + val privateExponent = getInteger(sequence, 3, "privateExponent") + val prime1 = getInteger(sequence, 4, "prime1") + val prime2 = getInteger(sequence, 5, "prime2") + val exponent1 = getInteger(sequence, 6, "exponent1") + val exponent2 = getInteger(sequence, 7, "exponent2") + val coefficient = getInteger(sequence, 8, "coefficient") + + val keySpec = if (version == 0) { + new RSAPrivateCrtKeySpec( + modulus, + publicExponent, + privateExponent, + prime1, + prime2, + exponent1, + exponent2, + coefficient) + } else { + // Does anyone even use multi-primes? Who knows, maybe this code will never be used. Anyway, I guess it will work, + // the spec isn't exactly complicated. + val otherPrimeInfosSequence = getSequence(sequence, 9, "otherPrimeInfos") + val otherPrimeInfos = (for (i <- 0 until otherPrimeInfosSequence.size()) yield { + val name = s"otherPrimeInfos[$i]" + val seq = getSequence(otherPrimeInfosSequence, i, name) + val prime = getInteger(seq, 0, s"$name.prime") + val exponent = getInteger(seq, 1, s"$name.exponent") + val coefficient = getInteger(seq, 2, s"$name.coefficient") + new RSAOtherPrimeInfo(prime, exponent, coefficient) + }).toArray + new RSAMultiPrimePrivateCrtKeySpec( + modulus, + publicExponent, + privateExponent, + prime1, + prime2, + exponent1, + exponent2, + coefficient, + otherPrimeInfos) + } + + val keyFactory = KeyFactory.getInstance("RSA") + keyFactory.generatePrivate(keySpec) + } + + private def getInteger(sequence: ASN1Sequence, index: Int, name: String): BigInteger = { + sequence.get(index) match { + case integer: ASN1Integer => integer.getValue + case other => + throw new IllegalArgumentException(s"Expected integer tag for $name at index $index, but got: ${other.getTag}") + } + } + + private def getSequence(sequence: ASN1Sequence, index: Int, name: String): ASN1Sequence = { + sequence.get(index) match { + case seq: ASN1Sequence => seq + case other => + throw new IllegalArgumentException(s"Expected sequence tag for $name at index $index, but got: ${other.getTag}") + } + } + + private def loadPkcs8PrivateKey(bytes: Array[Byte]) = { + val keySpec = new PKCS8EncodedKeySpec(bytes) + val keyFactory = KeyFactory.getInstance("RSA") + keyFactory.generatePrivate(keySpec) + } + +} diff --git a/akka-pki/src/main/scala/akka/pki/pem/PEMDecoder.scala b/akka-pki/src/main/scala/akka/pki/pem/PEMDecoder.scala new file mode 100644 index 0000000000..1f6023a9ed --- /dev/null +++ b/akka-pki/src/main/scala/akka/pki/pem/PEMDecoder.scala @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2020 Lightbend Inc. + */ + +package akka.pki.pem + +import java.util.Base64 + +import akka.annotation.ApiMayChange + +/** + * Decodes lax PEM encoded data, according to + * + * https://tools.ietf.org/html/rfc7468 + */ +object PEMDecoder { + + // I believe this regex matches the RFC7468 Lax ABNF semantics jkhdft exactly. + private val PEMRegex = { + // Luckily, Java Pattern's \s matches the RFCs W ABNF expression perfectly + // (space, tab, carriage return, line feed, form feed, vertical tab) + + // The variables here are named to match the expressions in the RFC7468 ABNF + // description. The content of the regex may not match the structure of the + // expression because sometimes there are nicer way to do things in regexes. + + // All printable ASCII characters minus hyphen + val labelchar = """[\p{Print}&&[^-]]""" + // Starts and finishes with a labelchar, with as many label chars and hyphens or + // spaces in between, but no double spaces or hyphens, also may be empty. + val label = raw"""(?:$labelchar(?:[\- ]?$labelchar)*)?""" + // capturing group so we can extract the label + val preeb = raw"""-----BEGIN ($label)-----""" + // we don't extract the end label because the RFC says we can ignore it (it + // doesn't have to match the begin label) + val posteb = raw"""-----END $label-----""" + // Any of the base64 chars (alphanum, +, /) and whitespace, followed by at most 2 + // padding characters, separated by zero to many whitespace characters + val laxbase64text = """[A-Za-z0-9\+/\s]*(?:=\s*){0,2}""" + + val laxtextualmessage = raw"""\s*$preeb($laxbase64text)$posteb\s*""" + + laxtextualmessage.r + } + + /** + * Decodes a PEM String into an identifier and the DER bytes of the content. + * + * See https://tools.ietf.org/html/rfc7468 and https://en.wikipedia.org/wiki/Privacy-Enhanced_Mail + * + * @param pemData the PEM data (pre-eb, base64-MIME data and ponst-eb) + * @return the decoded bytes and the content type. + */ + @throws[PEMLoadingException]( + "If the `pemData` is not valid PEM format (according to https://tools.ietf.org/html/rfc7468).") + @ApiMayChange + def decode(pemData: String): DERData = { + pemData match { + case PEMRegex(label, base64) => + try { + new DERData(label, Base64.getMimeDecoder.decode(base64)) + } catch { + case iae: IllegalArgumentException => + throw new PEMLoadingException( + s"Error decoding base64 data from PEM data (note: expected MIME-formatted Base64)", + iae) + } + + case _ => throw new PEMLoadingException("Not a PEM encoded data.") + } + } + + @ApiMayChange + final class DERData(val label: String, val bytes: Array[Byte]) + +} diff --git a/akka-pki/src/test/resources/certificate.pem b/akka-pki/src/test/resources/certificate.pem new file mode 100644 index 0000000000..4fb0ea7b86 --- /dev/null +++ b/akka-pki/src/test/resources/certificate.pem @@ -0,0 +1,19 @@ +-----BEGIN CERTIFICATE----- +MIIDCzCCAfOgAwIBAgIQfEHPfR1p1xuW9TQlfxAugjANBgkqhkiG9w0BAQsFADAv +MS0wKwYDVQQDEyQwZDIwN2I2OC05YTIwLTRlZTgtOTJjYi1iZjk2OTk1ODFjZjgw +HhcNMTkxMDExMTMyODUzWhcNMjQxMDA5MTQyODUzWjAvMS0wKwYDVQQDEyQwZDIw +N2I2OC05YTIwLTRlZTgtOTJjYi1iZjk2OTk1ODFjZjgwggEiMA0GCSqGSIb3DQEB +AQUAA4IBDwAwggEKAoIBAQDhD0BxlDzEOzcp45lPHL60lnM6k3puEGb2lKHL5/nR +F94FCnZL0FH8EdxWzzAYgys+kUwSdo4QMuWuvjY2Km4Wob6k4uAeYEFTCfBdi4/z +r4kpWzu8xLz+uZWimLQrjqVytNNK3DMv6ebWUJ/92VTDS4yzWk4YV0MVr2b2OgMK +SgMvaFQ8L/CwyML72PBWIqU67+MMvvcTLxQdyEgQTTjP0bbiXMLDvfZDarLJojsW +SNBz7AIkznhGkzIGGdhAa41PnPu9XaBFhaqx9Qe3+MG2/k1l/46eHtmxCqhOUde1 +i0vy6ZfgcGifua1tg1UBI/oT4S0dsq24dq7K1MYLyHTrAgMBAAGjIzAhMA4GA1Ud +DwEB/wQEAwICBDAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQAa +5YOlvob4wqps3HLaOIi7VzDihNHciP+BI0mzxHa7D3bGaecRPSeG3xEoD/Uxs9o4 +8cByPpYx1Wl8LLRx14wDcK0H+UPpo4gCI6h6q92cJj0YRjcSUUt8EIu3qnFMjtM+ +sl/uc21fGlq6cvMRZXqtVYENoSNTDHi5a5eEXRa2eZ8XntjvOanOhIKWmxvr8r4a +Voz4WdnXx1C8/BzB62UBoMu4QqMGMLk5wXP0D6hECUuespMest+BeoJAVhTq7wZs +rSP9q08n1stZFF4+bEBaxcqIqdhOLQdHcYELN+a5v0Mcwdsy7jJMagmNPfsKoOKC +hLOsmNYKHdmWg37Jib5o +-----END CERTIFICATE----- \ No newline at end of file diff --git a/akka-pki/src/test/resources/multi-prime-pkcs1.pem b/akka-pki/src/test/resources/multi-prime-pkcs1.pem new file mode 100644 index 0000000000..2ad888e5b6 --- /dev/null +++ b/akka-pki/src/test/resources/multi-prime-pkcs1.pem @@ -0,0 +1,28 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIE2AIBAQKCAQEAqFD3rwpvkqgkxSCIKyO2M///6CnJz/YqYycSxeUXJSeC+sf0 +Svu4mzSNyx9mglH6ubVJ1x01XlK7GDCAOuqi7Dgay23m+qRq+MF89gYZY9YuVPBr +jsFPK3XguIOLTIV2VCdskBLbW0G6b6VVDOVLCffD6fRuy/H43BI2l9+nuTAdYpCE +sX7IliRi2HR09nv1THSMrdrcq48EdLK+Qzj3f+9gedqeJP7ABY29eIBAOasUY356 +So/2dqwhfmMbXswHqIFVQIBd+y1F4Gwf4HxTN06bHhs5rOg4fAREmr1SU25CnhDA +SBw8c5zRFNGgekkzIxSZLFw27ezYGxZyIjnKawIDAQABAoIBAFrcKniRV52BqyfG +4fr3sjnr7gcz17+tkUApLZcqjg3+gFREcHmx3PvbqNeHwdyDyKdLV+sJ129tlZX/ +SJmFZCHEP6KlV1TiQOS7/msI69fbHPO5PTaCjTzkbA4WOAgv+M4XjR82RM1EusO1 +tPegWfLhj5dvdAfgTpodW1XDFs3QHoQbKkLLMREOb3j+LuK38npxN1UmtDNSRlII +xv77KywOij0LMG4CjIeXmjGdL9BWzlbHv5Zk0wuWHtGFvkoQO6EPn3NVb1uY1ZdX +IekdvA71qssliVFi30/3ZiFTwt3fXfNBawN7t1s0pJlsPAOz7iiucHWrUCvtaXD+ +miFFN4ECVgez3BQqe6o3lmHG7LQKy/RWBfDXLCc1JziipTeu+e1piNd7WnJcpsbl +31kTA3JBp7VU41EEXEBtWz8cW3I9y6AhukAyeWBnww9FSet8bKGAuYj1siCXAlYH +L+pEeS8NuGO1iigEEZzZrOMC3Yg9/evwhjXxbtwst9NewE22UdJGB7tm/2v7osaE +/DVQk4xrNe/AEDRcQf73XXEEq/wsoJjHp/Be8lvH6bLHpx0rBQJWA6Lxy3+HJQTb +dcwx4oqaYf/fDCa6TMR2gPGg6RouGt1RYtJ1EDsX7h2HBgnI/b9ri1vJem2BlFVM +2A3uSpBMO4zQ3xA1Z8N1PxdqX8g9jT412iiNcX8CVWC3atQH8iuwKhnSEqyuVw7s +e/kTMVcIsQMNENzn+/3JwaBQNXZFnGLJqhdhIMAkuVbcopiu9+/E4462gecCNRSX +XUyBHANxJIWicAtDYdXaDP6u3NUCVgSPOp+63cxSzMo7FLsYIAFH14VFEXKaw9s9 +JbMI4+dIUMfgs2fvCK6ibFpIrhESv4kaQ+uRjkTjyGv+OkKvP5jxoDUjKRA4WcLU +OHH2wrajfrFJ0sfhMIIBDDCCAQgCVgMKWFv+kAwf55zhJjiP/+dSyL7wnEFw5gcw +F8tBg1M7LR4BgTn+j/p/8qEg4scL0eDdDcxGNPaMNlYAtFjO56Qv/oAHJ7EdwfYR +knMUDxYZV2LU+aGpAlYCMhBIrnWbK9b3xOby5ZnolDF/IQXVhA+4lRQ5pR+OlScp +ifClzpxuSsMNdFAPaQuwlDEImJJakDoUtQGHODKysC3ailAxaMnORjY5f/y8+qPO +LPnvsQJWAYm7meCYk9a89TGWQfFl4l1W1dYlI/4FDYOirOvFd+/LICMk+9E43Qff +lqeAL45/QoXNEDpSVNy507ggtBbxqcEehjspYGqp76NRTvRMKgK9/XH4u2E= +-----END RSA PRIVATE KEY----- \ No newline at end of file diff --git a/akka-pki/src/test/resources/pkcs1.pem b/akka-pki/src/test/resources/pkcs1.pem new file mode 100644 index 0000000000..5f23b81867 --- /dev/null +++ b/akka-pki/src/test/resources/pkcs1.pem @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpQIBAAKCAQEA0IQFs9KpS6fpm7Bq5JcAzRzdZnYub7qGBJ9+QZX8F6qUYiXf +jbVPAZSIksNg6G5yMkzBLZ8r0UOm5WJs6sAHKGJOQk9DcwZEt3XpGbYXAnM5V1sb +xd5oNcbXLcHouU8jrEj+O2KvmgCzDDCUOf3SjGnF4dWqhsTT9tMJZvWVB0OjpKnT +zcoxFKO/XCz8Spb1+FgKUgt3afA+JTRpQWtaZJ41fuTg0rm0qtUSeZXPUEYkqqK4 +msoSe7dbu7uiEOkUPoeiP7wzSrwihQSCxOzwdphV2XtF5Xfs24/Ad4WKXTqRVUK/ +yldcQy2orFSsX41KBzS8PI1hnOC6uRA0PiGd/QIDAQABAoIBAQCDbxShGuK326m3 +B2b5m+1XXSB5m3j92FbtxxMwiDgVOuK5UyItEuIwHs5PpHQLTsMQzaze8vwNtlUX +Ngltl4lrfTvTNF9Ru9vIwLwkBtFOLA8y7yz8doq9iw7LuvTVCft0d7Y4/KWvr00t +G9nzC/mRpIKlLaeFt7/cT34XtikwH+Ez8uWGidYnrqkKZ0bsIZvD+e7gae+veohN +BnTcyDIk8P5nXG0vM4hxtjLo3KstemwOt6vCtiKzL2Vq/JAVD3nlec8WPBzft79I +k5tb3Qm/OnxIQaWF5AhAVkXgMsLL3ddJoAn/K6NZ6NtRGZozkwdP+m4nacrKFJVJ +6ew7OdAJAoGBAO65GvteHLXQ3d1NfU+X6RSBatJdpuf2S4Hc7P+sTkPjLDlQPDKL +ZFLVigPlCehMvgGASPOxL8UqPZugwEo83ywp5t/iOUccXZCPSISreSzWJPfOJ+dl +aKP2cSHHPNC/mISDpp/NF+xfgEAUQQ6AdOKwHGlsFWBvkkw810d4zmXvAoGBAN+b +QYv32wNa2IHC1Ri3VgtfHpQ20fMz+UO6TMefBbgkcgBk4L+O7gSCpThaqy9R7UnX +IEH0QyaBEXzQxB7qvCo1pczWiEUcG+vAyyz9U9oO9Hee/UTMOWL4Sj7qoavau2Be +5PFOO6qA+19JTnStuNb3swNrMmxDQpyNDvUkYAbTAoGBALuYkSCJ84vZaBBZrajX +mt13WieYWuocPXf+0euVTyfAJOehKr0ZlywVDNFEssVvUT1Cv5FpYz3QlPtwlsuA +DGzbPMghMZu1Kb3JK1a+nYnjeseVpPwNT+7RYlQGCr+MYOF5x336oNsqrVEt2XX4 +8mGVva4GtsHCy7fHc/GBeMjXAoGBALhEYkytER//okG0xBUdKFwwo6tyTavEndpx +UUqDwpvP9N5cQ1W4vG6dFviMx1s0gX4DOQMA/sFhRX79L1FnEW8bTKmz9RI2qs+p +zgUiMhKVlmJpc79ZKMVlZRHaGybbFuTA7pvoY4ULy5rndy7x5kvITg44LZJID0Gh +gL0Fn9ifAoGAEaWA7yxTA7phDIt0U91HZEVhNSM4cZDurE7614qWhKveSP8f0J4c +3d9y/re4RcCwmss/FtQWSkB+32WbD3qk+QB7hV1oFqJ5ObcfwevGYR8m6vOz1h2L +3pQNi4PcH3U8eeGG1laFKUQ295rBLNqIbOo2y+4hxMnC4tka1X118Ec= +-----END RSA PRIVATE KEY----- \ No newline at end of file diff --git a/akka-pki/src/test/resources/pkcs8.pem b/akka-pki/src/test/resources/pkcs8.pem new file mode 100644 index 0000000000..bd63aa1dc0 --- /dev/null +++ b/akka-pki/src/test/resources/pkcs8.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDQhAWz0qlLp+mb +sGrklwDNHN1mdi5vuoYEn35BlfwXqpRiJd+NtU8BlIiSw2DobnIyTMEtnyvRQ6bl +YmzqwAcoYk5CT0NzBkS3dekZthcCczlXWxvF3mg1xtctwei5TyOsSP47Yq+aALMM +MJQ5/dKMacXh1aqGxNP20wlm9ZUHQ6OkqdPNyjEUo79cLPxKlvX4WApSC3dp8D4l +NGlBa1pknjV+5ODSubSq1RJ5lc9QRiSqoriayhJ7t1u7u6IQ6RQ+h6I/vDNKvCKF +BILE7PB2mFXZe0Xld+zbj8B3hYpdOpFVQr/KV1xDLaisVKxfjUoHNLw8jWGc4Lq5 +EDQ+IZ39AgMBAAECggEBAINvFKEa4rfbqbcHZvmb7VddIHmbeP3YVu3HEzCIOBU6 +4rlTIi0S4jAezk+kdAtOwxDNrN7y/A22VRc2CW2XiWt9O9M0X1G728jAvCQG0U4s +DzLvLPx2ir2LDsu69NUJ+3R3tjj8pa+vTS0b2fML+ZGkgqUtp4W3v9xPfhe2KTAf +4TPy5YaJ1ieuqQpnRuwhm8P57uBp7696iE0GdNzIMiTw/mdcbS8ziHG2Mujcqy16 +bA63q8K2IrMvZWr8kBUPeeV5zxY8HN+3v0iTm1vdCb86fEhBpYXkCEBWReAywsvd +10mgCf8ro1no21EZmjOTB0/6bidpysoUlUnp7Ds50AkCgYEA7rka+14ctdDd3U19 +T5fpFIFq0l2m5/ZLgdzs/6xOQ+MsOVA8MotkUtWKA+UJ6Ey+AYBI87EvxSo9m6DA +SjzfLCnm3+I5RxxdkI9IhKt5LNYk984n52Voo/ZxIcc80L+YhIOmn80X7F+AQBRB +DoB04rAcaWwVYG+STDzXR3jOZe8CgYEA35tBi/fbA1rYgcLVGLdWC18elDbR8zP5 +Q7pMx58FuCRyAGTgv47uBIKlOFqrL1HtSdcgQfRDJoERfNDEHuq8KjWlzNaIRRwb +68DLLP1T2g70d579RMw5YvhKPuqhq9q7YF7k8U47qoD7X0lOdK241vezA2sybENC +nI0O9SRgBtMCgYEAu5iRIInzi9loEFmtqNea3XdaJ5ha6hw9d/7R65VPJ8Ak56Eq +vRmXLBUM0USyxW9RPUK/kWljPdCU+3CWy4AMbNs8yCExm7UpvckrVr6dieN6x5Wk +/A1P7tFiVAYKv4xg4XnHffqg2yqtUS3ZdfjyYZW9rga2wcLLt8dz8YF4yNcCgYEA +uERiTK0RH/+iQbTEFR0oXDCjq3JNq8Sd2nFRSoPCm8/03lxDVbi8bp0W+IzHWzSB +fgM5AwD+wWFFfv0vUWcRbxtMqbP1Ejaqz6nOBSIyEpWWYmlzv1koxWVlEdobJtsW +5MDum+hjhQvLmud3LvHmS8hODjgtkkgPQaGAvQWf2J8CgYARpYDvLFMDumEMi3RT +3UdkRWE1IzhxkO6sTvrXipaEq95I/x/Qnhzd33L+t7hFwLCayz8W1BZKQH7fZZsP +eqT5AHuFXWgWonk5tx/B68ZhHybq87PWHYvelA2Lg9wfdTx54YbWVoUpRDb3msEs +2ohs6jbL7iHEycLi2RrVfXXwRw== +-----END PRIVATE KEY----- diff --git a/akka-pki/src/test/scala/akka/pki/pem/DERPrivateKeyLoaderSpec.scala b/akka-pki/src/test/scala/akka/pki/pem/DERPrivateKeyLoaderSpec.scala new file mode 100644 index 0000000000..c10eb11930 --- /dev/null +++ b/akka-pki/src/test/scala/akka/pki/pem/DERPrivateKeyLoaderSpec.scala @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2020 Lightbend Inc. + */ + +package akka.pki.pem + +import java.io.File +import java.nio.charset.Charset +import java.nio.file.Files +import java.security.PrivateKey + +import org.scalatest.EitherValues +import org.scalatest.matchers.should.Matchers +import org.scalatest.wordspec.AnyWordSpec + +class DERPrivateKeyLoaderSpec extends AnyWordSpec with Matchers with EitherValues { + + "The DER Private Key loader" should { + "decode the same key in PKCS#1 and PKCS#8 formats" in { + val pkcs1 = load("pkcs1.pem") + val pkcs8 = load("pkcs8.pem") + pkcs1 should ===(pkcs8) + } + + "parse multi primes" in { + load("multi-prime-pkcs1.pem") + // Not much we can verify here - I actually think the default JDK security implementation ignores the extra + // primes, and it fails to parse a multi-prime PKCS#8 key. + } + + "fail on unsupported PEM contents (Certificates are not private keys)" in { + assertThrows[PEMLoadingException] { + load("certificate.pem") + } + } + + } + + private def load(resource: String): PrivateKey = { + val derData: PEMDecoder.DERData = loadDerData(resource) + DERPrivateKeyLoader.load(derData) + } + + private def loadDerData(resource: String) = { + val resourceUrl = getClass.getClassLoader.getResource(resource) + resourceUrl.getProtocol should ===("file") + val path = new File(resourceUrl.toURI).toPath + val bytes = Files.readAllBytes(path) + val str = new String(bytes, Charset.forName("UTF-8")) + val derData = PEMDecoder.decode(str) + derData + } + +} diff --git a/akka-pki/src/test/scala/akka/pki/pem/PEMDecoderSpec.scala b/akka-pki/src/test/scala/akka/pki/pem/PEMDecoderSpec.scala new file mode 100644 index 0000000000..6716e782e0 --- /dev/null +++ b/akka-pki/src/test/scala/akka/pki/pem/PEMDecoderSpec.scala @@ -0,0 +1,93 @@ +/* + * Copyright (C) 2020 Lightbend Inc. + */ + +package akka.pki.pem + +import java.util.Base64 + +import org.scalatest.EitherValues +import org.scalatest.matchers.should.Matchers +import org.scalatest.wordspec.AnyWordSpec + +class PEMDecoderSpec extends AnyWordSpec with Matchers with EitherValues { + + private val cert = + """-----BEGIN CERTIFICATE----- + |MIIDCzCCAfOgAwIBAgIQfEHPfR1p1xuW9TQlfxAugjANBgkqhkiG9w0BAQsFADAv + |MS0wKwYDVQQDEyQwZDIwN2I2OC05YTIwLTRlZTgtOTJjYi1iZjk2OTk1ODFjZjgw + |HhcNMTkxMDExMTMyODUzWhcNMjQxMDA5MTQyODUzWjAvMS0wKwYDVQQDEyQwZDIw + |N2I2OC05YTIwLTRlZTgtOTJjYi1iZjk2OTk1ODFjZjgwggEiMA0GCSqGSIb3DQEB + |AQUAA4IBDwAwggEKAoIBAQDhD0BxlDzEOzcp45lPHL60lnM6k3puEGb2lKHL5/nR + |F94FCnZL0FH8EdxWzzAYgys+kUwSdo4QMuWuvjY2Km4Wob6k4uAeYEFTCfBdi4/z + |r4kpWzu8xLz+uZWimLQrjqVytNNK3DMv6ebWUJ/92VTDS4yzWk4YV0MVr2b2OgMK + |SgMvaFQ8L/CwyML72PBWIqU67+MMvvcTLxQdyEgQTTjP0bbiXMLDvfZDarLJojsW + |SNBz7AIkznhGkzIGGdhAa41PnPu9XaBFhaqx9Qe3+MG2/k1l/46eHtmxCqhOUde1 + |i0vy6ZfgcGifua1tg1UBI/oT4S0dsq24dq7K1MYLyHTrAgMBAAGjIzAhMA4GA1Ud + |DwEB/wQEAwICBDAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQAa + |5YOlvob4wqps3HLaOIi7VzDihNHciP+BI0mzxHa7D3bGaecRPSeG3xEoD/Uxs9o4 + |8cByPpYx1Wl8LLRx14wDcK0H+UPpo4gCI6h6q92cJj0YRjcSUUt8EIu3qnFMjtM+ + |sl/uc21fGlq6cvMRZXqtVYENoSNTDHi5a5eEXRa2eZ8XntjvOanOhIKWmxvr8r4a + |Voz4WdnXx1C8/BzB62UBoMu4QqMGMLk5wXP0D6hECUuespMest+BeoJAVhTq7wZs + |rSP9q08n1stZFF4+bEBaxcqIqdhOLQdHcYELN+a5v0Mcwdsy7jJMagmNPfsKoOKC + |hLOsmNYKHdmWg37Jib5o + |-----END CERTIFICATE-----""".stripMargin + + "The PEM decoder" should { + "decode a real world certificate" in { + PEMDecoder.decode(cert).label should ===("CERTIFICATE") + } + + "decode data with no spaces" in { + val result = PEMDecoder.decode( + "-----BEGIN FOO-----" + Base64.getEncoder.encodeToString("abc".getBytes()) + "-----END FOO-----") + result.label should ===("FOO") + new String(result.bytes) should ===("abc") + } + + "decode data with lots of spaces" in { + val result = PEMDecoder.decode( + "\n \t \r -----BEGIN FOO-----\n" + + Base64.getEncoder.encodeToString("abc".getBytes()).flatMap(c => s"$c\n\r \t\n") + "-----END FOO-----\n \t \r ") + result.label should ===("FOO") + new String(result.bytes) should ===("abc") + } + + "decode data with two padding characters" in { + // A 4 byte input results in a 6 character output with 2 padding characters + val result = PEMDecoder.decode( + "-----BEGIN FOO-----" + Base64.getEncoder.encodeToString("abcd".getBytes()) + "-----END FOO-----") + result.label should ===("FOO") + new String(result.bytes) should ===("abcd") + } + + "decode data with one padding character" in { + // A 5 byte input results in a 7 character output with 1 padding character1 + val result = PEMDecoder.decode( + "-----BEGIN FOO-----" + Base64.getEncoder.encodeToString("abcde".getBytes()) + "-----END FOO-----") + result.label should ===("FOO") + new String(result.bytes) should ===("abcde") + } + + "fail decode when the format is wrong (not MIME BASE64, lines too long)" in { + val input = """-----BEGIN CERTIFICATE----- + |MIIDCzCCAfOgAwIBAgIQfEHPfR1p1xuW9TQlfxAugjANBgkqhkiG9w0BAQsFADAviZjk2OTk1ODFjZjgw + |HhcNMTkxMDExMTMyODUzWhcNMjQxMDA5MTQyODUzWjAvMS0wKwYDVQQDEyQwZDIwhLOsmNYKHdmWg37Jib5o + |-----END CERTIFICATE-----""".stripMargin + + assertThrows[PEMLoadingException] { + PEMDecoder.decode(input) + } + } + + "fail decode when the format is wrong (not PEM, invalid per/post-EB)" in { + val input = cert.replace("BEGIN", "BGN").replace("END ", "GLGLGL ") + + assertThrows[PEMLoadingException] { + PEMDecoder.decode(input) + } + } + + } + +} diff --git a/akka-remote-tests/src/test/java/akka/remote/artery/protobuf/TestMessages.java b/akka-remote-tests/src/test/java/akka/remote/artery/protobuf/TestMessages.java index 25542e4f19..408015da8a 100644 --- a/akka-remote-tests/src/test/java/akka/remote/artery/protobuf/TestMessages.java +++ b/akka-remote-tests/src/test/java/akka/remote/artery/protobuf/TestMessages.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2019-2020 Lightbend Inc. + * Copyright (C) 2020 Lightbend Inc. */ // Generated by the protocol buffer compiler. DO NOT EDIT! diff --git a/akka-remote/src/main/java/akka/remote/ArteryControlFormats.java b/akka-remote/src/main/java/akka/remote/ArteryControlFormats.java index 7122dbc0e9..4fff204269 100644 --- a/akka-remote/src/main/java/akka/remote/ArteryControlFormats.java +++ b/akka-remote/src/main/java/akka/remote/ArteryControlFormats.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2019-2020 Lightbend Inc. + * Copyright (C) 2020 Lightbend Inc. */ // Generated by the protocol buffer compiler. DO NOT EDIT! diff --git a/akka-remote/src/main/java/akka/remote/ContainerFormats.java b/akka-remote/src/main/java/akka/remote/ContainerFormats.java index 5bae17b263..c15a92afbe 100644 --- a/akka-remote/src/main/java/akka/remote/ContainerFormats.java +++ b/akka-remote/src/main/java/akka/remote/ContainerFormats.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2019-2020 Lightbend Inc. + * Copyright (C) 2020 Lightbend Inc. */ // Generated by the protocol buffer compiler. DO NOT EDIT! diff --git a/akka-remote/src/main/java/akka/remote/SystemMessageFormats.java b/akka-remote/src/main/java/akka/remote/SystemMessageFormats.java index 53a327d4ad..b810dc83db 100644 --- a/akka-remote/src/main/java/akka/remote/SystemMessageFormats.java +++ b/akka-remote/src/main/java/akka/remote/SystemMessageFormats.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2019-2020 Lightbend Inc. + * Copyright (C) 2020 Lightbend Inc. */ // Generated by the protocol buffer compiler. DO NOT EDIT! diff --git a/akka-remote/src/main/java/akka/remote/WireFormats.java b/akka-remote/src/main/java/akka/remote/WireFormats.java index 87237aa035..1104a931ee 100644 --- a/akka-remote/src/main/java/akka/remote/WireFormats.java +++ b/akka-remote/src/main/java/akka/remote/WireFormats.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2019-2020 Lightbend Inc. + * Copyright (C) 2020 Lightbend Inc. */ // Generated by the protocol buffer compiler. DO NOT EDIT! @@ -7671,19 +7671,25 @@ public final class WireFormats { /** * repeated string tags = 12; + * @return A list containing the tags. */ java.util.List getTagsList(); /** * repeated string tags = 12; + * @return The count of tags. */ int getTagsCount(); /** * repeated string tags = 12; + * @param index The index of the element to return. + * @return The tags at the given index. */ java.lang.String getTags(int index); /** * repeated string tags = 12; + * @param index The index of the value to return. + * @return The bytes of the tags at the given index. */ akka.protobufv3.internal.ByteString getTagsBytes(int index); @@ -8194,6 +8200,7 @@ public final class WireFormats { private akka.protobufv3.internal.LazyStringList tags_; /** * repeated string tags = 12; + * @return A list containing the tags. */ public akka.protobufv3.internal.ProtocolStringList getTagsList() { @@ -8201,18 +8208,23 @@ public final class WireFormats { } /** * repeated string tags = 12; + * @return The count of tags. */ public int getTagsCount() { return tags_.size(); } /** * repeated string tags = 12; + * @param index The index of the element to return. + * @return The tags at the given index. */ public java.lang.String getTags(int index) { return tags_.get(index); } /** * repeated string tags = 12; + * @param index The index of the value to return. + * @return The bytes of the tags at the given index. */ public akka.protobufv3.internal.ByteString getTagsBytes(int index) { @@ -9514,6 +9526,7 @@ public final class WireFormats { } /** * repeated string tags = 12; + * @return A list containing the tags. */ public akka.protobufv3.internal.ProtocolStringList getTagsList() { @@ -9521,18 +9534,23 @@ public final class WireFormats { } /** * repeated string tags = 12; + * @return The count of tags. */ public int getTagsCount() { return tags_.size(); } /** * repeated string tags = 12; + * @param index The index of the element to return. + * @return The tags at the given index. */ public java.lang.String getTags(int index) { return tags_.get(index); } /** * repeated string tags = 12; + * @param index The index of the value to return. + * @return The bytes of the tags at the given index. */ public akka.protobufv3.internal.ByteString getTagsBytes(int index) { @@ -9540,6 +9558,9 @@ public final class WireFormats { } /** * repeated string tags = 12; + * @param index The index to set the value at. + * @param value The tags to set. + * @return This builder for chaining. */ public Builder setTags( int index, java.lang.String value) { @@ -9553,6 +9574,8 @@ public final class WireFormats { } /** * repeated string tags = 12; + * @param value The tags to add. + * @return This builder for chaining. */ public Builder addTags( java.lang.String value) { @@ -9566,6 +9589,8 @@ public final class WireFormats { } /** * repeated string tags = 12; + * @param values The tags to add. + * @return This builder for chaining. */ public Builder addAllTags( java.lang.Iterable values) { @@ -9577,6 +9602,7 @@ public final class WireFormats { } /** * repeated string tags = 12; + * @return This builder for chaining. */ public Builder clearTags() { tags_ = akka.protobufv3.internal.LazyStringArrayList.EMPTY; @@ -9586,6 +9612,8 @@ public final class WireFormats { } /** * repeated string tags = 12; + * @param value The bytes of the tags to add. + * @return This builder for chaining. */ public Builder addTagsBytes( akka.protobufv3.internal.ByteString value) { diff --git a/akka-remote/src/main/resources/reference.conf b/akka-remote/src/main/resources/reference.conf index cc3afecc21..8e74e151c8 100644 --- a/akka-remote/src/main/resources/reference.conf +++ b/akka-remote/src/main/resources/reference.conf @@ -661,12 +661,18 @@ akka { # https://blogs.oracle.com/java-platform-group/entry/java_8_will_use_tls protocol = "TLSv1.2" - # Example: ["TLS_RSA_WITH_AES_128_CBC_SHA", "TLS_RSA_WITH_AES_256_CBC_SHA"] + # Example: ["TLS_DHE_RSA_WITH_AES_128_GCM_SHA256", + # "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", + # "TLS_DHE_RSA_WITH_AES_256_GCM_SHA384", + # "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384"] + # When doing rolling upgrades, make sure to include both the algorithm used + # by old nodes and the preferred algorithm. # If you use a JDK 8 prior to 8u161 you need to install # the JCE Unlimited Strength Jurisdiction Policy Files to use AES 256. # More info here: # https://www.oracle.com/java/technologies/javase-jce-all-downloads.html - enabled-algorithms = ["TLS_RSA_WITH_AES_128_CBC_SHA"] + enabled-algorithms = ["TLS_RSA_WITH_AES_128_CBC_SHA", + "TLS_DHE_RSA_WITH_AES_256_GCM_SHA384"] # There are two options, and the default SecureRandom is recommended: # "" or "SecureRandom" => (default) @@ -994,7 +1000,7 @@ akka { # Note that compression tables are "rolling" (i.e. a new table replaces the old # compression table once in a while), and this setting is only about the total number # of compressions within a single such table. - # Must be a positive natural number. + # Must be a positive natural number. Can be disabled with "off". max = 256 # interval between new table compression advertisements. @@ -1006,7 +1012,7 @@ akka { # Note that compression tables are "rolling" (i.e. a new table replaces the old # compression table once in a while), and this setting is only about the total number # of compressions within a single such table. - # Must be a positive natural number. + # Must be a positive natural number. Can be disabled with "off". max = 256 # interval between new table compression advertisements. @@ -1131,16 +1137,20 @@ akka { # Protocol to use for SSL encryption, choose from: # TLS 1.2 is available since JDK7, and default since JDK8: # https://blogs.oracle.com/java-platform-group/entry/java_8_will_use_tls - # TLS 1.3 is available since JDK 11: - # https://www.oracle.com/technetwork/java/javase/11-relnote-issues-5012449.html protocol = "TLSv1.2" - # Example: ["TLS_RSA_WITH_AES_128_CBC_SHA", "TLS_RSA_WITH_AES_256_CBC_SHA"] + # Example: ["TLS_DHE_RSA_WITH_AES_128_GCM_SHA256", + # "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", + # "TLS_DHE_RSA_WITH_AES_256_GCM_SHA384", + # "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384"] + # When doing rolling upgrades, make sure to include both the algorithm used + # by old nodes and the preferred algorithm. # If you use a JDK 8 prior to 8u161 you need to install # the JCE Unlimited Strength Jurisdiction Policy Files to use AES 256. # More info here: # https://www.oracle.com/java/technologies/javase-jce-all-downloads.html - enabled-algorithms = ["TLS_RSA_WITH_AES_128_CBC_SHA"] + enabled-algorithms = ["TLS_RSA_WITH_AES_128_CBC_SHA", + "TLS_DHE_RSA_WITH_AES_256_GCM_SHA384"] # There are two options, and the default SecureRandom is recommended: # "" or "SecureRandom" => (default) diff --git a/akka-remote/src/main/scala/akka/remote/Endpoint.scala b/akka-remote/src/main/scala/akka/remote/Endpoint.scala index 5097035922..a468d55015 100644 --- a/akka-remote/src/main/scala/akka/remote/Endpoint.scala +++ b/akka-remote/src/main/scala/akka/remote/Endpoint.scala @@ -899,9 +899,15 @@ private[remote] class EndpointWriter( remoteMetrics.logPayloadBytes(s.message, pduSize) if (pduSize > transport.maximumPayloadBytes) { - val reason = new OversizedPayloadException( - s"Discarding oversized payload sent to ${s.recipient}: max allowed size ${transport.maximumPayloadBytes} bytes, actual size of encoded ${s.message.getClass} was ${pdu.size} bytes.") - log.error(reason, "Transient association error (association remains live)") + val reasonText = + s"Discarding oversized payload sent to ${s.recipient}: max allowed size ${transport.maximumPayloadBytes} bytes, actual size of encoded ${s.message.getClass} was ${pdu.size} bytes." + log.error( + new OversizedPayloadException(reasonText), + "Transient association error (association remains live)") + extendedSystem.eventStream.publish(s.senderOption match { + case OptionVal.Some(msgSender) => Dropped(s.message, reasonText, msgSender, s.recipient) + case OptionVal.None => Dropped(s.message, reasonText, s.recipient) + }) true } else { val ok = h.write(pdu) diff --git a/akka-remote/src/main/scala/akka/remote/RemoteActorRefProvider.scala b/akka-remote/src/main/scala/akka/remote/RemoteActorRefProvider.scala index b83ab570d5..889c37a174 100644 --- a/akka-remote/src/main/scala/akka/remote/RemoteActorRefProvider.scala +++ b/akka-remote/src/main/scala/akka/remote/RemoteActorRefProvider.scala @@ -715,7 +715,7 @@ private[akka] class RemoteActorRef private[akka] ( else if (provider.remoteWatcher.isDefined) remote.send(message, OptionVal.None, this) else - provider.warnIfUnsafeDeathwatchWithoutCluster(watchee, watcher, "remote Watch") + provider.warnIfUnsafeDeathwatchWithoutCluster(watchee, watcher, "Watch") //Unwatch has a different signature, need to pattern match arguments against InternalActorRef case Unwatch(watchee: InternalActorRef, watcher: InternalActorRef) => diff --git a/akka-remote/src/main/scala/akka/remote/RemoteWatcher.scala b/akka-remote/src/main/scala/akka/remote/RemoteWatcher.scala index a727ef21aa..5eaf45a864 100644 --- a/akka-remote/src/main/scala/akka/remote/RemoteWatcher.scala +++ b/akka-remote/src/main/scala/akka/remote/RemoteWatcher.scala @@ -221,7 +221,7 @@ private[akka] class RemoteWatcher( // add watch from self, this will actually send a Watch to the target when necessary context.watch(watchee) - } else remoteProvider.warnIfUnsafeDeathwatchWithoutCluster(watcher, watchee, "Watch") + } else remoteProvider.warnIfUnsafeDeathwatchWithoutCluster(watchee, watcher, "Watch") } def watchNode(watchee: InternalActorRef): Unit = { @@ -250,7 +250,7 @@ private[akka] class RemoteWatcher( } case None => } - } else remoteProvider.warnIfUnsafeDeathwatchWithoutCluster(watcher, watchee, "Unwatch") + } else remoteProvider.warnIfUnsafeDeathwatchWithoutCluster(watchee, watcher, "Unwatch") } def removeWatchee(watchee: InternalActorRef): Unit = { diff --git a/akka-remote/src/main/scala/akka/remote/artery/ArterySettings.scala b/akka-remote/src/main/scala/akka/remote/artery/ArterySettings.scala index 25788a8331..9cf6e20beb 100644 --- a/akka-remote/src/main/scala/akka/remote/artery/ArterySettings.scala +++ b/akka-remote/src/main/scala/akka/remote/artery/ArterySettings.scala @@ -28,8 +28,8 @@ private[akka] final class ArterySettings private (config: Config) { def withDisabledCompression(): ArterySettings = ArterySettings(ConfigFactory.parseString("""|akka.remote.artery.advanced.compression { - | actor-refs.max = 0 - | manifests.max = 0 + | actor-refs.max = off + | manifests.max = off |}""".stripMargin).withFallback(config)) val Enabled: Boolean = getBoolean("enabled") @@ -243,21 +243,29 @@ private[akka] object ArterySettings { private[remote] final class Compression private[ArterySettings] (config: Config) { import config._ - private[akka] final val Enabled = ActorRefs.Max > 0 || Manifests.Max > 0 + private[akka] final val Enabled = ActorRefs.Enabled || Manifests.Enabled object ActorRefs { val config: Config = getConfig("actor-refs") import config._ val AdvertisementInterval: FiniteDuration = config.getMillisDuration("advertisement-interval") - val Max: Int = getInt("max") + val Max: Int = toRootLowerCase(getString("max")) match { + case "off" => 0 + case _ => getInt("max") + } + final val Enabled = Max > 0 } object Manifests { val config: Config = getConfig("manifests") import config._ val AdvertisementInterval: FiniteDuration = config.getMillisDuration("advertisement-interval") - val Max: Int = getInt("max") + val Max: Int = toRootLowerCase(getString("max")) match { + case "off" => 0 + case _ => getInt("max") + } + final val Enabled = Max > 0 } } object Compression { diff --git a/akka-remote/src/main/scala/akka/remote/artery/Codecs.scala b/akka-remote/src/main/scala/akka/remote/artery/Codecs.scala index 3a214ffeda..f505c89cf0 100644 --- a/akka-remote/src/main/scala/akka/remote/artery/Codecs.scala +++ b/akka-remote/src/main/scala/akka/remote/artery/Codecs.scala @@ -166,14 +166,26 @@ private[remote] class Encoder( Logging.messageClassName(outboundEnvelope.message)) throw e case _ if e.isInstanceOf[java.nio.BufferOverflowException] => - val reason = new OversizedPayloadException( - "Discarding oversized payload sent to " + + val reasonText = "Discarding oversized payload sent to " + s"${outboundEnvelope.recipient}: max allowed size ${envelope.byteBuffer.limit()} " + - s"bytes. Message type [${Logging.messageClassName(outboundEnvelope.message)}].") + s"bytes. Message type [${Logging.messageClassName(outboundEnvelope.message)}]." log.error( - reason, + new OversizedPayloadException(reasonText), "Failed to serialize oversized message [{}].", Logging.messageClassName(outboundEnvelope.message)) + system.eventStream.publish(outboundEnvelope.sender match { + case OptionVal.Some(msgSender) => + Dropped( + outboundEnvelope.message, + reasonText, + msgSender, + outboundEnvelope.recipient.getOrElse(ActorRef.noSender)) + case OptionVal.None => + Dropped( + outboundEnvelope.message, + reasonText, + outboundEnvelope.recipient.getOrElse(ActorRef.noSender)) + }) pull(in) case _ => log.error(e, "Failed to serialize message [{}].", Logging.messageClassName(outboundEnvelope.message)) @@ -388,16 +400,13 @@ private[remote] class Decoder( val tickDelay = 1.seconds scheduleWithFixedDelay(Tick, tickDelay, tickDelay) - if (settings.Advanced.Compression.Enabled) { - settings.Advanced.Compression.ActorRefs.AdvertisementInterval match { - case d: FiniteDuration => scheduleWithFixedDelay(AdvertiseActorRefsCompressionTable, d, d) - case _ => // not advertising actor ref compressions - } - settings.Advanced.Compression.Manifests.AdvertisementInterval match { - case d: FiniteDuration => - scheduleWithFixedDelay(AdvertiseClassManifestsCompressionTable, d, d) - case _ => // not advertising class manifest compressions - } + if (settings.Advanced.Compression.ActorRefs.Enabled) { + val d = settings.Advanced.Compression.ActorRefs.AdvertisementInterval + scheduleWithFixedDelay(AdvertiseActorRefsCompressionTable, d, d) + } + if (settings.Advanced.Compression.Manifests.Enabled) { + val d = settings.Advanced.Compression.Manifests.AdvertisementInterval + scheduleWithFixedDelay(AdvertiseClassManifestsCompressionTable, d, d) } } override def onPush(): Unit = diff --git a/akka-remote/src/main/scala/akka/remote/artery/compress/TopHeavyHitters.scala b/akka-remote/src/main/scala/akka/remote/artery/compress/TopHeavyHitters.scala index 2e62fd17b9..d829d9bf60 100644 --- a/akka-remote/src/main/scala/akka/remote/artery/compress/TopHeavyHitters.scala +++ b/akka-remote/src/main/scala/akka/remote/artery/compress/TopHeavyHitters.scala @@ -25,9 +25,12 @@ import scala.reflect.ClassTag */ private[remote] final class TopHeavyHitters[T >: Null](val max: Int)(implicit classTag: ClassTag[T]) { self => - require((max & (max - 1)) == 0, "Maximum numbers of heavy hitters should be in form of 2^k for any natural k") + private val adjustedMax = if (max == 0) 1 else max // need at least one + require( + (adjustedMax & (adjustedMax - 1)) == 0, + "Maximum numbers of heavy hitters should be in form of 2^k for any natural k") - val capacity = max * 2 + val capacity = adjustedMax * 2 val mask = capacity - 1 import TopHeavyHitters._ @@ -44,7 +47,7 @@ private[remote] final class TopHeavyHitters[T >: Null](val max: Int)(implicit cl private[this] val weights: Array[Long] = new Array(capacity) // Heap structure containing indices to slots in the hashmap - private[this] val heap: Array[Int] = Array.fill(max)(-1) + private[this] val heap: Array[Int] = Array.fill(adjustedMax)(-1) /* * Invariants (apart from heap and hashmap invariants): @@ -104,8 +107,10 @@ private[remote] final class TopHeavyHitters[T >: Null](val max: Int)(implicit cl new Iterator[T] { var i = 0 - @tailrec override final def hasNext: Boolean = + @tailrec override final def hasNext: Boolean = { + // note that this is using max and not adjustedMax so will be empty if disabled (max=0) (i < self.max) && ((value != null) || { next(); hasNext }) + } override final def next(): T = { val v = value @@ -258,7 +263,7 @@ private[remote] final class TopHeavyHitters[T >: Null](val max: Int)(implicit cl val leftIndex = index * 2 + 1 val rightIndex = index * 2 + 2 val currentWeight: Long = weights(heap(index)) - if (rightIndex < max) { + if (rightIndex < adjustedMax) { val leftValueIndex: Int = heap(leftIndex) val rightValueIndex: Int = heap(rightIndex) if (leftValueIndex < 0) { @@ -282,7 +287,7 @@ private[remote] final class TopHeavyHitters[T >: Null](val max: Int)(implicit cl } } } - } else if (leftIndex < max) { + } else if (leftIndex < adjustedMax) { val leftValueIndex: Int = heap(leftIndex) if (leftValueIndex < 0) { swapHeapNode(index, leftIndex) diff --git a/akka-remote/src/main/scala/akka/remote/artery/tcp/SSLEngineProvider.scala b/akka-remote/src/main/scala/akka/remote/artery/tcp/SSLEngineProvider.scala index 509e382117..29767e1f1d 100644 --- a/akka-remote/src/main/scala/akka/remote/artery/tcp/SSLEngineProvider.scala +++ b/akka-remote/src/main/scala/akka/remote/artery/tcp/SSLEngineProvider.scala @@ -12,10 +12,6 @@ import java.nio.file.Paths import java.security.GeneralSecurityException import java.security.KeyStore import java.security.SecureRandom - -import scala.util.Try - -import com.typesafe.config.Config import javax.net.ssl.KeyManager import javax.net.ssl.KeyManagerFactory import javax.net.ssl.SSLContext @@ -24,6 +20,10 @@ import javax.net.ssl.SSLSession import javax.net.ssl.TrustManager import javax.net.ssl.TrustManagerFactory +import scala.util.Try + +import com.typesafe.config.Config + import akka.actor.ActorSystem import akka.actor.ExtendedActorSystem import akka.actor.setup.Setup diff --git a/akka-remote/src/main/scala/akka/remote/transport/netty/SSLEngineProvider.scala b/akka-remote/src/main/scala/akka/remote/transport/netty/SSLEngineProvider.scala index 76fece3658..c00bf02224 100644 --- a/akka-remote/src/main/scala/akka/remote/transport/netty/SSLEngineProvider.scala +++ b/akka-remote/src/main/scala/akka/remote/transport/netty/SSLEngineProvider.scala @@ -11,9 +11,6 @@ import java.nio.file.Paths import java.security.GeneralSecurityException import java.security.KeyStore import java.security.SecureRandom - -import scala.util.Try - import javax.net.ssl.KeyManager import javax.net.ssl.KeyManagerFactory import javax.net.ssl.SSLContext @@ -21,6 +18,8 @@ import javax.net.ssl.SSLEngine import javax.net.ssl.TrustManager import javax.net.ssl.TrustManagerFactory +import scala.util.Try + import akka.actor.ActorSystem import akka.event.Logging import akka.event.MarkerLoggingAdapter diff --git a/akka-remote/src/test/java/akka/remote/ProtobufProtocol.java b/akka-remote/src/test/java/akka/remote/ProtobufProtocol.java index 9f9050ad42..a7b7c4fa03 100644 --- a/akka-remote/src/test/java/akka/remote/ProtobufProtocol.java +++ b/akka-remote/src/test/java/akka/remote/ProtobufProtocol.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2019-2020 Lightbend Inc. + * Copyright (C) 2020 Lightbend Inc. */ // Generated by the protocol buffer compiler. DO NOT EDIT! diff --git a/akka-remote/src/test/java/akka/remote/protobuf/v3/ProtobufProtocolV3.java b/akka-remote/src/test/java/akka/remote/protobuf/v3/ProtobufProtocolV3.java index 87ef7ef8cb..6fc6c9a302 100644 --- a/akka-remote/src/test/java/akka/remote/protobuf/v3/ProtobufProtocolV3.java +++ b/akka-remote/src/test/java/akka/remote/protobuf/v3/ProtobufProtocolV3.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2019-2020 Lightbend Inc. + * Copyright (C) 2020 Lightbend Inc. */ // Generated by the protocol buffer compiler. DO NOT EDIT! diff --git a/akka-remote/src/test/scala/akka/remote/RemoteConfigSpec.scala b/akka-remote/src/test/scala/akka/remote/RemoteConfigSpec.scala index 65fe6e2f23..321ba259a7 100644 --- a/akka-remote/src/test/scala/akka/remote/RemoteConfigSpec.scala +++ b/akka-remote/src/test/scala/akka/remote/RemoteConfigSpec.scala @@ -130,7 +130,8 @@ class RemoteConfigSpec extends AkkaSpec(""" sslSettings.SSLTrustStore should ===("truststore") sslSettings.SSLTrustStorePassword should ===("changeme") sslSettings.SSLProtocol should ===("TLSv1.2") - sslSettings.SSLEnabledAlgorithms should ===(Set("TLS_RSA_WITH_AES_128_CBC_SHA")) + sslSettings.SSLEnabledAlgorithms should ===( + Set("TLS_RSA_WITH_AES_128_CBC_SHA", "TLS_DHE_RSA_WITH_AES_256_GCM_SHA384")) sslSettings.SSLRandomNumberGenerator should ===("") } diff --git a/akka-remote/src/test/scala/akka/remote/Ticket1978CommunicationSpec.scala b/akka-remote/src/test/scala/akka/remote/Ticket1978CommunicationSpec.scala index 459be596ed..82e94275c2 100644 --- a/akka-remote/src/test/scala/akka/remote/Ticket1978CommunicationSpec.scala +++ b/akka-remote/src/test/scala/akka/remote/Ticket1978CommunicationSpec.scala @@ -111,10 +111,12 @@ object Configuration { } class Ticket1978SHA1PRNGSpec - extends Ticket1978CommunicationSpec(getCipherConfig("SHA1PRNG", "TLS_RSA_WITH_AES_128_CBC_SHA")) + extends Ticket1978CommunicationSpec( + getCipherConfig("SHA1PRNG", "TLS_RSA_WITH_AES_128_CBC_SHA", "TLS_DHE_RSA_WITH_AES_256_GCM_SHA384")) class Ticket1978DefaultRNGSecureSpec - extends Ticket1978CommunicationSpec(getCipherConfig("", "TLS_RSA_WITH_AES_128_CBC_SHA")) + extends Ticket1978CommunicationSpec( + getCipherConfig("", "TLS_RSA_WITH_AES_128_CBC_SHA", "TLS_DHE_RSA_WITH_AES_256_GCM_SHA384")) class Ticket1978CrappyRSAWithMD5OnlyHereToMakeSureThingsWorkSpec extends Ticket1978CommunicationSpec(getCipherConfig("", "SSL_RSA_WITH_NULL_MD5")) diff --git a/akka-remote/src/test/scala/akka/remote/Ticket1978ConfigSpec.scala b/akka-remote/src/test/scala/akka/remote/Ticket1978ConfigSpec.scala index b9c3dde0f2..e2114219e2 100644 --- a/akka-remote/src/test/scala/akka/remote/Ticket1978ConfigSpec.scala +++ b/akka-remote/src/test/scala/akka/remote/Ticket1978ConfigSpec.scala @@ -23,7 +23,8 @@ class Ticket1978ConfigSpec extends AkkaSpec(""" settings.SSLTrustStore should ===("truststore") settings.SSLTrustStorePassword should ===("changeme") settings.SSLProtocol should ===("TLSv1.2") - settings.SSLEnabledAlgorithms should ===(Set("TLS_RSA_WITH_AES_128_CBC_SHA")) + settings.SSLEnabledAlgorithms should ===( + Set("TLS_RSA_WITH_AES_128_CBC_SHA", "TLS_DHE_RSA_WITH_AES_256_GCM_SHA384")) settings.SSLRandomNumberGenerator should ===("SecureRandom") } } diff --git a/akka-remote/src/test/scala/akka/remote/artery/RemoteMessageSerializationSpec.scala b/akka-remote/src/test/scala/akka/remote/artery/RemoteMessageSerializationSpec.scala index 618ebcfe28..1cf702b63b 100644 --- a/akka-remote/src/test/scala/akka/remote/artery/RemoteMessageSerializationSpec.scala +++ b/akka-remote/src/test/scala/akka/remote/artery/RemoteMessageSerializationSpec.scala @@ -8,12 +8,10 @@ import java.io.NotSerializableException import java.util.concurrent.ThreadLocalRandom import scala.concurrent.duration._ - import com.github.ghik.silencer.silent - -import akka.actor.{ Actor, ActorRef, PoisonPill, Props } +import akka.actor.{ Actor, ActorRef, Dropped, PoisonPill, Props } import akka.remote.{ AssociationErrorEvent, DisassociatedEvent, OversizedPayloadException, RARP } -import akka.testkit.{ EventFilter, ImplicitSender, TestActors } +import akka.testkit.{ EventFilter, ImplicitSender, TestActors, TestProbe } import akka.util.ByteString object RemoteMessageSerializationSpec { @@ -52,6 +50,8 @@ class RemoteMessageSerializationSpec extends ArteryMultiNodeSpec with ImplicitSe } "drop sent messages over payload size" in { + val droppedProbe = TestProbe() + system.eventStream.subscribe(droppedProbe.ref, classOf[Dropped]) val oversized = byteStringOfSize(maxPayloadBytes + 1) EventFilter[OversizedPayloadException](start = "Failed to serialize oversized message", occurrences = 1) .intercept { @@ -59,6 +59,7 @@ class RemoteMessageSerializationSpec extends ArteryMultiNodeSpec with ImplicitSe expectNoMessage(1.second) // No AssocitionErrorEvent should be published } } + droppedProbe.expectMsgType[Dropped].message should ===(oversized) } // TODO max payload size is not configurable yet, so we cannot send a too big message, it fails no sending side diff --git a/akka-remote/src/test/scala/akka/remote/artery/RemoteSendConsistencySpec.scala b/akka-remote/src/test/scala/akka/remote/artery/RemoteSendConsistencySpec.scala index d4d4fd532b..e98c2b6a66 100644 --- a/akka-remote/src/test/scala/akka/remote/artery/RemoteSendConsistencySpec.scala +++ b/akka-remote/src/test/scala/akka/remote/artery/RemoteSendConsistencySpec.scala @@ -7,7 +7,9 @@ package akka.remote.artery import java.util.UUID import scala.concurrent.duration._ + import com.typesafe.config.{ Config, ConfigFactory } + import akka.actor.Actor import akka.actor.ActorIdentity import akka.actor.ActorPath diff --git a/akka-remote/src/test/scala/akka/remote/artery/compress/CompressionIntegrationSpec.scala b/akka-remote/src/test/scala/akka/remote/artery/compress/CompressionIntegrationSpec.scala index 475609aec9..199591739e 100644 --- a/akka-remote/src/test/scala/akka/remote/artery/compress/CompressionIntegrationSpec.scala +++ b/akka-remote/src/test/scala/akka/remote/artery/compress/CompressionIntegrationSpec.scala @@ -135,6 +135,72 @@ class CompressionIntegrationSpec } } + "not be advertised if ActorRef compression disabled" in { + val config = """ + akka.remote.artery.advanced.compression.actor-refs.max = off + akka.remote.artery.advanced.compression { + actor-refs.advertisement-interval = 50 ms + manifests.advertisement-interval = 50 ms + } + """ + val systemC = newRemoteSystem(Some(config)) + val systemD = newRemoteSystem(Some(config)) + val cRefProbe = TestProbe()(systemC) + val dRefProbe = TestProbe()(systemD) + systemC.eventStream.subscribe(cRefProbe.ref, classOf[CompressionProtocol.Events.ReceivedActorRefCompressionTable]) + systemD.eventStream.subscribe(dRefProbe.ref, classOf[CompressionProtocol.Events.ReceivedActorRefCompressionTable]) + + systemD.actorOf(TestActors.echoActorProps, "echo") + + val cProbe = TestProbe()(systemC) + systemC.actorSelection(rootActorPath(systemD) / "user" / "echo").tell(Identify(None), cProbe.ref) + val echoRefD = cProbe.expectMsgType[ActorIdentity].ref.get + + (1 to messagesToExchange).foreach { _ => + echoRefD.tell(TestMessage("hello"), cProbe.ref) + } + cProbe.receiveN(messagesToExchange) // the replies + cRefProbe.expectNoMessage(100.millis) + dRefProbe.expectNoMessage(100.millis) + + shutdown(systemC) + shutdown(systemD) + } + + "not be advertised if manifest compression disabled" in { + val config = """ + akka.remote.artery.advanced.compression.manifests.max = off + akka.remote.artery.advanced.compression { + actor-refs.advertisement-interval = 50 ms + manifests.advertisement-interval = 50 ms + } + """ + val systemC = newRemoteSystem(Some(config)) + val systemD = newRemoteSystem(Some(config)) + val cManifestProbe = TestProbe()(systemC) + val dManifestProbe = TestProbe()(systemD) + systemC.eventStream + .subscribe(cManifestProbe.ref, classOf[CompressionProtocol.Events.ReceivedClassManifestCompressionTable]) + systemD.eventStream + .subscribe(dManifestProbe.ref, classOf[CompressionProtocol.Events.ReceivedClassManifestCompressionTable]) + + systemD.actorOf(TestActors.echoActorProps, "echo") + + val cProbe = TestProbe()(systemC) + systemC.actorSelection(rootActorPath(systemD) / "user" / "echo").tell(Identify(None), cProbe.ref) + val echoRefD = cProbe.expectMsgType[ActorIdentity].ref.get + + (1 to messagesToExchange).foreach { _ => + echoRefD.tell(TestMessage("hello"), cProbe.ref) + } + cProbe.receiveN(messagesToExchange) // the replies + cManifestProbe.expectNoMessage(100.millis) + dManifestProbe.expectNoMessage(100.millis) + + shutdown(systemC) + shutdown(systemD) + } + } "handle noSender sender" in { diff --git a/akka-remote/src/test/scala/akka/remote/artery/compress/HeavyHittersSpec.scala b/akka-remote/src/test/scala/akka/remote/artery/compress/HeavyHittersSpec.scala index 2ea9aec76d..55ed187ea9 100644 --- a/akka-remote/src/test/scala/akka/remote/artery/compress/HeavyHittersSpec.scala +++ b/akka-remote/src/test/scala/akka/remote/artery/compress/HeavyHittersSpec.scala @@ -161,5 +161,15 @@ class HeavyHittersSpec extends AnyWordSpecLike with Matchers { hitters.lowestHitterWeight should ===(3) } + "be disabled with max=0" in { + val hitters = new TopHeavyHitters[String](0) + hitters.update("A", 10) shouldBe true + hitters.iterator.toSet should ===(Set.empty) + + hitters.update("B", 5) shouldBe false + hitters.update("C", 15) shouldBe true + hitters.iterator.toSet should ===(Set.empty) + } + } } diff --git a/akka-remote/src/test/scala/akka/remote/artery/tcp/TlsTcpSpec.scala b/akka-remote/src/test/scala/akka/remote/artery/tcp/TlsTcpSpec.scala index b417deccef..4925446996 100644 --- a/akka-remote/src/test/scala/akka/remote/artery/tcp/TlsTcpSpec.scala +++ b/akka-remote/src/test/scala/akka/remote/artery/tcp/TlsTcpSpec.scala @@ -8,12 +8,12 @@ package tcp import java.io.ByteArrayOutputStream import java.security.NoSuchAlgorithmException import java.util.zip.GZIPOutputStream +import javax.net.ssl.SSLEngine import scala.concurrent.duration._ import com.typesafe.config.Config import com.typesafe.config.ConfigFactory -import javax.net.ssl.SSLEngine import akka.actor.ActorIdentity import akka.actor.ActorPath @@ -33,7 +33,7 @@ class TlsTcpWithSHA1PRNGSpec extends TlsTcpSpec(ConfigFactory.parseString(""" akka.remote.artery.ssl.config-ssl-engine { random-number-generator = "SHA1PRNG" - enabled-algorithms = ["TLS_RSA_WITH_AES_128_CBC_SHA"] + enabled-algorithms = ["TLS_DHE_RSA_WITH_AES_256_GCM_SHA384"] } """)) @@ -41,7 +41,7 @@ class TlsTcpWithDefaultRNGSecureSpec extends TlsTcpSpec(ConfigFactory.parseString(""" akka.remote.artery.ssl.config-ssl-engine { random-number-generator = "" - enabled-algorithms = ["TLS_RSA_WITH_AES_128_CBC_SHA"] + enabled-algorithms = ["TLS_DHE_RSA_WITH_AES_256_GCM_SHA384"] } """)) diff --git a/akka-remote/src/test/scala/akka/remote/classic/RemotingSpec.scala b/akka-remote/src/test/scala/akka/remote/classic/RemotingSpec.scala index c61a93e5a1..8ac42971c0 100644 --- a/akka-remote/src/test/scala/akka/remote/classic/RemotingSpec.scala +++ b/akka-remote/src/test/scala/akka/remote/classic/RemotingSpec.scala @@ -73,7 +73,7 @@ object RemotingSpec { key-password = "changeme" trust-store-password = "changeme" protocol = "TLSv1.2" - enabled-algorithms = [TLS_RSA_WITH_AES_128_CBC_SHA] + enabled-algorithms = [TLS_RSA_WITH_AES_128_CBC_SHA, TLS_DHE_RSA_WITH_AES_256_GCM_SHA384] } common-netty-settings { @@ -479,6 +479,8 @@ class RemotingSpec extends AkkaSpec(RemotingSpec.cfg) with ImplicitSender with D } "drop sent messages over payload size" in { + val droppedProbe = TestProbe() + system.eventStream.subscribe(droppedProbe.ref, classOf[Dropped]) val oversized = byteStringOfSize(maxPayloadBytes + 1) EventFilter[OversizedPayloadException](pattern = ".*Discarding oversized payload sent.*", occurrences = 1) .intercept { @@ -486,6 +488,7 @@ class RemotingSpec extends AkkaSpec(RemotingSpec.cfg) with ImplicitSender with D expectNoMessage(1.second) // No AssocitionErrorEvent should be published } } + droppedProbe.expectMsgType[Dropped].message should ===(oversized) } "drop received messages over payload size" in { diff --git a/akka-serialization-jackson/src/main/resources/reference.conf b/akka-serialization-jackson/src/main/resources/reference.conf index 1a254fbd75..04df3d9279 100644 --- a/akka-serialization-jackson/src/main/resources/reference.conf +++ b/akka-serialization-jackson/src/main/resources/reference.conf @@ -179,9 +179,6 @@ akka.serialization.jackson { # Akka 2.6.4 or earlier, which was plain JSON format. jackson-cbor-264 = ${akka.serialization.jackson.jackson-cbor} - # Issue #28918 temporary in Akka 2.6.5 to support rolling update to 2.6.6. - jackson-cbor-265 = ${akka.serialization.jackson.jackson-cbor} - } #//#features @@ -191,6 +188,7 @@ akka.serialization.jackson.jackson-json.compression { # Compression algorithm. # - off : no compression # - gzip : using common java gzip + # - lz4 : using lz4-java algorithm = gzip # If compression is enabled with the `algorithm` setting the payload is compressed @@ -202,23 +200,19 @@ akka.serialization.jackson.jackson-json.compression { akka.actor { serializers { jackson-json = "akka.serialization.jackson.JacksonJsonSerializer" - jackson-cbor = "akka.serialization.jackson.JacksonJsonSerializer" + jackson-cbor = "akka.serialization.jackson.JacksonCborSerializer" # Issue #28918 for compatibility with data serialized with JacksonCborSerializer in # Akka 2.6.4 or earlier, which was plain JSON format. jackson-cbor-264 = "akka.serialization.jackson.JacksonJsonSerializer" - # Issue #28918 temporary in Akka 2.6.5 to support rolling update to 2.6.6. - jackson-cbor-265 = "akka.serialization.jackson.JacksonCborSerializer" } serialization-identifiers { jackson-json = 31 - jackson-cbor = 32 + jackson-cbor = 33 # Issue #28918 for compatibility with data serialized with JacksonCborSerializer in # Akka 2.6.4 or earlier, which was plain JSON format. jackson-cbor-264 = 32 - # Issue #28918 temporary in Akka 2.6.5 to support rolling update to 2.6.6. - jackson-cbor-265 = 33 } serialization-bindings { # Define bindings for classes or interfaces use Jackson serializer, e.g. diff --git a/akka-serialization-jackson/src/main/scala/akka/serialization/jackson/JacksonSerializer.scala b/akka-serialization-jackson/src/main/scala/akka/serialization/jackson/JacksonSerializer.scala index c83e3d74fa..001adf0106 100644 --- a/akka-serialization-jackson/src/main/scala/akka/serialization/jackson/JacksonSerializer.scala +++ b/akka-serialization-jackson/src/main/scala/akka/serialization/jackson/JacksonSerializer.scala @@ -4,29 +4,25 @@ package akka.serialization.jackson -import java.io.ByteArrayInputStream -import java.io.ByteArrayOutputStream -import java.io.NotSerializableException -import java.util.zip.GZIPInputStream -import java.util.zip.GZIPOutputStream +import java.io.{ ByteArrayInputStream, ByteArrayOutputStream, NotSerializableException } +import java.nio.ByteBuffer +import java.util.zip.{ GZIPInputStream, GZIPOutputStream } import scala.annotation.tailrec -import scala.util.Failure -import scala.util.Success +import scala.util.{ Failure, Success } import scala.util.control.NonFatal import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.databind.jsontype.impl.SubTypeValidator import com.fasterxml.jackson.dataformat.cbor.CBORFactory +import net.jpountz.lz4.LZ4Factory import akka.actor.ExtendedActorSystem import akka.annotation.InternalApi -import akka.event.LogMarker -import akka.event.Logging -import akka.serialization.BaseSerializer -import akka.serialization.SerializationExtension -import akka.serialization.SerializerWithStringManifest +import akka.event.{ LogMarker, Logging } +import akka.serialization.{ BaseSerializer, SerializationExtension, SerializerWithStringManifest } import akka.util.Helpers.toRootLowerCase +import akka.util.OptionVal /** * INTERNAL API @@ -86,6 +82,51 @@ import akka.util.Helpers.toRootLowerCase (bytes(0) == GZIPInputStream.GZIP_MAGIC.toByte) && (bytes(1) == (GZIPInputStream.GZIP_MAGIC >> 8).toByte) } + + final case class LZ4Meta(offset: Int, length: Int) { + import LZ4Meta._ + + def putInto(buffer: ByteBuffer): Unit = { + buffer.putInt(LZ4_MAGIC) + buffer.putInt(length) + } + + def prependTo(bytes: Array[Byte]): Array[Byte] = { + val buffer = ByteBuffer.allocate(bytes.length + offset) + putInto(buffer) + buffer.put(bytes) + buffer.array() + } + + } + + object LZ4Meta { + val LZ4_MAGIC = 0x87d96df6 // The last 4 bytes of `printf akka | sha512sum` + + def apply(bytes: Array[Byte]): LZ4Meta = { + LZ4Meta(8, bytes.length) + } + + def get(buffer: ByteBuffer): OptionVal[LZ4Meta] = { + if (buffer.remaining() < 4) { + OptionVal.None + } else if (buffer.getInt() != LZ4_MAGIC) { + OptionVal.None + } else { + OptionVal.Some(LZ4Meta(8, buffer.getInt())) + } + } + + def get(bytes: Array[Byte]): OptionVal[LZ4Meta] = { + get(ByteBuffer.wrap(bytes)) + } + + } + + def isLZ4(bytes: Array[Byte]): Boolean = { + LZ4Meta.get(bytes).isDefined + } + } /** @@ -114,7 +155,7 @@ import akka.util.Helpers.toRootLowerCase sealed trait Algoritm object Off extends Algoritm final case class GZip(largerThan: Long) extends Algoritm - // TODO add LZ4, issue #27066 + final case class LZ4(largerThan: Long) extends Algoritm } /** @@ -131,8 +172,7 @@ import akka.util.Helpers.toRootLowerCase val bindingName: String, val objectMapper: ObjectMapper) extends SerializerWithStringManifest { - import JacksonSerializer.GadgetClassBlacklist - import JacksonSerializer.isGZipped + import JacksonSerializer._ // TODO issue #27107: it should be possible to implement ByteBufferSerializer as well, using Jackson's // ByteBufferBackedOutputStream/ByteBufferBackedInputStream @@ -147,6 +187,9 @@ import akka.util.Helpers.toRootLowerCase case "gzip" => val compressLargerThan = conf.getBytes("compression.compress-larger-than") Compression.GZip(compressLargerThan) + case "lz4" => + val compressLargerThan = conf.getBytes("compression.compress-larger-than") + Compression.LZ4(compressLargerThan) case other => throw new IllegalArgumentException( s"Unknown compression algorithm [$other], possible values are " + @@ -209,6 +252,10 @@ import akka.util.Helpers.toRootLowerCase // doesn't have to be volatile, doesn't matter if check is run more than once private var serializationBindingsCheckedOk = false + private lazy val lz4Factory = LZ4Factory.fastestInstance() + private lazy val lz4Compressor = lz4Factory.fastCompressor() + private lazy val lz4Decompressor = lz4Factory.safeDecompressor() + override val identifier: Int = BaseSerializer.identifierFromConfig(bindingName, system) override def manifest(obj: AnyRef): String = { @@ -446,42 +493,47 @@ import akka.util.Helpers.toRootLowerCase def compress(bytes: Array[Byte]): Array[Byte] = { compressionAlgorithm match { - case Compression.Off => bytes - case Compression.GZip(largerThan) => - if (bytes.length > largerThan) compressGzip(bytes) else bytes + case Compression.Off => bytes + case Compression.GZip(largerThan) if bytes.length <= largerThan => bytes + case Compression.GZip(_) => + val bos = new ByteArrayOutputStream(BufferSize) + val zip = new GZIPOutputStream(bos) + try zip.write(bytes) + finally zip.close() + bos.toByteArray + case Compression.LZ4(largerThan) if bytes.length <= largerThan => bytes + case Compression.LZ4(_) => { + val meta = LZ4Meta(bytes) + val compressed = lz4Compressor.compress(bytes) + meta.prependTo(compressed) + } } } - private def compressGzip(bytes: Array[Byte]): Array[Byte] = { - val bos = new ByteArrayOutputStream(BufferSize) - val zip = new GZIPOutputStream(bos) - try zip.write(bytes) - finally zip.close() - bos.toByteArray - } - def decompress(bytes: Array[Byte]): Array[Byte] = { - if (isGZipped(bytes)) - decompressGzip(bytes) - else - bytes - } + if (isGZipped(bytes)) { + val in = new GZIPInputStream(new ByteArrayInputStream(bytes)) + val out = new ByteArrayOutputStream() + val buffer = new Array[Byte](BufferSize) - private def decompressGzip(bytes: Array[Byte]): Array[Byte] = { - val in = new GZIPInputStream(new ByteArrayInputStream(bytes)) - val out = new ByteArrayOutputStream() - val buffer = new Array[Byte](BufferSize) + @tailrec def readChunk(): Unit = in.read(buffer) match { + case -1 => () + case n => + out.write(buffer, 0, n) + readChunk() + } - @tailrec def readChunk(): Unit = in.read(buffer) match { - case -1 => () - case n => - out.write(buffer, 0, n) - readChunk() + try readChunk() + finally in.close() + out.toByteArray + } else { + LZ4Meta.get(bytes) match { + case OptionVal.None => bytes + case OptionVal.Some(meta) => + val srcLen = bytes.length - meta.offset + lz4Decompressor.decompress(bytes, meta.offset, srcLen, meta.length) + } } - - try readChunk() - finally in.close() - out.toByteArray } } diff --git a/akka-serialization-jackson/src/test/scala/akka/serialization/jackson/JacksonSerializerSpec.scala b/akka-serialization-jackson/src/test/scala/akka/serialization/jackson/JacksonSerializerSpec.scala index 1b45c25c8a..a75ce1f4b5 100644 --- a/akka-serialization-jackson/src/test/scala/akka/serialization/jackson/JacksonSerializerSpec.scala +++ b/akka-serialization-jackson/src/test/scala/akka/serialization/jackson/JacksonSerializerSpec.scala @@ -508,6 +508,41 @@ class JacksonJsonSerializerSpec extends JacksonSerializerSpec("jackson-json") { val bytes = serializeToBinary(msg) JacksonSerializer.isGZipped(bytes) should ===(false) } + + "compress large payload with lz4" in withSystem(""" + akka.serialization.jackson.jackson-json.compression { + algorithm = lz4 + compress-larger-than = 32 KiB + } + """) { sys => + val conf = JacksonObjectMapperProvider.configForBinding("jackson-json", sys.settings.config) + val compressLargerThan = conf.getBytes("compression.compress-larger-than") + def check(msg: AnyRef, compressed: Boolean): Unit = { + val bytes = serializeToBinary(msg, sys) + JacksonSerializer.isLZ4(bytes) should ===(compressed) + bytes.length should be < compressLargerThan.toInt + checkSerialization(msg, sys) + } + check(SimpleCommand("0" * (compressLargerThan + 1).toInt), true) + } + + "not compress small payload with lz4" in withSystem(""" + akka.serialization.jackson.jackson-json.compression { + algorithm = lz4 + compress-larger-than = 32 KiB + } + """) { sys => + val conf = JacksonObjectMapperProvider.configForBinding("jackson-json", sys.settings.config) + val compressLargerThan = conf.getBytes("compression.compress-larger-than") + def check(msg: AnyRef, compressed: Boolean): Unit = { + val bytes = serializeToBinary(msg, sys) + JacksonSerializer.isLZ4(bytes) should ===(compressed) + bytes.length should be < compressLargerThan.toInt + checkSerialization(msg, sys) + } + check(SimpleCommand("Bob"), false) + check(new SimpleCommandNotCaseClass("Bob"), false) + } } "JacksonJsonSerializer without type in manifest" should { @@ -639,13 +674,13 @@ abstract class JacksonSerializerSpec(serializerName: String) val serializer = serializerFor(obj, sys) val manifest = serializer.manifest(obj) val serializerId = serializer.identifier - val blob = serializeToBinary(obj) + val blob = serializeToBinary(obj, sys) // Issue #28918, check that CBOR format is used (not JSON). if (blob.length > 0) { serializer match { case _: JacksonJsonSerializer => - if (!JacksonSerializer.isGZipped(blob)) + if (!JacksonSerializer.isGZipped(blob) && !JacksonSerializer.isLZ4(blob)) new String(blob.take(1), StandardCharsets.UTF_8) should ===("{") case _: JacksonCborSerializer => new String(blob.take(1), StandardCharsets.UTF_8) should !==("{") diff --git a/akka-stream-tests/src/test/scala/akka/stream/io/DeprecatedTlsSpec.scala b/akka-stream-tests/src/test/scala/akka/stream/io/DeprecatedTlsSpec.scala index 4cc4629313..dfa6da1907 100644 --- a/akka-stream-tests/src/test/scala/akka/stream/io/DeprecatedTlsSpec.scala +++ b/akka-stream-tests/src/test/scala/akka/stream/io/DeprecatedTlsSpec.scala @@ -8,6 +8,7 @@ import java.security.KeyStore import java.security.SecureRandom import java.security.cert.CertificateException import java.util.concurrent.TimeoutException +import javax.net.ssl._ import scala.collection.immutable import scala.concurrent.Await @@ -17,7 +18,6 @@ import scala.util.Random import com.github.ghik.silencer.silent import com.typesafe.sslconfig.akka.AkkaSSLConfig -import javax.net.ssl._ import akka.NotUsed import akka.pattern.{ after => later } diff --git a/akka-stream-tests/src/test/scala/akka/stream/io/InputStreamSinkSpec.scala b/akka-stream-tests/src/test/scala/akka/stream/io/InputStreamSinkSpec.scala index fe40010579..3deeea3de7 100644 --- a/akka-stream-tests/src/test/scala/akka/stream/io/InputStreamSinkSpec.scala +++ b/akka-stream-tests/src/test/scala/akka/stream/io/InputStreamSinkSpec.scala @@ -263,10 +263,8 @@ class InputStreamSinkSpec extends StreamSpec(UnboundedMailboxConfig) { "propagate error to InputStream" in { val readTimeout = 3.seconds - val (probe, inputStream) = + val (probe, inputStream: InputStream) = TestSource.probe[ByteString].toMat(StreamConverters.asInputStream(readTimeout))(Keep.both).run() - - probe.sendNext(ByteString("one")) val error = new RuntimeException("failure") probe.sendError(error) val buffer = Array.ofDim[Byte](5) diff --git a/akka-stream-tests/src/test/scala/akka/stream/io/TcpSpec.scala b/akka-stream-tests/src/test/scala/akka/stream/io/TcpSpec.scala index 85d7eebcbc..af5d203fcd 100644 --- a/akka-stream-tests/src/test/scala/akka/stream/io/TcpSpec.scala +++ b/akka-stream-tests/src/test/scala/akka/stream/io/TcpSpec.scala @@ -921,7 +921,6 @@ class TcpSpec extends StreamSpec(""" // #setting-up-ssl-engine import java.security.KeyStore - import javax.net.ssl.KeyManagerFactory import javax.net.ssl.SSLContext import javax.net.ssl.SSLEngine @@ -1010,9 +1009,9 @@ class TcpSpec extends StreamSpec(""" def initSslMess() = { // #setting-up-ssl-context import java.security.KeyStore + import javax.net.ssl._ import com.typesafe.sslconfig.akka.AkkaSSLConfig - import javax.net.ssl._ import akka.stream.TLSClientAuth import akka.stream.TLSProtocol diff --git a/akka-stream-tests/src/test/scala/akka/stream/io/TlsSpec.scala b/akka-stream-tests/src/test/scala/akka/stream/io/TlsSpec.scala index b8f3f014eb..742a29d92f 100644 --- a/akka-stream-tests/src/test/scala/akka/stream/io/TlsSpec.scala +++ b/akka-stream-tests/src/test/scala/akka/stream/io/TlsSpec.scala @@ -8,6 +8,7 @@ import java.security.KeyStore import java.security.SecureRandom import java.security.cert.CertificateException import java.util.concurrent.TimeoutException +import javax.net.ssl._ import scala.collection.immutable import scala.concurrent.Await @@ -15,8 +16,6 @@ import scala.concurrent.Future import scala.concurrent.duration._ import scala.util.Random -import javax.net.ssl._ - import akka.NotUsed import akka.pattern.{ after => later } import akka.stream._ @@ -34,10 +33,10 @@ object TlsSpec { val rnd = new Random - val SSLEnabledAlgorithms: Set[String] = Set("TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA", "TLS_RSA_WITH_AES_128_CBC_SHA") - val SSLProtocol: String = "TLSv1.2" + val TLS12Ciphers: Set[String] = Set("TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA", "TLS_RSA_WITH_AES_128_CBC_SHA") + val TLS13Ciphers: Set[String] = Set("TLS_AES_128_GCM_SHA256", "TLS_AES_256_GCM_SHA384") - def initWithTrust(trustPath: String): SSLContext = { + def initWithTrust(trustPath: String, protocol: String): SSLContext = { val password = "changeme" val keyStore = KeyStore.getInstance(KeyStore.getDefaultType) @@ -52,12 +51,12 @@ object TlsSpec { val trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm) trustManagerFactory.init(trustStore) - val context = SSLContext.getInstance(SSLProtocol) + val context = SSLContext.getInstance(protocol) context.init(keyManagerFactory.getKeyManagers, trustManagerFactory.getTrustManagers, new SecureRandom) context } - def initSslContext(): SSLContext = initWithTrust("/truststore") + def initSslContext(protocol: String): SSLContext = initWithTrust("/truststore", protocol) /** * This is an operator that fires a TimeoutException failure 2 seconds after it was started, @@ -103,469 +102,486 @@ class TlsSpec extends StreamSpec(TlsSpec.configOverrides) with WithLogCapturing import system.dispatcher "SslTls" must { + "work for TLSv1.2" must { workFor("TLSv1.2", TLS12Ciphers) } - val sslContext = initSslContext() + if (JavaVersion.majorVersion >= 11) + "work for TLSv1.3" must { workFor("TLSv1.3", TLS13Ciphers) } - val debug = Flow[SslTlsInbound].map { x => - x match { - case SessionTruncated => system.log.debug(s" ----------- truncated ") - case SessionBytes(_, b) => system.log.debug(s" ----------- (${b.size}) ${b.take(32).utf8String}") - } - x - } + def workFor(protocol: String, ciphers: Set[String]): Unit = { + val sslContext = initSslContext(protocol) - def createSSLEngine(context: SSLContext, role: TLSRole): SSLEngine = - createSSLEngine2(context, role, hostnameVerification = false, hostInfo = None) - - def createSSLEngine2( - context: SSLContext, - role: TLSRole, - hostnameVerification: Boolean, - hostInfo: Option[(String, Int)]): SSLEngine = { - - val engine = hostInfo match { - case None => - if (hostnameVerification) - throw new IllegalArgumentException("hostInfo must be defined for hostnameVerification to work.") - context.createSSLEngine() - case Some((hostname, port)) => context.createSSLEngine(hostname, port) - } - - if (hostnameVerification && role == akka.stream.Client) { - val sslParams = sslContext.getDefaultSSLParameters - sslParams.setEndpointIdentificationAlgorithm("HTTPS") - engine.setSSLParameters(sslParams) - } - - engine.setUseClientMode(role == akka.stream.Client) - engine.setEnabledCipherSuites(SSLEnabledAlgorithms.toArray) - engine.setEnabledProtocols(Array(SSLProtocol)) - - engine - } - - def clientTls(closing: TLSClosing) = - TLS(() => createSSLEngine(sslContext, Client), closing) - def badClientTls(closing: TLSClosing) = - TLS(() => createSSLEngine(initWithTrust("/badtruststore"), Client), closing) - def serverTls(closing: TLSClosing) = - TLS(() => createSSLEngine(sslContext, Server), closing) - - trait Named { - def name: String = - getClass.getName.reverse.dropWhile(c => "$0123456789".indexOf(c) != -1).takeWhile(_ != '$').reverse - } - - trait CommunicationSetup extends Named { - def decorateFlow( - leftClosing: TLSClosing, - rightClosing: TLSClosing, - rhs: Flow[SslTlsInbound, SslTlsOutbound, Any]): Flow[SslTlsOutbound, SslTlsInbound, NotUsed] - def cleanup(): Unit = () - } - - object ClientInitiates extends CommunicationSetup { - def decorateFlow( - leftClosing: TLSClosing, - rightClosing: TLSClosing, - rhs: Flow[SslTlsInbound, SslTlsOutbound, Any]) = - clientTls(leftClosing).atop(serverTls(rightClosing).reversed).join(rhs) - } - - object ServerInitiates extends CommunicationSetup { - def decorateFlow( - leftClosing: TLSClosing, - rightClosing: TLSClosing, - rhs: Flow[SslTlsInbound, SslTlsOutbound, Any]) = - serverTls(leftClosing).atop(clientTls(rightClosing).reversed).join(rhs) - } - - def server(flow: Flow[ByteString, ByteString, Any]) = { - val server = Tcp().bind("localhost", 0).to(Sink.foreach(c => c.flow.join(flow).run())).run() - Await.result(server, 2.seconds) - } - - object ClientInitiatesViaTcp extends CommunicationSetup { - var binding: Tcp.ServerBinding = null - def decorateFlow( - leftClosing: TLSClosing, - rightClosing: TLSClosing, - rhs: Flow[SslTlsInbound, SslTlsOutbound, Any]) = { - binding = server(serverTls(rightClosing).reversed.join(rhs)) - clientTls(leftClosing).join(Tcp().outgoingConnection(binding.localAddress)) - } - override def cleanup(): Unit = binding.unbind() - } - - object ServerInitiatesViaTcp extends CommunicationSetup { - var binding: Tcp.ServerBinding = null - def decorateFlow( - leftClosing: TLSClosing, - rightClosing: TLSClosing, - rhs: Flow[SslTlsInbound, SslTlsOutbound, Any]) = { - binding = server(clientTls(rightClosing).reversed.join(rhs)) - serverTls(leftClosing).join(Tcp().outgoingConnection(binding.localAddress)) - } - override def cleanup(): Unit = binding.unbind() - } - - val communicationPatterns = - Seq(ClientInitiates, ServerInitiates, ClientInitiatesViaTcp, ServerInitiatesViaTcp) - - trait PayloadScenario extends Named { - def flow: Flow[SslTlsInbound, SslTlsOutbound, Any] = - Flow[SslTlsInbound].map { - var session: SSLSession = null - def setSession(s: SSLSession) = { - session = s - system.log.debug(s"new session: $session (${session.getId.mkString(",")})") - } - - { - case SessionTruncated => SendBytes(ByteString("TRUNCATED")) - case SessionBytes(s, b) if session == null => - setSession(s) - SendBytes(b) - case SessionBytes(s, b) if s != session => - setSession(s) - SendBytes(ByteString("NEWSESSION") ++ b) - case SessionBytes(_, b) => SendBytes(b) - } + val debug = Flow[SslTlsInbound].map { x => + x match { + case SessionTruncated => system.log.debug(s" ----------- truncated ") + case SessionBytes(_, b) => system.log.debug(s" ----------- (${b.size}) ${b.take(32).utf8String}") } - def leftClosing: TLSClosing = IgnoreComplete - def rightClosing: TLSClosing = IgnoreComplete + x + } - def inputs: immutable.Seq[SslTlsOutbound] - def output: ByteString + def createSSLEngine(context: SSLContext, role: TLSRole): SSLEngine = + createSSLEngine2(context, role, hostnameVerification = false, hostInfo = None) - protected def send(str: String) = SendBytes(ByteString(str)) - protected def send(ch: Char) = SendBytes(ByteString(ch.toByte)) - } + def createSSLEngine2( + context: SSLContext, + role: TLSRole, + hostnameVerification: Boolean, + hostInfo: Option[(String, Int)]): SSLEngine = { - object SingleBytes extends PayloadScenario { - val str = "0123456789" - def inputs = str.map(ch => SendBytes(ByteString(ch.toByte))) - def output = ByteString(str) - } + val engine = hostInfo match { + case None => + if (hostnameVerification) + throw new IllegalArgumentException("hostInfo must be defined for hostnameVerification to work.") + context.createSSLEngine() + case Some((hostname, port)) => context.createSSLEngine(hostname, port) + } - object MediumMessages extends PayloadScenario { - val strs = "0123456789".map(d => d.toString * (rnd.nextInt(9000) + 1000)) - def inputs = strs.map(s => SendBytes(ByteString(s))) - def output = ByteString(strs.foldRight("")(_ ++ _)) - } + if (hostnameVerification && role == akka.stream.Client) { + val sslParams = sslContext.getDefaultSSLParameters + sslParams.setEndpointIdentificationAlgorithm("HTTPS") + engine.setSSLParameters(sslParams) + } - object LargeMessages extends PayloadScenario { - // TLS max packet size is 16384 bytes - val strs = "0123456789".map(d => d.toString * (rnd.nextInt(9000) + 17000)) - def inputs = strs.map(s => SendBytes(ByteString(s))) - def output = ByteString(strs.foldRight("")(_ ++ _)) - } + engine.setUseClientMode(role == akka.stream.Client) + engine.setEnabledCipherSuites(ciphers.toArray) + engine.setEnabledProtocols(Array(protocol)) - object EmptyBytesFirst extends PayloadScenario { - def inputs = List(ByteString.empty, ByteString("hello")).map(SendBytes) - def output = ByteString("hello") - } + engine + } - object EmptyBytesInTheMiddle extends PayloadScenario { - def inputs = List(ByteString("hello"), ByteString.empty, ByteString(" world")).map(SendBytes) - def output = ByteString("hello world") - } + def clientTls(closing: TLSClosing) = + TLS(() => createSSLEngine(sslContext, Client), closing) - object EmptyBytesLast extends PayloadScenario { - def inputs = List(ByteString("hello"), ByteString.empty).map(SendBytes) - def output = ByteString("hello") - } + def badClientTls(closing: TLSClosing) = + TLS(() => createSSLEngine(initWithTrust("/badtruststore", protocol), Client), closing) - object CompletedImmediately extends PayloadScenario { - override def inputs: immutable.Seq[SslTlsOutbound] = Nil - override def output = ByteString.empty + def serverTls(closing: TLSClosing) = + TLS(() => createSSLEngine(sslContext, Server), closing) - override def leftClosing: TLSClosing = EagerClose - override def rightClosing: TLSClosing = EagerClose - } + trait Named { + def name: String = + getClass.getName.reverse.dropWhile(c => "$0123456789".indexOf(c) != -1).takeWhile(_ != '$').reverse + } - // this demonstrates that cancellation is ignored so that the five results make it back - object CancellingRHS extends PayloadScenario { - override def flow = - Flow[SslTlsInbound] - .mapConcat { - case SessionTruncated => SessionTruncated :: Nil - case SessionBytes(s, bytes) => bytes.map(b => SessionBytes(s, ByteString(b))) + trait CommunicationSetup extends Named { + def decorateFlow( + leftClosing: TLSClosing, + rightClosing: TLSClosing, + rhs: Flow[SslTlsInbound, SslTlsOutbound, Any]): Flow[SslTlsOutbound, SslTlsInbound, NotUsed] + def cleanup(): Unit = () + } + + object ClientInitiates extends CommunicationSetup { + def decorateFlow( + leftClosing: TLSClosing, + rightClosing: TLSClosing, + rhs: Flow[SslTlsInbound, SslTlsOutbound, Any]) = + clientTls(leftClosing).atop(serverTls(rightClosing).reversed).join(rhs) + } + + object ServerInitiates extends CommunicationSetup { + def decorateFlow( + leftClosing: TLSClosing, + rightClosing: TLSClosing, + rhs: Flow[SslTlsInbound, SslTlsOutbound, Any]) = + serverTls(leftClosing).atop(clientTls(rightClosing).reversed).join(rhs) + } + + def server(flow: Flow[ByteString, ByteString, Any]) = { + val server = Tcp().bind("localhost", 0).to(Sink.foreach(c => c.flow.join(flow).run())).run() + Await.result(server, 2.seconds) + } + + object ClientInitiatesViaTcp extends CommunicationSetup { + var binding: Tcp.ServerBinding = null + def decorateFlow( + leftClosing: TLSClosing, + rightClosing: TLSClosing, + rhs: Flow[SslTlsInbound, SslTlsOutbound, Any]) = { + binding = server(serverTls(rightClosing).reversed.join(rhs)) + clientTls(leftClosing).join(Tcp().outgoingConnection(binding.localAddress)) + } + override def cleanup(): Unit = binding.unbind() + } + + object ServerInitiatesViaTcp extends CommunicationSetup { + var binding: Tcp.ServerBinding = null + def decorateFlow( + leftClosing: TLSClosing, + rightClosing: TLSClosing, + rhs: Flow[SslTlsInbound, SslTlsOutbound, Any]) = { + binding = server(clientTls(rightClosing).reversed.join(rhs)) + serverTls(leftClosing).join(Tcp().outgoingConnection(binding.localAddress)) + } + override def cleanup(): Unit = binding.unbind() + } + + val communicationPatterns = + Seq(ClientInitiates, ServerInitiates, ClientInitiatesViaTcp, ServerInitiatesViaTcp) + + trait PayloadScenario extends Named { + def flow: Flow[SslTlsInbound, SslTlsOutbound, Any] = + Flow[SslTlsInbound].map { + var session: SSLSession = null + def setSession(s: SSLSession) = { + session = s + system.log.debug(s"new session: $session (${session.getId.mkString(",")})") + } + + { + case SessionTruncated => SendBytes(ByteString("TRUNCATED")) + case SessionBytes(s, b) if session == null => + setSession(s) + SendBytes(b) + case SessionBytes(s, b) if s != session => + setSession(s) + SendBytes(ByteString("NEWSESSION") ++ b) + case SessionBytes(_, b) => SendBytes(b) + } } - .take(5) - .mapAsync(5)(x => later(500.millis, system.scheduler)(Future.successful(x))) - .via(super.flow) - override def rightClosing = IgnoreCancel + def leftClosing: TLSClosing = IgnoreComplete + def rightClosing: TLSClosing = IgnoreComplete - val str = "abcdef" * 100 - def inputs = str.map(send) - def output = ByteString(str.take(5)) - } + def inputs: immutable.Seq[SslTlsOutbound] + def output: ByteString - object CancellingRHSIgnoresBoth extends PayloadScenario { - override def flow = - Flow[SslTlsInbound] - .mapConcat { - case SessionTruncated => SessionTruncated :: Nil - case SessionBytes(s, bytes) => bytes.map(b => SessionBytes(s, ByteString(b))) - } - .take(5) - .mapAsync(5)(x => later(500.millis, system.scheduler)(Future.successful(x))) - .via(super.flow) - override def rightClosing = IgnoreBoth - - val str = "abcdef" * 100 - def inputs = str.map(send) - def output = ByteString(str.take(5)) - } - - object LHSIgnoresBoth extends PayloadScenario { - override def leftClosing = IgnoreBoth - val str = "0123456789" - def inputs = str.map(ch => SendBytes(ByteString(ch.toByte))) - def output = ByteString(str) - } - - object BothSidesIgnoreBoth extends PayloadScenario { - override def leftClosing = IgnoreBoth - override def rightClosing = IgnoreBoth - val str = "0123456789" - def inputs = str.map(ch => SendBytes(ByteString(ch.toByte))) - def output = ByteString(str) - } - - object SessionRenegotiationBySender extends PayloadScenario { - def inputs = List(send("hello"), NegotiateNewSession, send("world")) - def output = ByteString("helloNEWSESSIONworld") - } - - // difference is that the RHS engine will now receive the handshake while trying to send - object SessionRenegotiationByReceiver extends PayloadScenario { - val str = "abcdef" * 100 - def inputs = str.map(send) ++ Seq(NegotiateNewSession) ++ "hello world".map(send) - def output = ByteString(str + "NEWSESSIONhello world") - } - - val logCipherSuite = Flow[SslTlsInbound].map { - var session: SSLSession = null - def setSession(s: SSLSession) = { - session = s - system.log.debug(s"new session: $session (${session.getId.mkString(",")})") + protected def send(str: String) = SendBytes(ByteString(str)) + protected def send(ch: Char) = SendBytes(ByteString(ch.toByte)) } - { - case SessionTruncated => SendBytes(ByteString("TRUNCATED")) - case SessionBytes(s, b) if s != session => - setSession(s) - SendBytes(ByteString(s.getCipherSuite) ++ b) - case SessionBytes(_, b) => SendBytes(b) - } - } - - object SessionRenegotiationFirstOne extends PayloadScenario { - override def flow = logCipherSuite - def inputs = NegotiateNewSession.withCipherSuites("TLS_RSA_WITH_AES_128_CBC_SHA") :: send("hello") :: Nil - def output = ByteString("TLS_RSA_WITH_AES_128_CBC_SHAhello") - } - - object SessionRenegotiationFirstTwo extends PayloadScenario { - override def flow = logCipherSuite - def inputs = NegotiateNewSession.withCipherSuites("TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA") :: send("hello") :: Nil - def output = ByteString("TLS_ECDHE_RSA_WITH_AES_128_CBC_SHAhello") - } - - val scenarios = - Seq( - SingleBytes, - MediumMessages, - LargeMessages, - EmptyBytesFirst, - EmptyBytesInTheMiddle, - EmptyBytesLast, - CompletedImmediately, - CancellingRHS, - CancellingRHSIgnoresBoth, - LHSIgnoresBoth, - BothSidesIgnoreBoth, - SessionRenegotiationBySender, - SessionRenegotiationByReceiver, - SessionRenegotiationFirstOne, - SessionRenegotiationFirstTwo) - - for { - commPattern <- communicationPatterns - scenario <- scenarios - } { - s"work in mode ${commPattern.name} while sending ${scenario.name}" in assertAllStagesStopped { - val onRHS = debug.via(scenario.flow) - val output = - Source(scenario.inputs) - .via(commPattern.decorateFlow(scenario.leftClosing, scenario.rightClosing, onRHS)) - .via(new SimpleLinearGraphStage[SslTlsInbound] { - override def createLogic(inheritedAttributes: Attributes) = - new GraphStageLogic(shape) with InHandler with OutHandler { - setHandlers(in, out, this) - - override def onPush() = push(out, grab(in)) - override def onPull() = pull(in) - - override def onDownstreamFinish(cause: Throwable) = { - system.log.debug(s"me cancelled, cause {}", cause) - completeStage() - } - } - }) - .via(debug) - .collect { case SessionBytes(_, b) => b } - .scan(ByteString.empty)(_ ++ _) - .filter(_.nonEmpty) - .via(new Timeout(10.seconds)) - .dropWhile(_.size < scenario.output.size) - .runWith(Sink.headOption) - - Await.result(output, 12.seconds).getOrElse(ByteString.empty).utf8String should be(scenario.output.utf8String) - - commPattern.cleanup() - } - } - - "emit an error if the TLS handshake fails certificate checks" in assertAllStagesStopped { - val getError = Flow[SslTlsInbound] - .map[Either[SslTlsInbound, SSLException]](i => Left(i)) - .recover { case e: SSLException => Right(e) } - .collect { case Right(e) => e } - .toMat(Sink.head)(Keep.right) - - val simple = Flow.fromSinkAndSourceMat(getError, Source.maybe[SslTlsOutbound])(Keep.left) - - // The creation of actual TCP connections is necessary. It is the easiest way to decouple the client and server - // under error conditions, and has the bonus of matching most actual SSL deployments. - val (server, serverErr) = Tcp() - .bind("localhost", 0) - .mapAsync(1)(c => c.flow.joinMat(serverTls(IgnoreBoth).reversed.joinMat(simple)(Keep.right))(Keep.right).run()) - .toMat(Sink.head)(Keep.both) - .run() - - val clientErr = simple - .join(badClientTls(IgnoreBoth)) - .join(Tcp().outgoingConnection(Await.result(server, 1.second).localAddress)) - .run() - - Await.result(serverErr, 1.second).getMessage should include("certificate_unknown") - val clientErrText = Await.result(clientErr, 1.second).getMessage - if (JavaVersion.majorVersion >= 11) - clientErrText should include("unable to find valid certification path to requested target") - else - clientErrText should equal("General SSLEngine problem") - } - - "reliably cancel subscriptions when TransportIn fails early" in assertAllStagesStopped { - val ex = new Exception("hello") - val (sub, out1, out2) = - RunnableGraph - .fromGraph( - GraphDSL.create(Source.asSubscriber[SslTlsOutbound], Sink.head[ByteString], Sink.head[SslTlsInbound])( - (_, _, _)) { implicit b => (s, o1, o2) => - val tls = b.add(clientTls(EagerClose)) - s ~> tls.in1; tls.out1 ~> o1 - o2 <~ tls.out2; tls.in2 <~ Source.failed(ex) - ClosedShape - }) - .run() - the[Exception] thrownBy Await.result(out1, 1.second) should be(ex) - the[Exception] thrownBy Await.result(out2, 1.second) should be(ex) - Thread.sleep(500) - val pub = TestPublisher.probe() - pub.subscribe(sub) - pub.expectSubscription().expectCancellation() - } - - "reliably cancel subscriptions when UserIn fails early" in assertAllStagesStopped { - val ex = new Exception("hello") - val (sub, out1, out2) = - RunnableGraph - .fromGraph(GraphDSL.create(Source.asSubscriber[ByteString], Sink.head[ByteString], Sink.head[SslTlsInbound])( - (_, _, _)) { implicit b => (s, o1, o2) => - val tls = b.add(clientTls(EagerClose)) - Source.failed[SslTlsOutbound](ex) ~> tls.in1; tls.out1 ~> o1 - o2 <~ tls.out2; tls.in2 <~ s - ClosedShape - }) - .run() - the[Exception] thrownBy Await.result(out1, 1.second) should be(ex) - the[Exception] thrownBy Await.result(out2, 1.second) should be(ex) - Thread.sleep(500) - val pub = TestPublisher.probe() - pub.subscribe(sub) - pub.expectSubscription().expectCancellation() - } - - "complete if TLS connection is truncated" in assertAllStagesStopped { - - val ks = KillSwitches.shared("ks") - - val scenario = SingleBytes - - val outFlow = { - val terminator = BidiFlow.fromFlows(Flow[ByteString], ks.flow[ByteString]) - clientTls(scenario.leftClosing) - .atop(terminator) - .atop(serverTls(scenario.rightClosing).reversed) - .join(debug.via(scenario.flow)) - .via(debug) + object SingleBytes extends PayloadScenario { + val str = "0123456789" + def inputs = str.map(ch => SendBytes(ByteString(ch.toByte))) + def output = ByteString(str) } - val inFlow = Flow[SslTlsInbound] - .collect { case SessionBytes(_, b) => b } - .scan(ByteString.empty)(_ ++ _) - .via(new Timeout(6.seconds.dilated)) - .dropWhile(_.size < scenario.output.size) + object MediumMessages extends PayloadScenario { + val strs = "0123456789".map(d => d.toString * (rnd.nextInt(9000) + 1000)) + def inputs = strs.map(s => SendBytes(ByteString(s))) + def output = ByteString(strs.foldRight("")(_ ++ _)) + } - val f = - Source(scenario.inputs) - .via(outFlow) - .via(inFlow) - .map(result => { - ks.shutdown(); result - }) - .runWith(Sink.last) + object LargeMessages extends PayloadScenario { + // TLS max packet size is 16384 bytes + val strs = "0123456789".map(d => d.toString * (rnd.nextInt(9000) + 17000)) + def inputs = strs.map(s => SendBytes(ByteString(s))) + def output = ByteString(strs.foldRight("")(_ ++ _)) + } - Await.result(f, 8.second.dilated).utf8String should be(scenario.output.utf8String) - } + object EmptyBytesFirst extends PayloadScenario { + def inputs = List(ByteString.empty, ByteString("hello")).map(SendBytes) + def output = ByteString("hello") + } - "verify hostname" in assertAllStagesStopped { - def run(hostName: String): Future[akka.Done] = { - val rhs = Flow[SslTlsInbound].map { - case SessionTruncated => SendBytes(ByteString.empty) + object EmptyBytesInTheMiddle extends PayloadScenario { + def inputs = List(ByteString("hello"), ByteString.empty, ByteString(" world")).map(SendBytes) + def output = ByteString("hello world") + } + + object EmptyBytesLast extends PayloadScenario { + def inputs = List(ByteString("hello"), ByteString.empty).map(SendBytes) + def output = ByteString("hello") + } + + object CompletedImmediately extends PayloadScenario { + override def inputs: immutable.Seq[SslTlsOutbound] = Nil + override def output = ByteString.empty + + override def leftClosing: TLSClosing = EagerClose + override def rightClosing: TLSClosing = EagerClose + } + + // this demonstrates that cancellation is ignored so that the five results make it back + object CancellingRHS extends PayloadScenario { + override def flow = + Flow[SslTlsInbound] + .mapConcat { + case SessionTruncated => SessionTruncated :: Nil + case SessionBytes(s, bytes) => bytes.map(b => SessionBytes(s, ByteString(b))) + } + .take(5) + .mapAsync(5)(x => later(500.millis, system.scheduler)(Future.successful(x))) + .via(super.flow) + override def rightClosing = IgnoreCancel + + val str = "abcdef" * 100 + def inputs = str.map(send) + def output = ByteString(str.take(5)) + } + + object CancellingRHSIgnoresBoth extends PayloadScenario { + override def flow = + Flow[SslTlsInbound] + .mapConcat { + case SessionTruncated => SessionTruncated :: Nil + case SessionBytes(s, bytes) => bytes.map(b => SessionBytes(s, ByteString(b))) + } + .take(5) + .mapAsync(5)(x => later(500.millis, system.scheduler)(Future.successful(x))) + .via(super.flow) + override def rightClosing = IgnoreBoth + val str = "abcdef" * 100 + def inputs = str.map(send) + def output = ByteString(str.take(5)) + } + + object LHSIgnoresBoth extends PayloadScenario { + override def leftClosing = IgnoreBoth + val str = "0123456789" + def inputs = str.map(ch => SendBytes(ByteString(ch.toByte))) + def output = ByteString(str) + } + + object BothSidesIgnoreBoth extends PayloadScenario { + override def leftClosing = IgnoreBoth + override def rightClosing = IgnoreBoth + val str = "0123456789" + def inputs = str.map(ch => SendBytes(ByteString(ch.toByte))) + def output = ByteString(str) + } + + object SessionRenegotiationBySender extends PayloadScenario { + def inputs = List(send("hello"), NegotiateNewSession, send("world")) + def output = ByteString("helloNEWSESSIONworld") + } + + // difference is that the RHS engine will now receive the handshake while trying to send + object SessionRenegotiationByReceiver extends PayloadScenario { + val str = "abcdef" * 100 + def inputs = str.map(send) ++ Seq(NegotiateNewSession) ++ "hello world".map(send) + def output = ByteString(str + "NEWSESSIONhello world") + } + + val logCipherSuite = Flow[SslTlsInbound].map { + var session: SSLSession = null + def setSession(s: SSLSession) = { + session = s + system.log.debug(s"new session: $session (${session.getId.mkString(",")})") + } + + { + case SessionTruncated => SendBytes(ByteString("TRUNCATED")) + case SessionBytes(s, b) if s != session => + setSession(s) + SendBytes(ByteString(s.getCipherSuite) ++ b) case SessionBytes(_, b) => SendBytes(b) } - val clientTls = TLS( - () => createSSLEngine2(sslContext, Client, hostnameVerification = true, hostInfo = Some((hostName, 80))), - EagerClose) - - val flow = clientTls.atop(serverTls(EagerClose).reversed).join(rhs) - - Source.single(SendBytes(ByteString.empty)).via(flow).runWith(Sink.ignore) - } - Await.result(run("akka-remote"), 3.seconds) // CN=akka-remote - val cause = intercept[Exception] { - Await.result(run("unknown.example.org"), 3.seconds) } - val rootCause = - if (JavaVersion.majorVersion >= 11) { - cause.getClass should ===(classOf[SSLHandshakeException]) //General SSLEngine problem - cause.getCause - } else { - cause.getClass should ===(classOf[SSLHandshakeException]) //General SSLEngine problem - val cause2 = cause.getCause - cause2.getClass should ===(classOf[SSLHandshakeException]) //General SSLEngine problem - cause2.getCause + object SessionRenegotiationFirstOne extends PayloadScenario { + override def flow = logCipherSuite + def inputs = NegotiateNewSession.withCipherSuites("TLS_RSA_WITH_AES_128_CBC_SHA") :: send("hello") :: Nil + def output = ByteString("TLS_RSA_WITH_AES_128_CBC_SHAhello") + } + + object SessionRenegotiationFirstTwo extends PayloadScenario { + override def flow = logCipherSuite + def inputs = NegotiateNewSession.withCipherSuites("TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA") :: send("hello") :: Nil + def output = ByteString("TLS_ECDHE_RSA_WITH_AES_128_CBC_SHAhello") + } + + val scenarios = + Seq( + SingleBytes, + MediumMessages, + LargeMessages, + EmptyBytesFirst, + EmptyBytesInTheMiddle, + EmptyBytesLast, + CompletedImmediately, + CancellingRHS, + CancellingRHSIgnoresBoth, + LHSIgnoresBoth, + BothSidesIgnoreBoth) ++ + (if (protocol == "TLSv1.2") + Seq( + SessionRenegotiationBySender, + SessionRenegotiationByReceiver, + SessionRenegotiationFirstOne, + SessionRenegotiationFirstTwo) + else // TLSv1.3 doesn't support renegotiation + Nil) + + for { + commPattern <- communicationPatterns + scenario <- scenarios + } { + s"work in mode ${commPattern.name} while sending ${scenario.name}" in assertAllStagesStopped { + val onRHS = debug.via(scenario.flow) + val output = + Source(scenario.inputs) + .via(commPattern.decorateFlow(scenario.leftClosing, scenario.rightClosing, onRHS)) + .via(new SimpleLinearGraphStage[SslTlsInbound] { + override def createLogic(inheritedAttributes: Attributes) = + new GraphStageLogic(shape) with InHandler with OutHandler { + setHandlers(in, out, this) + + override def onPush() = push(out, grab(in)) + override def onPull() = pull(in) + + override def onDownstreamFinish(cause: Throwable) = { + system.log.debug(s"me cancelled, cause {}", cause) + completeStage() + } + } + }) + .via(debug) + .collect { case SessionBytes(_, b) => b } + .scan(ByteString.empty)(_ ++ _) + .filter(_.nonEmpty) + .via(new Timeout(10.seconds)) + .dropWhile(_.size < scenario.output.size) + .runWith(Sink.headOption) + + Await.result(output, 12.seconds).getOrElse(ByteString.empty).utf8String should be(scenario.output.utf8String) + + commPattern.cleanup() } - rootCause.getClass should ===(classOf[CertificateException]) - rootCause.getMessage should ===("No name matching unknown.example.org found") - } + } + "emit an error if the TLS handshake fails certificate checks" in assertAllStagesStopped { + val getError = Flow[SslTlsInbound] + .map[Either[SslTlsInbound, SSLException]](i => Left(i)) + .recover { case e: SSLException => Right(e) } + .collect { case Right(e) => e } + .toMat(Sink.head)(Keep.right) + + val simple = Flow.fromSinkAndSourceMat(getError, Source.maybe[SslTlsOutbound])(Keep.left) + + // The creation of actual TCP connections is necessary. It is the easiest way to decouple the client and server + // under error conditions, and has the bonus of matching most actual SSL deployments. + val (server, serverErr) = Tcp() + .bind("localhost", 0) + .mapAsync(1)(c => + c.flow.joinMat(serverTls(IgnoreBoth).reversed.joinMat(simple)(Keep.right))(Keep.right).run()) + .toMat(Sink.head)(Keep.both) + .run() + + val clientErr = simple + .join(badClientTls(IgnoreBoth)) + .join(Tcp().outgoingConnection(Await.result(server, 1.second).localAddress)) + .run() + + Await.result(serverErr, 1.second).getMessage should include("certificate_unknown") + val clientErrText = Await.result(clientErr, 1.second).getMessage + if (JavaVersion.majorVersion >= 11) + clientErrText should include("unable to find valid certification path to requested target") + else + clientErrText should equal("General SSLEngine problem") + } + + "reliably cancel subscriptions when TransportIn fails early" in assertAllStagesStopped { + val ex = new Exception("hello") + val (sub, out1, out2) = + RunnableGraph + .fromGraph( + GraphDSL.create(Source.asSubscriber[SslTlsOutbound], Sink.head[ByteString], Sink.head[SslTlsInbound])( + (_, _, _)) { implicit b => (s, o1, o2) => + val tls = b.add(clientTls(EagerClose)) + s ~> tls.in1 + tls.out1 ~> o1 + o2 <~ tls.out2 + tls.in2 <~ Source.failed(ex) + ClosedShape + }) + .run() + the[Exception] thrownBy Await.result(out1, 1.second) should be(ex) + the[Exception] thrownBy Await.result(out2, 1.second) should be(ex) + Thread.sleep(500) + val pub = TestPublisher.probe() + pub.subscribe(sub) + pub.expectSubscription().expectCancellation() + } + + "reliably cancel subscriptions when UserIn fails early" in assertAllStagesStopped { + val ex = new Exception("hello") + val (sub, out1, out2) = + RunnableGraph + .fromGraph( + GraphDSL.create(Source.asSubscriber[ByteString], Sink.head[ByteString], Sink.head[SslTlsInbound])( + (_, _, _)) { implicit b => (s, o1, o2) => + val tls = b.add(clientTls(EagerClose)) + Source.failed[SslTlsOutbound](ex) ~> tls.in1 + tls.out1 ~> o1 + o2 <~ tls.out2 + tls.in2 <~ s + ClosedShape + }) + .run() + the[Exception] thrownBy Await.result(out1, 1.second) should be(ex) + the[Exception] thrownBy Await.result(out2, 1.second) should be(ex) + Thread.sleep(500) + val pub = TestPublisher.probe() + pub.subscribe(sub) + pub.expectSubscription().expectCancellation() + } + + "complete if TLS connection is truncated" in assertAllStagesStopped { + + val ks = KillSwitches.shared("ks") + + val scenario = SingleBytes + + val outFlow = { + val terminator = BidiFlow.fromFlows(Flow[ByteString], ks.flow[ByteString]) + clientTls(scenario.leftClosing) + .atop(terminator) + .atop(serverTls(scenario.rightClosing).reversed) + .join(debug.via(scenario.flow)) + .via(debug) + } + + val inFlow = Flow[SslTlsInbound] + .collect { case SessionBytes(_, b) => b } + .scan(ByteString.empty)(_ ++ _) + .via(new Timeout(6.seconds.dilated)) + .dropWhile(_.size < scenario.output.size) + + val f = + Source(scenario.inputs) + .via(outFlow) + .via(inFlow) + .map(result => { + ks.shutdown() + result + }) + .runWith(Sink.last) + Await.result(f, 8.second.dilated).utf8String should be(scenario.output.utf8String) + } + + "verify hostname" in assertAllStagesStopped { + def run(hostName: String): Future[akka.Done] = { + val rhs = Flow[SslTlsInbound].map { + case SessionTruncated => SendBytes(ByteString.empty) + case SessionBytes(_, b) => SendBytes(b) + } + val clientTls = TLS( + () => createSSLEngine2(sslContext, Client, hostnameVerification = true, hostInfo = Some((hostName, 80))), + EagerClose) + + val flow = clientTls.atop(serverTls(EagerClose).reversed).join(rhs) + + Source.single(SendBytes(ByteString.empty)).via(flow).runWith(Sink.ignore) + } + + Await.result(run("akka-remote"), 3.seconds) // CN=akka-remote + val cause = intercept[Exception] { + Await.result(run("unknown.example.org"), 3.seconds) + } + + val rootCause = + if (JavaVersion.majorVersion >= 11) { + cause.getClass should ===(classOf[SSLHandshakeException]) //General SSLEngine problem + cause.getCause + } else { + cause.getClass should ===(classOf[SSLHandshakeException]) //General SSLEngine problem + val cause2 = cause.getCause + cause2.getClass should ===(classOf[SSLHandshakeException]) //General SSLEngine problem + cause2.getCause + } + rootCause.getClass should ===(classOf[CertificateException]) + rootCause.getMessage should ===("No name matching unknown.example.org found") + } + } } "A SslTlsPlacebo" must { diff --git a/akka-stream-tests/src/test/scala/akka/stream/scaladsl/FlowFlatMapPrefixSpec.scala b/akka-stream-tests/src/test/scala/akka/stream/scaladsl/FlowFlatMapPrefixSpec.scala index 0fc84c31ae..cbc39b20da 100644 --- a/akka-stream-tests/src/test/scala/akka/stream/scaladsl/FlowFlatMapPrefixSpec.scala +++ b/akka-stream-tests/src/test/scala/akka/stream/scaladsl/FlowFlatMapPrefixSpec.scala @@ -8,6 +8,7 @@ import akka.{ Done, NotUsed } import akka.stream.{ AbruptStageTerminationException, AbruptTerminationException, + Attributes, Materializer, NeverMaterializedException, SubscriptionWithCancelException @@ -19,526 +20,572 @@ import akka.stream.testkit.scaladsl.StreamTestKit.assertAllStagesStopped class FlowFlatMapPrefixSpec extends StreamSpec { def src10(i: Int = 0) = Source(i until (i + 10)) - "A PrefixAndDownstream" must { - - "work in the simple identity case" in assertAllStagesStopped { - src10() - .flatMapPrefixMat(2) { _ => - Flow[Int] - }(Keep.left) - .runWith(Sink.seq[Int]) - .futureValue should ===(2 until 10) - } - - "expose mat value in the simple identity case" in assertAllStagesStopped { - val (prefixF, suffixF) = src10() - .flatMapPrefixMat(2) { prefix => - Flow[Int].mapMaterializedValue(_ => prefix) - }(Keep.right) - .toMat(Sink.seq)(Keep.both) - .run() - - prefixF.futureValue should ===(0 until 2) - suffixF.futureValue should ===(2 until 10) - } - - "work when source is exactly the required prefix" in assertAllStagesStopped { - val (prefixF, suffixF) = src10() - .flatMapPrefixMat(10) { prefix => - Flow[Int].mapMaterializedValue(_ => prefix) - }(Keep.right) - .toMat(Sink.seq)(Keep.both) - .run() - - prefixF.futureValue should ===(0 until 10) - suffixF.futureValue should be(empty) - } - - "work when source has less than the required prefix" in assertAllStagesStopped { - val (prefixF, suffixF) = src10() - .flatMapPrefixMat(20) { prefix => - Flow[Int].mapMaterializedValue(_ => prefix) - }(Keep.right) - .toMat(Sink.seq)(Keep.both) - .run() - - prefixF.futureValue should ===(0 until 10) - suffixF.futureValue should be(empty) - } - - "simple identity case when downstream completes before consuming the entire stream" in assertAllStagesStopped { - val (prefixF, suffixF) = Source(0 until 100) - .flatMapPrefixMat(10) { prefix => - Flow[Int].mapMaterializedValue(_ => prefix) - }(Keep.right) - .take(10) - .toMat(Sink.seq)(Keep.both) - .run() - - prefixF.futureValue should ===(0 until 10) - suffixF.futureValue should ===(10 until 20) - } - - "propagate failure to create the downstream flow" in assertAllStagesStopped { - val suffixF = Source(0 until 100) - .flatMapPrefixMat(10) { prefix => - throw TE(s"I hate mondays! (${prefix.size})") - }(Keep.right) - .to(Sink.ignore) - .run() - - val ex = suffixF.failed.futureValue - ex.getCause should not be null - ex.getCause should ===(TE("I hate mondays! (10)")) - } - - "propagate flow failures" in assertAllStagesStopped { - val (prefixF, suffixF) = Source(0 until 100) - .flatMapPrefixMat(10) { prefix => - Flow[Int].mapMaterializedValue(_ => prefix).map { - case 15 => throw TE("don't like 15 either!") - case n => n - } - }(Keep.right) - .toMat(Sink.ignore)(Keep.both) - .run() - prefixF.futureValue should ===(0 until 10) - val ex = suffixF.failed.futureValue - ex should ===(TE("don't like 15 either!")) - } - - "produce multiple elements per input" in assertAllStagesStopped { - val (prefixF, suffixF) = src10() - .flatMapPrefixMat(7) { prefix => - Flow[Int].mapMaterializedValue(_ => prefix).mapConcat(n => List.fill(n - 6)(n)) - }(Keep.right) - .toMat(Sink.seq[Int])(Keep.both) - .run() - - prefixF.futureValue should ===(0 until 7) - suffixF.futureValue should ===(7 :: 8 :: 8 :: 9 :: 9 :: 9 :: Nil) - } - - "succeed when upstream produces no elements" in assertAllStagesStopped { - val (prefixF, suffixF) = Source - .empty[Int] - .flatMapPrefixMat(7) { prefix => - Flow[Int].mapMaterializedValue(_ => prefix).mapConcat(n => List.fill(n - 6)(n)) - }(Keep.right) - .toMat(Sink.seq[Int])(Keep.both) - .run() - - prefixF.futureValue should be(empty) - suffixF.futureValue should be(empty) - } - - "apply materialized flow's semantics when upstream produces no elements" in assertAllStagesStopped { - val (prefixF, suffixF) = Source - .empty[Int] - .flatMapPrefixMat(7) { prefix => - Flow[Int].mapMaterializedValue(_ => prefix).mapConcat(n => List.fill(n - 6)(n)).prepend(Source(100 to 101)) - }(Keep.right) - .toMat(Sink.seq[Int])(Keep.both) - .run() - - prefixF.futureValue should be(empty) - suffixF.futureValue should ===(100 :: 101 :: Nil) - } - - "handles upstream completion" in assertAllStagesStopped { - val publisher = TestPublisher.manualProbe[Int]() - val subscriber = TestSubscriber.manualProbe[Int]() - - val matValue = Source - .fromPublisher(publisher) - .flatMapPrefixMat(2) { prefix => - Flow[Int].mapMaterializedValue(_ => prefix).prepend(Source(100 to 101)) - }(Keep.right) - .to(Sink.fromSubscriber(subscriber)) - .run() - - matValue.value should be(empty) - - val upstream = publisher.expectSubscription() - val downstream = subscriber.expectSubscription() - - downstream.request(1000) - - upstream.expectRequest() - //completing publisher - upstream.sendComplete() - - matValue.futureValue should ===(Nil) - - subscriber.expectNext(100) - - subscriber.expectNext(101).expectComplete() - - } - - "work when materialized flow produces no downstream elements" in assertAllStagesStopped { - val (prefixF, suffixF) = Source(0 until 100) - .flatMapPrefixMat(4) { prefix => - Flow[Int].mapMaterializedValue(_ => prefix).filter(_ => false) - }(Keep.right) - .toMat(Sink.seq)(Keep.both) - .run() - - prefixF.futureValue should ===(0 until 4) - suffixF.futureValue should be(empty) - } - - "work when materialized flow does not consume upstream" in assertAllStagesStopped { - val (prefixF, suffixF) = Source(0 until 100) - .map { i => - i should be <= 4 - i - } - .flatMapPrefixMat(4) { prefix => - Flow[Int].mapMaterializedValue(_ => prefix).take(0) - }(Keep.right) - .toMat(Sink.seq)(Keep.both) - .run() - - prefixF.futureValue should ===(0 until 4) - suffixF.futureValue should be(empty) - } - - "work when materialized flow cancels upstream but keep producing" in assertAllStagesStopped { - val (prefixF, suffixF) = src10() - .flatMapPrefixMat(4) { prefix => - Flow[Int].mapMaterializedValue(_ => prefix).take(0).concat(Source(11 to 12)) - }(Keep.right) - .toMat(Sink.seq)(Keep.both) - .run() - - prefixF.futureValue should ===(0 until 4) - suffixF.futureValue should ===(11 :: 12 :: Nil) - } - - "propagate materialization failure (when application of 'f' succeeds)" in assertAllStagesStopped { - val (prefixF, suffixF) = src10() - .flatMapPrefixMat(4) { prefix => - Flow[Int].mapMaterializedValue(_ => throw TE(s"boom-bada-bang (${prefix.size})")) - }(Keep.right) - .toMat(Sink.seq)(Keep.both) - .run() - - prefixF.failed.futureValue should be(a[NeverMaterializedException]) - prefixF.failed.futureValue.getCause should ===(TE("boom-bada-bang (4)")) - suffixF.failed.futureValue should ===(TE("boom-bada-bang (4)")) - } - - "succeed when materialized flow completes downstream but keep consuming elements" in assertAllStagesStopped { - val (prefixAndTailF, suffixF) = src10() - .flatMapPrefixMat(4) { prefix => - Flow[Int] - .mapMaterializedValue(_ => prefix) - .viaMat { - Flow.fromSinkAndSourceMat(Sink.seq[Int], Source.empty[Int])(Keep.left) - }(Keep.both) - }(Keep.right) - .toMat(Sink.seq)(Keep.both) - .run() - - suffixF.futureValue should be(empty) - val (prefix, suffix) = prefixAndTailF.futureValue - prefix should ===(0 until 4) - suffix.futureValue should ===(4 until 10) - } - - "propagate downstream cancellation via the materialized flow" in assertAllStagesStopped { - val publisher = TestPublisher.manualProbe[Int]() - val subscriber = TestSubscriber.manualProbe[Int]() - - val ((srcWatchTermF, innerMatVal), sinkMatVal) = src10() - .watchTermination()(Keep.right) - .flatMapPrefixMat(2) { prefix => - prefix should ===(0 until 2) - Flow.fromSinkAndSource(Sink.fromSubscriber(subscriber), Source.fromPublisher(publisher)) - }(Keep.both) - .take(1) - .toMat(Sink.seq)(Keep.both) - .run() - - val subUpstream = publisher.expectSubscription() - val subDownstream = subscriber.expectSubscription() - - // inner stream was materialized - innerMatVal.futureValue should ===(NotUsed) - - subUpstream.expectRequest() should be >= (1L) - subDownstream.request(1) - subscriber.expectNext(2) - subUpstream.sendNext(22) - subUpstream.expectCancellation() // because take(1) - // this should not automatically pass the cancellation upstream of nested flow - srcWatchTermF.isCompleted should ===(false) - sinkMatVal.futureValue should ===(Seq(22)) - - // the nested flow then decides to cancel, which moves upstream - subDownstream.cancel() - srcWatchTermF.futureValue should ===(Done) - } - - "early downstream cancellation is later handed out to materialized flow" in assertAllStagesStopped { - val publisher = TestPublisher.manualProbe[Int]() - val subscriber = TestSubscriber.manualProbe[Int]() - - val (srcWatchTermF, matFlowWatchTermFF) = Source - .fromPublisher(publisher) - .watchTermination()(Keep.right) - .flatMapPrefixMat(3) { prefix => - prefix should ===(0 until 3) - Flow[Int].watchTermination()(Keep.right) - }(Keep.both) - .to(Sink.fromSubscriber(subscriber)) - .run() - val matFlowWatchTerm = matFlowWatchTermFF.flatten - - matFlowWatchTerm.value should be(empty) - srcWatchTermF.value should be(empty) - - val subDownstream = subscriber.expectSubscription() - val subUpstream = publisher.expectSubscription() - subDownstream.request(1) - subUpstream.expectRequest() should be >= (1L) - subUpstream.sendNext(0) - subUpstream.sendNext(1) - subDownstream.cancel() - - //subflow not materialized yet, hence mat value (future) isn't ready yet - matFlowWatchTerm.value should be(empty) - srcWatchTermF.value should be(empty) - - //this one is sent AFTER downstream cancellation - subUpstream.sendNext(2) - - subUpstream.expectCancellation() - - matFlowWatchTerm.futureValue should ===(Done) - srcWatchTermF.futureValue should ===(Done) - - } - - "early downstream failure is deferred until prefix completion" in assertAllStagesStopped { - val publisher = TestPublisher.manualProbe[Int]() - val subscriber = TestSubscriber.manualProbe[Int]() - - val (srcWatchTermF, matFlowWatchTermFF) = Source - .fromPublisher(publisher) - .watchTermination()(Keep.right) - .flatMapPrefixMat(3) { prefix => - prefix should ===(0 until 3) - Flow[Int].watchTermination()(Keep.right) - }(Keep.both) - .to(Sink.fromSubscriber(subscriber)) - .run() - val matFlowWatchTerm = matFlowWatchTermFF.flatten - - matFlowWatchTerm.value should be(empty) - srcWatchTermF.value should be(empty) - - val subDownstream = subscriber.expectSubscription() - val subUpstream = publisher.expectSubscription() - subDownstream.request(1) - subUpstream.expectRequest() should be >= (1L) - subUpstream.sendNext(0) - subUpstream.sendNext(1) - subDownstream.asInstanceOf[SubscriptionWithCancelException].cancel(TE("that again?!")) - - matFlowWatchTerm.value should be(empty) - srcWatchTermF.value should be(empty) - - subUpstream.sendNext(2) - - matFlowWatchTerm.failed.futureValue should ===(TE("that again?!")) - srcWatchTermF.failed.futureValue should ===(TE("that again?!")) - - subUpstream.expectCancellation() - } - - "downstream failure is propagated via the materialized flow" in assertAllStagesStopped { - val publisher = TestPublisher.manualProbe[Int]() - val subscriber = TestSubscriber.manualProbe[Int]() - - val ((srcWatchTermF, notUsedF), suffixF) = src10() - .watchTermination()(Keep.right) - .flatMapPrefixMat(2) { prefix => - prefix should ===(0 until 2) - Flow.fromSinkAndSourceCoupled(Sink.fromSubscriber(subscriber), Source.fromPublisher(publisher)) - }(Keep.both) - .map { - case 2 => 2 - case 3 => throw TE("3!?!?!?") - case i => fail(s"unexpected value $i") - } - .toMat(Sink.seq)(Keep.both) - .run() - - notUsedF.value should be(empty) - suffixF.value should be(empty) - srcWatchTermF.value should be(empty) - - val subUpstream = publisher.expectSubscription() - val subDownstream = subscriber.expectSubscription() - - notUsedF.futureValue should ===(NotUsed) - - subUpstream.expectRequest() should be >= (1L) - subDownstream.request(1) - subscriber.expectNext(2) - subUpstream.sendNext(2) - subDownstream.request(1) - subscriber.expectNext(3) - subUpstream.sendNext(3) - subUpstream.expectCancellation() should ===(TE("3!?!?!?")) - subscriber.expectError(TE("3!?!?!?")) - - suffixF.failed.futureValue should ===(TE("3!?!?!?")) - srcWatchTermF.failed.futureValue should ===(TE("3!?!?!?")) - } - - "complete mat value with failures on abrupt termination before materializing the flow" in assertAllStagesStopped { - val mat = Materializer(system) - val publisher = TestPublisher.manualProbe[Int]() - - val flow = Source - .fromPublisher(publisher) - .flatMapPrefixMat(2) { prefix => - fail(s"unexpected prefix (length = ${prefix.size})") - Flow[Int] - }(Keep.right) - .toMat(Sink.ignore)(Keep.both) - - val (prefixF, doneF) = flow.run()(mat) - - publisher.expectSubscription() - prefixF.value should be(empty) - doneF.value should be(empty) - - mat.shutdown() - - prefixF.failed.futureValue match { - case _: AbruptTerminationException => - case ex: NeverMaterializedException => - ex.getCause should not be null - ex.getCause should be(a[AbruptTerminationException]) + for { + att <- List( + Attributes.NestedMaterializationCancellationPolicy.EagerCancellation, + Attributes.NestedMaterializationCancellationPolicy.PropagateToNested) + delayDownstreanCancellation = att.propagateToNestedMaterialization + attributes = Attributes(att) + } { + + s"A PrefixAndDownstream with $att" must { + + "work in the simple identity case" in assertAllStagesStopped { + src10() + .flatMapPrefixMat(2) { _ => + Flow[Int] + }(Keep.left) + .withAttributes(attributes) + .runWith(Sink.seq[Int]) + .futureValue should ===(2 until 10) } - doneF.failed.futureValue should be(a[AbruptTerminationException]) - } - "respond to abrupt termination after flow materialization" in assertAllStagesStopped { - val mat = Materializer(system) - val countFF = src10() - .flatMapPrefixMat(2) { prefix => - prefix should ===(0 until 2) - Flow[Int] - .concat(Source.repeat(3)) - .fold(0L) { - case (acc, _) => acc + 1 + "expose mat value in the simple identity case" in assertAllStagesStopped { + val (prefixF, suffixF) = src10() + .flatMapPrefixMat(2) { prefix => + Flow[Int].mapMaterializedValue(_ => prefix) + }(Keep.right) + .toMat(Sink.seq)(Keep.both) + .withAttributes(attributes) + .run + + prefixF.futureValue should ===(0 until 2) + suffixF.futureValue should ===(2 until 10) + } + + "work when source is exactly the required prefix" in assertAllStagesStopped { + val (prefixF, suffixF) = src10() + .flatMapPrefixMat(10) { prefix => + Flow[Int].mapMaterializedValue(_ => prefix) + }(Keep.right) + .toMat(Sink.seq)(Keep.both) + .withAttributes(attributes) + .run + + prefixF.futureValue should ===(0 until 10) + suffixF.futureValue should be(empty) + } + + "work when source has less than the required prefix" in assertAllStagesStopped { + val (prefixF, suffixF) = src10() + .flatMapPrefixMat(20) { prefix => + Flow[Int].mapMaterializedValue(_ => prefix) + }(Keep.right) + .toMat(Sink.seq)(Keep.both) + .withAttributes(attributes) + .run + + prefixF.futureValue should ===(0 until 10) + suffixF.futureValue should be(empty) + } + + "simple identity case when downstream completes before consuming the entire stream" in assertAllStagesStopped { + val (prefixF, suffixF) = Source(0 until 100) + .flatMapPrefixMat(10) { prefix => + Flow[Int].mapMaterializedValue(_ => prefix) + }(Keep.right) + .take(10) + .toMat(Sink.seq)(Keep.both) + .withAttributes(attributes) + .run + + prefixF.futureValue should ===(0 until 10) + suffixF.futureValue should ===(10 until 20) + } + + "propagate failure to create the downstream flow" in assertAllStagesStopped { + val suffixF = Source(0 until 100) + .flatMapPrefixMat(10) { prefix => + throw TE(s"I hate mondays! (${prefix.size})") + }(Keep.right) + .to(Sink.ignore) + .withAttributes(attributes) + .run + + val ex = suffixF.failed.futureValue + ex.getCause should not be null + ex.getCause should ===(TE("I hate mondays! (10)")) + } + + "propagate flow failures" in assertAllStagesStopped { + val (prefixF, suffixF) = Source(0 until 100) + .flatMapPrefixMat(10) { prefix => + Flow[Int].mapMaterializedValue(_ => prefix).map { + case 15 => throw TE("don't like 15 either!") + case n => n } - .alsoToMat(Sink.head)(Keep.right) - }(Keep.right) - .to(Sink.ignore) - .run()(mat) - val countF = countFF.futureValue - //at this point we know the flow was materialized, now we can stop the materializer - mat.shutdown() - //expect the nested flow to be terminated abruptly. - countF.failed.futureValue should be(a[AbruptStageTerminationException]) - } - - "behave like via when n = 0" in assertAllStagesStopped { - val (prefixF, suffixF) = src10() - .flatMapPrefixMat(0) { prefix => - prefix should be(empty) - Flow[Int].mapMaterializedValue(_ => prefix) - }(Keep.right) - .toMat(Sink.seq)(Keep.both) - .run() - - prefixF.futureValue should be(empty) - suffixF.futureValue should ===(0 until 10) - } - - "behave like via when n = 0 and upstream produces no elements" in assertAllStagesStopped { - val (prefixF, suffixF) = Source - .empty[Int] - .flatMapPrefixMat(0) { prefix => - prefix should be(empty) - Flow[Int].mapMaterializedValue(_ => prefix) - }(Keep.right) - .toMat(Sink.seq)(Keep.both) - .run() - - prefixF.futureValue should be(empty) - suffixF.futureValue should be(empty) - } - - "propagate errors during flow's creation when n = 0" in assertAllStagesStopped { - val (prefixF, suffixF) = src10() - .flatMapPrefixMat(0) { prefix => - prefix should be(empty) - throw TE("not this time my friend!") - Flow[Int].mapMaterializedValue(_ => prefix) - }(Keep.right) - .toMat(Sink.seq)(Keep.both) - .run() - - prefixF.failed.futureValue should be(a[NeverMaterializedException]) - prefixF.failed.futureValue.getCause === (TE("not this time my friend!")) - suffixF.failed.futureValue should ===(TE("not this time my friend!")) - } - - "propagate materialization failures when n = 0" in assertAllStagesStopped { - val (prefixF, suffixF) = src10() - .flatMapPrefixMat(0) { prefix => - prefix should be(empty) - Flow[Int].mapMaterializedValue(_ => throw TE("Bang! no materialization this time")) - }(Keep.right) - .toMat(Sink.seq)(Keep.both) - .run() - - prefixF.failed.futureValue should be(a[NeverMaterializedException]) - prefixF.failed.futureValue.getCause === (TE("Bang! no materialization this time")) - suffixF.failed.futureValue should ===(TE("Bang! no materialization this time")) - } - - "run a detached flow" in assertAllStagesStopped { - val publisher = TestPublisher.manualProbe[Int]() - val subscriber = TestSubscriber.manualProbe[String]() - - val detachedFlow = Flow.fromSinkAndSource(Sink.cancelled[Int], Source(List("a", "b", "c"))).via { - Flow.fromSinkAndSource(Sink.fromSubscriber(subscriber), Source.empty[Int]) + }(Keep.right) + .toMat(Sink.ignore)(Keep.both) + .withAttributes(attributes) + .run + prefixF.futureValue should ===(0 until 10) + val ex = suffixF.failed.futureValue + ex should ===(TE("don't like 15 either!")) } - val fHeadOpt = Source - .fromPublisher(publisher) - .flatMapPrefix(2) { prefix => - prefix should ===(0 until 2) - detachedFlow + + "produce multiple elements per input" in assertAllStagesStopped { + val (prefixF, suffixF) = src10() + .flatMapPrefixMat(7) { prefix => + Flow[Int].mapMaterializedValue(_ => prefix).mapConcat(n => List.fill(n - 6)(n)) + }(Keep.right) + .toMat(Sink.seq[Int])(Keep.both) + .withAttributes(attributes) + .run() + + prefixF.futureValue should ===(0 until 7) + suffixF.futureValue should ===(7 :: 8 :: 8 :: 9 :: 9 :: 9 :: Nil) + } + + "succeed when upstream produces no elements" in assertAllStagesStopped { + val (prefixF, suffixF) = Source + .empty[Int] + .flatMapPrefixMat(7) { prefix => + Flow[Int].mapMaterializedValue(_ => prefix).mapConcat(n => List.fill(n - 6)(n)) + }(Keep.right) + .toMat(Sink.seq[Int])(Keep.both) + .withAttributes(attributes) + .run() + + prefixF.futureValue should be(empty) + suffixF.futureValue should be(empty) + } + + "apply materialized flow's semantics when upstream produces no elements" in assertAllStagesStopped { + val (prefixF, suffixF) = Source + .empty[Int] + .flatMapPrefixMat(7) { prefix => + Flow[Int].mapMaterializedValue(_ => prefix).mapConcat(n => List.fill(n - 6)(n)).prepend(Source(100 to 101)) + }(Keep.right) + .toMat(Sink.seq[Int])(Keep.both) + .withAttributes(attributes) + .run() + + prefixF.futureValue should be(empty) + suffixF.futureValue should ===(100 :: 101 :: Nil) + } + + "handles upstream completion" in assertAllStagesStopped { + val publisher = TestPublisher.manualProbe[Int]() + val subscriber = TestSubscriber.manualProbe[Int]() + + val matValue = Source + .fromPublisher(publisher) + .flatMapPrefixMat(2) { prefix => + Flow[Int].mapMaterializedValue(_ => prefix).prepend(Source(100 to 101)) + }(Keep.right) + .to(Sink.fromSubscriber(subscriber)) + .withAttributes(attributes) + .run() + + matValue.value should be(empty) + + val upstream = publisher.expectSubscription() + val downstream = subscriber.expectSubscription() + + downstream.request(1000) + + upstream.expectRequest() + //completing publisher + upstream.sendComplete() + + matValue.futureValue should ===(Nil) + + subscriber.expectNext(100) + + subscriber.expectNext(101).expectComplete() + + } + + "work when materialized flow produces no downstream elements" in assertAllStagesStopped { + val (prefixF, suffixF) = Source(0 until 100) + .flatMapPrefixMat(4) { prefix => + Flow[Int].mapMaterializedValue(_ => prefix).filter(_ => false) + }(Keep.right) + .toMat(Sink.seq)(Keep.both) + .withAttributes(attributes) + .run + + prefixF.futureValue should ===(0 until 4) + suffixF.futureValue should be(empty) + } + + "work when materialized flow does not consume upstream" in assertAllStagesStopped { + val (prefixF, suffixF) = Source(0 until 100) + .map { i => + i should be <= 4 + i + } + .flatMapPrefixMat(4) { prefix => + Flow[Int].mapMaterializedValue(_ => prefix).take(0) + }(Keep.right) + .toMat(Sink.seq)(Keep.both) + .withAttributes(attributes) + .withAttributes(attributes) + .run + + prefixF.futureValue should ===(0 until 4) + suffixF.futureValue should be(empty) + } + + "work when materialized flow cancels upstream but keep producing" in assertAllStagesStopped { + val (prefixF, suffixF) = src10() + .flatMapPrefixMat(4) { prefix => + Flow[Int].mapMaterializedValue(_ => prefix).take(0).concat(Source(11 to 12)) + }(Keep.right) + .toMat(Sink.seq)(Keep.both) + .withAttributes(attributes) + .run + + prefixF.futureValue should ===(0 until 4) + suffixF.futureValue should ===(11 :: 12 :: Nil) + } + + "propagate materialization failure (when application of 'f' succeeds)" in assertAllStagesStopped { + val (prefixF, suffixF) = src10() + .flatMapPrefixMat(4) { prefix => + Flow[Int].mapMaterializedValue(_ => throw TE(s"boom-bada-bang (${prefix.size})")) + }(Keep.right) + .toMat(Sink.seq)(Keep.both) + .withAttributes(attributes) + .run + + prefixF.failed.futureValue should be(a[NeverMaterializedException]) + prefixF.failed.futureValue.getCause should ===(TE("boom-bada-bang (4)")) + suffixF.failed.futureValue should ===(TE("boom-bada-bang (4)")) + } + + "succeed when materialized flow completes downstream but keep consuming elements" in assertAllStagesStopped { + val (prefixAndTailF, suffixF) = src10() + .flatMapPrefixMat(4) { prefix => + Flow[Int] + .mapMaterializedValue(_ => prefix) + .viaMat { + Flow.fromSinkAndSourceMat(Sink.seq[Int], Source.empty[Int])(Keep.left) + }(Keep.both) + }(Keep.right) + .toMat(Sink.seq)(Keep.both) + .withAttributes(attributes) + .run + + suffixF.futureValue should be(empty) + val (prefix, suffix) = prefixAndTailF.futureValue + prefix should ===(0 until 4) + suffix.futureValue should ===(4 until 10) + } + + "propagate downstream cancellation via the materialized flow" in assertAllStagesStopped { + val publisher = TestPublisher.manualProbe[Int]() + val subscriber = TestSubscriber.manualProbe[Int]() + + val ((srcWatchTermF, innerMatVal), sinkMatVal) = src10() + .watchTermination()(Keep.right) + .flatMapPrefixMat(2) { prefix => + prefix should ===(0 until 2) + Flow.fromSinkAndSource(Sink.fromSubscriber(subscriber), Source.fromPublisher(publisher)) + }(Keep.both) + .take(1) + .toMat(Sink.seq)(Keep.both) + .withAttributes(attributes) + .run() + + val subUpstream = publisher.expectSubscription() + val subDownstream = subscriber.expectSubscription() + + // inner stream was materialized + innerMatVal.futureValue should ===(NotUsed) + + subUpstream.expectRequest() should be >= (1L) + subDownstream.request(1) + subscriber.expectNext(2) + subUpstream.sendNext(22) + subUpstream.expectCancellation() // because take(1) + // this should not automatically pass the cancellation upstream of nested flow + srcWatchTermF.isCompleted should ===(false) + sinkMatVal.futureValue should ===(Seq(22)) + + // the nested flow then decides to cancel, which moves upstream + subDownstream.cancel() + srcWatchTermF.futureValue should ===(Done) + } + + "early downstream cancellation is later handed out to materialized flow" in assertAllStagesStopped { + val publisher = TestPublisher.manualProbe[Int]() + val subscriber = TestSubscriber.manualProbe[Int]() + + val (srcWatchTermF, matFlowWatchTermFF) = Source + .fromPublisher(publisher) + .watchTermination()(Keep.right) + .flatMapPrefixMat(3) { prefix => + prefix should ===(0 until 3) + Flow[Int].watchTermination()(Keep.right) + }(Keep.both) + .to(Sink.fromSubscriber(subscriber)) + .withAttributes(attributes) + .run() + val matFlowWatchTerm = matFlowWatchTermFF.flatten + + matFlowWatchTerm.value should be(empty) + srcWatchTermF.value should be(empty) + + val subDownstream = subscriber.expectSubscription() + val subUpstream = publisher.expectSubscription() + subDownstream.request(1) + subUpstream.expectRequest() should be >= (1L) + subUpstream.sendNext(0) + subUpstream.sendNext(1) + subDownstream.cancel() + + //subflow not materialized yet, hence mat value (future) isn't ready yet + matFlowWatchTerm.value should be(empty) + + if (delayDownstreanCancellation) { + srcWatchTermF.value should be(empty) + //this one is sent AFTER downstream cancellation + subUpstream.sendNext(2) + + subUpstream.expectCancellation() + + matFlowWatchTerm.futureValue should ===(Done) + srcWatchTermF.futureValue should ===(Done) + } else { + srcWatchTermF.futureValue should ===(Done) + matFlowWatchTerm.failed.futureValue should be(a[NeverMaterializedException]) } - .runWith(Sink.headOption) + } - subscriber.expectNoMessage() - val subsc = publisher.expectSubscription() - subsc.expectRequest() should be >= 2L - subsc.sendNext(0) - subscriber.expectNoMessage() - subsc.sendNext(1) - val sinkSubscription = subscriber.expectSubscription() - //this indicates - fHeadOpt.futureValue should be(empty) + "early downstream failure is deferred until prefix completion" in assertAllStagesStopped { + val publisher = TestPublisher.manualProbe[Int]() + val subscriber = TestSubscriber.manualProbe[Int]() - //materializef flow immediately cancels upstream - subsc.expectCancellation() - //at this point both ends of the 'external' fow are closed + val (srcWatchTermF, matFlowWatchTermFF) = Source + .fromPublisher(publisher) + .watchTermination()(Keep.right) + .flatMapPrefixMat(3) { prefix => + prefix should ===(0 until 3) + Flow[Int].watchTermination()(Keep.right) + }(Keep.both) + .to(Sink.fromSubscriber(subscriber)) + .withAttributes(attributes) + .run() + val matFlowWatchTerm = matFlowWatchTermFF.flatten - sinkSubscription.request(10) - subscriber.expectNext("a", "b", "c") - subscriber.expectComplete() + matFlowWatchTerm.value should be(empty) + srcWatchTermF.value should be(empty) + + val subDownstream = subscriber.expectSubscription() + val subUpstream = publisher.expectSubscription() + subDownstream.request(1) + subUpstream.expectRequest() should be >= (1L) + subUpstream.sendNext(0) + subUpstream.sendNext(1) + subDownstream.asInstanceOf[SubscriptionWithCancelException].cancel(TE("that again?!")) + + if (delayDownstreanCancellation) { + matFlowWatchTerm.value should be(empty) + srcWatchTermF.value should be(empty) + + subUpstream.sendNext(2) + + matFlowWatchTerm.failed.futureValue should ===(TE("that again?!")) + srcWatchTermF.failed.futureValue should ===(TE("that again?!")) + + subUpstream.expectCancellation() + } else { + subUpstream.expectCancellation() + srcWatchTermF.failed.futureValue should ===(TE("that again?!")) + matFlowWatchTerm.failed.futureValue should be(a[NeverMaterializedException]) + matFlowWatchTerm.failed.futureValue.getCause should ===(TE("that again?!")) + } + } + + "downstream failure is propagated via the materialized flow" in assertAllStagesStopped { + val publisher = TestPublisher.manualProbe[Int]() + val subscriber = TestSubscriber.manualProbe[Int]() + + val ((srcWatchTermF, notUsedF), suffixF) = src10() + .watchTermination()(Keep.right) + .flatMapPrefixMat(2) { prefix => + prefix should ===(0 until 2) + Flow.fromSinkAndSourceCoupled(Sink.fromSubscriber(subscriber), Source.fromPublisher(publisher)) + }(Keep.both) + .map { + case 2 => 2 + case 3 => throw TE("3!?!?!?") + case i => fail(s"unexpected value $i") + } + .toMat(Sink.seq)(Keep.both) + .withAttributes(attributes) + .run() + + notUsedF.value should be(empty) + suffixF.value should be(empty) + srcWatchTermF.value should be(empty) + + val subUpstream = publisher.expectSubscription() + val subDownstream = subscriber.expectSubscription() + + notUsedF.futureValue should ===(NotUsed) + + subUpstream.expectRequest() should be >= (1L) + subDownstream.request(1) + subscriber.expectNext(2) + subUpstream.sendNext(2) + subDownstream.request(1) + subscriber.expectNext(3) + subUpstream.sendNext(3) + subUpstream.expectCancellation() should ===(TE("3!?!?!?")) + subscriber.expectError(TE("3!?!?!?")) + + suffixF.failed.futureValue should ===(TE("3!?!?!?")) + srcWatchTermF.failed.futureValue should ===(TE("3!?!?!?")) + } + + "complete mat value with failures on abrupt termination before materializing the flow" in assertAllStagesStopped { + val mat = Materializer(system) + val publisher = TestPublisher.manualProbe[Int]() + + val flow = Source + .fromPublisher(publisher) + .flatMapPrefixMat(2) { prefix => + fail(s"unexpected prefix (length = ${prefix.size})") + Flow[Int] + }(Keep.right) + .toMat(Sink.ignore)(Keep.both) + .withAttributes(attributes) + + val (prefixF, doneF) = flow.run()(mat) + + publisher.expectSubscription() + prefixF.value should be(empty) + doneF.value should be(empty) + + mat.shutdown() + + prefixF.failed.futureValue match { + case _: AbruptTerminationException => + case ex: NeverMaterializedException => + ex.getCause should not be null + ex.getCause should be(a[AbruptTerminationException]) + } + doneF.failed.futureValue should be(a[AbruptTerminationException]) + } + + "respond to abrupt termination after flow materialization" in assertAllStagesStopped { + val mat = Materializer(system) + val countFF = src10() + .flatMapPrefixMat(2) { prefix => + prefix should ===(0 until 2) + Flow[Int] + .concat(Source.repeat(3)) + .fold(0L) { + case (acc, _) => acc + 1 + } + .alsoToMat(Sink.head)(Keep.right) + }(Keep.right) + .to(Sink.ignore) + .withAttributes(attributes) + .run()(mat) + val countF = countFF.futureValue + //at this point we know the flow was materialized, now we can stop the materializer + mat.shutdown() + //expect the nested flow to be terminated abruptly. + countF.failed.futureValue should be(a[AbruptStageTerminationException]) + } + + "behave like via when n = 0" in assertAllStagesStopped { + val (prefixF, suffixF) = src10() + .flatMapPrefixMat(0) { prefix => + prefix should be(empty) + Flow[Int].mapMaterializedValue(_ => prefix) + }(Keep.right) + .toMat(Sink.seq)(Keep.both) + .withAttributes(attributes) + .run() + + prefixF.futureValue should be(empty) + suffixF.futureValue should ===(0 until 10) + } + + "behave like via when n = 0 and upstream produces no elements" in assertAllStagesStopped { + val (prefixF, suffixF) = Source + .empty[Int] + .flatMapPrefixMat(0) { prefix => + prefix should be(empty) + Flow[Int].mapMaterializedValue(_ => prefix) + }(Keep.right) + .toMat(Sink.seq)(Keep.both) + .withAttributes(attributes) + .run() + + prefixF.futureValue should be(empty) + suffixF.futureValue should be(empty) + } + + "propagate errors during flow's creation when n = 0" in assertAllStagesStopped { + val (prefixF, suffixF) = src10() + .flatMapPrefixMat(0) { prefix => + prefix should be(empty) + throw TE("not this time my friend!") + Flow[Int].mapMaterializedValue(_ => prefix) + }(Keep.right) + .toMat(Sink.seq)(Keep.both) + .withAttributes(attributes) + .run() + + prefixF.failed.futureValue should be(a[NeverMaterializedException]) + prefixF.failed.futureValue.getCause === (TE("not this time my friend!")) + suffixF.failed.futureValue should ===(TE("not this time my friend!")) + } + + "propagate materialization failures when n = 0" in assertAllStagesStopped { + val (prefixF, suffixF) = src10() + .flatMapPrefixMat(0) { prefix => + prefix should be(empty) + Flow[Int].mapMaterializedValue(_ => throw TE("Bang! no materialization this time")) + }(Keep.right) + .toMat(Sink.seq)(Keep.both) + .withAttributes(attributes) + .run() + + prefixF.failed.futureValue should be(a[NeverMaterializedException]) + prefixF.failed.futureValue.getCause === (TE("Bang! no materialization this time")) + suffixF.failed.futureValue should ===(TE("Bang! no materialization this time")) + } + + "run a detached flow" in assertAllStagesStopped { + val publisher = TestPublisher.manualProbe[Int]() + val subscriber = TestSubscriber.manualProbe[String]() + + val detachedFlow = Flow.fromSinkAndSource(Sink.cancelled[Int], Source(List("a", "b", "c"))).via { + Flow.fromSinkAndSource(Sink.fromSubscriber(subscriber), Source.empty[Int]) + } + val fHeadOpt = Source + .fromPublisher(publisher) + .flatMapPrefix(2) { prefix => + prefix should ===(0 until 2) + detachedFlow + } + .withAttributes(attributes) + .runWith(Sink.headOption) + + subscriber.expectNoMessage() + val subsc = publisher.expectSubscription() + subsc.expectRequest() should be >= 2L + subsc.sendNext(0) + subscriber.expectNoMessage() + subsc.sendNext(1) + val sinkSubscription = subscriber.expectSubscription() + //this indicates + fHeadOpt.futureValue should be(empty) + + //materialize flow immediately cancels upstream + subsc.expectCancellation() + //at this point both ends of the 'external' fow are closed + + sinkSubscription.request(10) + subscriber.expectNext("a", "b", "c") + subscriber.expectComplete() + } } - } - } diff --git a/akka-stream-tests/src/test/scala/akka/stream/scaladsl/FlowFutureFlowSpec.scala b/akka-stream-tests/src/test/scala/akka/stream/scaladsl/FlowFutureFlowSpec.scala new file mode 100644 index 0000000000..f3abc71c98 --- /dev/null +++ b/akka-stream-tests/src/test/scala/akka/stream/scaladsl/FlowFutureFlowSpec.scala @@ -0,0 +1,529 @@ +/* + * Copyright (C) 2020 Lightbend Inc. + */ + +package akka.stream.scaladsl + +import akka.NotUsed +import akka.stream.SubscriptionWithCancelException.NonFailureCancellation +import akka.stream.{ AbruptStageTerminationException, Attributes, Materializer, NeverMaterializedException } +import akka.stream.testkit.StreamSpec +import akka.stream.testkit.Utils.TE +import akka.stream.testkit.scaladsl.StreamTestKit.assertAllStagesStopped + +import scala.concurrent.{ Future, Promise } + +class FlowFutureFlowSpec extends StreamSpec { + def src10(i: Int = 0) = Source(i until (i + 10)) + def src10WithFailure(i: Int = 0)(failOn: Int) = src10(i).map { + case `failOn` => throw TE(s"fail on $failOn") + case x => x + } + + //this stage's behaviour in case of an 'early' downstream cancellation is governed by an attribute + //so we run all tests cases using both modes of the attributes. + //please notice most of the cases don't exhibit any difference in behaviour between the two modes + for { + att <- List( + Attributes.NestedMaterializationCancellationPolicy.EagerCancellation, + Attributes.NestedMaterializationCancellationPolicy.PropagateToNested) + delayDownstreanCancellation = att.propagateToNestedMaterialization + attributes = Attributes(att) + } { + + s"a futureFlow with $att" must { + "work in the simple case with a completed future" in assertAllStagesStopped { + val (fNotUsed, fSeq) = src10() + .viaMat { + Flow.futureFlow { + Future.successful(Flow[Int]) + } + }(Keep.right) + .toMat(Sink.seq)(Keep.both) + .withAttributes(attributes) + .run() + + fNotUsed.futureValue should be(NotUsed) + fSeq.futureValue should equal(0 until 10) + } + + "work in the simple case with a late future" in assertAllStagesStopped { + val prFlow = Promise[Flow[Int, Int, NotUsed]] + val (fNotUsed, fSeq) = src10() + .viaMat { + Flow.futureFlow(prFlow.future) + }(Keep.right) + .toMat(Sink.seq)(Keep.both) + .withAttributes(attributes) + .run() + + fNotUsed.value should be(empty) + fSeq.value should be(empty) + + prFlow.success(Flow[Int]) + + fNotUsed.futureValue should be(NotUsed) + fSeq.futureValue should equal(0 until 10) + } + + "fail properly when future is a completed failed future" in assertAllStagesStopped { + val (fNotUsed, fSeq) = src10() + .viaMat { + Flow.futureFlow { + Future.failed[Flow[Int, Int, NotUsed]](TE("damn!")) + } + }(Keep.right) + .toMat(Sink.seq)(Keep.both) + .withAttributes(attributes) + .run() + + fNotUsed.failed.futureValue should be(a[NeverMaterializedException]) + fNotUsed.failed.futureValue.getCause should equal(TE("damn!")) + + fSeq.failed.futureValue should equal(TE("damn!")) + + } + + "fail properly when future is late completed failed future" in assertAllStagesStopped { + val prFlow = Promise[Flow[Int, Int, NotUsed]] + val (fNotUsed, fSeq) = src10() + .viaMat { + Flow.futureFlow(prFlow.future) + }(Keep.right) + .toMat(Sink.seq)(Keep.both) + .withAttributes(attributes) + .run() + + fNotUsed.value should be(empty) + fSeq.value should be(empty) + + prFlow.failure(TE("damn!")) + + fNotUsed.failed.futureValue should be(a[NeverMaterializedException]) + fNotUsed.failed.futureValue.getCause should equal(TE("damn!")) + + fSeq.failed.futureValue should equal(TE("damn!")) + + } + + "handle upstream failure when future is pre-completed" in assertAllStagesStopped { + val (fNotUsed, fSeq) = src10WithFailure()(5) + .viaMat { + Flow.futureFlow { + Future.successful { + Flow[Int].recover { + case TE("fail on 5") => 99 + } + } + } + }(Keep.right) + .toMat(Sink.seq)(Keep.both) + .withAttributes(attributes) + .run() + + fNotUsed.futureValue should be(NotUsed) + fSeq.futureValue should equal(List(0, 1, 2, 3, 4, 99)) + } + + "handle upstream failure when future is late-completed" in assertAllStagesStopped { + val prFlow = Promise[Flow[Int, Int, NotUsed]] + val (fNotUsed, fSeq) = src10WithFailure()(5) + .viaMat { + Flow.futureFlow(prFlow.future) + }(Keep.right) + .toMat(Sink.seq)(Keep.both) + .withAttributes(attributes) + .run() + + fNotUsed.value should be(empty) + fSeq.value should be(empty) + + prFlow.success { + Flow[Int].recover { + case TE("fail on 5") => 99 + } + } + + fNotUsed.futureValue should be(NotUsed) + fSeq.futureValue should equal(List(0, 1, 2, 3, 4, 99)) + } + + "propagate upstream failure when future is pre-completed" in assertAllStagesStopped { + val (fNotUsed, fSeq) = src10WithFailure()(5) + .viaMat { + Flow.futureFlow { + Future.successful { + Flow[Int] + } + } + }(Keep.right) + .toMat(Sink.seq)(Keep.both) + .withAttributes(attributes) + .run() + + fNotUsed.futureValue should be(NotUsed) + fSeq.failed.futureValue should equal(TE("fail on 5")) + } + + "propagate upstream failure when future is late-completed" in assertAllStagesStopped { + val prFlow = Promise[Flow[Int, Int, NotUsed]] + val (fNotUsed, fSeq) = src10WithFailure()(5) + .viaMat { + Flow.futureFlow(prFlow.future) + }(Keep.right) + .toMat(Sink.seq)(Keep.both) + .withAttributes(attributes) + .run() + + fNotUsed.value should be(empty) + fSeq.value should be(empty) + + prFlow.success { + Flow[Int] + } + + fNotUsed.futureValue should be(NotUsed) + fSeq.failed.futureValue should equal(TE("fail on 5")) + } + + "handle early upstream error when flow future is pre-completed" in assertAllStagesStopped { + val (fNotUsed, fSeq) = Source + .failed(TE("not today my friend")) + .viaMat { + Flow.futureFlow { + Future.successful { + Flow[Int] + .recover { + case TE("not today my friend") => 99 + } + .concat(src10()) + } + } + }(Keep.right) + .toMat(Sink.seq)(Keep.both) + .withAttributes(attributes) + .run() + + fNotUsed.futureValue should be(NotUsed) + fSeq.futureValue should equal(99 +: (0 until 10)) + + } + + "handle early upstream error when flow future is late-completed" in assertAllStagesStopped { + val prFlow = Promise[Flow[Int, Int, NotUsed]] + val (fNotUsed, fSeq) = Source + .failed(TE("not today my friend")) + .viaMat { + Flow.futureFlow(prFlow.future) + }(Keep.right) + .toMat(Sink.seq)(Keep.both) + .withAttributes(attributes) + .run() + + fNotUsed.value should be(empty) + fSeq.value should be(empty) + + prFlow.success { + Flow[Int] + .recover { + case TE("not today my friend") => 99 + } + .concat(src10()) + } + + fNotUsed.futureValue should be(NotUsed) + fSeq.futureValue should equal(99 +: (0 until 10)) + + } + + "handle closed downstream when flow future is pre completed" in assertAllStagesStopped { + val (fSeq1, fSeq2) = src10() + .viaMat { + Flow.futureFlow { + Future.successful { + Flow[Int].alsoToMat(Sink.seq)(Keep.right) + } + } + }(Keep.right) + .mapMaterializedValue(_.flatten) + .take(0) + .toMat(Sink.seq)(Keep.both) + .withAttributes(attributes) + .run() + + fSeq1.futureValue should be(empty) + fSeq2.futureValue should be(empty) + + } + + "handle closed downstream when flow future is late completed" in assertAllStagesStopped { + val prFlow = Promise[Flow[Int, Int, Future[collection.immutable.Seq[Int]]]] + val (fSeq1, fSeq2) = src10() + .viaMat { + Flow.futureFlow(prFlow.future) + }(Keep.right) + .mapMaterializedValue(_.flatten) + .take(0) + .toMat(Sink.seq)(Keep.both) + .withAttributes(attributes) + .run() + + if (delayDownstreanCancellation) { + fSeq1.value should be(empty) + fSeq2.value should be(empty) + + prFlow.success { + Flow[Int].alsoToMat(Sink.seq)(Keep.right) + } + + fSeq1.futureValue should be(empty) + fSeq2.futureValue should be(empty) + } else { + fSeq1.failed.futureValue should be(a[NeverMaterializedException]) + fSeq1.failed.futureValue.getCause should be(a[NonFailureCancellation]) + fSeq2.futureValue should be(empty) + } + } + + "handle early downstream failure when flow future is pre-completed" in assertAllStagesStopped { + val (fSeq1, fSeq2) = src10() + .viaMat { + Flow.futureFlow { + Future.successful { + Flow[Int].alsoToMat(Sink.seq)(Keep.right) + } + } + }(Keep.right) + .mapMaterializedValue(_.flatten) + .prepend(Source.failed(TE("damn!"))) + .toMat(Sink.seq)(Keep.both) + .withAttributes(attributes) + .run() + + fSeq1.failed.futureValue should equal(TE("damn!")) + fSeq2.failed.futureValue should equal(TE("damn!")) + } + + "handle early downstream failure when flow future is late completed" in assertAllStagesStopped { + val prFlow = Promise[Flow[Int, Int, Future[collection.immutable.Seq[Int]]]] + val (fSeq1, fSeq2) = src10() + .viaMat { + Flow.futureFlow(prFlow.future) + }(Keep.right) + .mapMaterializedValue(_.flatten) + .prepend(Source.failed(TE("damn!"))) + .toMat(Sink.seq)(Keep.both) + .withAttributes(attributes) + .run() + + if (delayDownstreanCancellation) { + fSeq2.failed.futureValue should equal(TE("damn!")) + fSeq1.value should be(empty) + + prFlow.success { + Flow[Int].alsoToMat(Sink.seq)(Keep.right) + } + + fSeq1.failed.futureValue should equal(TE("damn!")) + } else { + fSeq1.failed.futureValue should be(a[NeverMaterializedException]) + fSeq1.failed.futureValue.getCause should equal(TE("damn!")) + fSeq2.failed.futureValue should equal(TE("damn!")) + } + } + + "handle early upstream completion when flow future is pre-completed" in assertAllStagesStopped { + val (fNotUsed, fSeq) = Source + .empty[Int] + .viaMat { + Flow.futureFlow { + Future.successful { + Flow[Int].orElse(Source.single(99)) + } + } + }(Keep.right) + .toMat(Sink.seq)(Keep.both) + .withAttributes(attributes) + .run() + + fNotUsed.futureValue should be(NotUsed) + fSeq.futureValue should equal(99 :: Nil) + } + + "handle early upstream completion when flow future is late-completed" in assertAllStagesStopped { + val prFlow = Promise[Flow[Int, Int, NotUsed]] + val (fNotUsed, fSeq) = Source + .empty[Int] + .viaMat { + Flow.futureFlow(prFlow.future) + }(Keep.right) + .toMat(Sink.seq)(Keep.both) + .withAttributes(attributes) + .run() + + fNotUsed.value should be(empty) + fSeq.value should be(empty) + + prFlow.success { + Flow[Int].orElse(Source.single(99)) + } + + fNotUsed.futureValue should be(NotUsed) + fSeq.futureValue should equal(99 :: Nil) + } + + "fails properly on materialization failure with a completed future" in assertAllStagesStopped { + val (fNotUsed, fSeq) = src10() + .viaMat { + Flow.futureFlow { + Future.successful(Flow[Int].mapMaterializedValue[NotUsed](_ => throw TE("BBOM!"))) + } + }(Keep.right) + .toMat(Sink.seq)(Keep.both) + .withAttributes(attributes) + .run() + + fNotUsed.failed.futureValue should be(a[NeverMaterializedException]) + fNotUsed.failed.futureValue.getCause should equal(TE("BBOM!")) + fSeq.failed.futureValue should equal(TE("BBOM!")) + } + + "fails properly on materialization failure with a late future" in assertAllStagesStopped { + val prFlow = Promise[Flow[Int, Int, NotUsed]] + val (fNotUsed, fSeq) = src10() + .viaMat { + Flow.futureFlow(prFlow.future) + }(Keep.right) + .toMat(Sink.seq)(Keep.both) + .withAttributes(attributes) + .run() + + fNotUsed.value should be(empty) + fSeq.value should be(empty) + + prFlow.success(Flow[Int].mapMaterializedValue[NotUsed](_ => throw TE("BBOM!"))) + + fNotUsed.failed.futureValue should be(a[NeverMaterializedException]) + fNotUsed.failed.futureValue.getCause should equal(TE("BBOM!")) + fSeq.failed.futureValue should equal(TE("BBOM!")) + } + + "propagate flow failures with a completed future" in assertAllStagesStopped { + val (fNotUsed, fSeq) = src10() + .viaMat { + Flow.futureFlow { + Future.successful { + Flow[Int].map { + case 5 => throw TE("fail on 5") + case x => x + } + } + } + }(Keep.right) + .toMat(Sink.seq)(Keep.both) + .withAttributes(attributes) + .run() + + fNotUsed.futureValue should be(NotUsed) + fSeq.failed.futureValue should equal(TE("fail on 5")) + } + + "propagate flow failures with a late future" in assertAllStagesStopped { + val prFlow = Promise[Flow[Int, Int, NotUsed]] + val (fNotUsed, fSeq) = src10() + .viaMat { + Flow.futureFlow(prFlow.future) + }(Keep.right) + .toMat(Sink.seq)(Keep.both) + .withAttributes(attributes) + .run() + + fNotUsed.value should be(empty) + fSeq.value should be(empty) + + prFlow.success { + Flow[Int].map { + case 5 => throw TE("fail on 5") + case x => x + } + } + + fNotUsed.futureValue should be(NotUsed) + fSeq.failed.futureValue should equal(TE("fail on 5")) + } + + "allow flow to handle downstream completion with a completed future" in assertAllStagesStopped { + val (fSeq1, fSeq2) = src10() + .viaMat { + Flow.futureFlow { + Future.successful { + Flow.fromSinkAndSourceMat(Sink.seq[Int], src10(10))(Keep.left) + } + } + }(Keep.right) + .take(5) + .toMat(Sink.seq)(Keep.both) + .withAttributes(attributes) + .run() + + fSeq1.flatten.futureValue should be(0 until 10) + fSeq2.futureValue should equal(10 until 15) + } + + "allow flow to handle downstream completion with a late future" in assertAllStagesStopped { + val pr = Promise[Flow[Int, Int, Future[Seq[Int]]]] + val (fSeq1, fSeq2) = src10() + .viaMat { + Flow.futureFlow(pr.future) + }(Keep.right) + .take(5) + .toMat(Sink.seq)(Keep.both) + .withAttributes(attributes) + .run() + + fSeq1.value should be(empty) + fSeq2.value should be(empty) + + pr.success { + Flow.fromSinkAndSourceMat(Sink.seq[Int], src10(10))(Keep.left) + } + + fSeq1.flatten.futureValue should be(0 until 10) + fSeq2.futureValue should equal(10 until 15) + } + + "abrupt termination before future completion" in assertAllStagesStopped { + val mat = Materializer(system) + val prFlow = Promise[Flow[Int, Int, Future[collection.immutable.Seq[Int]]]] + val (fSeq1, fSeq2) = src10() + .viaMat { + Flow.futureFlow(prFlow.future) + }(Keep.right) + .take(5) + .toMat(Sink.seq)(Keep.both) + .withAttributes(attributes) + .run()(mat) + + fSeq1.value should be(empty) + fSeq2.value should be(empty) + + mat.shutdown() + + fSeq1.failed.futureValue should be(a[AbruptStageTerminationException]) + fSeq2.failed.futureValue should be(a[AbruptStageTerminationException]) + } + } + } + + "NestedMaterializationCancellationPolicy" must { + "default to false" in assertAllStagesStopped { + val fl = Flow.fromMaterializer { + case (_, attributes) => + val att = attributes.mandatoryAttribute[Attributes.NestedMaterializationCancellationPolicy] + att.propagateToNestedMaterialization should be(false) + Flow[Any] + } + Source.empty.via(fl).runWith(Sink.headOption).futureValue should be(empty) + } + } +} diff --git a/akka-stream-tests/src/test/scala/akka/stream/scaladsl/LazyFlowSpec.scala b/akka-stream-tests/src/test/scala/akka/stream/scaladsl/LazyFlowSpec.scala index 062e2e5e44..e807f54ab1 100644 --- a/akka-stream-tests/src/test/scala/akka/stream/scaladsl/LazyFlowSpec.scala +++ b/akka-stream-tests/src/test/scala/akka/stream/scaladsl/LazyFlowSpec.scala @@ -8,10 +8,8 @@ import scala.collection.immutable import scala.concurrent.Future import scala.concurrent.Promise import scala.concurrent.duration._ - import com.github.ghik.silencer.silent - -import akka.NotUsed +import akka.{ Done, NotUsed } import akka.stream.AbruptStageTerminationException import akka.stream.Materializer import akka.stream.NeverMaterializedException @@ -127,7 +125,8 @@ class LazyFlowSpec extends StreamSpec(""" val deferredMatVal = result._1 val list = result._2 list.failed.futureValue shouldBe a[TE] - deferredMatVal.failed.futureValue shouldBe a[TE] + deferredMatVal.failed.futureValue shouldBe a[NeverMaterializedException] + deferredMatVal.failed.futureValue.getCause shouldBe a[TE] } "fail the flow when the future is initially failed" in assertAllStagesStopped { @@ -140,7 +139,8 @@ class LazyFlowSpec extends StreamSpec(""" val deferredMatVal = result._1 val list = result._2 list.failed.futureValue shouldBe a[TE] - deferredMatVal.failed.futureValue shouldBe a[TE] + deferredMatVal.failed.futureValue shouldBe a[NeverMaterializedException] + deferredMatVal.failed.futureValue.getCause shouldBe a[TE] } "fail the flow when the future is failed after the fact" in assertAllStagesStopped { @@ -156,7 +156,28 @@ class LazyFlowSpec extends StreamSpec(""" promise.failure(TE("later-no-flow-for-you")) list.failed.futureValue shouldBe a[TE] - deferredMatVal.failed.futureValue shouldBe a[TE] + deferredMatVal.failed.futureValue shouldBe a[NeverMaterializedException] + deferredMatVal.failed.futureValue.getCause shouldBe a[TE] + } + + "work for a single element when the future is completed after the fact" in assertAllStagesStopped { + import system.dispatcher + val flowPromise = Promise[Flow[Int, String, NotUsed]]() + val firstElementArrived = Promise[Done]() + + val result: Future[immutable.Seq[String]] = + Source(List(1)) + .via(Flow.lazyFutureFlow { () => + firstElementArrived.success(Done) + flowPromise.future + }) + .runWith(Sink.seq) + + firstElementArrived.future.map { _ => + flowPromise.success(Flow[Int].map(_.toString)) + } + + result.futureValue shouldBe List("1") } "fail the flow when the future materialization fails" in assertAllStagesStopped { @@ -170,7 +191,9 @@ class LazyFlowSpec extends StreamSpec(""" val deferredMatVal = result._1 val list = result._2 list.failed.futureValue shouldBe a[TE] - deferredMatVal.failed.futureValue shouldBe a[TE] + //futureFlow's behaviour in case of mat failure (follows flatMapPrefix) + deferredMatVal.failed.futureValue shouldBe a[NeverMaterializedException] + deferredMatVal.failed.futureValue.getCause shouldEqual TE("mat-failed") } "fail the flow when there was elements but the inner flow failed" in assertAllStagesStopped { diff --git a/akka-stream-tests/src/test/scala/akka/stream/scaladsl/NeverSourceSpec.scala b/akka-stream-tests/src/test/scala/akka/stream/scaladsl/NeverSourceSpec.scala new file mode 100644 index 0000000000..102a44db54 --- /dev/null +++ b/akka-stream-tests/src/test/scala/akka/stream/scaladsl/NeverSourceSpec.scala @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2014-2020 Lightbend Inc. + */ + +package akka.stream.scaladsl + +import scala.concurrent.duration._ + +import akka.stream.testkit.{ StreamSpec, TestSubscriber } +import akka.stream.testkit.scaladsl.StreamTestKit._ +import akka.testkit.DefaultTimeout + +class NeverSourceSpec extends StreamSpec with DefaultTimeout { + + "The Never Source" must { + + "never completes" in assertAllStagesStopped { + val neverSource = Source.never[Int] + val pubSink = Sink.asPublisher[Int](false) + + val neverPub = neverSource.toMat(pubSink)(Keep.right).run() + + val c = TestSubscriber.manualProbe[Int]() + neverPub.subscribe(c) + val subs = c.expectSubscription() + + subs.request(1) + c.expectNoMessage(300.millis) + + subs.cancel() + } + } +} diff --git a/akka-stream-tests/src/test/scala/akka/stream/snapshot/MaterializerStateSpec.scala b/akka-stream-tests/src/test/scala/akka/stream/snapshot/MaterializerStateSpec.scala index 18aa45d71c..77d1fb1d26 100644 --- a/akka-stream-tests/src/test/scala/akka/stream/snapshot/MaterializerStateSpec.scala +++ b/akka-stream-tests/src/test/scala/akka/stream/snapshot/MaterializerStateSpec.scala @@ -5,18 +5,14 @@ package akka.stream.snapshot import scala.concurrent.Promise +import java.net.InetSocketAddress import akka.stream.FlowShape import akka.stream.Materializer -import akka.stream.scaladsl.Flow -import akka.stream.scaladsl.GraphDSL -import akka.stream.scaladsl.Keep -import akka.stream.scaladsl.Merge -import akka.stream.scaladsl.Partition -import akka.stream.scaladsl.Sink -import akka.stream.scaladsl.Source +import akka.stream.scaladsl.{ Flow, GraphDSL, Keep, Merge, Partition, Sink, Source, Tcp } import akka.stream.testkit.StreamSpec import akka.stream.testkit.scaladsl.TestSink +import javax.net.ssl.SSLContext class MaterializerStateSpec extends StreamSpec { @@ -53,6 +49,20 @@ class MaterializerStateSpec extends StreamSpec { promise.success(1) } + "snapshot a running stream that includes a TLSActor" in { + Source.never + .via(Tcp().outgoingConnectionWithTls(InetSocketAddress.createUnresolved("akka.io", 443), () => { + val engine = SSLContext.getDefault.createSSLEngine("akka.io", 443) + engine.setUseClientMode(true) + engine + })) + .runWith(Sink.seq) + + val snapshots = MaterializerState.streamSnapshots(system).futureValue + snapshots.size should be(2) + snapshots.toString should include("TLS-") + } + "snapshot a stream that has a stopped stage" in { implicit val mat = Materializer(system) try { diff --git a/akka-stream/src/main/java/akka/stream/StreamRefMessages.java b/akka-stream/src/main/java/akka/stream/StreamRefMessages.java index dd272df005..dcea5b873e 100644 --- a/akka-stream/src/main/java/akka/stream/StreamRefMessages.java +++ b/akka-stream/src/main/java/akka/stream/StreamRefMessages.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2019-2020 Lightbend Inc. + * Copyright (C) 2020 Lightbend Inc. */ // Generated by the protocol buffer compiler. DO NOT EDIT! diff --git a/akka-stream/src/main/mima-filters/2.6.5.backwards.excludes/28729-future-flow.backwards.excludes b/akka-stream/src/main/mima-filters/2.6.5.backwards.excludes/28729-future-flow.backwards.excludes new file mode 100644 index 0000000000..3a3654c452 --- /dev/null +++ b/akka-stream/src/main/mima-filters/2.6.5.backwards.excludes/28729-future-flow.backwards.excludes @@ -0,0 +1,2 @@ +# Changes to internals +ProblemFilters.exclude[MissingClassProblem]("akka.stream.impl.fusing.LazyFlow") diff --git a/akka-stream/src/main/mima-filters/2.6.5.backwards.excludes/change-StreamSnapshotImpl-toString.backwards.excludes b/akka-stream/src/main/mima-filters/2.6.5.backwards.excludes/change-StreamSnapshotImpl-toString.backwards.excludes new file mode 100644 index 0000000000..5f4c2016b0 --- /dev/null +++ b/akka-stream/src/main/mima-filters/2.6.5.backwards.excludes/change-StreamSnapshotImpl-toString.backwards.excludes @@ -0,0 +1 @@ +ProblemFilters.exclude[MissingTypesProblem]("akka.stream.snapshot.StreamSnapshotImpl") diff --git a/akka-stream/src/main/resources/reference.conf b/akka-stream/src/main/resources/reference.conf index 8cf220c6c8..dd982714a1 100644 --- a/akka-stream/src/main/resources/reference.conf +++ b/akka-stream/src/main/resources/reference.conf @@ -3,7 +3,7 @@ ##################################### # eager creation of the system wide materializer -akka.library-extensions += "akka.stream.SystemMaterializer" +akka.library-extensions += "akka.stream.SystemMaterializer$" akka { stream { diff --git a/akka-stream/src/main/scala/akka/stream/ActorMaterializer.scala b/akka-stream/src/main/scala/akka/stream/ActorMaterializer.scala index a06e7c0cdf..9b8aceb4c7 100644 --- a/akka-stream/src/main/scala/akka/stream/ActorMaterializer.scala +++ b/akka-stream/src/main/scala/akka/stream/ActorMaterializer.scala @@ -745,6 +745,7 @@ final class ActorMaterializerSettings @InternalApi private ( // for stream refs and io live with the respective stages Attributes.InputBuffer(initialInputBufferSize, maxInputBufferSize) :: Attributes.CancellationStrategy.Default :: // FIXME: make configurable, see https://github.com/akka/akka/issues/28000 + Attributes.NestedMaterializationCancellationPolicy.Default :: ActorAttributes.Dispatcher(dispatcher) :: ActorAttributes.SupervisionStrategy(supervisionDecider) :: ActorAttributes.DebugLogging(debugLogging) :: diff --git a/akka-stream/src/main/scala/akka/stream/Attributes.scala b/akka-stream/src/main/scala/akka/stream/Attributes.scala index f86fb8eb32..72f09ea988 100644 --- a/akka-stream/src/main/scala/akka/stream/Attributes.scala +++ b/akka-stream/src/main/scala/akka/stream/Attributes.scala @@ -439,6 +439,78 @@ object Attributes { strategy: CancellationStrategy.Strategy): CancellationStrategy.Strategy = CancellationStrategy.AfterDelay(delay, strategy) + /** + * Nested materialization cancellation strategy provides a way to configure the cancellation behavior of stages that materialize a nested flow. + * + * When cancelled before materializing their nested flows, these stages can either immediately cancel (default behaviour) without materializing the nested flow + * or wait for the nested flow to materialize and then propagate the cancellation signal through it. + * + * This applies to [[akka.stream.scaladsl.FlowOps.flatMapPrefix]], [[akka.stream.scaladsl.Flow.futureFlow]] (and derivations such as [[akka.stream.scaladsl.Flow.lazyFutureFlow]]). + * These operators either delay the nested flow's materialization or wait for a future to complete before doing so, + * in this period of time they may receive a downstream cancellation signal. When this happens these operators will behave according to + * this [[Attribute]]: when set to true they will 'stash' the signal and later deliver it to the materialized nested flow + * , otherwise these stages will immediately cancel without materializing the nested flow. + */ + @ApiMayChange + class NestedMaterializationCancellationPolicy private[NestedMaterializationCancellationPolicy] ( + val propagateToNestedMaterialization: Boolean) + extends MandatoryAttribute + + @ApiMayChange + object NestedMaterializationCancellationPolicy { + + /** + * A [[NestedMaterializationCancellationPolicy]] that configures graph stages + * delaying nested flow materialization to cancel immediately when downstream cancels before + * nested flow materialization. + * This applies to [[akka.stream.scaladsl.FlowOps.flatMapPrefix]], [[akka.stream.scaladsl.Flow.futureFlow]] and derived operators. + */ + val EagerCancellation = new NestedMaterializationCancellationPolicy(false) + + /** + * A [[NestedMaterializationCancellationPolicy]] that configures graph stages + * delaying nested flow materialization to delay cancellation when downstream cancels before + * nested flow materialization. Once the nested flow is materialized it will be cancelled immediately. + * This applies to [[akka.stream.scaladsl.FlowOps.flatMapPrefix]], [[akka.stream.scaladsl.Flow.futureFlow]] and derived operators. + */ + val PropagateToNested = new NestedMaterializationCancellationPolicy(true) + + /** + * Default [[NestedMaterializationCancellationPolicy]], + * please see [[akka.stream.Attributes.NestedMaterializationCancellationPolicy.EagerCancellation()]] for details. + */ + val Default = EagerCancellation + } + + /** + * JAVA API + * A [[NestedMaterializationCancellationPolicy]] that configures graph stages + * delaying nested flow materialization to cancel immediately when downstream cancels before + * nested flow materialization. + * This applies to [[akka.stream.scaladsl.FlowOps.flatMapPrefix]], [[akka.stream.scaladsl.Flow.futureFlow]] and derived operators. + */ + @ApiMayChange + def nestedMaterializationCancellationPolicyEagerCancellation(): NestedMaterializationCancellationPolicy = + NestedMaterializationCancellationPolicy.EagerCancellation + + /** + * JAVA API + * A [[NestedMaterializationCancellationPolicy]] that configures graph stages + * delaying nested flow materialization to delay cancellation when downstream cancels before + * nested flow materialization. Once the nested flow is materialized it will be cancelled immediately. + * This applies to [[akka.stream.scaladsl.FlowOps.flatMapPrefix]], [[akka.stream.scaladsl.Flow.futureFlow]] and derived operators. + */ + @ApiMayChange + def nestedMaterializationCancellationPolicyPropagateToNested(): NestedMaterializationCancellationPolicy = + NestedMaterializationCancellationPolicy.PropagateToNested + + /** + * Default [[NestedMaterializationCancellationPolicy]], + * please see [[akka.stream.Attributes#nestedMaterializationCancellationPolicyEagerCancellation()]] for details. + */ + def nestedMaterializationCancellationPolicyDefault(): NestedMaterializationCancellationPolicy = + NestedMaterializationCancellationPolicy.Default + object LogLevels { /** Use to disable logging on certain operations when configuring [[Attributes#logLevels]] */ diff --git a/akka-stream/src/main/scala/akka/stream/SslTlsOptions.scala b/akka-stream/src/main/scala/akka/stream/SslTlsOptions.scala index d14092fe08..9961721efa 100644 --- a/akka-stream/src/main/scala/akka/stream/SslTlsOptions.scala +++ b/akka-stream/src/main/scala/akka/stream/SslTlsOptions.scala @@ -4,11 +4,11 @@ package akka.stream +import javax.net.ssl._ + import scala.annotation.varargs import scala.collection.immutable -import javax.net.ssl._ - import akka.util.ByteString /** diff --git a/akka-stream/src/main/scala/akka/stream/impl/PhasedFusingActorMaterializer.scala b/akka-stream/src/main/scala/akka/stream/impl/PhasedFusingActorMaterializer.scala index 894c7767fb..9fea322785 100644 --- a/akka-stream/src/main/scala/akka/stream/impl/PhasedFusingActorMaterializer.scala +++ b/akka-stream/src/main/scala/akka/stream/impl/PhasedFusingActorMaterializer.scala @@ -967,7 +967,7 @@ private final case class SavedIslandData( val props = TLSActor.props(maxInputBuffer, tls.createSSLEngine, tls.verifySession, tls.closing).withDispatcher(dispatcher) - tlsActor = materializer.actorOf(props, islandName) + tlsActor = materializer.actorOf(props, "TLS-for-" + islandName) def factory(id: Int) = new ActorPublisher[Any](tlsActor) { override val wakeUpMsg = FanOut.SubstreamSubscribePending(id) } diff --git a/akka-stream/src/main/scala/akka/stream/impl/Stages.scala b/akka-stream/src/main/scala/akka/stream/impl/Stages.scala index 3dd5239b4c..1cbfc50e23 100755 --- a/akka-stream/src/main/scala/akka/stream/impl/Stages.scala +++ b/akka-stream/src/main/scala/akka/stream/impl/Stages.scala @@ -104,6 +104,7 @@ import akka.stream.Attributes._ val singleSource = name("singleSource") val emptySource = name("emptySource") val maybeSource = name("MaybeSource") + val neverSource = name("neverSource") val failedSource = name("failedSource") val concatSource = name("concatSource") val concatMatSource = name("concatMatSource") diff --git a/akka-stream/src/main/scala/akka/stream/impl/Timers.scala b/akka-stream/src/main/scala/akka/stream/impl/Timers.scala index 917fbfcb5a..525bb9087d 100644 --- a/akka-stream/src/main/scala/akka/stream/impl/Timers.scala +++ b/akka-stream/src/main/scala/akka/stream/impl/Timers.scala @@ -255,20 +255,27 @@ import akka.stream.stage._ if (isClosed(in)) completeStage() else pull(in) } else { - val time = System.nanoTime - if (nextDeadline - time < 0) { - nextDeadline = time + timeout.toNanos + val now = System.nanoTime() + // Idle timeout triggered a while ago and we were just waiting for pull. + // In the case of now == deadline, the deadline has not passed strictly, but scheduling another thunk + // for that seems wasteful. + if (now - nextDeadline >= 0) { + nextDeadline = now + timeout.toNanos push(out, inject()) - } else scheduleOnce(GraphStageLogicTimer, FiniteDuration(nextDeadline - time, TimeUnit.NANOSECONDS)) + } else + scheduleOnce(GraphStageLogicTimer, FiniteDuration(nextDeadline - now, TimeUnit.NANOSECONDS)) } } override protected def onTimer(timerKey: Any): Unit = { - val time = System.nanoTime - if ((nextDeadline - time < 0) && isAvailable(out)) { - push(out, inject()) - nextDeadline = time + timeout.toNanos - } + val now = System.nanoTime() + // Timer is reliably cancelled if a regular element arrives first. Scheduler rather schedules too late + // than too early so the deadline must have passed at this time. + assert( + now - nextDeadline >= 0, + s"Timer should have triggered only after deadline but now is $now and deadline was $nextDeadline diff ${now - nextDeadline}.") + push(out, inject()) + nextDeadline = now + timeout.toNanos } } diff --git a/akka-stream/src/main/scala/akka/stream/impl/fusing/FlatMapPrefix.scala b/akka-stream/src/main/scala/akka/stream/impl/fusing/FlatMapPrefix.scala index 66aff9a909..80f005093c 100644 --- a/akka-stream/src/main/scala/akka/stream/impl/fusing/FlatMapPrefix.scala +++ b/akka-stream/src/main/scala/akka/stream/impl/fusing/FlatMapPrefix.scala @@ -27,7 +27,11 @@ import akka.util.OptionVal override def initialAttributes: Attributes = DefaultAttributes.flatMapPrefix override def createLogicAndMaterializedValue(inheritedAttributes: Attributes): (GraphStageLogic, Future[M]) = { - val matPromise = Promise[M]() + val propagateToNestedMaterialization = + inheritedAttributes + .mandatoryAttribute[Attributes.NestedMaterializationCancellationPolicy] + .propagateToNestedMaterialization + val matPromise = Promise[M] val logic = new GraphStageLogic(shape) with InHandler with OutHandler { val accumulated = collection.mutable.Buffer.empty[In] @@ -90,7 +94,10 @@ import akka.util.OptionVal override def onDownstreamFinish(cause: Throwable): Unit = { subSink match { - case OptionVal.None => downstreamCause = OptionVal.Some(cause) + case OptionVal.None if propagateToNestedMaterialization => downstreamCause = OptionVal.Some(cause) + case OptionVal.None => + matPromise.failure(new NeverMaterializedException(cause)) + cancelStage(cause) case OptionVal.Some(s) => s.cancel(cause) } } diff --git a/akka-stream/src/main/scala/akka/stream/impl/fusing/FutureFlow.scala b/akka-stream/src/main/scala/akka/stream/impl/fusing/FutureFlow.scala new file mode 100644 index 0000000000..c3c99cc8c1 --- /dev/null +++ b/akka-stream/src/main/scala/akka/stream/impl/fusing/FutureFlow.scala @@ -0,0 +1,135 @@ +/* + * Copyright (C) 2020 Lightbend Inc. + */ + +package akka.stream.impl.fusing + +import akka.annotation.InternalApi +import akka.dispatch.ExecutionContexts +import akka.stream.{ AbruptStageTerminationException, Attributes, FlowShape, Inlet, NeverMaterializedException, Outlet } +import akka.stream.scaladsl.{ Flow, Keep, Source } +import akka.stream.stage.{ GraphStageLogic, GraphStageWithMaterializedValue, InHandler, OutHandler } +import akka.util.OptionVal + +import scala.concurrent.{ Future, Promise } +import scala.util.control.NonFatal +import scala.util.{ Failure, Success, Try } + +@InternalApi private[akka] final class FutureFlow[In, Out, M](futureFlow: Future[Flow[In, Out, M]]) + extends GraphStageWithMaterializedValue[FlowShape[In, Out], Future[M]] { + val in = Inlet[In](s"${this}.in") + val out = Outlet[Out](s"${this}.out") + override val shape: FlowShape[In, Out] = FlowShape(in, out) + + override def createLogicAndMaterializedValue(inheritedAttributes: Attributes): (GraphStageLogic, Future[M]) = { + val propagateToNestedMaterialization = + inheritedAttributes + .mandatoryAttribute[Attributes.NestedMaterializationCancellationPolicy] + .propagateToNestedMaterialization + val innerMatValue = Promise[M] + val logic = new GraphStageLogic(shape) { + + //seems like we must set handlers BEFORE preStart + setHandlers(in, out, Initializing) + + override def preStart(): Unit = { + futureFlow.value match { + case Some(tryFlow) => + Initializing.onFuture(tryFlow) + case None => + val cb = getAsyncCallback(Initializing.onFuture) + futureFlow.onComplete(cb.invoke)(ExecutionContexts.parasitic) + //in case both ports are closed before future completion + setKeepGoing(true) + } + } + + override def postStop(): Unit = { + if (!innerMatValue.isCompleted) + innerMatValue.failure(new AbruptStageTerminationException(this)) + } + + object Initializing extends InHandler with OutHandler { + // we don't expect a push since we bever pull upstream during initialization + override def onPush(): Unit = throw new IllegalStateException("unexpected push during initialization") + + var upstreamFailure = OptionVal.none[Throwable] + + override def onUpstreamFailure(ex: Throwable): Unit = { + upstreamFailure = OptionVal.Some(ex) + } + + //will later be propagated to the materialized flow (by examining isClosed(in)) + override def onUpstreamFinish(): Unit = {} + + //will later be propagated to the materialized flow (by examining isAvailable(out)) + override def onPull(): Unit = {} + + var downstreamCause = OptionVal.none[Throwable] + + override def onDownstreamFinish(cause: Throwable): Unit = + if (propagateToNestedMaterialization) { + downstreamCause = OptionVal.Some(cause) + } else { + innerMatValue.failure(new NeverMaterializedException(cause)) + cancelStage(cause) + } + + def onFuture(futureRes: Try[Flow[In, Out, M]]) = futureRes match { + case Failure(exception) => + setKeepGoing(false) + innerMatValue.failure(new NeverMaterializedException(exception)) + failStage(exception) + case Success(flow) => + //materialize flow, connect inlet and outlet, feed with potential events and set handlers + connect(flow) + setKeepGoing(false) + } + + def connect(flow: Flow[In, Out, M]): Unit = { + val subSource = new SubSourceOutlet[In](s"${FutureFlow.this}.subIn") + val subSink = new SubSinkInlet[Out](s"${FutureFlow.this}.subOut") + + subSource.setHandler { + new OutHandler { + override def onPull(): Unit = if (!isClosed(in)) tryPull(in) + override def onDownstreamFinish(cause: Throwable): Unit = if (!isClosed(in)) cancel(in, cause) + } + } + subSink.setHandler { + new InHandler { + override def onPush(): Unit = push(out, subSink.grab()) + override def onUpstreamFinish(): Unit = complete(out) + override def onUpstreamFailure(ex: Throwable): Unit = fail(out, ex) + } + } + try { + val matVal = + Source.fromGraph(subSource.source).viaMat(flow)(Keep.right).to(subSink.sink).run()(subFusingMaterializer) + innerMatValue.success(matVal) + upstreamFailure match { + case OptionVal.Some(ex) => subSource.fail(ex) + case OptionVal.None => if (isClosed(in)) subSource.complete() + } + downstreamCause match { + case OptionVal.Some(cause) => subSink.cancel(cause) + case OptionVal.None => if (isAvailable(out)) subSink.pull() + } + setHandlers(in, out, new InHandler with OutHandler { + override def onPull(): Unit = subSink.pull() + override def onDownstreamFinish(cause: Throwable): Unit = subSink.cancel(cause) + override def onPush(): Unit = subSource.push(grab(in)) + override def onUpstreamFinish(): Unit = subSource.complete() + override def onUpstreamFailure(ex: Throwable): Unit = subSource.fail(ex) + }) + } catch { + case NonFatal(ex) => + innerMatValue.failure(new NeverMaterializedException(ex)) + failStage(ex) + } + } + } + } + (logic, innerMatValue.future) + } +} diff --git a/akka-stream/src/main/scala/akka/stream/impl/fusing/Ops.scala b/akka-stream/src/main/scala/akka/stream/impl/fusing/Ops.scala index c187c9f706..7339cd67b7 100644 --- a/akka-stream/src/main/scala/akka/stream/impl/fusing/Ops.scala +++ b/akka-stream/src/main/scala/akka/stream/impl/fusing/Ops.scala @@ -9,7 +9,7 @@ import java.util.concurrent.TimeUnit.NANOSECONDS import scala.annotation.tailrec import scala.collection.immutable import scala.collection.immutable.VectorBuilder -import scala.concurrent.{ Future, Promise } +import scala.concurrent.Future import scala.concurrent.duration.{ FiniteDuration, _ } import scala.util.{ Failure, Success, Try } import scala.util.control.{ NoStackTrace, NonFatal } @@ -19,7 +19,6 @@ import com.github.ghik.silencer.silent import akka.actor.{ ActorRef, Terminated } import akka.annotation.{ DoNotInherit, InternalApi } -import akka.dispatch.ExecutionContexts import akka.event.{ LogMarker, LogSource, Logging, LoggingAdapter, MarkerLoggingAdapter } import akka.event.Logging.LogLevel import akka.stream.{ Supervision, _ } @@ -29,7 +28,7 @@ import akka.stream.OverflowStrategies._ import akka.stream.impl.{ ReactiveStreamsCompliance, Buffer => BufferImpl } import akka.stream.impl.Stages.DefaultAttributes import akka.stream.impl.fusing.GraphStages.SimpleLinearGraphStage -import akka.stream.scaladsl.{ DelayStrategy, Flow, Keep, Source } +import akka.stream.scaladsl.{ DelayStrategy, Source } import akka.stream.stage._ import akka.util.OptionVal import akka.util.unused @@ -2224,199 +2223,3 @@ private[stream] object Collect { override def toString = "StatefulMapConcat" } - -/** - * INTERNAL API - */ -@InternalApi private[akka] final class LazyFlow[I, O, M](flowFactory: I => Future[Flow[I, O, M]]) - extends GraphStageWithMaterializedValue[FlowShape[I, O], Future[M]] { - - // FIXME: when removing the deprecated I => Flow factories we can remove that complication from this stage - - val in = Inlet[I]("LazyFlow.in") - val out = Outlet[O]("LazyFlow.out") - - override def initialAttributes = DefaultAttributes.lazyFlow - - override val shape: FlowShape[I, O] = FlowShape.of(in, out) - - override def toString: String = "LazyFlow" - - override def createLogicAndMaterializedValue(inheritedAttributes: Attributes): (GraphStageLogic, Future[M]) = { - val matPromise = Promise[M]() - val stageLogic = new GraphStageLogic(shape) with InHandler with OutHandler { - var switching = false - - // - // implementation of handler methods in initial state - // - private def onFlowFutureComplete(firstElement: I)(result: Try[Flow[I, O, M]]) = result match { - case Success(flow) => - // check if the stage is still in need for the lazy flow - // (there could have been an onUpstreamFailure or onDownstreamFinish in the meantime that has completed the promise) - if (!matPromise.isCompleted) { - try { - val mat = switchTo(flow, firstElement) - matPromise.success(mat) - } catch { - case NonFatal(e) => - matPromise.failure(e) - failStage(e) - } - } - case Failure(e) => - matPromise.failure(e) - failStage(e) - } - - override def onPush(): Unit = - try { - val element = grab(in) - switching = true - val futureFlow = flowFactory(element) - - // optimization avoid extra scheduling if already completed - futureFlow.value match { - case Some(completed) => - onFlowFutureComplete(element)(completed) - case None => - val cb = getAsyncCallback[Try[Flow[I, O, M]]](onFlowFutureComplete(element)) - futureFlow.onComplete(cb.invoke)(ExecutionContexts.parasitic) - } - } catch { - case NonFatal(e) => - matPromise.failure(e) - failStage(e) - } - - override def onUpstreamFinish(): Unit = { - if (!matPromise.isCompleted) - matPromise.tryFailure(new NeverMaterializedException) - // ignore onUpstreamFinish while the stage is switching but setKeepGoing - if (switching) { - setKeepGoing(true) - } else { - super.onUpstreamFinish() - } - } - - override def onUpstreamFailure(ex: Throwable): Unit = { - super.onUpstreamFailure(ex) - if (!matPromise.isCompleted) - matPromise.tryFailure(new NeverMaterializedException(ex)) - } - - override def onPull(): Unit = { - pull(in) - } - - override def postStop(): Unit = { - if (!matPromise.isCompleted) - matPromise.tryFailure(new AbruptStageTerminationException(this)) - } - - setHandler(in, this) - setHandler(out, this) - - private def switchTo(flow: Flow[I, O, M], firstElement: I): M = { - - // - // ports are wired in the following way: - // - // in ~> subOutlet ~> lazyFlow ~> subInlet ~> out - // - - val subInlet = new SubSinkInlet[O]("LazyFlowSubSink") - val subOutlet = new SubSourceOutlet[I]("LazyFlowSubSource") - - val matVal = Source - .fromGraph(subOutlet.source) - .prepend(Source.single(firstElement)) - .viaMat(flow)(Keep.right) - .toMat(subInlet.sink)(Keep.left) - .run()(interpreter.subFusingMaterializer) - - // The lazily materialized flow may be constructed from a sink and a source. Therefore termination - // signals (completion, cancellation, and errors) are not guaranteed to pass through the flow. This - // means that this stage must not be completed as soon as one side of the flow is finished. - // - // Invariant: isClosed(out) == subInlet.isClosed after each event because termination signals (i.e. - // completion, cancellation, and failure) between these two ports are always forwarded. - // - // However, isClosed(in) and subOutlet.isClosed may be different. This happens if upstream completes before - // the cached element was pushed. - def maybeCompleteStage(): Unit = { - if (isClosed(in) && subOutlet.isClosed && isClosed(out)) { - completeStage() - } - } - - // The stage must not be shut down automatically; it is completed when maybeCompleteStage decides - setKeepGoing(true) - - setHandler( - in, - new InHandler { - override def onPush(): Unit = { - subOutlet.push(grab(in)) - } - override def onUpstreamFinish(): Unit = { - subOutlet.complete() - maybeCompleteStage() - } - override def onUpstreamFailure(ex: Throwable): Unit = { - // propagate exception irrespective if the cached element has been pushed or not - subOutlet.fail(ex) - maybeCompleteStage() - } - }) - - setHandler(out, new OutHandler { - override def onPull(): Unit = { - subInlet.pull() - } - override def onDownstreamFinish(cause: Throwable): Unit = { - subInlet.cancel(cause) - maybeCompleteStage() - } - }) - - subOutlet.setHandler(new OutHandler { - override def onPull(): Unit = { - pull(in) - } - override def onDownstreamFinish(cause: Throwable): Unit = { - if (!isClosed(in)) { - cancel(in, cause) - } - maybeCompleteStage() - } - }) - - subInlet.setHandler(new InHandler { - override def onPush(): Unit = { - push(out, subInlet.grab()) - } - override def onUpstreamFinish(): Unit = { - complete(out) - maybeCompleteStage() - } - override def onUpstreamFailure(ex: Throwable): Unit = { - fail(out, ex) - maybeCompleteStage() - } - }) - - if (isClosed(out)) { - // downstream may have been canceled while the stage was switching - subInlet.cancel() - } else { - subInlet.pull() - } - - matVal - } - } - (stageLogic, matPromise.future) - } -} diff --git a/akka-stream/src/main/scala/akka/stream/impl/io/TLSActor.scala b/akka-stream/src/main/scala/akka/stream/impl/io/TLSActor.scala index 524d22f28f..ca75256f65 100644 --- a/akka-stream/src/main/scala/akka/stream/impl/io/TLSActor.scala +++ b/akka-stream/src/main/scala/akka/stream/impl/io/TLSActor.scala @@ -6,15 +6,15 @@ package akka.stream.impl.io import java.nio.ByteBuffer -import scala.annotation.tailrec -import scala.util.{ Failure, Success, Try } -import scala.util.control.NonFatal - import javax.net.ssl._ import javax.net.ssl.SSLEngineResult.HandshakeStatus import javax.net.ssl.SSLEngineResult.HandshakeStatus._ import javax.net.ssl.SSLEngineResult.Status._ +import scala.annotation.tailrec +import scala.util.{ Failure, Success, Try } +import scala.util.control.NonFatal + import akka.actor._ import akka.annotation.InternalApi import akka.stream._ @@ -22,6 +22,8 @@ import akka.stream.TLSProtocol._ import akka.stream.impl._ import akka.stream.impl.FanIn.InputBunch import akka.stream.impl.FanOut.OutputBunch +import akka.stream.impl.fusing.ActorGraphInterpreter +import akka.stream.snapshot.StreamSnapshotImpl import akka.util.ByteString /** @@ -267,7 +269,7 @@ import akka.util.ByteString } def completeOrFlush(): Unit = - if (engine.isOutboundDone) nextPhase(completedPhase) + if (engine.isOutboundDone || (engine.isInboundDone && userInChoppingBlock.isEmpty)) nextPhase(completedPhase) else nextPhase(flushingOutbound) private def doInbound(isOutboundClosed: Boolean, inboundState: TransferState): Boolean = @@ -393,7 +395,9 @@ import akka.util.ByteString result.getStatus match { case OK => result.getHandshakeStatus match { - case NEED_WRAP => flushToUser() + case NEED_WRAP => + flushToUser() + transportInChoppingBlock.putBack(transportInBuffer) case FINISHED => flushToUser() handshakeFinished() @@ -404,8 +408,7 @@ import akka.util.ByteString } case CLOSED => flushToUser() - if (engine.isOutboundDone) nextPhase(completedPhase) - else nextPhase(flushingOutbound) + completeOrFlush() case BUFFER_UNDERFLOW => flushToUser() case BUFFER_OVERFLOW => @@ -442,7 +445,10 @@ import akka.util.ByteString } } - override def receive = inputBunch.subreceive.orElse[Any, Unit](outputBunch.subreceive) + override def receive = inputBunch.subreceive.orElse[Any, Unit](outputBunch.subreceive).orElse { + case ActorGraphInterpreter.Snapshot => + sender() ! StreamSnapshotImpl(self.path, Seq.empty, Seq.empty) + } initialPhase(2, bidirectional) diff --git a/akka-stream/src/main/scala/akka/stream/impl/io/TlsModule.scala b/akka-stream/src/main/scala/akka/stream/impl/io/TlsModule.scala index ffda5d5c80..5f1961dfb7 100644 --- a/akka-stream/src/main/scala/akka/stream/impl/io/TlsModule.scala +++ b/akka-stream/src/main/scala/akka/stream/impl/io/TlsModule.scala @@ -4,10 +4,10 @@ package akka.stream.impl.io -import scala.util.Try - import javax.net.ssl.{ SSLEngine, SSLSession } +import scala.util.Try + import akka.NotUsed import akka.actor.ActorSystem import akka.annotation.InternalApi diff --git a/akka-stream/src/main/scala/akka/stream/javadsl/Flow.scala b/akka-stream/src/main/scala/akka/stream/javadsl/Flow.scala index f29779b5c7..0f7e7d4611 100755 --- a/akka-stream/src/main/scala/akka/stream/javadsl/Flow.scala +++ b/akka-stream/src/main/scala/akka/stream/javadsl/Flow.scala @@ -6,7 +6,6 @@ package akka.stream.javadsl import java.util.Comparator import java.util.Optional -import java.util.concurrent.CompletableFuture import java.util.concurrent.CompletionStage import java.util.function.BiFunction import java.util.function.Supplier @@ -30,7 +29,6 @@ import akka.japi.Util import akka.japi.function import akka.japi.function.Creator import akka.stream._ -import akka.stream.impl.fusing.LazyFlow import akka.util.ConstantFun import akka.util.JavaDurationConverters._ import akka.util.Timeout @@ -263,9 +261,9 @@ object Flow { flowFactory: function.Function[I, CompletionStage[Flow[I, O, M]]], fallback: function.Creator[M]): Flow[I, O, M] = { import scala.compat.java8.FutureConverters._ - val sflow = scaladsl.Flow - .fromGraph(new LazyFlow[I, O, M](t => flowFactory.apply(t).toScala.map(_.asScala)(ExecutionContexts.parasitic))) - .mapMaterializedValue(_ => fallback.create()) + val sflow = scaladsl.Flow.lazyInit( + (flowFactory.apply(_)).andThen(_.toScala.map(_.asScala)(ExecutionContexts.parasitic)), + fallback.create _) new Flow(sflow) } @@ -304,8 +302,12 @@ object Flow { * The materialized completion stage value is completed with the materialized value of the future flow or failed with a * [[NeverMaterializedException]] if upstream fails or downstream cancels before the completion stage has completed. */ - def completionStageFlow[I, O, M](flow: CompletionStage[Flow[I, O, M]]): Flow[I, O, CompletionStage[M]] = - lazyCompletionStageFlow(() => flow) + def completionStageFlow[I, O, M](flow: CompletionStage[Flow[I, O, M]]): Flow[I, O, CompletionStage[M]] = { + import scala.compat.java8.FutureConverters._ + val sflow = + scaladsl.Flow.futureFlow(flow.toScala.map(_.asScala)(ExecutionContexts.parasitic)).mapMaterializedValue(_.toJava) + new javadsl.Flow(sflow) + } /** * Defers invoking the `create` function to create a future flow until there is downstream demand and passing @@ -322,8 +324,15 @@ object Flow { * * '''Cancels when''' downstream cancels */ - def lazyFlow[I, O, M](create: Creator[Flow[I, O, M]]): Flow[I, O, CompletionStage[M]] = - lazyCompletionStageFlow(() => CompletableFuture.completedFuture(create.create())) + def lazyFlow[I, O, M](create: Creator[Flow[I, O, M]]): Flow[I, O, CompletionStage[M]] = { + import scala.compat.java8.FutureConverters._ + val sflow = scaladsl.Flow + .lazyFlow { () => + create.create().asScala + } + .mapMaterializedValue(_.toJava) + new javadsl.Flow(sflow) + } /** * Defers invoking the `create` function to create a future flow until there downstream demand has caused upstream @@ -365,7 +374,9 @@ object Flow { } -/** Create a `Flow` which can process elements of type `T`. */ +/** + * A `Flow` is a set of stream processing steps that has one open input and one open output. + */ final class Flow[In, Out, Mat](delegate: scaladsl.Flow[In, Out, Mat]) extends Graph[FlowShape[In, Out], Mat] { import akka.util.ccompat.JavaConverters._ diff --git a/akka-stream/src/main/scala/akka/stream/javadsl/Source.scala b/akka-stream/src/main/scala/akka/stream/javadsl/Source.scala index 7356dd0acc..770f33fd5b 100755 --- a/akka-stream/src/main/scala/akka/stream/javadsl/Source.scala +++ b/akka-stream/src/main/scala/akka/stream/javadsl/Source.scala @@ -304,6 +304,13 @@ object Source { def future[T](futureElement: Future[T]): Source[T, NotUsed] = scaladsl.Source.future(futureElement).asJava + /** + * Never emits any elements, never completes and never fails. + * This stream could be useful in tests. + */ + def never[T]: Source[T, NotUsed] = + scaladsl.Source.never.asJava + /** * Emits a single value when the given `CompletionStage` is successfully completed and then completes the stream. * If the `CompletionStage` is completed with a failure the stream is failed. diff --git a/akka-stream/src/main/scala/akka/stream/javadsl/TLS.scala b/akka-stream/src/main/scala/akka/stream/javadsl/TLS.scala index 383bf830cf..e0bdf57aec 100644 --- a/akka-stream/src/main/scala/akka/stream/javadsl/TLS.scala +++ b/akka-stream/src/main/scala/akka/stream/javadsl/TLS.scala @@ -6,12 +6,12 @@ package akka.stream.javadsl import java.util.Optional import java.util.function.{ Consumer, Supplier } +import javax.net.ssl.{ SSLContext, SSLEngine, SSLSession } import scala.compat.java8.OptionConverters import scala.util.Try import com.typesafe.sslconfig.akka.AkkaSSLConfig -import javax.net.ssl.{ SSLContext, SSLEngine, SSLSession } import akka.{ japi, NotUsed } import akka.stream._ @@ -36,7 +36,7 @@ import akka.util.ByteString * * '''IMPORTANT NOTE''' * - * The TLS specification does not permit half-closing of the user data session + * The TLS specification until version 1.2 did not permit half-closing of the user data session * that it transports—to be precise a half-close will always promptly lead to a * full close. This means that canceling the plaintext output or completing the * plaintext input of the SslTls operator will lead to full termination of the @@ -50,7 +50,8 @@ import akka.util.ByteString * order to terminate the connection the client will then need to cancel the * plaintext output as soon as all expected bytes have been received. When * ignoring both types of events the operator will shut down once both events have - * been received. See also [[TLSClosing]]. + * been received. See also [[TLSClosing]]. For now, half-closing is also not + * supported with TLS 1.3 where the spec allows it. */ object TLS { diff --git a/akka-stream/src/main/scala/akka/stream/javadsl/Tcp.scala b/akka-stream/src/main/scala/akka/stream/javadsl/Tcp.scala index 9f47a48e7f..f17b12202f 100644 --- a/akka-stream/src/main/scala/akka/stream/javadsl/Tcp.scala +++ b/akka-stream/src/main/scala/akka/stream/javadsl/Tcp.scala @@ -10,6 +10,9 @@ import java.util.Optional import java.util.concurrent.CompletionStage import java.util.function.{ Function => JFunction } import java.util.function.Supplier +import javax.net.ssl.SSLContext +import javax.net.ssl.SSLEngine +import javax.net.ssl.SSLSession import scala.compat.java8.FutureConverters._ import scala.compat.java8.OptionConverters._ @@ -18,9 +21,6 @@ import scala.util.Failure import scala.util.Success import com.github.ghik.silencer.silent -import javax.net.ssl.SSLContext -import javax.net.ssl.SSLEngine -import javax.net.ssl.SSLSession import akka.{ Done, NotUsed } import akka.actor.ActorSystem diff --git a/akka-stream/src/main/scala/akka/stream/scaladsl/Flow.scala b/akka-stream/src/main/scala/akka/stream/scaladsl/Flow.scala index 6f0db2bc75..9ffefd69d2 100755 --- a/akka-stream/src/main/scala/akka/stream/scaladsl/Flow.scala +++ b/akka-stream/src/main/scala/akka/stream/scaladsl/Flow.scala @@ -578,13 +578,22 @@ object Flow { * * '''Completes when''' upstream completes and all elements have been emitted from the internal flow * - * '''Cancels when''' downstream cancels + * '''Cancels when''' downstream cancels (see below) + * + * The operator's default behaviour in case of downstream cancellation before nested flow materialization (future completion) is to cancel immediately. + * This behaviour can be controlled by setting the [[akka.stream.Attributes.NestedMaterializationCancellationPolicy.PropagateToNested]] attribute, + * this will delay downstream cancellation until nested flow's materialization which is then immediately cancelled (with the original cancellation cause). */ @deprecated( "Use 'Flow.futureFlow' in combination with prefixAndTail(1) instead, see `futureFlow` operator docs for details", "2.6.0") def lazyInit[I, O, M](flowFactory: I => Future[Flow[I, O, M]], fallback: () => M): Flow[I, O, M] = - Flow.fromGraph(new LazyFlow[I, O, M](flowFactory)).mapMaterializedValue(_ => fallback()) + Flow[I] + .flatMapPrefix(1) { + case Seq(a) => futureFlow(flowFactory(a)).mapMaterializedValue(_ => NotUsed) + case Nil => Flow[I].asInstanceOf[Flow[I, O, NotUsed]] + } + .mapMaterializedValue(_ => fallback()) /** * Creates a real `Flow` upon receiving the first element. Internal `Flow` will not be created @@ -600,13 +609,17 @@ object Flow { * * '''Completes when''' upstream completes and all elements have been emitted from the internal flow * - * '''Cancels when''' downstream cancels + * '''Cancels when''' downstream cancels (see below) + * + * The operator's default behaviour in case of downstream cancellation before nested flow materialization (future completion) is to cancel immediately. + * This behaviour can be controlled by setting the [[akka.stream.Attributes.NestedMaterializationCancellationPolicy.PropagateToNested]] attribute, + * this will delay downstream cancellation until nested flow's materialization which is then immediately cancelled (with the original cancellation cause). */ @deprecated("Use 'Flow.lazyFutureFlow' instead", "2.6.0") def lazyInitAsync[I, O, M](flowFactory: () => Future[Flow[I, O, M]]): Flow[I, O, Future[Option[M]]] = - Flow.fromGraph(new LazyFlow[I, O, M](_ => flowFactory())).mapMaterializedValue { v => + Flow.lazyFutureFlow(flowFactory).mapMaterializedValue { implicit val ec = akka.dispatch.ExecutionContexts.parasitic - v.map[Option[M]](Some.apply _).recover { case _: NeverMaterializedException => None } + _.map(Some.apply).recover { case _: NeverMaterializedException => None } } /** @@ -615,9 +628,13 @@ object Flow { * * The materialized future value is completed with the materialized value of the future flow or failed with a * [[NeverMaterializedException]] if upstream fails or downstream cancels before the future has completed. + * + * The operator's default behaviour in case of downstream cancellation before nested flow materialization (future completion) is to cancel immediately. + * This behaviour can be controlled by setting the [[akka.stream.Attributes.NestedMaterializationCancellationPolicy.PropagateToNested]] attribute, + * this will delay downstream cancellation until nested flow's materialization which is then immediately cancelled (with the original cancellation cause). */ def futureFlow[I, O, M](flow: Future[Flow[I, O, M]]): Flow[I, O, Future[M]] = - lazyFutureFlow(() => flow) + Flow.fromGraph(new FutureFlow(flow)) /** * Defers invoking the `create` function to create a future flow until there is downstream demand and passing @@ -638,7 +655,11 @@ object Flow { * * '''Completes when''' upstream completes and all elements have been emitted from the internal flow * - * '''Cancels when''' downstream cancels + * '''Cancels when''' downstream cancels (see below) + * + * The operator's default behaviour in case of downstream cancellation before nested flow materialization (future completion) is to cancel immediately. + * This behaviour can be controlled by setting the [[akka.stream.Attributes.NestedMaterializationCancellationPolicy.PropagateToNested]] attribute, + * this will delay downstream cancellation until nested flow's materialization which is then immediately cancelled (with the original cancellation cause). */ def lazyFlow[I, O, M](create: () => Flow[I, O, M]): Flow[I, O, Future[M]] = lazyFutureFlow(() => Future.successful(create())) @@ -662,10 +683,27 @@ object Flow { * * '''Completes when''' upstream completes and all elements have been emitted from the internal flow * - * '''Cancels when''' downstream cancels + * '''Cancels when''' downstream cancels (see below) + * + * The operator's default behaviour in case of downstream cancellation before nested flow materialization (future completion) is to cancel immediately. + * This behaviour can be controlled by setting the [[akka.stream.Attributes.NestedMaterializationCancellationPolicy.PropagateToNested]] attribute, + * this will delay downstream cancellation until nested flow's materialization which is then immediately cancelled (with the original cancellation cause). */ def lazyFutureFlow[I, O, M](create: () => Future[Flow[I, O, M]]): Flow[I, O, Future[M]] = - Flow.fromGraph(new LazyFlow(_ => create())) + Flow[I] + .flatMapPrefixMat(1) { + case Seq(a) => + val f: Flow[I, O, Future[M]] = + futureFlow(create() + .map(Flow[I].prepend(Source.single(a)).viaMat(_)(Keep.right))(akka.dispatch.ExecutionContexts.parasitic)) + f + case Nil => + val f: Flow[I, O, Future[M]] = Flow[I] + .asInstanceOf[Flow[I, O, NotUsed]] + .mapMaterializedValue(_ => Future.failed[M](new NeverMaterializedException())) + f + }(Keep.right) + .mapMaterializedValue(_.flatten) } @@ -1945,7 +1983,9 @@ trait FlowOps[+Out, +Mat] { * the resulting flow will be materialized and signalled for upstream completion, it can then complete or continue to emit elements at its own discretion. * * '''Cancels when''' the materialized flow cancels. - * Notice that when downstream cancels prior to prefix completion, the cancellation cause is stashed until prefix completion (or upstream completion) and then handed to the materialized flow. + * When downstream cancels before materialization of the nested flow, the operator's default behaviour is to cancel immediately, + * this behaviour can be controlled by setting the [[akka.stream.Attributes.NestedMaterializationCancellationPolicy]] attribute on the flow. + * When this attribute is configured to true, downstream cancellation is delayed until the nested flow's materialization which is then immediately cancelled (with the original cancellation cause). * * @param n the number of elements to accumulate before materializing the downstream flow. * @param f a function that produces the downstream flow based on the upstream's prefix. diff --git a/akka-stream/src/main/scala/akka/stream/scaladsl/Source.scala b/akka-stream/src/main/scala/akka/stream/scaladsl/Source.scala index 18bb3f3cd3..8270764fc3 100644 --- a/akka-stream/src/main/scala/akka/stream/scaladsl/Source.scala +++ b/akka-stream/src/main/scala/akka/stream/scaladsl/Source.scala @@ -512,6 +512,14 @@ object Source { def future[T](futureElement: Future[T]): Source[T, NotUsed] = fromGraph(new FutureSource[T](futureElement)) + /** + * Never emits any elements, never completes and never fails. + * This stream could be useful in tests. + */ + def never[T]: Source[T, NotUsed] = _never + private[this] val _never: Source[Nothing, NotUsed] = + future(Future.never).withAttributes(DefaultAttributes.neverSource) + /** * Emits a single value when the given `CompletionStage` is successfully completed and then completes the stream. * If the `CompletionStage` is completed with a failure the stream is failed. diff --git a/akka-stream/src/main/scala/akka/stream/scaladsl/TLS.scala b/akka-stream/src/main/scala/akka/stream/scaladsl/TLS.scala index 74c934b102..410f4b7ac8 100644 --- a/akka-stream/src/main/scala/akka/stream/scaladsl/TLS.scala +++ b/akka-stream/src/main/scala/akka/stream/scaladsl/TLS.scala @@ -5,12 +5,12 @@ package akka.stream.scaladsl import java.util.Collections +import javax.net.ssl.{ SNIHostName, SSLContext, SSLEngine, SSLSession } +import javax.net.ssl.SSLParameters import scala.util.{ Failure, Success, Try } import com.typesafe.sslconfig.akka.AkkaSSLConfig -import javax.net.ssl.{ SNIHostName, SSLContext, SSLEngine, SSLSession } -import javax.net.ssl.SSLParameters import akka.NotUsed import akka.actor.ActorSystem @@ -37,7 +37,7 @@ import akka.util.ByteString * * '''IMPORTANT NOTE''' * - * The TLS specification does not permit half-closing of the user data session + * The TLS specification until version 1.2 did not permit half-closing of the user data session * that it transports—to be precise a half-close will always promptly lead to a * full close. This means that canceling the plaintext output or completing the * plaintext input of the SslTls operator will lead to full termination of the @@ -51,7 +51,8 @@ import akka.util.ByteString * order to terminate the connection the client will then need to cancel the * plaintext output as soon as all expected bytes have been received. When * ignoring both types of events the operator will shut down once both events have - * been received. See also [[TLSClosing]]. + * been received. See also [[TLSClosing]]. For now, half-closing is also not + * supported with TLS 1.3 where the spec allows it. */ object TLS { @@ -229,7 +230,6 @@ object TLSPlacebo { import java.security.Principal import java.security.cert.Certificate - import javax.net.ssl.{ SSLPeerUnverifiedException, SSLSession } /** Allows access to an SSLSession with Scala types */ diff --git a/akka-stream/src/main/scala/akka/stream/scaladsl/Tcp.scala b/akka-stream/src/main/scala/akka/stream/scaladsl/Tcp.scala index 0272d5551f..94721bfdd2 100644 --- a/akka-stream/src/main/scala/akka/stream/scaladsl/Tcp.scala +++ b/akka-stream/src/main/scala/akka/stream/scaladsl/Tcp.scala @@ -6,6 +6,9 @@ package akka.stream.scaladsl import java.net.InetSocketAddress import java.util.concurrent.TimeoutException +import javax.net.ssl.SSLContext +import javax.net.ssl.SSLEngine +import javax.net.ssl.SSLSession import scala.collection.immutable import scala.concurrent.Future @@ -17,9 +20,6 @@ import scala.util.Try import scala.util.control.NoStackTrace import com.github.ghik.silencer.silent -import javax.net.ssl.SSLContext -import javax.net.ssl.SSLEngine -import javax.net.ssl.SSLSession import akka.Done import akka.NotUsed diff --git a/akka-stream/src/main/scala/akka/stream/snapshot/MaterializerState.scala b/akka-stream/src/main/scala/akka/stream/snapshot/MaterializerState.scala index ac4968d83c..1166c5dd31 100644 --- a/akka-stream/src/main/scala/akka/stream/snapshot/MaterializerState.scala +++ b/akka-stream/src/main/scala/akka/stream/snapshot/MaterializerState.scala @@ -170,8 +170,9 @@ final private[akka] case class StreamSnapshotImpl( self: ActorPath, activeInterpreters: Seq[RunningInterpreter], newShells: Seq[UninitializedInterpreter]) - extends StreamSnapshot - with HideImpl + extends StreamSnapshot { + override def toString: String = s"StreamSnapshot($self, $activeInterpreters, $newShells)" +} /** * INTERNAL API diff --git a/akka-stream/src/main/scala/com/typesafe/sslconfig/akka/AkkaSSLConfig.scala b/akka-stream/src/main/scala/com/typesafe/sslconfig/akka/AkkaSSLConfig.scala index 1f136691b2..3aad9c62d6 100644 --- a/akka-stream/src/main/scala/com/typesafe/sslconfig/akka/AkkaSSLConfig.scala +++ b/akka-stream/src/main/scala/com/typesafe/sslconfig/akka/AkkaSSLConfig.scala @@ -7,11 +7,11 @@ package com.typesafe.sslconfig.akka import java.security.KeyStore import java.security.cert.CertPathValidatorException import java.util.Collections +import javax.net.ssl._ import com.typesafe.sslconfig.akka.util.AkkaLoggerFactory import com.typesafe.sslconfig.ssl._ import com.typesafe.sslconfig.util.LoggerFactory -import javax.net.ssl._ import akka.actor._ import akka.annotation.InternalApi diff --git a/akka-stream/src/main/scala/com/typesafe/sslconfig/akka/SSLEngineConfigurator.scala b/akka-stream/src/main/scala/com/typesafe/sslconfig/akka/SSLEngineConfigurator.scala index 89a7ccfee8..4d98ffdb57 100644 --- a/akka-stream/src/main/scala/com/typesafe/sslconfig/akka/SSLEngineConfigurator.scala +++ b/akka-stream/src/main/scala/com/typesafe/sslconfig/akka/SSLEngineConfigurator.scala @@ -4,9 +4,10 @@ package com.typesafe.sslconfig.akka -import com.typesafe.sslconfig.ssl.SSLConfigSettings import javax.net.ssl.{ SSLContext, SSLEngine } +import com.typesafe.sslconfig.ssl.SSLConfigSettings + /** * Gives the chance to configure the SSLContext before it is going to be used. * The passed in context will be already set in client mode and provided with hostInfo during initialization. diff --git a/build.sbt b/build.sbt index 63c5e1c222..df345dbc1f 100644 --- a/build.sbt +++ b/build.sbt @@ -1,4 +1,4 @@ -import akka.{AutomaticModuleName, CopyrightHeaderForBuild, Paradox, ParadoxSupport, ScalafixIgnoreFilePlugin} +import akka.{ AutomaticModuleName, CopyrightHeaderForBuild, Paradox, ScalafixIgnoreFilePlugin } enablePlugins( UnidocRoot, @@ -11,18 +11,18 @@ enablePlugins( disablePlugins(MimaPlugin) addCommandAlias( name = "fixall", - value = ";scalafixEnable;compile:scalafix;test:scalafix;multi-jvm:scalafix;scalafmtAll;test:compile;multi-jvm:compile;reload") + value = + ";scalafixEnable;compile:scalafix;test:scalafix;multi-jvm:scalafix;scalafmtAll;test:compile;multi-jvm:compile;reload") addCommandAlias( name = "sortImports", - value = ";scalafixEnable;compile:scalafix SortImports;test:scalafix SortImports;multi-jvm:scalafix SortImports;" + - "CompileJdk9:scalafix SortImports;TestJdk9:scalafix SortImports;scalafmtAll;test:compile;multi-jvm:compile;reload") + value = ";scalafixEnable;compile:scalafix SortImports;test:scalafix SortImports;scalafmtAll") import akka.AkkaBuild._ -import akka.{AkkaBuild, Dependencies, OSGi, Protobuf, SigarLoader, VersionGenerator} +import akka.{ AkkaBuild, Dependencies, OSGi, Protobuf, SigarLoader, VersionGenerator } import com.typesafe.sbt.SbtMultiJvm.MultiJvmKeys.MultiJvm import com.typesafe.tools.mima.plugin.MimaPlugin -import sbt.Keys.{initialCommands, parallelExecution} +import sbt.Keys.{ initialCommands, parallelExecution } import spray.boilerplate.BoilerplatePlugin initialize := { @@ -68,6 +68,7 @@ lazy val aggregatedProjects: Seq[ProjectReference] = List[ProjectReference]( persistenceTestkit, protobuf, protobufV3, + pki, remote, remoteTests, slf4j, @@ -118,7 +119,12 @@ lazy val benchJmh = akkaModule("akka-bench-jmh") .disablePlugins(MimaPlugin, WhiteSourcePlugin, ValidatePullRequest, CopyrightHeaderInPr) lazy val cluster = akkaModule("akka-cluster") - .dependsOn(remote, remoteTests % "test->test", testkit % "test->test", jackson % "test->test") + .dependsOn( + remote, + coordination % "compile->compile;test->test", + remoteTests % "test->test", + testkit % "test->test", + jackson % "test->test") .settings(Dependencies.cluster) .settings(AutomaticModuleName.settings("akka.cluster")) .settings(OSGi.cluster) @@ -160,7 +166,10 @@ lazy val clusterSharding = akkaModule("akka-cluster-sharding") .enablePlugins(MultiNode, ScaladocNoVerificationOfDiagrams) lazy val clusterTools = akkaModule("akka-cluster-tools") - .dependsOn(cluster % "compile->compile;test->test;multi-jvm->multi-jvm", coordination, jackson % "test->test") + .dependsOn( + cluster % "compile->compile;test->test;multi-jvm->multi-jvm", + coordination % "compile->compile;test->test", + jackson % "test->test") .settings(Dependencies.clusterTools) .settings(AutomaticModuleName.settings("akka.cluster.tools")) .settings(OSGi.clusterTools) @@ -205,7 +214,6 @@ lazy val docs = akkaModule("akka-docs") persistenceTestkit % "compile->compile;test->test") .settings(Dependencies.docs) .settings(Paradox.settings) - .settings(ParadoxSupport.paradoxWithCustomDirectives) .settings(javacOptions += "-parameters") // for Jackson .enablePlugins( AkkaParadoxPlugin, @@ -315,6 +323,14 @@ lazy val protobufV3 = akkaModule("akka-protobuf-v3") test in assembly := {}, // assembly runs tests for unknown reason which introduces another cyclic dependency to packageBin via exportedJars description := "Akka Protobuf V3 is a shaded version of the protobuf runtime. Original POM: https://github.com/protocolbuffers/protobuf/blob/v3.9.0/java/pom.xml") +lazy val pki = + akkaModule("akka-pki") + .dependsOn(actor) // this dependency only exists for "@ApiMayChange" + .settings(Dependencies.pki) + .settings(AutomaticModuleName.settings("akka.pki")) + // The akka-pki artifact was added in Akka 2.6.2, no MiMa checks yet. + .disablePlugins(MimaPlugin) + lazy val remote = akkaModule("akka-remote") .dependsOn( @@ -492,7 +508,7 @@ lazy val discovery = akkaModule("akka-discovery") .settings(OSGi.discovery) lazy val coordination = akkaModule("akka-coordination") - .dependsOn(actor, testkit % "test->test", actorTests % "test->test", cluster % "test->test") + .dependsOn(actor, testkit % "test->test", actorTests % "test->test") .settings(Dependencies.coordination) .settings(AutomaticModuleName.settings("akka.coordination")) .settings(OSGi.coordination) diff --git a/project/Dependencies.scala b/project/Dependencies.scala index 3dbe4a905a..6c7aa30173 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -21,8 +21,8 @@ object Dependencies { // https://github.com/real-logic/aeron/blob/1.x.y/build.gradle val agronaVersion = "1.4.1" val nettyVersion = "3.10.6.Final" - val jacksonVersion = "2.10.3" - val protobufJavaVersion = "3.10.0" + val jacksonVersion = "2.10.4" + val protobufJavaVersion = "3.11.4" val logbackVersion = "1.2.3" val scala212Version = "2.12.11" @@ -83,6 +83,8 @@ object Dependencies { // Added explicitly for when artery tcp is used val agrona = "org.agrona" % "agrona" % agronaVersion // ApacheV2 + val asnOne = "com.hierynomus" % "asn-one" % "0.4.0" // ApacheV2 + val jacksonCore = "com.fasterxml.jackson.core" % "jackson-core" % jacksonVersion // ApacheV2 val jacksonAnnotations = "com.fasterxml.jackson.core" % "jackson-annotations" % jacksonVersion // ApacheV2 val jacksonDatabind = "com.fasterxml.jackson.core" % "jackson-databind" % jacksonVersion // ApacheV2 @@ -91,6 +93,7 @@ object Dependencies { val jacksonScala = "com.fasterxml.jackson.module" %% "jackson-module-scala" % jacksonVersion // ApacheV2 val jacksonParameterNames = "com.fasterxml.jackson.module" % "jackson-module-parameter-names" % jacksonVersion // ApacheV2 val jacksonCbor = "com.fasterxml.jackson.dataformat" % "jackson-dataformat-cbor" % jacksonVersion // ApacheV2 + val lz4Java = "org.lz4" % "lz4-java" % "1.7.1" // ApacheV2 val logback = "ch.qos.logback" % "logback-classic" % logbackVersion // EPL 1.0 @@ -128,8 +131,8 @@ object Dependencies { val dockerClient = "com.spotify" % "docker-client" % "8.16.0" % "test" // ApacheV2 // metrics, measurements, perf testing - val metrics = "io.dropwizard.metrics" % "metrics-core" % "4.1.6" % "test" // ApacheV2 - val metricsJvm = "io.dropwizard.metrics" % "metrics-jvm" % "4.1.6" % "test" // ApacheV2 + val metrics = "io.dropwizard.metrics" % "metrics-core" % "4.1.9" % "test" // ApacheV2 + val metricsJvm = "io.dropwizard.metrics" % "metrics-jvm" % "4.1.9" % "test" // ApacheV2 val latencyUtils = "org.latencyutils" % "LatencyUtils" % "2.0.3" % "test" // Free BSD val hdrHistogram = "org.hdrhistogram" % "HdrHistogram" % "2.1.12" % "test" // CC0 val metricsAll = Seq(metrics, metricsJvm, latencyUtils, hdrHistogram) @@ -195,6 +198,13 @@ object Dependencies { val actorTestkitTyped = l ++= Seq(Provided.logback, Provided.junit, Provided.scalatest, Test.scalatestJUnit) + val pki = l ++= + Seq( + asnOne, + // pull up slf4j version from the one provided transitively in asnOne to fix unidoc + Compile.slf4jApi % "provided", + Test.scalatest) + val remoteDependencies = Seq(netty, aeronDriver, aeronClient) val remoteOptionalDependencies = remoteDependencies.map(_ % "optional") @@ -256,6 +266,7 @@ object Dependencies { jacksonJsr310, jacksonParameterNames, jacksonCbor, + lz4Java, Test.junit, Test.scalatest) diff --git a/project/Paradox.scala b/project/Paradox.scala index aeccc71ad7..2f6b2b4690 100644 --- a/project/Paradox.scala +++ b/project/Paradox.scala @@ -33,7 +33,7 @@ object Paradox { "javadoc.akka.http.base_url" -> "https://doc.akka.io/japi/akka-http/current", "javadoc.akka.http.link_style" -> "frames", "scala.version" -> scalaVersion.value, - "scala.binary_version" -> scalaBinaryVersion.value, + "scala.binary.version" -> scalaBinaryVersion.value, "akka.version" -> version.value, "scalatest.version" -> Dependencies.scalaTestVersion, "sigar_loader.version" -> "1.6.6-rev002", diff --git a/project/ParadoxSupport.scala b/project/ParadoxSupport.scala deleted file mode 100644 index 4d8289534c..0000000000 --- a/project/ParadoxSupport.scala +++ /dev/null @@ -1,94 +0,0 @@ -/* - * Copyright (C) 2016-2020 Lightbend Inc. - */ - -package akka - -import java.io.{File, FileNotFoundException} - -import com.lightbend.paradox.markdown._ -import com.lightbend.paradox.sbt.ParadoxPlugin.autoImport._ -import org.pegdown.Printer -import org.pegdown.ast._ -import sbt.Keys._ -import sbt._ - -import scala.annotation.tailrec -import scala.io.{Codec, Source} -import scala.collection.JavaConverters._ - -object ParadoxSupport { - val paradoxWithCustomDirectives = Seq( - paradoxDirectives ++= Def.taskDyn { - val log = streams.value.log - val classpath = (fullClasspath in Compile).value.files.map(_.toURI.toURL).toArray - val classloader = new java.net.URLClassLoader(classpath, this.getClass().getClassLoader()) - import _root_.io.github.classgraph.ClassGraph - lazy val scanner = new ClassGraph() - .whitelistPackages("akka") - .addClassLoader(classloader) - .scan() - val allClasses = scanner.getAllClasses.getNames.asScala.toVector - Def.task { Seq( - { context: Writer.Context => - new SignatureDirective(context.location.tree.label, context.properties, context) - }, - )} - }.value - ) - - class SignatureDirective(page: Page, variables: Map[String, String], ctx: Writer.Context) extends LeafBlockDirective("signature") { - def render(node: DirectiveNode, visitor: Visitor, printer: Printer): Unit = - try { - val labels = node.attributes.values("identifier").asScala.map(_.toLowerCase()) - val source = node.source match { - case direct: DirectiveNode.Source.Direct => direct.value - case _ => sys.error("Source references are not supported") - } - val file = - if (source startsWith "/") { - // snip.build.base_dir defined by Paradox - val base = new File(PropertyUrl("snip.build.base_dir", variables.get).base.trim) - new File(base, source) - } else new File(page.file.getParentFile, source) - - val Signature = """\s*((def|val|type) (\w+)(?=[:(\[]).*)(\s+\=.*)""".r // stupid approximation to match a signature - - val text = - getDefs(file).collect { - case line@Signature(signature, kind, l, definition) if labels contains l.replaceAll("Mat$", "").toLowerCase() => - //println(s"Found label '$l' with sig '$full' in line $line") - if (kind == "type") signature + definition - else signature - }.mkString("\n") - - if (text.trim.isEmpty) { - throw new IllegalArgumentException( - s"Did not find any signatures with one of those names [${labels.mkString(", ")}] in $source " + - s"(was referenced from [${page.path}])") - } else { - val lang = Option(node.attributes.value("type")).getOrElse(Snippet.language(file)) - new VerbatimNode(text, lang).accept(visitor) - } - } catch { - case e: FileNotFoundException => - ctx.error(s"Unknown snippet [${e.getMessage}]", node) - } - } - - def getDefs(file: File): Seq[String] = { - val Indented = "(\\s*)(.*)".r - - @tailrec - def rec(lines: Iterator[String], currentDef: Option[String], defIndent: Integer, soFar: Seq[String]): Seq[String] = { - if (!lines.hasNext) soFar ++ currentDef - else lines.next() match { - case Indented(indent, line) => - if (line.startsWith("def")) rec(lines, Some(line), indent.length, soFar ++ currentDef) - else if (indent.length == defIndent + 4) rec(lines, currentDef.map(_ ++ line), defIndent, soFar) - else rec(lines, None, 0, soFar ++ currentDef) - } - } - rec(Source.fromFile(file)(Codec.UTF8).getLines, None, 0, Nil) - } -} diff --git a/project/ProjectFileIgnoreSupport.scala b/project/ProjectFileIgnoreSupport.scala index 5be747f960..f36f9dfa83 100644 --- a/project/ProjectFileIgnoreSupport.scala +++ b/project/ProjectFileIgnoreSupport.scala @@ -7,14 +7,23 @@ package akka import java.io.File import com.typesafe.config.ConfigFactory -import sbt.AutoPlugin -import sbt.Def -import sbt.file import sbt.internal.sbtscalafix.Compat class ProjectFileIgnoreSupport(ignoreConfigFile: File, descriptor: String) { private val stdoutLogger = Compat.ConsoleLogger(System.out) + private val javaSourceDirectories = Set( + "java", + Jdk9.JAVA_SOURCE_DIRECTORY, + Jdk9.JAVA_TEST_SOURCE_DIRECTORY + ) + + private val scalaSourceDirectories = Set( + "scala", + Jdk9.SCALA_SOURCE_DIRECTORY, + Jdk9.SCALA_TEST_SOURCE_DIRECTORY + ) + private lazy val ignoreConfig = { require(ignoreConfigFile.exists(), s"Expected ignore configuration for $descriptor at ${ignoreConfigFile.getAbsolutePath} but was missing") ConfigFactory.parseFile(ignoreConfigFile) @@ -55,7 +64,7 @@ class ProjectFileIgnoreSupport(ignoreConfigFile: File, descriptor: String) { case Some(packageName) => val ignored = packageName.startsWith(pkg) if (ignored) { - stdoutLogger.debug(s"$descriptor ignored file with pkg:$pkg file:[${file.toPath}] ") + stdoutLogger.debug(s"$descriptor ignored file with pkg:$pkg for package:$packageName file:[${file.toPath}] ") } ignored case None => false @@ -65,22 +74,23 @@ class ProjectFileIgnoreSupport(ignoreConfigFile: File, descriptor: String) { } private def getPackageName(fileName: String): Option[String] = { - def getPackageName0(fileType: String): String = { + def getPackageName0(sourceDirectories:Set[String]): String = { import java.io.{File => JFile} - fileName.split(JFile.separatorChar) - .dropWhile(part => part != fileType) + val packageName = fileName.split(JFile.separatorChar) + .dropWhile(part => !sourceDirectories(part)) .drop(1) .dropRight(1) .mkString(".") + packageName } fileName.split('.').lastOption match { case Some(fileType) => fileType match { case "java" => - Option(getPackageName0("java")) + Option(getPackageName0(javaSourceDirectories)) case "scala" => - Option(getPackageName0("scala")) + Option(getPackageName0(scalaSourceDirectories)) case _ => None } case None => None diff --git a/project/Protobuf.scala b/project/Protobuf.scala index 77cde53ac3..af75c3af1d 100644 --- a/project/Protobuf.scala +++ b/project/Protobuf.scala @@ -32,7 +32,7 @@ object Protobuf { Compile / unmanagedJars ++= Seq( baseDirectory.value / ".." / "akka-protobuf-v3" / "target" / s"scala-${scalaBinaryVersion.value}" / s"akka-protobuf-v3-assembly-${version.value}.jar"), protoc := "protoc", - protocVersion := "3.10.0", + protocVersion := "3.11.4", generate := { val sourceDirs = paths.value val targetDirs = outputPaths.value diff --git a/project/Publish.scala b/project/Publish.scala index c359c8144e..ab9037ca65 100644 --- a/project/Publish.scala +++ b/project/Publish.scala @@ -35,7 +35,7 @@ object Publish extends AutoPlugin { akka-contributors Akka Contributors - akka-dev@googlegroups.com + akka.official@gmail.com https://github.com/akka/akka/graphs/contributors @@ -43,7 +43,8 @@ object Publish extends AutoPlugin { private def akkaPublishTo = Def.setting { val key = new java.io.File( - Option(System.getProperty("akka.gustav.key")).getOrElse(System.getProperty("user.home") + "/.ssh/id_rsa_gustav.pem")) + Option(System.getProperty("akka.gustav.key")) + .getOrElse(System.getProperty("user.home") + "/.ssh/id_rsa_gustav.pem")) if (isSnapshot.value) Resolver.sftp("Akka snapshots", "gustav.akka.io", "/home/akkarepo/www/snapshots").as("akkarepo", key) else diff --git a/project/ScalaFixExtraRulesPlugin.scala b/project/ScalaFixExtraRulesPlugin.scala index dcf6fc6aff..601071f6dd 100644 --- a/project/ScalaFixExtraRulesPlugin.scala +++ b/project/ScalaFixExtraRulesPlugin.scala @@ -15,6 +15,6 @@ object ScalaFixExtraRulesPlugin extends AutoPlugin with ScalafixSupport{ import sbt._ import scalafix.sbt.ScalafixPlugin.autoImport.scalafixDependencies override def projectSettings: Seq[Def.Setting[_]] = super.projectSettings ++ { - scalafixDependencies in ThisBuild += "com.nequissimus" %% "sort-imports" % "0.4.0" + scalafixDependencies in ThisBuild += "com.nequissimus" %% "sort-imports" % "0.5.0" } } diff --git a/project/ScalaFixForJdk9Plugin.scala b/project/ScalaFixForJdk9Plugin.scala index ff6ae854d3..06f1fb5386 100644 --- a/project/ScalaFixForJdk9Plugin.scala +++ b/project/ScalaFixForJdk9Plugin.scala @@ -6,15 +6,26 @@ package akka import sbt.{AutoPlugin, PluginTrigger, Plugins, ScalafixSupport} import scalafix.sbt.ScalafixPlugin -object ScalaFixForJdk9Plugin extends AutoPlugin with ScalafixSupport{ +object ScalaFixForJdk9Plugin extends AutoPlugin with ScalafixSupport { override def trigger: PluginTrigger = allRequirements import Jdk9._ - override def requires: Plugins = Jdk9 && ScalafixPlugin + override def requires: Plugins = Jdk9 import ScalafixPlugin.autoImport.scalafixConfigSettings import sbt._ - override def projectSettings: Seq[Def.Setting[_]] = super.projectSettings ++ { - inConfig(TestJdk9)(scalafixConfigSettings(TestJdk9)) ++ - inConfig(CompileJdk9)(scalafixConfigSettings(CompileJdk9)) - } + + lazy val scalafixIgnoredSetting: Seq[Setting[_]] = Seq( + ignore(TestJdk9) + ) + + override def projectSettings: Seq[Def.Setting[_]] = + Seq(CompileJdk9, TestJdk9).flatMap(c => inConfig(c)(scalafixConfigSettings(c))) ++ + scalafixIgnoredSetting ++ Seq( + updateProjectCommands( + alias = "fixall", + value = ";scalafixEnable;compile:scalafix;test:scalafix;multi-jvm:scalafix;scalafmtAll;test:compile;multi-jvm:compile;reload"), + updateProjectCommands( + alias = "sortImports", + value = ";scalafixEnable;compile:scalafix SortImports;test:scalafix SortImports;CompileJdk9:scalafix SortImports;TestJdk9:scalafix SortImports;scalafmtAll") + ) } diff --git a/project/ScalafixForMultiNodePlugin.scala b/project/ScalafixForMultiNodePlugin.scala index 2f590ef992..1fc02619e2 100644 --- a/project/ScalafixForMultiNodePlugin.scala +++ b/project/ScalafixForMultiNodePlugin.scala @@ -23,6 +23,10 @@ object ScalafixForMultiNodePlugin extends AutoPlugin with ScalafixSupport { Seq(MultiJvm).flatMap(c => inConfig(c)(scalafixConfigSettings(c))) ++ scalafixIgnoredSetting ++ Seq( updateProjectCommands( - alias = "fix", - value = ";scalafixEnable;compile:scalafix;test:scalafix;multi-jvm:scalafix;test:compile;reload")) + alias = "fixall", + value = ";scalafixEnable;compile:scalafix;test:scalafix;multi-jvm:scalafix;scalafmtAll"), + updateProjectCommands( + alias = "sortImports", + value = ";scalafixEnable;compile:scalafix SortImports;test:scalafix SortImports;multi-jvm:scalafix SortImports;scalafmtAll") + ) } diff --git a/project/project-info.conf b/project/project-info.conf index 252c8cc582..a6d8a82332 100644 --- a/project/project-info.conf +++ b/project/project-info.conf @@ -250,7 +250,7 @@ project-info { text: "API (Scaladoc)" } { - url: ${project-info.javadoc}"coordination/package-summary.html" + url: ${project-info.javadoc}"coordination/lease/package-summary.html" text: "API (Javadoc)" } ] diff --git a/project/scripts/release b/project/scripts/release index dbd265b667..a0316f6a33 100755 --- a/project/scripts/release +++ b/project/scripts/release @@ -67,7 +67,7 @@ # # 3.3) Also make it available for publishing snapshots. # From the command line: -# shell> cp ~/.ssh/id_rsa.pub ~/.ssh/id_rsa_gustav.pem +# shell> cp ~/.ssh/id_rsa ~/.ssh/id_rsa_gustav.pem # shell> ssh-keygen -p -f ~/.ssh/id_rsa_gustav.pem -m pem # # 4) Have access to github.com/akka/akka. This should be a given.