diff --git a/akka-docs-new/src/main/paradox/guide/index.md b/akka-docs-new/src/main/paradox/guide/index.md index 0801b18d9a..bb5ec4817e 100644 --- a/akka-docs-new/src/main/paradox/guide/index.md +++ b/akka-docs-new/src/main/paradox/guide/index.md @@ -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) @@@ \ No newline at end of file diff --git a/akka-docs-new/src/main/paradox/guide/tutorial_1.md b/akka-docs-new/src/main/paradox/guide/tutorial_1.md index 064f284fce..83963ef277 100644 --- a/akka-docs-new/src/main/paradox/guide/tutorial_1.md +++ b/akka-docs-new/src/main/paradox/guide/tutorial_1.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 diff --git a/akka-docs-new/src/main/paradox/guide/tutorial_2.md b/akka-docs-new/src/main/paradox/guide/tutorial_2.md new file mode 100644 index 0000000000..d9f804d5d5 --- /dev/null +++ b/akka-docs-new/src/main/paradox/guide/tutorial_2.md @@ -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. \ No newline at end of file diff --git a/akka-docs-new/src/test/scala/tutorial_2/Device.scala b/akka-docs-new/src/test/scala/tutorial_2/Device.scala new file mode 100644 index 0000000000..02a286aa0c --- /dev/null +++ b/akka-docs-new/src/test/scala/tutorial_2/Device.scala @@ -0,0 +1,38 @@ +/** + * Copyright (C) 2009-2017 Lightbend Inc. + */ +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 diff --git a/akka-docs-new/src/test/scala/tutorial_2/DeviceInProgress.scala b/akka-docs-new/src/test/scala/tutorial_2/DeviceInProgress.scala new file mode 100644 index 0000000000..418a5b06cf --- /dev/null +++ b/akka-docs-new/src/test/scala/tutorial_2/DeviceInProgress.scala @@ -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 +} \ No newline at end of file diff --git a/akka-docs-new/src/test/scala/tutorial_2/DeviceSpec.scala b/akka-docs-new/src/test/scala/tutorial_2/DeviceSpec.scala new file mode 100644 index 0000000000..e308ac3e56 --- /dev/null +++ b/akka-docs-new/src/test/scala/tutorial_2/DeviceSpec.scala @@ -0,0 +1,51 @@ +/** + * Copyright (C) 2009-2017 Lightbend Inc. + */ +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 + + } + +}