Add java examples to getting started guide, with language switch (#22919)

* Java version of new Hello World
* Define paradox groups, use shorthand directives
* Make tutorial apply to both java and scala
* Use sbt-paradox-akka
This commit is contained in:
Arnout Engelen 2017-05-12 04:42:10 -07:00 committed by GitHub
parent 513dbfbec0
commit 4cb9c2436f
8 changed files with 359 additions and 69 deletions

View file

@ -13,6 +13,7 @@ additionalTasks in ValidatePR += paradox in Compile
enablePlugins(ScaladocNoVerificationOfDiagrams)
disablePlugins(MimaPlugin)
enablePlugins(ParadoxPlugin)
enablePlugins(AkkaParadoxPlugin)
paradoxProperties ++= Map(
"extref.wikipedia.base_url" -> "https://en.wikipedia.org/wiki/%s",
@ -21,9 +22,7 @@ paradoxProperties ++= Map(
"snip.code.base_dir" -> (sourceDirectory in Test).value.getAbsolutePath,
"snip.akka.base_dir" -> ((baseDirectory in Test).value / "..").getAbsolutePath
)
paradoxGroups := Map("Languages" -> Seq("Scala", "Java"))
resolvers += Resolver.bintrayRepo("2m", "maven")
paradoxTheme := Some("com.lightbend.akka" % "paradox-theme-akka" % "b74885b8+20170511-1711")
paradoxNavigationDepth := 1
paradoxNavigationExpandDepth := Some(1)
paradoxNavigationIncludeHeaders := true
resolvers += Resolver.bintrayRepo("2m", "sbt-plugin-releases")

View file

@ -111,14 +111,26 @@ The usual pattern is to have your system set up to stop on external signal (i.e.
Once there is an `ActorSystem` we can populate it with actors. This is done by using the `actorOf` method. The `actorOf` method expects a `Props` instance and the name of the actor to be created. You can think of the `Props` as a configuration value for what actor to create and how it should be created. Creating an actor with the `actorOf` method will return an `ActorRef` instance. Think of the `ActorRef` as a unique address with which it is possible to message the actor instance. The `ActorRef` object contains a few methods with which you can send messages to the actor instance. One of them is called `tell`, or in the Scala case simply `!` (bang), and this method is used in the example here below. Calling the `!` method is an asynchronous operation and it instructs Akka to send a message to the actor instance that is uniquely identified by the actor reference.
@@snip [HelloWorldApp.scala]($code$/scala/quickstart/HelloWorldApp.scala) { #create-send }
Scala
: @@snip [HelloWorldApp.scala]($code$/scala/quickstart/HelloWorldApp.scala) { #create-send }
Before we can create any actor in the actor system we must define one first. Luckily, creating actors in Akka is quite simple! Just have your actor class extend `akka.actor.Actor` and override the method `receive: Receive` and you are good to go. As for our `HelloWorldActor` class, it extends `Actor` and overrides the `receive` method as per the requirement. Our implementation of the `receive` method expects messages of type `String`. For every `String` message it receives it will print "Hello " and the value of the `String`. Since the message we send in the main class is "World" we expect the string "Hello World" to be printed when running the application.
Java
: @@snip [HelloWorldMain.java]($code$/java/jdocs/quickstart/HelloWorldMain.java) { #create-send }
@@snip [HelloWorldApp.scala]($code$/scala/quickstart/HelloWorldApp.scala) { #actor-impl }
Before we can create any actor in the actor system we must define one first. Luckily, creating actors in Akka is quite simple! Just have your actor class extend @scala[`Actor`] @java[`AbstractActor`] and override the method @scala[`receive: Receive`] @java[`public Receive createReceive()`] and you are good to go. As for our `HelloWorldActor` class, it extends @scala[`Actor`] @java[`AbstractActor`] and overrides the @scala[`receive`] @java[`createReceive`] method as per the requirement. Our implementation of the @scala[`receive`] @java[`createReceive`] method expects messages of type `String`. For every `String` message it receives it will print "Hello " and the value of the `String`. Since the message we send in the main class is "World" we expect the string "Hello World" to be printed when running the application.
Scala
: @@snip [HelloWorldApp.scala]($code$/scala/quickstart/HelloWorldApp.scala) { #actor-impl }
Java
: @@snip [HelloWorldActor.java]($code$/java/jdocs/quickstart/HelloWorldActor.java) { }
Here is the full example:
@@snip [HelloWorldApp.scala]($code$/scala/quickstart/HelloWorldApp.scala) { #full-example }
Scala
: @@snip [HelloWorldApp.scala]($code$/scala/quickstart/HelloWorldApp.scala) { #full-example }
Java
: @@snip [HelloWorldMain.java]($code$/java/jdocs/quickstart/HelloWorldMain.java) { #full-example }
Now that you have seen the basics of an Akka application it is time to dive deeper.

View file

@ -78,7 +78,11 @@ convenient terminology, and we will stick to it.
Creating a non-top-level actor is possible from any actor, by invoking `context.actorOf()` which has the exact same
signature as its top-level counterpart. This is how it looks like in practice:
@@snip [Hello.scala]($code$/scala/tutorial_1/ActorHierarchyExperiments.scala) { #print-refs }
Scala
: @@snip [ActorHierarchyExperiments.scala]($code$/scala/tutorial_1/ActorHierarchyExperiments.scala) { #print-refs }
Java
: @@snip [ActorHierarchyExperiments.java]($code$/java/jdocs/tutorial_1/ActorHierarchyExperiments.java) { #print-refs }
We see that the following two lines are printed
@ -126,7 +130,11 @@ The actor API exposes many lifecycle hooks that the actor implementation can ove
Again, we can try out all this with a simple experiment:
@@snip [Hello.scala]($code$/scala/tutorial_1/ActorHierarchyExperiments.scala) { #start-stop }
Scala
: @@snip [ActorHierarchyExperiments.scala]($code$/scala/tutorial_1/ActorHierarchyExperiments.scala) { #start-stop }
Java
: @@snip [ActorHierarchyExperiments.java]($code$/java/jdocs/tutorial_1/ActorHierarchyExperiments.java) { #start-stop }
After running it, we get the output
@ -151,7 +159,11 @@ to the parent, which decides how to handle the exception caused by the child act
stop and restart the child. If you don't change the default strategy all failures result in a restart. We won't change
the default strategy in this simple experiment:
@@snip [Hello.scala]($code$/scala/tutorial_1/ActorHierarchyExperiments.scala) { #supervise }
Scala
: @@snip [ActorHierarchyExperiments.scala]($code$/scala/tutorial_1/ActorHierarchyExperiments.scala) { #supervise }
Java
: @@snip [ActorHierarchyExperiments.java]($code$/java/jdocs/tutorial_1/ActorHierarchyExperiments.java) { #supervise }
After running the snippet, we see the following output on the console:
@ -208,14 +220,22 @@ represents the whole application. In other words, we will have a single top-leve
the main components as children of this actor.
The first actor happens to be rather simple now, as we have not implemented any of the components yet. What is new
is that we have dropped using `println()` and instead use the `ActorLogging` helper trait which allows us to use the
logging facility built into Akka directly. Furthermore, we are using a recommended creational pattern for actors; define a `props()` method in the [companion object](http://docs.scala-lang.org/tutorials/tour/singleton-objects.html#companions) of the actor:
is that we have dropped using `println()` and instead use @scala[the `ActorLogging` helper trait] @java[`akka.event.Logging`] which allows us to use the
logging facility built into Akka directly. Furthermore, we are using a recommended creational pattern for actors; define a `props()` @scala[method in the [companion object](http://docs.scala-lang.org/tutorials/tour/singleton-objects.html#companions) of] @java[static method on] the actor:
@@snip [Hello.scala]($code$/scala/tutorial_1/IotSupervisor.scala) { #iot-supervisor }
Scala
: @@snip [IotSupervisor.scala]($code$/scala/tutorial_1/IotSupervisor.scala) { #iot-supervisor }
Java
: @@snip [IotSupervisor.java]($code$/java/jdocs/tutorial_1/IotSupervisor.java) { #iot-supervisor }
All we need now is to tie this up with a class with the `main` entry point:
@@snip [Hello.scala]($code$/scala/tutorial_1/IotApp.scala) { #iot-app }
Scala
: @@snip [IotApp.scala]($code$/scala/tutorial_1/IotApp.scala) { #iot-app }
Java
: @@snip [IotMain.java]($code$/java/jdocs/tutorial_1/IotMain.java) { #iot-app }
This application does very little for now, but we have the first actor in place and we are ready to extend it further.

View file

@ -29,7 +29,11 @@ The protocol for obtaining the current temperature from the device actor is rath
We need two messages, one for the request, and one for the reply. A first attempt could look like this:
@@snip [Hello.scala]($code$/scala/tutorial_2/DeviceInProgress.scala) { #read-protocol-1 }
Scala
: @@snip [DeviceInProgress.scala]($code$/scala/tutorial_2/DeviceInProgress.scala) { #read-protocol-1 }
Java
: @@snip [DeviceInProgress.java]($code$/java/jdocs/tutorial_2/DeviceInProgress.java) { #read-protocol-1 }
This is a fine approach, but it limits the flexibility of the protocol. To understand why we need to talk
about message ordering and message delivery guarantees in general.
@ -139,20 +143,32 @@ can be helpful to put an additional query ID field in the message which helps us
Hence, we add one more field to our messages, so that an ID can be provided by the requester:
@@snip [Hello.scala]($code$/scala/tutorial_2/DeviceInProgress.scala) { #read-protocol-2 }
Scala
: @@snip [DeviceInProgress.scala]($code$/scala/tutorial_2/DeviceInProgress.scala) { #read-protocol-2 }
Java
: @@snip [DeviceInProgress2.java]($code$/java/jdocs/tutorial_2/inprogress2/DeviceInProgress2.java) { #read-protocol-2 }
Our device actor has the responsibility to use the same ID for the response of a given query. Now we can sketch
our device actor:
@@snip [Hello.scala]($code$/scala/tutorial_2/DeviceInProgress.scala) { #device-with-read }
Scala
: @@snip [DeviceInProgress.scala]($code$/scala/tutorial_2/DeviceInProgress.scala) { #device-with-read }
We maintain the current temperature, initially set to `None`, and we simply report it back if queried. We also
Java
: @@snip [DeviceInProgress2.java]($code$/java/jdocs/tutorial_2/inprogress2/DeviceInProgress2.java) { #device-with-read }
We maintain the current temperature, initially set to @scala[`None`] @java[`Optional.empty()`], and we simply report it back if queried. We also
added fields for the ID of the device and the group it belongs to, which we will use later.
We can already write a simple test for this functionality (we use ScalaTest but any other test framework can be
used with the Akka Testkit):
We can already write a simple test for this functionality @scala[(we use ScalaTest but any other test framework can be
used with the Akka Testkit)]:
@@snip [Hello.scala]($code$/scala/tutorial_2/DeviceSpec.scala) { #device-read-test }
Scala
: @@snip [DeviceSpec.scala]($code$/scala/tutorial_2/DeviceSpec.scala) { #device-read-test }
Java
: @@snip [DeviceTest.java]($code$/java/jdocs/tutorial_2/DeviceTest.java) { #device-read-test }
## The Write Protocol
@ -162,22 +178,34 @@ As a first attempt, we could model recording the current temperature in the devi
Such a message could possibly look like this:
@@snip [Hello.scala]($code$/scala/tutorial_2/DeviceInProgress.scala) { #write-protocol-1 }
Scala
: @@snip [DeviceInProgress.scala]($code$/scala/tutorial_2/DeviceInProgress.scala) { #write-protocol-1 }
Java
: @@snip [DeviceInProgress3.java]($code$/java/jdocs/tutorial_2/DeviceInProgress3.java) { #write-protocol-1 }
The problem with this approach is that the sender of the record temperature message can never be sure if the message
was processed or not. We have seen that Akka does not guarantee delivery of these messages and leaves it to the
application to provide success notifications. In our case, we would like to send an acknowledgment to the sender
once we have updated our last temperature recording, e.g. `final case class TemperatureRecorded(requestId: Long)`.
once we have updated our last temperature recording, e.g. @scala[`final case class TemperatureRecorded(requestId: Long)`] @java[`TemperatureRecorded`].
Just like in the case of temperature queries and responses, it is a good idea to include an ID field to provide maximum flexibility.
Putting read and write protocol together, the device actor will look like this:
@@snip [Hello.scala]($code$/scala/tutorial_2/Device.scala) { #full-device }
Scala
: @@snip [Device.scala]($code$/scala/tutorial_2/Device.scala) { #full-device }
Java
: @@snip [Device.java]($code$/java/jdocs/tutorial_2/Device.java) { #full-device }
We are also responsible for writing a new test case now, exercising both the read/query and write/record functionality
together:
@@snip [Hello.scala]($code$/scala/tutorial_2/DeviceSpec.scala) { #device-write-read-test }
Scala:
: @@snip [DeviceSpec.scala]($code$/scala/tutorial_2/DeviceSpec.scala) { #device-write-read-test }
Java:
: @@snip [DeviceTest.java]($code$/java/jdocs/tutorial_2/DeviceTest.java) { #device-write-read-test }
## What is Next?

View file

@ -81,7 +81,7 @@ is known up front: device groups and device actors are created on-demand. The st
Now that the steps are defined, we only need to define the messages that we will use to communicate requests and
their acknowledgement:
@@snip [Hello.scala]($code$/scala/tutorial_3/DeviceManager.scala) { #device-manager-msgs }
@@snip [DeviceManager.scala]($code$/scala/tutorial_3/DeviceManager.scala) { #device-manager-msgs }
As you see, in this case, we have not included a request ID field in the messages. Since registration is usually happening
once, at the component that connects the system to some network protocol, we will usually have no use for the ID.
@ -100,48 +100,68 @@ message is preserved in the upper layers.* We will show you in the next section
We also add a safeguard against requests that come with a mismatched group or device ID. This is how the resulting
the code looks like:
> NOTE: We used a feature of scala pattern matching where we can match if a certain field equals to an expected
>@scala[NOTE: We used a feature of scala pattern matching where we can match if a certain field equals to an expected
value. This is achieved by variables included in backticks, like `` `variable` ``, and it means that the pattern
only match if it contains the value of `variable` in that position.
only match if it contains the value of `variable` in that position.]
@@snip [Hello.scala]($code$/scala/tutorial_3/Device.scala) { #device-with-register }
Scala
: @@snip [Device.scala]($code$/scala/tutorial_3/Device.scala) { #device-with-register }
Java
: @@snip [Device.java]($code$/java/jdocs/tutorial_3/Device.java) { #device-with-register }
We should not leave features untested, so we immediately write two new test cases, one exercising successful
registration, the other testing the case when IDs don't match:
> NOTE: We used the `expectNoMsg()` helper method from `TestProbe`. This assertion waits until the defined time-limit
> NOTE: We used the `expectNoMsg()` helper method from @scala[`TestProbe`] @java[`TestKit`]. This assertion waits until the defined time-limit
and fails if it receives any messages during this period. If no messages are received during the waiting period the
assertion passes. It is usually a good idea to keep these timeouts low (but not too low) because they add significant
test execution time otherwise.
@@snip [Hello.scala]($code$/scala/tutorial_3/DeviceSpec.scala) { #device-registration-tests }
Scala
: @@snip [DeviceSpec.scala]($code$/scala/tutorial_3/DeviceSpec.scala) { #device-registration-tests }
Java
: @@snip [DeviceTest.java]($code$/java/jdocs/tutorial_3/DeviceTest.java) { #device-registration-tests }
## Device Group
We are done with the registration support at the device level, now we have to implement it at the group level. A group
has more work to do when it comes to registrations. It must either forward the request to an existing child, or it
should create one. To be able to look up child actors by their device IDs we will use a `Map[String, ActorRef]`.
should create one. To be able to look up child actors by their device IDs we will use a @scala[`Map[String, ActorRef]`] @java[`Map<String, ActorRef>`].
We also want to keep the original sender of the request so that our device actor can reply directly. This is possible
by using `forward` instead of the `!` operator. The only difference between the two is that `forward` keeps the original
sender while `!` always sets the sender to be the current actor. Just like with our device actor, we ensure that we don't
by using `forward` instead of the @scala[`!`] @java[`tell`] operator. The only difference between the two is that `forward` keeps the original
sender while @scala[`!`] @java[`tell`] always sets the sender to be the current actor. Just like with our device actor, we ensure that we don't
respond to wrong group IDs:
@@snip [Hello.scala]($code$/scala/tutorial_3/DeviceGroup.scala) { #device-group-register }
Scala
: @@snip [DeviceGroup.scala]($code$/scala/tutorial_3/DeviceGroup.scala) { #device-group-register }
Java
: @@snip [DeviceGroup.java]($code$/java/jdocs/tutorial_3/DeviceGroup.java) { #device-group-register }
Just as we did with the device, we test this new functionality. We also test that the actors returned for the two
different IDs are actually different, and we also attempt to record a temperature reading for each of the devices
to see if the actors are responding.
@@snip [Hello.scala]($code$/scala/tutorial_3/DeviceGroupSpec.scala) { #device-group-test-registration }
Scala
: @@snip [DeviceGroupSpec.scala]($code$/scala/tutorial_3/DeviceGroupSpec.scala) { #device-group-test-registration }
Java
: @@snip [DeviceGroupTest.java]($code$/java/jdocs/tutorial_3/DeviceGroupTest.java) { #device-group-test-registration }
It might be, that a device actor already exists for the registration request. In this case, we would like to use
the existing actor instead of a new one. We have not tested this yet, so we need to fix this:
@@snip [Hello.scala]($code$/scala/tutorial_3/DeviceGroupSpec.scala) { #device-group-test3 }
Scala
: @@snip [DeviceGroupSpec.scala]($code$/scala/tutorial_3/DeviceGroupSpec.scala) { #device-group-test3 }
Java
: @@snip [DeviceGroupTest.java]($code$/java/jdocs/tutorial_3/DeviceGroupTest.java) { #device-group-test3 }
So far, we have implemented everything for registering device actors in the group. Devices come and go, however, so
we will need a way to remove those from the `Map[String, ActorRef]`. We will assume that when a device is removed, its corresponding device actor
we will need a way to remove those from the @scala[`Map[String, ActorRef]`] @java[`Map<String, ActorRef>`]. We will assume that when a device is removed, its corresponding device actor
is simply stopped. We need some way for the parent to be notified when one of the device actors are stopped. Unfortunately,
supervision will not help because it is used for error scenarios, not graceful stopping.
@ -155,42 +175,58 @@ longer perform its duties after its collaborator actor has been stopped. In our
after one device have been stopped, so we need to handle this message. The steps we need to follow are the following:
1. Whenever we create a new device actor, we must also watch it.
2. When we are notified that a device actor has been stopped we also need to remove it from the `Map[String, ActorRef]` which maps
2. When we are notified that a device actor has been stopped we also need to remove it from the @scala[`Map[String, ActorRef]`] @java[`Map<String, ActorRef>`] which maps
devices to device actors.
Unfortunately, the `Terminated` message contains only contains the `ActorRef` of the child actor but we do not know
its ID, which we need to remove it from the map of existing device to device actor mappings. To be able to do this removal, we
need to introduce another placeholder, `Map[ActorRef, String]`, that allow us to find out the device ID corresponding to a given `ActorRef`. Putting
need to introduce another placeholder, @scala[`Map[String, ActorRef]`] @java[`Map<String, ActorRef>`], that allow us to find out the device ID corresponding to a given `ActorRef`. Putting
this together the result is:
@@snip [Hello.scala]($code$/scala/tutorial_3/DeviceGroup.scala) { #device-group-remove }
Scala
: @@snip [DeviceGroup.scala]($code$/scala/tutorial_3/DeviceGroup.scala) { #device-group-remove }
Java
: @@snip [DeviceGroup.java]($code$/java/jdocs/tutorial_3/DeviceGroup.java) { #device-group-remove }
So far we have no means to get what devices the group device actor keeps track of and, therefore, we cannot test our
new functionality yet. To make it testable, we add a new query capability (message `RequestDeviceList(requestId: Long)`) that simply lists the currently active
new functionality yet. To make it testable, we add a new query capability (message @scala[`RequestDeviceList(requestId: Long)`] @java[`RequestDeviceList`]) that simply lists the currently active
device IDs:
@@snip [Hello.scala]($code$/scala/tutorial_3/DeviceGroup.scala) { #device-group-full }
Scala
: @@snip [DeviceGroup.scala]($code$/scala/tutorial_3/DeviceGroup.scala) { #device-group-full }
Java
: @@snip [DeviceGroup.java]($code$/java/jdocs/tutorial_3/DeviceGroup.java) { #device-group-full }
We almost have everything to test the removal of devices. What is missing is:
* Stopping a device actor from our test case, from the outside: any actor can be stopped by simply sending a special
the built-in message, `PoisonPill`, which instructs the actor to stop.
* Be notified once the device actor is stopped: we can use the _Death Watch_ facility for this purpose, too. Thankfully
the `TestProbe` has two messages that we can easily use, `watch()` to watch a specific actor, and `expectTerminated`
the @scala[`TestProbe`] @java[`TestKit`] has two messages that we can easily use, `watch()` to watch a specific actor, and `expectTerminated`
to assert that the watched actor has been terminated.
We add two more test cases now. In the first, we just test that we get back the list of proper IDs once we have added
a few devices. The second test case makes sure that the device ID is properly removed after the device actor has
been stopped:
@@snip [Hello.scala]($code$/scala/tutorial_3/DeviceGroupSpec.scala) { #device-group-list-terminate-test }
Scala
: @@snip [DeviceGroupSpec.scala]($code$/scala/tutorial_3/DeviceGroupSpec.scala) { #device-group-list-terminate-test }
Java
: @@snip [DeviceGroupTest.java]($code$/java/jdocs/tutorial_3/DeviceGroupTest.java) { #device-group-list-terminate-test }
## Device Manager
The only part that remains now is the entry point for our device manager component. This actor is very similar to
the device group actor, with the only difference that it creates device group actors instead of device actors:
@@snip [Hello.scala]($code$/scala/tutorial_3/DeviceManager.scala) { #device-manager-full }
Scala
: @@snip [DeviceManager.scala]($code$/scala/tutorial_3/DeviceManager.scala) { #device-manager-full }
Java
: @@snip [DeviceManager.java]($code$/java/jdocs/tutorial_3/DeviceManager.java) { #device-manager-full }
We leave tests of the device manager as an exercise as it is very similar to the tests we have written for the group
actor.

View file

@ -34,14 +34,18 @@ we would like to give a deadline to our query:
Given these decisions, and the fact that a device might not have a temperature to record, we can define four states
that each device can be in, according to the query:
* It has a temperature available: `Temperature(value)`.
* It has a temperature available: @scala[`Temperature(value)`] @java[`Temperature`].
* It has responded, but has no temperature available yet: `TemperatureNotAvailable`.
* It has stopped before answering: `DeviceNotAvailable`.
* It did not respond before the deadline: `DeviceTimedOut`.
Summarizing these in message types we can add the following to `DeviceGroup`:
@@snip [Hello.scala]($code$/scala/tutorial_4/DeviceGroup.scala) { #query-protocol }
Scala
: @@snip [DeviceGroup.scala]($code$/scala/tutorial_4/DeviceGroup.scala) { #query-protocol }
Java
: @@snip [DeviceGroup.java]($code$/java/jdocs/tutorial_4/DeviceGroup.java) { #query-protocol }
## Implementing the Query
@ -71,14 +75,14 @@ need to be able to work:
Since we need to have a timeout for how long we are willing to wait for responses, it is time to introduce a new feature that we have
not used yet: timers. Akka has a built-in scheduler facility for this exact purpose. Using it is simple, the
`scheduler.scheduleOnce(time, actorRef, message)` method will schedule the message `message` into the future by the
@scala[`scheduler.scheduleOnce(time, actorRef, message)`] @java[`scheduler.scheduleOnce(time, actorRef, message, executor, sender)`] method will schedule the message `message` into the future by the
specified `time` and send it to the actor `actorRef`. To implement our query timeout we need to create a message
that represents the query timeout. We create a simple message `CollectionTimeout` without any parameters for
this purpose. The return value from `scheduleOnce` is a `Cancellable` which can be used to cancel the timer
if the query finishes successfully in time. Getting the scheduler is possible from the `ActorSystem`, which, in turn,
is accessible from the actor's context: `context.system.scheduler`. This needs an implicit `ExecutionContext` which
is accessible from the actor's context: @scala[`context.system.scheduler`] @java[`getContext().getSystem().scheduler()`]. This needs an @scala[implicit] `ExecutionContext` which
is basically the thread-pool that will execute the timer task itself. In our case, we use the same dispatcher
as the actor by importing `import context.dispatcher`.
as the actor by @scala[importing `import context.dispatcher`] @java[passing in `getContext().dispatcher()`].
At the start of the query, we need to ask each of the device actors for the current temperature. To be able to quickly
detect devices that stopped before they got the `ReadTemperature` message we will also watch each of the actors. This
@ -87,14 +91,18 @@ until the timeout to mark these as not available.
Putting together all these, the outline of our actor looks like this:
@@snip [Hello.scala]($code$/scala/tutorial_4/DeviceGroupQuery.scala) { #query-outline }
Scala
: @@snip [DeviceGroupQuery.scala]($code$/scala/tutorial_4/DeviceGroupQuery.scala) { #query-outline }
Java
: @@snip [DeviceGroupQuery.java]($code$/java/jdocs/tutorial_4/DeviceGroupQuery.java) { #query-outline }
The query actor, apart from the pending timer, has one stateful aspect about it: the actors that did not answer so far or,
from the other way around, the set of actors that have replied or stopped. One way to track this state is
to create a mutable field in the actor (a `var`). There is another approach. It is also possible to change how
to create a mutable field in the actor @scala[(a `var`)]. There is another approach. It is also possible to change how
the actor responds to messages. By default, the `receive` block defines the behavior of the actor, but it is possible
to change it, several times, during the life of the actor. This is possible by calling `context.become(newBehavior)`
where `newBehavior` is anything with type `Receive` (which is just a shorthand for `PartialFunction[Any, Unit]`). A
where `newBehavior` is anything with type `Receive` @scala[(which is just a shorthand for `PartialFunction[Any, Unit]`)]. A
`Receive` is just a function (or an object, if you like) that can be returned from another function. We will leverage this
feature to track the state of our actor.
@ -108,7 +116,11 @@ we will discuss later. In the case of timeout, we need to simply take all the ac
(the members of the set `stillWaiting`) and put a `DeviceTimedOut` as the status in the final reply. Then we
reply to the submitter of the query with the collected results and stop the query actor:
@@snip [Hello.scala]($code$/scala/tutorial_4/DeviceGroupQuery.scala) { #query-state }
Scala
: @@snip [DeviceGroupQuery.scala]($code$/scala/tutorial_4/DeviceGroupQuery.scala) { #query-state }
Java
: @@snip [DeviceGroupQuery.java]($code$/java/jdocs/tutorial_4/DeviceGroupQuery.java) { #query-state }
What is not yet clear, how we will "mutate" the `answersSoFar` and `stillWaiting` data structures. One important
thing to note is that the function `waitingForReplies` **does not handle the messages directly. It returns a `Receive`
@ -137,7 +149,11 @@ only the first call will have any effect, the rest is simply ignored.
With all this knowledge, we can create the `receivedResponse` method:
@@snip [Hello.scala]($code$/scala/tutorial_4/DeviceGroupQuery.scala) { #query-collect-reply }
Scala
: @@snip [DeviceGroupQuery.scala]($code$/scala/tutorial_4/DeviceGroupQuery.scala) { #query-collect-reply }
Java
: @@snip [DeviceGroupQuery.java]($code$/java/jdocs/tutorial_4/DeviceGroupQuery.java) { #query-collect-reply }
It is quite natural to ask at this point, what have we gained by using the `context.become()` trick instead of
just making the `repliesSoFar` and `stillWaiting` structures mutable fields of the actor (i.e. `var`s)? In this
@ -146,42 +162,66 @@ _more kinds_ of states. Since each state
might have temporary data that is relevant itself, keeping these as fields would pollute the global state
of the actor, i.e. it is unclear what fields are used in what state. Using parameterized `Receive` "factory"
methods we can keep data private that is only relevant to the state. It is still a good exercise to
rewrite the query using `var`s instead of `context.become()`. However, it is recommended to get comfortable
rewrite the query using @scala[`var`s] @java[mutable fields] instead of `context.become()`. However, it is recommended to get comfortable
with the solution we have used here as it helps structuring more complex actor code in a cleaner and more maintainable way.
Or query actor is now done:
Our query actor is now done:
@@snip [Hello.scala]($code$/scala/tutorial_4/DeviceGroupQuery.scala) { #query-full }
Scala
: @@snip [DeviceGroupQuery.scala]($code$/scala/tutorial_4/DeviceGroupQuery.scala) { #query-full }
Java
: @@snip [DeviceGroupQuery.java]($code$/java/jdocs/tutorial_4/DeviceGroupQuery.java) { #query-full }
## Testing
Now let's verify the correctness of the query actor implementation. There are various scenarios we need to test individually to make
sure everything works as expected. To be able to do this, we need to simulate the device actors somehow to exercise
various normal or failure scenarios. Thankfully we took the list of collaborators (actually a `Map`) as a parameter
to the query actor, so we can easily pass in `TestProbe` references. In our first test, we try out the case when
to the query actor, so we can easily pass in @scala[`TestProbe`] @java[`TestKit`] references. In our first test, we try out the case when
there are two devices and both report a temperature:
@@snip [Hello.scala]($code$/scala/tutorial_4/DeviceGroupQuerySpec.scala) { #query-test-normal }
Scala
: @@snip [DeviceGroupQuerySpec.scala]($code$/scala/tutorial_4/DeviceGroupQuerySpec.scala) { #query-test-normal }
Java
: @@snip [DeviceGroupQueryTest.java]($code$/java/jdocs/tutorial_4/DeviceGroupQueryTest.java) { #query-test-normal }
That was the happy case, but we know that sometimes devices cannot provide a temperature measurement. This
scenario is just slightly different from the previous:
@@snip [Hello.scala]($code$/scala/tutorial_4/DeviceGroupQuerySpec.scala) { #query-test-no-reading }
Scala
: @@snip [DeviceGroupQuerySpec.scala]($code$/scala/tutorial_4/DeviceGroupQuerySpec.scala) { #query-test-no-reading }
Java
: @@snip [DeviceGroupQueryTest.java]($code$/java/jdocs/tutorial_4/DeviceGroupQueryTest.java) { #query-test-no-reading }
We also know, that sometimes device actors stop before answering:
@@snip [Hello.scala]($code$/scala/tutorial_4/DeviceGroupQuerySpec.scala) { #query-test-stopped }
Scala
: @@snip [DeviceGroupQuerySpec.scala]($code$/scala/tutorial_4/DeviceGroupQuerySpec.scala) { #query-test-stopped }
Java
: @@snip [DeviceGroupQueryTest.java]($code$/java/jdocs/tutorial_4/DeviceGroupQueryTest.java) { #query-test-stopped }
If you remember, there is another case related to device actors stopping. It is possible that we get a normal reply
from a device actor, but then receive a `Terminated` for the same actor later. In this case, we would like to keep
the first reply and not mark the device as `DeviceNotAvailable`. We should test this, too:
@@snip [Hello.scala]($code$/scala/tutorial_4/DeviceGroupQuerySpec.scala) { #query-test-stopped-later }
Scala
: @@snip [DeviceGroupQuerySpec.scala]($code$/scala/tutorial_4/DeviceGroupQuerySpec.scala) { #query-test-stopped-later }
Java
: @@snip [DeviceGroupQueryTest.java]($code$/java/jdocs/tutorial_4/DeviceGroupQueryTest.java) { #query-test-stopped-later }
The final case is when not all devices respond in time. To keep our test relatively fast, we will construct the
`DeviceGroupQuery` actor with a smaller timeout:
@@snip [Hello.scala]($code$/scala/tutorial_4/DeviceGroupQuerySpec.scala) { #query-test-timeout }
Scala
: @@snip [DeviceGroupQuerySpec.scala]($code$/scala/tutorial_4/DeviceGroupQuerySpec.scala) { #query-test-timeout }
Java
: @@snip [DeviceGroupQueryTest.java]($code$/java/jdocs/tutorial_4/DeviceGroupQueryTest.java) { #query-test-timeout }
Our query works as expected now, it is time to include this new functionality in the `DeviceGroup` actor now.
@ -190,7 +230,11 @@ Our query works as expected now, it is time to include this new functionality in
Including the query feature in the group actor is fairly simple now. We did all the heavy lifting in the query actor
itself, the group actor only needs to create it with the right initial parameters and nothing else.
@@snip [Hello.scala]($code$/scala/tutorial_4/DeviceGroup.scala) { #query-added }
Scala
: @@snip [DeviceGroup.scala]($code$/scala/tutorial_4/DeviceGroup.scala) { #query-added }
Java
: @@snip [DeviceGroup.java]($code$/java/jdocs/tutorial_4/DeviceGroup.java) { #query-added }
It is probably worth to reiterate what we said at the beginning of the chapter. By keeping the temporary state
that is only relevant to the query itself in a separate actor we keep the group actor implementation very simple. It delegates
@ -202,4 +246,8 @@ would significantly improve throughput.
We close this chapter by testing that everything works together. This test is just a variant of the previous ones,
now exercising the group query feature:
@@snip [Hello.scala]($code$/scala/tutorial_4/DeviceGroupSpec.scala) { #group-query-integration-test }
Scala
: @@snip [DeviceGroupSpec.scala]($code$/scala/tutorial_4/DeviceGroupSpec.scala) { #group-query-integration-test }
Java
: @@snip [DeviceGroupTest.java]($code$/java/jdocs/tutorial_4/DeviceGroupTest.java) { #group-query-integration-test }

View file

@ -0,0 +1,147 @@
package jdocs.tutorial_1;
import akka.actor.AbstractActor;
import akka.actor.AbstractActor.Receive;
import akka.actor.ActorRef;
import akka.actor.ActorSystem;
import akka.actor.Props;
import akka.testkit.javadsl.TestKit;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Test;
import org.scalatest.junit.JUnitSuite;
//#print-refs
class PrintMyActorRefActor extends AbstractActor {
@Override
public Receive createReceive() {
return receiveBuilder()
.matchEquals("printit", p -> {
ActorRef secondRef = getContext().actorOf(Props.empty(), "second-actor");
System.out.println("Second: " + secondRef);
})
.build();
}
}
//#print-refs
//#start-stop
class StartStopActor1 extends AbstractActor {
@Override
public void preStart() {
System.out.println("first started");
getContext().actorOf(Props.create(StartStopActor2.class), "second");
}
@Override
public void postStop() {
System.out.println("first stopped");
}
@Override
public Receive createReceive() {
return receiveBuilder()
.matchEquals("stop", s -> {
getContext().stop(getSelf());
})
.build();
}
}
class StartStopActor2 extends AbstractActor {
@Override
public void preStart() {
System.out.println("second started");
}
@Override
public void postStop() {
System.out.println("second stopped");
}
// Actor.emptyBehavior is a useful placeholder when we don't
// want to handle any messages in the actor.
@Override
public Receive createReceive() {
return receiveBuilder()
.build();
}
}
//#start-stop
//#supervise
class SupervisingActor extends AbstractActor {
ActorRef child = getContext().actorOf(Props.create(SupervisedActor.class), "supervised-actor");
@Override
public Receive createReceive() {
return receiveBuilder()
.matchEquals("failChild", f -> {
child.tell("fail", getSelf());
})
.build();
}
}
class SupervisedActor extends AbstractActor {
@Override
public void preStart() {
System.out.println("supervised actor started");
}
@Override
public void postStop() {
System.out.println("supervised actor stopped");
}
@Override
public Receive createReceive() {
return receiveBuilder()
.matchEquals("fail", f -> {
System.out.println("supervised actor fails now");
throw new Exception("I failed!");
})
.build();
}
}
//#supervise
class ActorHierarchyExperimentsTest extends JUnitSuite {
static ActorSystem system;
@BeforeClass
public static void setup() {
system = ActorSystem.create();
}
@AfterClass
public static void teardown() {
TestKit.shutdownActorSystem(system);
system = null;
}
@Test
public void testCreateTopAndChildActor() {
//#print-refs
ActorRef firstRef = system.actorOf(Props.create(PrintMyActorRefActor.class), "first-actor");
System.out.println("First : " + firstRef);
firstRef.tell("printit", ActorRef.noSender());
//#print-refs
}
@Test
public void testStartAndStopActors() {
//#start-stop
ActorRef first = system.actorOf(Props.create(StartStopActor1.class), "first");
first.tell("stop", ActorRef.noSender());
//#start-stop
}
@Test
public void testSuperviseActors() {
//#supervise
ActorRef supervisingActor = system.actorOf(Props.create(SupervisingActor.class), "supervising-actor");
supervisingActor.tell("failChild", ActorRef.noSender());
//#supervise
}
}

View file

@ -41,4 +41,4 @@ addSbtPlugin("com.timushev.sbt" % "sbt-updates" % "0.1.8")
resolvers += Resolver.url("2m-sbt-plugin-releases", url("https://dl.bintray.com/2m/sbt-plugin-releases/"))(Resolver.ivyStylePatterns)
resolvers += Resolver.bintrayRepo("2m", "sbt-plugin-releases")
addSbtPlugin("com.lightbend.paradox" % "sbt-paradox" % "0.2.10+12-0d7476ee+20170511-1700")
addSbtPlugin("com.lightbend.akka" % "sbt-paradox-akka" % "7e5bfc0b")