better Typed HelloWorld, #24491

* single actor hello world is too unrealistic and
  shows the wrong things
* although this is longer I think it's better

java and feedback
This commit is contained in:
Patrik Nordwall 2018-03-08 08:22:23 +01:00
parent 4b54941947
commit 054573e7e1
7 changed files with 182 additions and 117 deletions

View file

@ -5,21 +5,19 @@
package jdocs.akka.typed; package jdocs.akka.typed;
//#imports //#imports
import akka.actor.typed.ActorRef; import akka.actor.typed.ActorRef;
import akka.actor.typed.ActorSystem; import akka.actor.typed.ActorSystem;
import akka.actor.typed.Behavior; import akka.actor.typed.Behavior;
import akka.actor.typed.Terminated; import akka.actor.typed.Terminated;
import akka.actor.typed.javadsl.Behaviors; import akka.actor.typed.javadsl.Behaviors;
import akka.actor.typed.javadsl.AskPattern;
import akka.util.Timeout;
//#imports //#imports
import java.net.URLEncoder; import java.net.URLEncoder;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.concurrent.CompletionStage;
import java.util.concurrent.TimeUnit;
public class IntroTest { public class IntroTest {
@ -42,35 +40,81 @@ public class IntroTest {
public static final class Greeted { public static final class Greeted {
public final String whom; public final String whom;
public final ActorRef<Greet> from;
public Greeted(String whom) { public Greeted(String whom, ActorRef<Greet> from) {
this.whom = whom; this.whom = whom;
this.from = from;
} }
} }
public static final Behavior<Greet> greeter = Behaviors.receive((ctx, msg) -> { public static final Behavior<Greet> greeter = Behaviors.receive((ctx, msg) -> {
System.out.println("Hello " + msg.whom + "!"); ctx.getLog().info("Hello {}!", msg.whom);
msg.replyTo.tell(new Greeted(msg.whom)); msg.replyTo.tell(new Greeted(msg.whom, ctx.getSelf()));
return Behaviors.same(); return Behaviors.same();
}); });
} }
//#hello-world-actor //#hello-world-actor
public static void main(String[] args) { //#hello-world-bot
//#hello-world public abstract static class HelloWorldBot {
final ActorSystem<HelloWorld.Greet> system = private HelloWorldBot() {
ActorSystem.create(HelloWorld.greeter, "hello"); }
final CompletionStage<HelloWorld.Greeted> reply = public static final Behavior<HelloWorld.Greeted> bot(int greetingCounter, int max) {
AskPattern.ask(system, return Behaviors.receive((ctx, msg) -> {
(ActorRef<HelloWorld.Greeted> replyTo) -> new HelloWorld.Greet("world", replyTo), int n = greetingCounter + 1;
new Timeout(3, TimeUnit.SECONDS), system.scheduler()); 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 -> { //#hello-world-main
System.out.println("result: " + greeting.whom); public abstract static class HelloWorldMain {
system.terminate(); private HelloWorldMain() {
}); }
public static class Start {
public final String name;
public Start(String name) {
this.name = name;
}
}
public static final Behavior<Start> main =
Behaviors.setup( context -> {
final ActorRef<HelloWorld.Greet> greeter =
context.spawn(HelloWorld.greeter, "greeter");
return Behaviors.receiveMessage(msg -> {
ActorRef<HelloWorld.Greeted> 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 //#hello-world
final ActorSystem<HelloWorldMain.Start> 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 //#chatroom-actor

View file

@ -5,19 +5,17 @@
package docs.akka.typed package docs.akka.typed
//#imports //#imports
import java.net.URLEncoder
import java.nio.charset.StandardCharsets
import akka.NotUsed import akka.NotUsed
import akka.actor.typed.scaladsl.AskPattern._
import akka.actor.typed.scaladsl.Behaviors import akka.actor.typed.scaladsl.Behaviors
import akka.actor.typed.{ ActorRef, ActorSystem, Behavior, Terminated } import akka.actor.typed.{ ActorRef, ActorSystem, Behavior, Terminated }
import akka.testkit.typed.scaladsl.ActorTestKit
import scala.concurrent.duration._
import scala.concurrent.{ Await, Future }
//#imports //#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 import akka.actor.typed.TypedAkkaSpecWithShutdown
object IntroSpec { object IntroSpec {
@ -25,16 +23,51 @@ object IntroSpec {
//#hello-world-actor //#hello-world-actor
object HelloWorld { object HelloWorld {
final case class Greet(whom: String, replyTo: ActorRef[Greeted]) 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) val greeter: Behavior[Greet] = Behaviors.receive { (ctx, msg)
println(s"Hello ${msg.whom}!") ctx.log.info("Hello {}!", msg.whom)
msg.replyTo ! Greeted(msg.whom) msg.replyTo ! Greeted(msg.whom, ctx.self)
Behaviors.same Behaviors.same
} }
} }
//#hello-world-actor //#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 //#chatroom-actor
object ChatRoom { object ChatRoom {
//#chatroom-protocol //#chatroom-protocol
@ -63,7 +96,7 @@ object IntroSpec {
chatRoom(List.empty) chatRoom(List.empty)
private def chatRoom(sessions: List[ActorRef[SessionCommand]]): Behavior[RoomCommand] = private def chatRoom(sessions: List[ActorRef[SessionCommand]]): Behavior[RoomCommand] =
Behaviors.receive[RoomCommand] { (ctx, msg) Behaviors.receive { (ctx, msg)
msg match { msg match {
case GetSession(screenName, client) case GetSession(screenName, client)
// create a child actor for further interaction with the client // create a child actor for further interaction with the client
@ -107,46 +140,38 @@ class IntroSpec extends ActorTestKit with TypedAkkaSpecWithShutdown {
"Hello world" must { "Hello world" must {
"say hello" in { "say hello" in {
// TODO Implicits.global is not something we would like to encourage in docs
//#hello-world //#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 //#hello-world
Thread.sleep(500) // it will not fail if too short
ActorTestKit.shutdown(system)
} }
"chat" in { "chat" in {
//#chatroom-gabbler //#chatroom-gabbler
import ChatRoom._ import ChatRoom._
val gabbler = val gabbler: Behavior[SessionEvent] =
Behaviors.receive[SessionEvent] { (_, msg) Behaviors.receiveMessage {
msg match { //#chatroom-gabbler
//#chatroom-gabbler // We document that the compiler warns about the missing handler for `SessionDenied`
// We document that the compiler warns about the missing handler for `SessionDenied` case SessionDenied(reason)
case SessionDenied(reason) println(s"cannot start chat room session: $reason")
println(s"cannot start chat room session: $reason") Behaviors.stopped
Behaviors.stopped //#chatroom-gabbler
//#chatroom-gabbler case SessionGranted(handle)
case SessionGranted(handle) handle ! PostMessage("Hello World!")
handle ! PostMessage("Hello World!") Behaviors.same
Behaviors.same case MessagePosted(screenName, message)
case MessagePosted(screenName, message) println(s"message has been posted by '$screenName': $message")
println(s"message has been posted by '$screenName': $message") Behaviors.stopped
Behaviors.stopped
}
} }
//#chatroom-gabbler //#chatroom-gabbler

View file

@ -211,11 +211,11 @@ object Behaviors {
* final Behavior[DbCommand] dbConnector = ... * final Behavior[DbCommand] dbConnector = ...
* *
* final Behavior[DbCommand] dbRestarts = * final Behavior[DbCommand] dbRestarts =
* Actor.supervise(dbConnector) * Behaviors.supervise(dbConnector)
* .onFailure(SupervisorStrategy.restart) // handle all NonFatal exceptions * .onFailure(SupervisorStrategy.restart) // handle all NonFatal exceptions
* *
* final Behavior[DbCommand] dbSpecificResumes = * final Behavior[DbCommand] dbSpecificResumes =
* Actor.supervise(dbConnector) * Behaviors.supervise(dbConnector)
* .onFailure[IndexOutOfBoundsException](SupervisorStrategy.resume) // resume for IndexOutOfBoundsException exceptions * .onFailure[IndexOutOfBoundsException](SupervisorStrategy.resume) // resume for IndexOutOfBoundsException exceptions
* }}} * }}}
*/ */

View file

@ -8,7 +8,7 @@ import scala.concurrent.duration.FiniteDuration
/** /**
* Support for scheduled `self` messages in an actor. * 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 * lifecycle of the timers such as cancelling them when the actor
* is restarted or stopped. * is restarted or stopped.
* *

View file

@ -171,11 +171,11 @@ object Behaviors {
* val dbConnector: Behavior[DbCommand] = ... * val dbConnector: Behavior[DbCommand] = ...
* *
* val dbRestarts = * val dbRestarts =
* Actor.supervise(dbConnector) * Behaviors.supervise(dbConnector)
* .onFailure(SupervisorStrategy.restart) // handle all NonFatal exceptions * .onFailure(SupervisorStrategy.restart) // handle all NonFatal exceptions
* *
* val dbSpecificResumes = * val dbSpecificResumes =
* Actor.supervise(dbConnector) * Behaviors.supervise(dbConnector)
* .onFailure[IndexOutOfBoundsException](SupervisorStrategy.resume) // resume for IndexOutOfBoundsException exceptions * .onFailure[IndexOutOfBoundsException](SupervisorStrategy.resume) // resume for IndexOutOfBoundsException exceptions
* }}} * }}}
*/ */

View file

@ -8,7 +8,7 @@ import scala.concurrent.duration.FiniteDuration
/** /**
* Support for scheduled `self` messages in an actor. * 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, * Timers are bound to the lifecycle of the actor that owns it,
* and thus are cancelled automatically when it is restarted or stopped. * and thus are cancelled automatically when it is restarted or stopped.
* *

View file

@ -51,9 +51,9 @@ message.
The behavior of the Actor is defined as the `greeter` value with the help 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 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 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 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 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 next behavior is "the same as the current one".
The type of the messages handled by this behavior is declared to be of class 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 protocol is bundled together with the behavior that implements it in a nicely
wrapped scope—the `HelloWorld` @scala[object]@java[class]. 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: Now we want to try out this Actor, so we must start an ActorSystem to host it:
Scala Scala
@ -81,58 +106,29 @@ Scala
Java Java
: @@snip [IntroSpec.scala]($akka$/akka-actor-typed-tests/src/test/java/jdocs/akka/typed/IntroTest.java) { #hello-world } : @@snip [IntroSpec.scala]($akka$/akka-actor-typed-tests/src/test/java/jdocs/akka/typed/IntroTest.java) { #hello-world }
After importing the Actors protocol definition we start an Actor system from We start an Actor system from the defined `main` behavior and send two `Start` messages that
the defined `greeter` behavior. 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 The console output may look like this:
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`].
@@@ div {.group-scala} ```
[INFO] [03/13/2018 15:50:05.814] [hello-akka.actor.default-dispatcher-4] [akka://hello/user/greeter] Hello World!
Note that the `Future` that is returned by the “ask” operation is [INFO] [03/13/2018 15:50:05.815] [hello-akka.actor.default-dispatcher-4] [akka://hello/user/greeter] Hello Akka!
properly typed already, no type checks or casts needed. This is possible due to [INFO] [03/13/2018 15:50:05.815] [hello-akka.actor.default-dispatcher-2] [akka://hello/user/World] Greeting 1 for World
the type information that is part of the message protocol: the `?` operator [INFO] [03/13/2018 15:50:05.815] [hello-akka.actor.default-dispatcher-4] [akka://hello/user/Akka] Greeting 1 for Akka
takes as argument a function that accepts an `ActorRef[U]` (which [INFO] [03/13/2018 15:50:05.815] [hello-akka.actor.default-dispatcher-5] [akka://hello/user/greeter] Hello World!
explains the `_` hole in the expression on line 7 above) and the `replyTo` [INFO] [03/13/2018 15:50:05.815] [hello-akka.actor.default-dispatcher-5] [akka://hello/user/greeter] Hello Akka!
parameter which we fill in is of type `ActorRef[Greeted]`, which [INFO] [03/13/2018 15:50:05.815] [hello-akka.actor.default-dispatcher-4] [akka://hello/user/World] Greeting 2 for World
means that the value that fulfills the `Promise` can only be of type [INFO] [03/13/2018 15:50:05.815] [hello-akka.actor.default-dispatcher-5] [akka://hello/user/greeter] Hello World!
`Greeted`. [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
@@@ 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<U>`, 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.
## A More Complex Example ## 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 * Using a sealed trait and case class/objects to represent multiple messages an actor can receive
* Handle sessions by using child actors * Handle sessions by using child actors