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:
parent
513dbfbec0
commit
4cb9c2436f
8 changed files with 359 additions and 69 deletions
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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?
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue