Tutorial pt2
This commit is contained in:
parent
4fa2c8cf20
commit
8ae34e56f8
6 changed files with 347 additions and 9 deletions
|
|
@ -7,5 +7,6 @@
|
|||
* [Akka Libraries and Modules](modules.md)
|
||||
* [Your First Akka Application - Hello World](quickstart.md)
|
||||
* [Your second Akka application, part 1: Top-level architecture](tutorial_1.md)
|
||||
* [Your second Akka application, part 2: The Device actor](tutorial_2.md)
|
||||
|
||||
@@@
|
||||
|
|
@ -16,10 +16,10 @@ proficient with testing actors very early.
|
|||
We will build a simple IoT application with the bare essentials to demonstrate designing an Akka based system. The
|
||||
application will consist of two main components:
|
||||
|
||||
* *Device data collection:* This component has the responsibility to maintain a local representation of the
|
||||
* **Device data collection:** This component has the responsibility to maintain a local representation of the
|
||||
otherwise remote devices. The devices will be organized into device groups, grouping together sensors belonging
|
||||
to a home.
|
||||
* *User dashboards:* This component has the responsibility to periodically collect data from the devices for a
|
||||
* **User dashboards:** This component has the responsibility to periodically collect data from the devices for a
|
||||
logged in user and present the results as a report.
|
||||
|
||||
For simplicity, we will only collect temperature data for the devices, but in a real application our local representations
|
||||
|
|
@ -55,7 +55,7 @@ actors in the system:
|
|||
|
||||
- `/` the so called _root guardian_. This is the parent of all actors in the system, and the last one to stop
|
||||
when the system itself is terminated.
|
||||
- `/user` the _guardian_. *This is the parent actor for all user created actors*. The name `user` should not confuse
|
||||
- `/user` the _guardian_. **This is the parent actor for all user created actors**. The name `user` should not confuse
|
||||
you, it has nothing to do with the logged in user, nor user handling in general. This name really means _userspace_
|
||||
as this is the place where actors that do not access Akka internals live, i.e. all the actors created by users
|
||||
of the Akka library. Every actor you will create will have the constant path `/user/` prepended to it.
|
||||
|
|
@ -107,7 +107,7 @@ This is usually not something the user needs to be concerned with, and we leave
|
|||
|
||||
### Hierarchy and lifecycle of actors
|
||||
|
||||
We have so far seen that actors are organized into a *strict hierarchy*. This hierarchy consists of a predefined
|
||||
We have so far seen that actors are organized into a **strict hierarchy**. This hierarchy consists of a predefined
|
||||
upper layer of three actors (the root guardian, the user guardian and the system guardian), then the user created
|
||||
top-level actors (those directly living under `/user`) and the children of those. We understand now how the hierarchy
|
||||
looks like, but there is the nagging question left: _Why do we need this hierarchy? What is it used for?_
|
||||
|
|
@ -118,8 +118,8 @@ too. This is a very useful property and greatly simplifies cleaning up resources
|
|||
sockets files, etc.). In fact, one of the overlooked difficulties when dealing with low-level multi-threaded code is
|
||||
the management of the lifecycle of various concurrent resources.
|
||||
|
||||
Stopping an actor can be done by the call `context.stop(actorRef)`. *It is considered a bad practice to stop arbitrary
|
||||
actors this way*. The recommended pattern is to call `context.stop(self)` inside an actor to stop itself, usually as
|
||||
Stopping an actor can be done by the call `context.stop(actorRef)`. **It is considered a bad practice to stop arbitrary
|
||||
actors this way**. The recommended pattern is to call `context.stop(self)` inside an actor to stop itself, usually as
|
||||
a response to some user defined stop message or when the actor is done with its job.
|
||||
|
||||
The actor API exposes many lifecycle hooks that the actor implementation can override. The most commonly used are
|
||||
|
|
@ -186,8 +186,8 @@ exception that was handled, in this case our test exception. We only used here `
|
|||
which are the default to be called after and before restarts, so we cannot distinguish from inside the actor if it
|
||||
was started for the first time or restarted. This is usually the right thing to do, the purpose of the restart is to
|
||||
set the actor in a known-good state, which usually means a clean starting stage. What actually happens though is
|
||||
that *the preRestart()` and `postRestart()` methods are called which, if not overridden, by default delegate to
|
||||
`postStop()` and `preStart()` respectively*. You can experiment with overriding these additional methods and see
|
||||
that **the preRestart()` and `postRestart()` methods are called which, if not overridden, by default delegate to
|
||||
`postStop()` and `preStart()` respectively**. You can experiment with overriding these additional methods and see
|
||||
how the output changes.
|
||||
|
||||
For the impatient, we also recommend looking into the supervision reference page (TODO: reference) for more in-depth
|
||||
|
|
@ -227,7 +227,7 @@ This application does very little for now, but we have the first actor in place
|
|||
|
||||
## What is next?
|
||||
|
||||
In the following chapters we will grow the application step-by-step
|
||||
In the following chapters we will grow the application step-by-step:
|
||||
|
||||
1. We will create the representation for a device
|
||||
2. We create the device management component
|
||||
|
|
|
|||
188
akka-docs-new/src/main/paradox/guide/tutorial_2.md
Normal file
188
akka-docs-new/src/main/paradox/guide/tutorial_2.md
Normal file
|
|
@ -0,0 +1,188 @@
|
|||
# Your second Akka application, part 2: The Device actor
|
||||
|
||||
In part 1 we explained how to view actor systems _in the large_, i.e. how components should be represented, how
|
||||
actor should be arranged in the hierarchy. In this part we will look at actors _in the small_ by implementing an
|
||||
actor with the most common conversational patterns.
|
||||
|
||||
In particular, leaving the components aside for a while, we will implement the actor that represents a device. The
|
||||
tasks of this actor will be rather simple:
|
||||
|
||||
* Collect temperature measurements
|
||||
* Report the last measured temperature if asked
|
||||
|
||||
When working with objects we usually design our API as _interfaces_, which are basically a collection of abstract
|
||||
methods to be filled in by the actual implementation. In the world of actors, the counterpart of interfaces are
|
||||
protocols. While it is not possible to formalize general protocols in the programming language, we can formalize
|
||||
its most basic elements: the messages.
|
||||
|
||||
## The query protocol
|
||||
|
||||
Just because a device have been started it does not mean that it has immediately a temperature measurement. Hence, we
|
||||
need to account for the case that a temperature is not present in our protocol. This fortunately means that we
|
||||
can test the query part of the actor without the write part present, as it can simply report an empty result.
|
||||
|
||||
The protocol for obtaining the current temperature from the device actor is rather simple looking:
|
||||
|
||||
1. Wait for a request for current temperature
|
||||
2. respond to the request with a reply containing the current temperature or an indication that it is not yet
|
||||
available
|
||||
|
||||
We need two messages, one for the requests, and one for the replies. A first attempt could look like this:
|
||||
|
||||
@@snip [Hello.scala](../../../test/scala/tutorial_2/DeviceInProgress.scala) { #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.
|
||||
|
||||
## Message ordering, delivery guarantees
|
||||
|
||||
In order to give some context to the discussion below, consider an application which spans multiple network hosts.
|
||||
The basic mechanism for communication is the same whether sending to an actor on the local JVM or to a remote actor,
|
||||
but of course there will be observable differences in the latency of delivery (possibly also depending on the bandwidth
|
||||
of the network link and the message size) and the reliability. In case of a remote message send there are obviously
|
||||
more steps involved which means that more can go wrong. Another aspect is that local sending will just pass a
|
||||
reference to the message inside the same JVM, without any restrictions on the underlying object which is sent,
|
||||
whereas a remote transport will place a limit on the message size.
|
||||
|
||||
It is also important to keep in mind, that while passing inside the same JVM is significantly more reliable, if an
|
||||
actor fails due to a programmer error while processing the message, the effect is basically the same as if a remote,
|
||||
network request fails due to the remote host crashing while processing the message. Even though in both cases the
|
||||
service is recovered after a while (the actor is restarted by its supervisor, the host is restarted by an operator
|
||||
or by a monitoring system) individual requests are lost during the crash. **Writing your actors such that every
|
||||
message could possibly be lost is the safe, pessimistic bet.**
|
||||
|
||||
These are the rules in Akka for message sends:
|
||||
|
||||
* at-most-once delivery, i.e. no guaranteed delivery
|
||||
* message ordering is maintained per sender, receiver pair
|
||||
|
||||
### What does "at-most-once" mean?
|
||||
|
||||
When it comes to describing the semantics of a delivery mechanism, there are three basic categories:
|
||||
|
||||
* **at-most-once delivery** means that for each message handed to the mechanism, that message is delivered zero or
|
||||
one times; in more casual terms it means that messages may be lost, but never duplicated.
|
||||
* **at-least-once delivery** means that for each message handed to the mechanism potentially multiple attempts are made
|
||||
at delivering it, such that at least one succeeds; again, in more casual terms this means that messages may be
|
||||
duplicated but not lost.
|
||||
* **exactly-once delivery** means that for each message handed to the mechanism exactly one delivery is made to
|
||||
the recipient; the message can neither be lost nor duplicated.
|
||||
|
||||
The first one is the cheapest, highest performance, least implementation overhead because it can be done in a
|
||||
fire-and-forget fashion without keeping state at the sending end or in the transport mechanism.
|
||||
The second one requires retries to counter transport losses, which means keeping state at the sending end and
|
||||
having an acknowledgement mechanism at the receiving end. The third is most expensive, and has consequently worst
|
||||
performance: in addition to the second it requires state to be kept at the receiving end in order to filter out
|
||||
duplicate deliveries.
|
||||
|
||||
### Why No Guaranteed Delivery?
|
||||
|
||||
At the core of the problem lies the question what exactly this guarantee shall mean, i.e. at which point does
|
||||
the delivery considered to be guaranteed:
|
||||
|
||||
1. The message is sent out on the network?
|
||||
2. The message is received by the other host?
|
||||
3. The message is put into the target actor's mailbox?
|
||||
4. The message is starting to be processed by the target actor?
|
||||
5. The message is processed successfully by the target actor?
|
||||
|
||||
Most frameworks/protocols claiming guaranteed delivery actually provide something similar to point 4 and 5. While this
|
||||
sounds fair, **is this actually useful?** To understand the implications, consider a simple, practical example:
|
||||
a user attempts to place an order and we only want to claim that it has successfully processed once it is actually on
|
||||
disk in the database containing orders.
|
||||
|
||||
If we rely on the guarantees of such system it will report success as soon as the order has been submitted to the
|
||||
internal API that has the responsibility to validate it, process it and put it into the database. Unfortunately,
|
||||
immediately after the API has been invoked
|
||||
|
||||
* the host can immediately crash
|
||||
* deserialization can fail
|
||||
* validation can fail
|
||||
* the database might be unavailable
|
||||
* a programming error might occur
|
||||
|
||||
The problem is that the **guarantee of delivery** does not translate to the **domain level guarantee**. We only want to
|
||||
report success once the order has been actually fully processed and persisted. **The only entity that can report
|
||||
success is the application itself, since only it has any understanding of the domain guarantees required. No generalized
|
||||
framework can figure out the specifics of a particular domain and what is consisered a success in that domain**. In
|
||||
this particular example, we only want to signal success after a successful database write, where the database acknowledged
|
||||
that the order is now safely stored. **For these reasons Akka lifts the responsibilities of guarantees to the application
|
||||
itelf, i.e. you have to implement them yourself. On the other hand, you are in full control of the guarantees that you want
|
||||
to provide**.
|
||||
|
||||
### Message ordering
|
||||
|
||||
The rule is that for a given pair of actors, messages sent directly from the first to the second will not be
|
||||
received out-of-order. The word directly emphasizes that this guarantee only applies when sending with the tell
|
||||
operator directly to the final destination, but not when employing mediators.
|
||||
|
||||
If
|
||||
|
||||
* Actor `A1` sends messages `M1`, `M2`, `M3` to `A2`
|
||||
* Actor `A3` sends messages `M4`, `M5`, `M6` to `A2`
|
||||
|
||||
This means that:
|
||||
|
||||
* If `M1` is delivered it must be delivered before `M2` and `M3`
|
||||
* If `M2` is delivered it must be delivered before `M3`
|
||||
* If `M4` is delivered it must be delivered before `M5` and `M6`
|
||||
* If `M5` is delivered it must be delivered before `M6`
|
||||
* `A2` can see messages from `A1` interleaved with messages from `A3`
|
||||
* Since there is no guaranteed delivery, any of the messages may be dropped, i.e. not arrive at `A2`
|
||||
|
||||
For the full details on delivery guarantees please refer to the reference page (TODO reference).
|
||||
|
||||
### Revisiting the query protocol
|
||||
|
||||
There is nothing wrong with our first query protocol but it limits our flexibility. If we want to implement resends
|
||||
in the actor that queries our device actor (because of timed out requests) or want to query multiple actors it
|
||||
can be helpful to put an additional query ID field in the message which helps us correlate requests with responses.
|
||||
|
||||
Hence, we add one more field to our messages, so that an ID can be provided by the requester:
|
||||
|
||||
@@snip [Hello.scala](../../../test/scala/tutorial_2/DeviceInProgress.scala) { #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](../../../test/scala/tutorial_2/DeviceInProgress.scala) { #device-with-read }
|
||||
|
||||
We maintain the current temperature (which can be empty initially), 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):
|
||||
|
||||
@@snip [Hello.scala](../../../test/scala/tutorial_2/DeviceSpec.scala) { #device-read-test }
|
||||
|
||||
## The write protocol
|
||||
|
||||
As a first attempt, we could model recording the current temperature in the device actor as a single message:
|
||||
|
||||
* When a temperature record request is received, update the `currentTemperature` field.
|
||||
|
||||
Such a message could possibly look like this:
|
||||
|
||||
@@snip [Hello.scala](../../../test/scala/tutorial_2/DeviceInProgress.scala) { #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 acknowledgement to the sender
|
||||
once we have updated our last temperature recording. Just like in the case of temperature queries and responses, it
|
||||
is a good idea to include an ID field in this case, too, to provide maximum flexibility.
|
||||
|
||||
Putting these together, the device actor will look like this:
|
||||
|
||||
@@snip [Hello.scala](../../../test/scala/tutorial_2/Device.scala) { #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](../../../test/scala/tutorial_2/DeviceSpec.scala) { #device-write-read-test }
|
||||
|
||||
## What is next?
|
||||
|
||||
So far, we have started designing our overall architecture, and we wrote our first actor directly corresponding to the
|
||||
domain. We now have to create the component that is responsible for maintaining groups of devices and the device
|
||||
actors themselves.
|
||||
38
akka-docs-new/src/test/scala/tutorial_2/Device.scala
Normal file
38
akka-docs-new/src/test/scala/tutorial_2/Device.scala
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
/**
|
||||
* Copyright (C) 2009-2017 Lightbend Inc. <http://www.lightbend.com>
|
||||
*/
|
||||
package tutorial_2
|
||||
|
||||
//#full-device
|
||||
import akka.actor.{Actor, ActorLogging, Props}
|
||||
import tutorial_3.Device.{ReadTemperature, RecordTemperature, RespondTemperature, TemperatureRecorded}
|
||||
import tutorial_3.DeviceManager.{DeviceRegistered, RequestTrackDevice}
|
||||
|
||||
object Device {
|
||||
|
||||
def props(groupId: String, deviceId: String): Props = Props(new Device(groupId, deviceId))
|
||||
|
||||
final case class RecordTemperature(requestId: Long, value: Double)
|
||||
final case class TemperatureRecorded(requestId: Long)
|
||||
|
||||
final case class ReadTemperature(requestId: Long)
|
||||
final case class RespondTemperature(requestId: Long, value: Option[Double])
|
||||
}
|
||||
|
||||
class Device(groupId: String, deviceId: String) extends Actor with ActorLogging {
|
||||
var lastTemperatureReading: Option[Double] = None
|
||||
|
||||
override def preStart(): Unit = log.info("Device actor {}-{} started", groupId, deviceId)
|
||||
override def postStop(): Unit = log.info("Device actor {}-{} stopped", groupId, deviceId)
|
||||
|
||||
override def receive: Receive = {
|
||||
case RecordTemperature(id, value) =>
|
||||
log.info("Recorded temperature reading {} with {}", value, id)
|
||||
lastTemperatureReading = Some(value)
|
||||
sender() ! TemperatureRecorded(id)
|
||||
|
||||
case ReadTemperature(id) =>
|
||||
sender() ! RespondTemperature(id, lastTemperatureReading)
|
||||
}
|
||||
}
|
||||
//#full-device
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
package tutorial_2
|
||||
|
||||
import tutorial_3.Device.{ReadTemperature, RecordTemperature, RespondTemperature, TemperatureRecorded}
|
||||
|
||||
object DeviceInProgress1 {
|
||||
|
||||
//#read-protocol-1
|
||||
object Device {
|
||||
final case object ReadTemperature
|
||||
final case class RespondTemperature(value: Option[Double])
|
||||
}
|
||||
//#read-protocol-1
|
||||
|
||||
}
|
||||
|
||||
object DeviceInProgress2 {
|
||||
|
||||
//#read-protocol-2
|
||||
object Device {
|
||||
|
||||
final case class ReadTemperature(requestId: Long)
|
||||
|
||||
final case class RespondTemperature(requestId: Long, value: Option[Double])
|
||||
|
||||
}
|
||||
|
||||
//#read-protocol-2
|
||||
|
||||
//#device-with-read
|
||||
import akka.actor.{Actor, ActorLogging}
|
||||
|
||||
class Device(groupId: String, deviceId: String) extends Actor with ActorLogging {
|
||||
var lastTemperatureReading: Option[Double] = None
|
||||
|
||||
override def preStart(): Unit = log.info("Device actor {}-{} started", groupId, deviceId)
|
||||
|
||||
override def postStop(): Unit = log.info("Device actor {}-{} stopped", groupId, deviceId)
|
||||
|
||||
override def receive: Receive = {
|
||||
case ReadTemperature(id) =>
|
||||
sender() ! RespondTemperature(id, lastTemperatureReading)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
//#device-with-read
|
||||
|
||||
}
|
||||
|
||||
object DeviceInProgress3 {
|
||||
|
||||
//#write-protocol-1
|
||||
object Device {
|
||||
final case class ReadTemperature(requestId: Long)
|
||||
final case class RespondTemperature(requestId: Long, value: Option[Double])
|
||||
|
||||
final case class RecordTemperature(value: Double)
|
||||
}
|
||||
//#write-protocol-1
|
||||
}
|
||||
51
akka-docs-new/src/test/scala/tutorial_2/DeviceSpec.scala
Normal file
51
akka-docs-new/src/test/scala/tutorial_2/DeviceSpec.scala
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
/**
|
||||
* Copyright (C) 2009-2017 Lightbend Inc. <http://www.lightbend.com>
|
||||
*/
|
||||
package tutorial_2
|
||||
|
||||
import akka.testkit.{AkkaSpec, TestProbe}
|
||||
|
||||
import scala.concurrent.duration._
|
||||
|
||||
class DeviceSpec extends AkkaSpec {
|
||||
|
||||
"Device actor" must {
|
||||
|
||||
//#device-read-test
|
||||
"reply with empty reading if no temperature is known" in {
|
||||
val probe = TestProbe()
|
||||
val deviceActor = system.actorOf(Device.props("group", "device"))
|
||||
|
||||
deviceActor.tell(Device.ReadTemperature(requestId = 42), probe.ref)
|
||||
val response = probe.expectMsgType[Device.RespondTemperature]
|
||||
response.requestId should ===(42)
|
||||
response.value should ===(None)
|
||||
}
|
||||
//#device-read-test
|
||||
|
||||
//#device-write-read-test
|
||||
"reply with latest temperature reading" in {
|
||||
val probe = TestProbe()
|
||||
val deviceActor = system.actorOf(Device.props("group", "device"))
|
||||
|
||||
deviceActor.tell(Device.RecordTemperature(requestId = 1, 24.0), probe.ref)
|
||||
probe.expectMsg(Device.TemperatureRecorded(requestId = 1))
|
||||
|
||||
deviceActor.tell(Device.ReadTemperature(requestId = 2), probe.ref)
|
||||
val response1 = probe.expectMsgType[Device.RespondTemperature]
|
||||
response1.requestId should ===(2)
|
||||
response1.value should ===(Some(24.0))
|
||||
|
||||
deviceActor.tell(Device.RecordTemperature(requestId = 3, 55.0), probe.ref)
|
||||
probe.expectMsg(Device.TemperatureRecorded(requestId = 3))
|
||||
|
||||
deviceActor.tell(Device.ReadTemperature(requestId = 4), probe.ref)
|
||||
val response2 = probe.expectMsgType[Device.RespondTemperature]
|
||||
response2.requestId should ===(4)
|
||||
response2.value should ===(Some(55.0))
|
||||
}
|
||||
//#device-write-read-test
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue