diff --git a/akka-actor-typed-tests/src/test/java/jdocs/akka/typed/IntroTest.java b/akka-actor-typed-tests/src/test/java/jdocs/akka/typed/IntroTest.java index 5785bd7f21..e56d610fce 100644 --- a/akka-actor-typed-tests/src/test/java/jdocs/akka/typed/IntroTest.java +++ b/akka-actor-typed-tests/src/test/java/jdocs/akka/typed/IntroTest.java @@ -5,21 +5,19 @@ package jdocs.akka.typed; //#imports + import akka.actor.typed.ActorRef; import akka.actor.typed.ActorSystem; import akka.actor.typed.Behavior; import akka.actor.typed.Terminated; import akka.actor.typed.javadsl.Behaviors; -import akka.actor.typed.javadsl.AskPattern; -import akka.util.Timeout; //#imports + import java.net.URLEncoder; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.List; -import java.util.concurrent.CompletionStage; -import java.util.concurrent.TimeUnit; public class IntroTest { @@ -42,35 +40,81 @@ public class IntroTest { public static final class Greeted { public final String whom; + public final ActorRef from; - public Greeted(String whom) { + public Greeted(String whom, ActorRef from) { this.whom = whom; + this.from = from; } } public static final Behavior greeter = Behaviors.receive((ctx, msg) -> { - System.out.println("Hello " + msg.whom + "!"); - msg.replyTo.tell(new Greeted(msg.whom)); + ctx.getLog().info("Hello {}!", msg.whom); + msg.replyTo.tell(new Greeted(msg.whom, ctx.getSelf())); return Behaviors.same(); }); } //#hello-world-actor - public static void main(String[] args) { - //#hello-world - final ActorSystem system = - ActorSystem.create(HelloWorld.greeter, "hello"); + //#hello-world-bot + public abstract static class HelloWorldBot { + private HelloWorldBot() { + } - final CompletionStage reply = - AskPattern.ask(system, - (ActorRef replyTo) -> new HelloWorld.Greet("world", replyTo), - new Timeout(3, TimeUnit.SECONDS), system.scheduler()); + public static final Behavior bot(int greetingCounter, int max) { + return Behaviors.receive((ctx, msg) -> { + int n = greetingCounter + 1; + ctx.getLog().info("Greeting {} for {}", n, msg.whom); + if (n == max) { + return Behaviors.stopped(); + } else { + msg.from.tell(new HelloWorld.Greet(msg.whom, ctx.getSelf())); + return bot(n, max); + } + }); + } + } + //#hello-world-bot - reply.thenAccept(greeting -> { - System.out.println("result: " + greeting.whom); - system.terminate(); - }); + //#hello-world-main + public abstract static class HelloWorldMain { + private HelloWorldMain() { + } + + public static class Start { + public final String name; + + public Start(String name) { + this.name = name; + } + } + + public static final Behavior main = + Behaviors.setup( context -> { + final ActorRef greeter = + context.spawn(HelloWorld.greeter, "greeter"); + + return Behaviors.receiveMessage(msg -> { + ActorRef replyTo = + context.spawn(HelloWorldBot.bot(0, 3), msg.name); + greeter.tell(new HelloWorld.Greet(msg.name, replyTo)); + return Behaviors.same(); + }); + }); + } + //#hello-world-main + + public static void main(String[] args) throws Exception { //#hello-world + final ActorSystem system = + ActorSystem.create(HelloWorldMain.main, "hello"); + + system.tell(new HelloWorldMain.Start("World")); + system.tell(new HelloWorldMain.Start("Akka")); + //#hello-world + + Thread.sleep(3000); + system.terminate(); } //#chatroom-actor diff --git a/akka-actor-typed-tests/src/test/scala/docs/akka/typed/IntroSpec.scala b/akka-actor-typed-tests/src/test/scala/docs/akka/typed/IntroSpec.scala index 5195e63709..65586b7ee9 100644 --- a/akka-actor-typed-tests/src/test/scala/docs/akka/typed/IntroSpec.scala +++ b/akka-actor-typed-tests/src/test/scala/docs/akka/typed/IntroSpec.scala @@ -5,19 +5,17 @@ package docs.akka.typed //#imports -import java.net.URLEncoder -import java.nio.charset.StandardCharsets - import akka.NotUsed -import akka.actor.typed.scaladsl.AskPattern._ import akka.actor.typed.scaladsl.Behaviors import akka.actor.typed.{ ActorRef, ActorSystem, Behavior, Terminated } -import akka.testkit.typed.scaladsl.ActorTestKit - -import scala.concurrent.duration._ -import scala.concurrent.{ Await, Future } //#imports +import akka.testkit.typed.scaladsl.ActorTestKit +import java.net.URLEncoder +import java.nio.charset.StandardCharsets +import scala.concurrent.Await +import scala.concurrent.duration._ + import akka.actor.typed.TypedAkkaSpecWithShutdown object IntroSpec { @@ -25,16 +23,51 @@ object IntroSpec { //#hello-world-actor object HelloWorld { final case class Greet(whom: String, replyTo: ActorRef[Greeted]) - final case class Greeted(whom: String) + final case class Greeted(whom: String, from: ActorRef[Greet]) - val greeter = Behaviors.receive[Greet] { (_, msg) ⇒ - println(s"Hello ${msg.whom}!") - msg.replyTo ! Greeted(msg.whom) + val greeter: Behavior[Greet] = Behaviors.receive { (ctx, msg) ⇒ + ctx.log.info("Hello {}!", msg.whom) + msg.replyTo ! Greeted(msg.whom, ctx.self) Behaviors.same } } //#hello-world-actor + //#hello-world-bot + object HelloWorldBot { + + def bot(greetingCounter: Int, max: Int): Behavior[HelloWorld.Greeted] = + Behaviors.receive { (ctx, msg) ⇒ + val n = greetingCounter + 1 + ctx.log.info("Greeting {} for {}", n, msg.whom) + if (n == max) { + Behaviors.stopped + } else { + msg.from ! HelloWorld.Greet(msg.whom, ctx.self) + bot(n, max) + } + } + } + //#hello-world-bot + + //#hello-world-main + object HelloWorldMain { + + final case class Start(name: String) + + val main: Behavior[Start] = + Behaviors.setup { context ⇒ + val greeter = context.spawn(HelloWorld.greeter, "greeter") + + Behaviors.receiveMessage { msg ⇒ + val replyTo = context.spawn(HelloWorldBot.bot(greetingCounter = 0, max = 3), msg.name) + greeter ! HelloWorld.Greet(msg.name, replyTo) + Behaviors.same + } + } + } + //#hello-world-main + //#chatroom-actor object ChatRoom { //#chatroom-protocol @@ -63,7 +96,7 @@ object IntroSpec { chatRoom(List.empty) private def chatRoom(sessions: List[ActorRef[SessionCommand]]): Behavior[RoomCommand] = - Behaviors.receive[RoomCommand] { (ctx, msg) ⇒ + Behaviors.receive { (ctx, msg) ⇒ msg match { case GetSession(screenName, client) ⇒ // create a child actor for further interaction with the client @@ -107,46 +140,38 @@ class IntroSpec extends ActorTestKit with TypedAkkaSpecWithShutdown { "Hello world" must { "say hello" in { - // TODO Implicits.global is not something we would like to encourage in docs //#hello-world - import HelloWorld._ - // using global pool since we want to run tasks after system.terminate - import scala.concurrent.ExecutionContext.Implicits.global - val system: ActorSystem[Greet] = ActorSystem(greeter, "hello") + val system: ActorSystem[HelloWorldMain.Start] = + ActorSystem(HelloWorldMain.main, "hello") - val future: Future[Greeted] = system ? (Greet("world", _)) + system ! HelloWorldMain.Start("World") + system ! HelloWorldMain.Start("Akka") - for { - greeting ← future.recover { case ex ⇒ ex.getMessage } - done ← { - println(s"result: $greeting") - system.terminate() - } - } println("system terminated") //#hello-world + + Thread.sleep(500) // it will not fail if too short + ActorTestKit.shutdown(system) } "chat" in { //#chatroom-gabbler import ChatRoom._ - val gabbler = - Behaviors.receive[SessionEvent] { (_, msg) ⇒ - msg match { - //#chatroom-gabbler - // We document that the compiler warns about the missing handler for `SessionDenied` - case SessionDenied(reason) ⇒ - println(s"cannot start chat room session: $reason") - Behaviors.stopped - //#chatroom-gabbler - case SessionGranted(handle) ⇒ - handle ! PostMessage("Hello World!") - Behaviors.same - case MessagePosted(screenName, message) ⇒ - println(s"message has been posted by '$screenName': $message") - Behaviors.stopped - } + val gabbler: Behavior[SessionEvent] = + Behaviors.receiveMessage { + //#chatroom-gabbler + // We document that the compiler warns about the missing handler for `SessionDenied` + case SessionDenied(reason) ⇒ + println(s"cannot start chat room session: $reason") + Behaviors.stopped + //#chatroom-gabbler + case SessionGranted(handle) ⇒ + handle ! PostMessage("Hello World!") + Behaviors.same + case MessagePosted(screenName, message) ⇒ + println(s"message has been posted by '$screenName': $message") + Behaviors.stopped } //#chatroom-gabbler diff --git a/akka-actor-typed/src/main/scala/akka/actor/typed/javadsl/Behaviors.scala b/akka-actor-typed/src/main/scala/akka/actor/typed/javadsl/Behaviors.scala index ef4f925a65..100470ca71 100644 --- a/akka-actor-typed/src/main/scala/akka/actor/typed/javadsl/Behaviors.scala +++ b/akka-actor-typed/src/main/scala/akka/actor/typed/javadsl/Behaviors.scala @@ -211,11 +211,11 @@ object Behaviors { * final Behavior[DbCommand] dbConnector = ... * * final Behavior[DbCommand] dbRestarts = - * Actor.supervise(dbConnector) + * Behaviors.supervise(dbConnector) * .onFailure(SupervisorStrategy.restart) // handle all NonFatal exceptions * * final Behavior[DbCommand] dbSpecificResumes = - * Actor.supervise(dbConnector) + * Behaviors.supervise(dbConnector) * .onFailure[IndexOutOfBoundsException](SupervisorStrategy.resume) // resume for IndexOutOfBoundsException exceptions * }}} */ diff --git a/akka-actor-typed/src/main/scala/akka/actor/typed/javadsl/TimerScheduler.scala b/akka-actor-typed/src/main/scala/akka/actor/typed/javadsl/TimerScheduler.scala index eb3845f19d..db533d5d1f 100644 --- a/akka-actor-typed/src/main/scala/akka/actor/typed/javadsl/TimerScheduler.scala +++ b/akka-actor-typed/src/main/scala/akka/actor/typed/javadsl/TimerScheduler.scala @@ -8,7 +8,7 @@ import scala.concurrent.duration.FiniteDuration /** * Support for scheduled `self` messages in an actor. - * It is used with `Actor.withTimers`, which also takes care of the + * It is used with `Behaviors.withTimers`, which also takes care of the * lifecycle of the timers such as cancelling them when the actor * is restarted or stopped. * diff --git a/akka-actor-typed/src/main/scala/akka/actor/typed/scaladsl/Behaviors.scala b/akka-actor-typed/src/main/scala/akka/actor/typed/scaladsl/Behaviors.scala index cf1ff89e8c..5e9f3c6b28 100644 --- a/akka-actor-typed/src/main/scala/akka/actor/typed/scaladsl/Behaviors.scala +++ b/akka-actor-typed/src/main/scala/akka/actor/typed/scaladsl/Behaviors.scala @@ -171,11 +171,11 @@ object Behaviors { * val dbConnector: Behavior[DbCommand] = ... * * val dbRestarts = - * Actor.supervise(dbConnector) + * Behaviors.supervise(dbConnector) * .onFailure(SupervisorStrategy.restart) // handle all NonFatal exceptions * * val dbSpecificResumes = - * Actor.supervise(dbConnector) + * Behaviors.supervise(dbConnector) * .onFailure[IndexOutOfBoundsException](SupervisorStrategy.resume) // resume for IndexOutOfBoundsException exceptions * }}} */ diff --git a/akka-actor-typed/src/main/scala/akka/actor/typed/scaladsl/TimerScheduler.scala b/akka-actor-typed/src/main/scala/akka/actor/typed/scaladsl/TimerScheduler.scala index 45fc995266..5ffb6c1fe6 100644 --- a/akka-actor-typed/src/main/scala/akka/actor/typed/scaladsl/TimerScheduler.scala +++ b/akka-actor-typed/src/main/scala/akka/actor/typed/scaladsl/TimerScheduler.scala @@ -8,7 +8,7 @@ import scala.concurrent.duration.FiniteDuration /** * Support for scheduled `self` messages in an actor. - * It is used with `Actor.withTimers`. + * It is used with `Behaviors.withTimers`. * Timers are bound to the lifecycle of the actor that owns it, * and thus are cancelled automatically when it is restarted or stopped. * diff --git a/akka-docs/src/main/paradox/typed/actors.md b/akka-docs/src/main/paradox/typed/actors.md index 4776033532..21680bec15 100644 --- a/akka-docs/src/main/paradox/typed/actors.md +++ b/akka-docs/src/main/paradox/typed/actors.md @@ -51,9 +51,9 @@ message. The behavior of the Actor is defined as the `greeter` value with the help of the `receive` behavior factory. Processing the next message then results -in a new behavior that can potentially be different from this one. State is -updated by returning a new behavior that holds the new immutable state. In this -case we don't need to update any state, so we return `Same`, which means +in a new behavior that can potentially be different from this one. State is +updated by returning a new behavior that holds the new immutable state. In this +case we don't need to update any state, so we return `same`, which means the next behavior is "the same as the current one". The type of the messages handled by this behavior is declared to be of class @@ -73,6 +73,31 @@ protocol but Actors can model arbitrarily complex protocols when needed. The protocol is bundled together with the behavior that implements it in a nicely wrapped scope—the `HelloWorld` @scala[object]@java[class]. +As Carl Hewitt said, one Actor is no Actor—it would be quite lonely with +nobody to talk to. We need another Actor that that interacts with the `greeter`. +Let's make a `bot` that receives the reply from the `greeter` and sends a number +of additional greeting messages and collect the replies until a given max number +of messages have been reached. + +Scala +: @@snip [IntroSpec.scala]($akka$/akka-actor-typed-tests/src/test/scala/docs/akka/typed/IntroSpec.scala) { #hello-world-bot } + +Java +: @@snip [IntroSpec.scala]($akka$/akka-actor-typed-tests/src/test/java/jdocs/akka/typed/IntroTest.java) { #hello-world-bot } + +Note how this Actor manages the counter by changing the behavior for each `Greeted` reply +rather than using any variables. + + + +A third actor spawns the `greeter` and the `bot` and starts the interaction between those. + +Scala +: @@snip [IntroSpec.scala]($akka$/akka-actor-typed-tests/src/test/scala/docs/akka/typed/IntroSpec.scala) { #hello-world-main } + +Java +: @@snip [IntroSpec.scala]($akka$/akka-actor-typed-tests/src/test/java/jdocs/akka/typed/IntroTest.java) { #hello-world-main } + Now we want to try out this Actor, so we must start an ActorSystem to host it: Scala @@ -81,58 +106,29 @@ Scala Java : @@snip [IntroSpec.scala]($akka$/akka-actor-typed-tests/src/test/java/jdocs/akka/typed/IntroTest.java) { #hello-world } -After importing the Actor’s protocol definition we start an Actor system from -the defined `greeter` behavior. +We start an Actor system from the defined `main` behavior and send two `Start` messages that +will kick-off the interaction between two separate `bot` actors and the single `greeter` actor. -As Carl Hewitt said, one Actor is no Actor—it would be quite lonely with -nobody to talk to. In this sense the example is a little cruel because we only -give the `HelloWorld` Actor a fake person to talk to—the “ask” pattern -(represented by the `?` operator) can be used to send a message such that the -reply fulfills a @scala[`Promise` to which we get back the corresponding `Future`]@java[`CompletionStage`]. +The console output may look like this: -@@@ div {.group-scala} - -Note that the `Future` that is returned by the “ask” operation is -properly typed already, no type checks or casts needed. This is possible due to -the type information that is part of the message protocol: the `?` operator -takes as argument a function that accepts an `ActorRef[U]` (which -explains the `_` hole in the expression on line 7 above) and the `replyTo` -parameter which we fill in is of type `ActorRef[Greeted]`, which -means that the value that fulfills the `Promise` can only be of type -`Greeted`. - -@@@ - -@@@ div {.group-java} - -Note that the `CompletionStage` that is returned by the “ask” operation is -properly typed already, no type checks or casts needed. This is possible due to -the type information that is part of the message protocol: the `ask` operator -takes as argument a function that pass an `ActorRef`, which is the -`replyTo` parameter of the `Greet` message, which means that when sending -the reply message to that `ActorRef` the message that fulfills the -`CompletionStage` can only be of type `Greeted`. - -@@@ - -We use this here to send the `Greet` command to the Actor and when the -reply comes back we will print it out and tell the actor system to shut down. - -@@@ div {.group-scala} - -The `recovery` combinator on the original `Future` is -needed in order to ensure proper system shutdown even in case something went -wrong; the `flatMap` and `map` combinators that the `for` expression gets -turned into care only about the “happy path” and if the `future` failed with -a timeout then no `greeting` would be extracted and nothing would happen. - -@@@ - -In the next section we demonstrate this on a more realistic example. +``` +[INFO] [03/13/2018 15:50:05.814] [hello-akka.actor.default-dispatcher-4] [akka://hello/user/greeter] Hello World! +[INFO] [03/13/2018 15:50:05.815] [hello-akka.actor.default-dispatcher-4] [akka://hello/user/greeter] Hello Akka! +[INFO] [03/13/2018 15:50:05.815] [hello-akka.actor.default-dispatcher-2] [akka://hello/user/World] Greeting 1 for World +[INFO] [03/13/2018 15:50:05.815] [hello-akka.actor.default-dispatcher-4] [akka://hello/user/Akka] Greeting 1 for Akka +[INFO] [03/13/2018 15:50:05.815] [hello-akka.actor.default-dispatcher-5] [akka://hello/user/greeter] Hello World! +[INFO] [03/13/2018 15:50:05.815] [hello-akka.actor.default-dispatcher-5] [akka://hello/user/greeter] Hello Akka! +[INFO] [03/13/2018 15:50:05.815] [hello-akka.actor.default-dispatcher-4] [akka://hello/user/World] Greeting 2 for World +[INFO] [03/13/2018 15:50:05.815] [hello-akka.actor.default-dispatcher-5] [akka://hello/user/greeter] Hello World! +[INFO] [03/13/2018 15:50:05.815] [hello-akka.actor.default-dispatcher-4] [akka://hello/user/Akka] Greeting 2 for Akka +[INFO] [03/13/2018 15:50:05.816] [hello-akka.actor.default-dispatcher-5] [akka://hello/user/greeter] Hello Akka! +[INFO] [03/13/2018 15:50:05.816] [hello-akka.actor.default-dispatcher-4] [akka://hello/user/World] Greeting 3 for World +[INFO] [03/13/2018 15:50:05.816] [hello-akka.actor.default-dispatcher-6] [akka://hello/user/Akka] Greeting 3 for Akka +``` ## A More Complex Example -The next example demonstrates some important patterns: +The next example is more realistic and demonstrates some important patterns: * Using a sealed trait and case class/objects to represent multiple messages an actor can receive * Handle sessions by using child actors