Tutorial pt2

This commit is contained in:
Endre Sándor Varga 2017-03-29 12:31:00 +02:00
parent 4fa2c8cf20
commit 8ae34e56f8
6 changed files with 347 additions and 9 deletions

View file

@ -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)
@@@

View file

@ -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

View 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.

View 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

View file

@ -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
}

View 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
}
}