Document MutableBehavior (#25685)

* Document MutableBehavior #25631
This commit is contained in:
Johan Andrén 2018-09-28 07:50:00 +02:00 committed by Christopher Batey
parent 9a54ae92d5
commit 081e6bd8ba
5 changed files with 227 additions and 9 deletions

View file

@ -298,15 +298,16 @@ public class IntroTest {
ctx.watch(gabbler);
chatRoom.tell(new ChatRoom.GetSession("ol Gabbler", gabbler));
return Behaviors.receive(Void.class)
.onSignal(Terminated.class, (c, sig) -> Behaviors.stopped())
.build();
return Behaviors.<Void>receiveSignal(
(c, sig) -> {
if (sig instanceof Terminated) return Behaviors.stopped();
else return Behaviors.unhandled();
}
);
});
final ActorSystem<Void> system =
ActorSystem.create(main, "ChatRoomDemo");
system.getWhenTerminated().toCompletableFuture().get();
//#chatroom-main
}

View file

@ -5,8 +5,11 @@
package jdocs.akka.typed;
//#imports
import akka.NotUsed;
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.Behaviors.Receive;
import akka.actor.typed.javadsl.ActorContext;
@ -138,4 +141,54 @@ public class MutableIntroTest {
}
//#chatroom-actor
//#chatroom-gabbler
public static abstract class Gabbler {
private Gabbler() {
}
public static Behavior<ChatRoom.SessionEvent> behavior() {
return Behaviors.receive(ChatRoom.SessionEvent.class)
.onMessage(ChatRoom.SessionDenied.class, (ctx, msg) -> {
System.out.println("cannot start chat room session: " + msg.reason);
return Behaviors.stopped();
})
.onMessage(ChatRoom.SessionGranted.class, (ctx, msg) -> {
msg.handle.tell(new ChatRoom.PostMessage("Hello World!"));
return Behaviors.same();
})
.onMessage(ChatRoom.MessagePosted.class, (ctx, msg) -> {
System.out.println("message has been posted by '" +
msg.screenName +"': " + msg.message);
return Behaviors.stopped();
})
.build();
}
}
//#chatroom-gabbler
public static void runChatRoom() throws Exception {
//#chatroom-main
Behavior<Void> main = Behaviors.setup(ctx -> {
ActorRef<ChatRoom.RoomCommand> chatRoom =
ctx.spawn(ChatRoom.behavior(), "chatRoom");
ActorRef<ChatRoom.SessionEvent> gabbler =
ctx.spawn(Gabbler.behavior(), "gabbler");
ctx.watch(gabbler);
chatRoom.tell(new ChatRoom.GetSession("ol Gabbler", gabbler));
return Behaviors.<Void>receiveSignal(
(c, sig) -> {
if (sig instanceof Terminated) return Behaviors.stopped();
else return Behaviors.unhandled();
}
);
});
final ActorSystem<Void> system =
ActorSystem.create(main, "ChatRoomDemo");
//#chatroom-main
}
}

View file

@ -229,7 +229,6 @@ class IntroSpec extends ScalaTestWithActorTestKit with WordSpecLike {
}
val system = ActorSystem(main, "ChatRoomDemo")
Await.result(system.whenTerminated, 3.seconds)
//#chatroom-main
}
}

View file

@ -130,7 +130,6 @@ class MutableIntroSpec extends ScalaTestWithActorTestKit with WordSpecLike {
val system = ActorSystem(main, "ChatRoomDemo")
system ! "go"
Await.result(system.whenTerminated, 1.second)
//#chatroom-main
}
}

View file

@ -139,6 +139,8 @@ The console output may look like this:
## A More Complex Example
### Functional Style
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
@ -210,7 +212,7 @@ former simply speaks more languages than the latter. The opposite would be
problematic, so passing an @scala[`ActorRef[PublishSessionMessage]`]@java[`ActorRef<PublishSessionMessage>`] where
@scala[`ActorRef[RoomCommand]`]@java[`ActorRef<RoomCommand>`] is required will lead to a type error.
### Trying it out
#### Trying it out
In order to see this chat room in action we need to write a client Actor that can use it:
@ -268,7 +270,171 @@ called `ctx.watch` for it. This allows us to shut down the Actor system: when
the main Actor terminates there is nothing more to do.
Therefore after creating the Actor system with the `main` Actors
`Behavior` the only thing we do is await its termination.
`Behavior` we can let the `main` method return, the `ActorSystem` will continue running and
the JVM alive until the root actor stops.
### Object-oriented style
The samples shown so far are all based on a functional programming style
where you pass a function to a factory which then constructs a behavior, for stateful
actors this means passing immutable state around as parameters and switching to a new behavior
whenever you need to act on a changed state. An alternative way to express the same is a more
object oriented style where a concrete class for the actor behavior is defined and mutable
state is kept inside of it as fields.
Some reasons why you may want to do this are:
@@@ div {.group-java}
* Java lambdas can only close over final or effectively final fields, making it
impractical to use this style in behaviors that mutate their fields
* some state is not immutable, e.g. immutable collections are not widely used in Java
* it could be more familiar and easier to migrate existing untyped actors to this style
* mutable state can sometimes have better performance, e.g. mutable collections and
avoiding allocating new instance for next behavior (be sure to benchmark if this is your
motivation)
@@@
@@@ div {.group-scala}
* some state is not immutable
* it could be more familiar and easier to migrate existing untyped actors to this style
* mutable state can sometimes have better performance, e.g. mutable collections and
avoiding allocating new instance for next behavior (be sure to benchmark if this is your
motivation)
@@@
#### MutableBehavior API
Defining a class based actor behavior starts with extending
@scala[`akka.actor.typed.scaladsl.MutableBehavior[T]`]
@java[`akka.actor.typed.javadsl.MutableBehavior<T>`] where `T` is the type of messages
the behavior will accept.
Let's repeat the chat room sample from @ref:[A more complex example above](#a-more-complex-example) but implemented
using `MutableBehavior`. The protocol for interacting with the actor looks the same:
Scala
: @@snip [MutableIntroSpec.scala](/akka-actor-typed-tests/src/test/scala/docs/akka/typed/MutableIntroSpec.scala) { #chatroom-protocol }
Java
: @@snip [MutableIntroTest.java](/akka-actor-typed-tests/src/test/java/jdocs/akka/typed/MutableIntroTest.java) { #chatroom-protocol }
Initially the client Actors only get access to an @scala[`ActorRef[GetSession]`]@java[`ActorRef<GetSession>`]
which allows them to make the first step. Once a clients session has been
established it gets a `SessionGranted` message that contains a `handle` to
unlock the next protocol step, posting messages. The `PostMessage`
command will need to be sent to this particular address that represents the
session that has been added to the chat room. The other aspect of a session is
that the client has revealed its own address, via the `replyTo` argument, so that subsequent
`MessagePosted` events can be sent to it.
This illustrates how Actors can express more than just the equivalent of method
calls on Java objects. The declared message types and their contents describe a
full protocol that can involve multiple Actors and that can evolve over
multiple steps. Here's the `MutableBehavior` implementation of the chat room protocol:
Scala
: @@snip [MutableIntroSpec.scala](/akka-actor-typed-tests/src/test/scala/docs/akka/typed/MutableIntroSpec.scala) { #chatroom-behavior }
Java
: @@snip [MutableIntroTest.java](/akka-actor-typed-tests/src/test/java/jdocs/akka/typed/MutableIntroTest.java) { #chatroom-behavior }
The state is managed through fields in the class, just like with a regular object oriented class.
As the state is mutable, we never return a different behavior from the message logic, but can return
the `MutableBehavior` instance itself (`this`) as a behavior to use for processing the next message coming in.
We could also return `Behavior.same` to achieve the same.
It is also possible to return a new different `MutableBehavior`, for example to represent a different state in a
finite state machine (FSM), or use one of the functional behavior factories to combine the object oriented
with the functional style for different parts of the lifecycle of the same Actor behavior.
When a new `GetSession` command comes in we add that client to the
list of current sessions. Then we also need to create the sessions
`ActorRef` that will be used to post messages. In this case we want to
create a very simple Actor that repackages the `PostMessage`
command into a `PublishSessionMessage` command which also includes the
screen name.
To implement the logic where we spawn a child for the session we need access
to the `ActorContext`. This is injected as a constructor parameter upon creation
of the behavior, note how we combine the `MutableBehavior` with `Behaviors.setup`
to do this in the `behavior` method.
The behavior that we declare here can handle both subtypes of `RoomCommand`.
`GetSession` has been explained already and the
`PublishSessionMessage` commands coming from the session Actors will
trigger the dissemination of the contained chat room message to all connected
clients. But we do not want to give the ability to send
`PublishSessionMessage` commands to arbitrary clients, we reserve that
right to the internal session actors we create—otherwise clients could pose as completely
different screen names (imagine the `GetSession` protocol to include
authentication information to further secure this). Therefore `PublishSessionMessage`
has `private` visibility and can't be created outside the `ChatRoom` @scala[object]@java[class].
If we did not care about securing the correspondence between a session and a
screen name then we could change the protocol such that `PostMessage` is
removed and all clients just get an @scala[`ActorRef[PublishSessionMessage]`]@java[`ActorRef<PublishSessionMessage>`] to
send to. In this case no session actor would be needed and we could use
@scala[`ctx.self`]@java[`ctx.getSelf()`]. The type-checks work out in that case because
@scala[`ActorRef[-T]`]@java[`ActorRef<T>`] is contravariant in its type parameter, meaning that we
can use a @scala[`ActorRef[RoomCommand]`]@java[`ActorRef<RoomCommand>`] wherever an
@scala[`ActorRef[PublishSessionMessage]`]@java[`ActorRef<PublishSessionMessage>`] is needed—this makes sense because the
former simply speaks more languages than the latter. The opposite would be
problematic, so passing an @scala[`ActorRef[PublishSessionMessage]`]@java[`ActorRef<PublishSessionMessage>`] where
@scala[`ActorRef[RoomCommand]`]@java[`ActorRef<RoomCommand>`] is required will lead to a type error.
#### Trying it out
In order to see this chat room in action we need to write a client Actor that can use it, for this
stateless actor it doesn't make much sense to use the `MutableBehavior` so let's just reuse the
functional style gabbler from the sample above:
Scala
: @@snip [MutableIntroSpec.scala](/akka-actor-typed-tests/src/test/scala/docs/akka/typed/MutableIntroSpec.scala) { #chatroom-gabbler }
Java
: @@snip [MutableIntroTest.java](/akka-actor-typed-tests/src/test/java/jdocs/akka/typed/MutableIntroTest.java) { #chatroom-gabbler }
Now to try things out we must start both a chat room and a gabbler and of
course we do this inside an Actor system. Since there can be only one guardian
supervisor we could either start the chat room from the gabbler (which we dont
want—it complicates its logic) or the gabbler from the chat room (which is
nonsensical) or we start both of them from a third Actor—our only sensible
choice:
Scala
: @@snip [MutableIntroSpec.scala](/akka-actor-typed-tests/src/test/scala/docs/akka/typed/MutableIntroSpec.scala) { #chatroom-main }
Java
: @@snip [MutableIntroTest.java](/akka-actor-typed-tests/src/test/java/jdocs/akka/typed/MutableIntroTest.java) { #chatroom-main }
In good tradition we call the `main` Actor what it is, it directly
corresponds to the `main` method in a traditional Java application. This
Actor will perform its job on its own accord, we do not need to send messages
from the outside, so we declare it to be of type @scala[`NotUsed`]@java[`Void`]. Actors receive not
only external messages, they also are notified of certain system events,
so-called Signals. In order to get access to those we choose to implement this
particular one using the `receive` behavior decorator. The
provided `onSignal` function will be invoked for signals (subclasses of `Signal`)
or the `onMessage` function for user messages.
This particular `main` Actor is created using `Behaviors.setup`, which is like a factory for a behavior.
Creation of the behavior instance is deferred until the actor is started, as opposed to `Behaviors.receive`
that creates the behavior instance immediately before the actor is running. The factory function in
`setup` is passed the `ActorContext` as parameter and that can for example be used for spawning child actors.
This `main` Actor creates the chat room and the gabbler and the session between them is initiated, and when the
gabbler is finished we will receive the `Terminated` event due to having
called `ctx.watch` for it. This allows us to shut down the Actor system: when
the main Actor terminates there is nothing more to do.
Therefore after creating the Actor system with the `main` Actors
`Behavior` we can let the `main` method return, the `ActorSystem` will continue running and
the JVM alive until the root actor stops.
## Relation to Akka (untyped) Actors