Tutorial pt1
This commit is contained in:
parent
4ffb4d30b9
commit
4fa2c8cf20
5 changed files with 365 additions and 36 deletions
|
|
@ -6,5 +6,6 @@
|
||||||
* [What are Actors?](actors-intro.md)
|
* [What are Actors?](actors-intro.md)
|
||||||
* [Akka Libraries and Modules](modules.md)
|
* [Akka Libraries and Modules](modules.md)
|
||||||
* [Your First Akka Application - Hello World](quickstart.md)
|
* [Your First Akka Application - Hello World](quickstart.md)
|
||||||
|
* [Your second Akka application, part 1: Top-level architecture](tutorial_1.md)
|
||||||
|
|
||||||
@@@
|
@@@
|
||||||
|
|
@ -1,41 +1,235 @@
|
||||||
supervision
|
# Your second Akka application, part 1: Top-level architecture
|
||||||
deathwatch
|
|
||||||
testing
|
|
||||||
conversation patterns
|
|
||||||
timeouts
|
|
||||||
|
|
||||||
create app structure
|
In this and the following chapters we will build a sample Akka application to introduce you to the language of
|
||||||
iot collectors
|
actors and how problems can be formulated with them. It is a common hurdle for beginners to translate their problem
|
||||||
dashboards
|
into actors even though they understand what they do on the high-level. We will build here the core logic of a small
|
||||||
analysis backend
|
application and guide you through common patterns that help you kickstart your projects with Akka.
|
||||||
|
|
||||||
|
The application we aim to write will be a (simplified) IoT system where devices can report temperature data coming
|
||||||
|
from sensors installed at the homes of users. Users will be able to query the current state of these sensors. To keep
|
||||||
|
things simple, we will not actually expose the application via HTTP or (other external API), we will concentrate only to the
|
||||||
|
core logic. On the other hand, we will write tests for the pieces of the application to get you comfortable and
|
||||||
|
proficient with testing actors very early.
|
||||||
|
|
||||||
|
## Our goals for the IoT system
|
||||||
|
|
||||||
|
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
|
||||||
|
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
|
||||||
|
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
|
||||||
|
for a remote device (which we will model as an actor) would have many more responsibilities. Among others: reading the
|
||||||
|
configuration of the device, changing the configuration, checking if the devices are unresponsive, etc. We leave
|
||||||
|
these complexities for now as they can be easily added as an exercise.
|
||||||
|
|
||||||
|
We will also not address the means by which the remote devices communicate with the local representations (actors). Instead
|
||||||
|
we just build an actor based API which such a network protocol could use. We will use tests for our API everywhere though.
|
||||||
|
|
||||||
|
The architecture of the application will look like this:
|
||||||
|
|
||||||
|
ARCH_BOXES_DIAGRAM
|
||||||
|
|
||||||
|
## Top level architecture
|
||||||
|
|
||||||
|
When writing prose, the hardest part is usually to write the first couple of sentences. There is a similar feeling
|
||||||
|
when you try to build your Akka system: what should be the first actor? Where should it live, what should it do?
|
||||||
|
Fortunately, unlike with prose, there are established best practices that guide you through these initial steps.
|
||||||
|
|
||||||
|
When one creates an actor in Akka it always belongs to a certain parent. This means that actors are always organized
|
||||||
|
into a tree. In general, creating an actor can only happen from inside another actor. This creator actor becomes the
|
||||||
|
_parent_ of the newly created _child_ actor. You might ask then, who is the parent of the _first_ actor you create?
|
||||||
|
As we have seen in the previous chapters, to create a top-level actor one must call `context.actorOf()`. This does
|
||||||
|
not create a "freestanding" actor though, instead, it injects the corresponding actor as a child into an already
|
||||||
|
existing tree:
|
||||||
|
|
||||||
|
TREE_DIAGRAM
|
||||||
|
|
||||||
|
As you see, creating actors from the "top" injects those actors under the path `/user/`, so for example creating
|
||||||
|
an actor named `myActor` will end up having the path `/user/myActor`. In fact, there are three already existing
|
||||||
|
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
|
||||||
|
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.
|
||||||
|
- `/sytem` the _system guardian_.
|
||||||
|
|
||||||
|
The names of these built-in actors contain _guardian_ because these are _supervising_ every actor living as a child
|
||||||
|
of them (i.e. under their path). We will explain supervision in more detail, all you need to know now that every
|
||||||
|
unhandled failure from actors bubble up to their parent that, in turn, can decide how to handle this failure. These
|
||||||
|
predefined actors are guardians in the sense that they are the final lines of defense, where all unhandled failures
|
||||||
|
from user (or system) actors end up.
|
||||||
|
|
||||||
|
> Does the root guardian (the root path `/`) have a parent? As it turns out, it has. This special entity is called
|
||||||
|
> the "Bubble-Walker". This special entity is invisible for the user and only has uses internally.
|
||||||
|
|
||||||
|
### Structure of an ActorRef and paths of actors
|
||||||
|
|
||||||
|
The easiest way to see this in action is to simply print `ActorRef` instances. In this small experiment, we print
|
||||||
|
the reference of the first actor we create and then we create a child of this actor, and print its reference. We have
|
||||||
|
already created actors with `system.actorOf()`, which creates an actor under `/user` directly. We call these kind
|
||||||
|
of actors _top level_, even though in practice they are not on the top of the hierarchy, only on the top of the
|
||||||
|
_user defined_ hierarchy. Since in practice we usually concern ourselves about actors under `/user` this is still a
|
||||||
|
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](../../../test/scala/tutorial_1/ActorHierarchyExperiments.scala) { #print-refs }
|
||||||
|
|
||||||
|
We see that the following two lines are printed
|
||||||
|
|
||||||
|
```
|
||||||
|
Actor[akka://testSystem/user/first-actor#1053618476]
|
||||||
|
Actor[akka://testSystem/user/first-actor/second-actor#-1544706041]
|
||||||
|
```
|
||||||
|
|
||||||
|
First, we notice that all of the paths start with `akka://testSystem/`. Since all actor references are valid URLs, there
|
||||||
|
is a protocol field needed, which is `akka://` in the case of actors. Then, just like on the World Wide Web, the system
|
||||||
|
is identified. In our case this is `testSystem`, but could be any other name (if remote communications between multiple
|
||||||
|
systems is enabled this name is the hostname of the system so other systems can find it on the network). Our two actors,
|
||||||
|
as we have discussed before, live under user, and form a hierarchy:
|
||||||
|
|
||||||
|
* `akka://testSystem/user/first-actor` is the first actor we created, which lives directly under the user guardian,
|
||||||
|
`/user`
|
||||||
|
* `akka://testSystem/user/first-actor/second-actor` is the second actor we created, using `context.actorOf`. As we
|
||||||
|
see it lives directly under the first actor.
|
||||||
|
|
||||||
|
The last part of the actor reference, like `#1053618476` is a unique identifier of the actor living under the path.
|
||||||
|
This is usually not something the user needs to be concerned with, and we leave the discussion of this field for later.
|
||||||
|
|
||||||
|
### Hierarchy and lifecycle of actors
|
||||||
|
|
||||||
|
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?_
|
||||||
|
|
||||||
|
The first use of the hierarchy is to manage the lifecycle of actors. Actors pop into existence when created, then later,
|
||||||
|
when the user requests, they are stopped. Whenever an actor is stopped, all of its children are _recursively stopped_,
|
||||||
|
too. This is a very useful property and greatly simplifies cleaning up resources and avoiding resource leaks (like open
|
||||||
|
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
|
||||||
|
a response to some user defined stop message or when the actor is done with its job.
|
||||||
|
|
||||||
STEP 1
|
The actor API exposes many lifecycle hooks that the actor implementation can override. The most commonly used are
|
||||||
create entry point
|
`preStart()` and `postStop()`.
|
||||||
- just the usual stuff
|
|
||||||
explain supervision
|
* `preStart()` is invoked after the actor has started but before it processes its first message.
|
||||||
- don't do too much top level actors
|
* `postStop()` is invoked just before the actor stops. No messages are processed after this point.
|
||||||
create top level supervisor
|
|
||||||
- lifecycle hooks
|
|
||||||
- emptyBehavior
|
|
||||||
create device actor
|
|
||||||
create device group actor
|
|
||||||
create device manager
|
|
||||||
|
|
||||||
STEP 2
|
Again, we can try out all this with a simple experiment:
|
||||||
create dashboard manager
|
|
||||||
create dashboard actor
|
|
||||||
- resilience to device group actor failures
|
|
||||||
- resilience to individual device actor failures
|
|
||||||
|
|
||||||
STEP 3
|
|
||||||
create analytics manager
|
|
||||||
set up aggregator
|
|
||||||
set up pool of workers
|
|
||||||
hook analytics up with dashboard (show global average)
|
|
||||||
make it resilient to analytics failure
|
|
||||||
|
|
||||||
STEP 4
|
@@snip [Hello.scala](../../../test/scala/tutorial_1/ActorHierarchyExperiments.scala) { #start-stop }
|
||||||
set up configuration
|
|
||||||
set up logging
|
After running it, we get the output
|
||||||
|
|
||||||
|
```
|
||||||
|
first started
|
||||||
|
second started
|
||||||
|
second stopped
|
||||||
|
first stopped
|
||||||
|
```
|
||||||
|
|
||||||
|
We see that once we stopped the first actor, it recursively stopped our second actor, too, then it finished itself.
|
||||||
|
This ordering is strict, all `postStop()` hooks of the children are called before the `postStop()` hook of the parent
|
||||||
|
is called.
|
||||||
|
|
||||||
|
The family of these lifecycle hooks is rich, and we recommend reading the actor lifecycle (TODO: reference-doc) section
|
||||||
|
of the reference for all the details.
|
||||||
|
|
||||||
|
### Hierarchy and failure handling (Supervision)
|
||||||
|
|
||||||
|
Parents and children are not only connected by their lifecycles. Whenever an actor fails (throws an exception or
|
||||||
|
an unhandled exception bubbles out from `receive`) it is temporarily suspended. The failure information is propagated
|
||||||
|
then to the parent, which can decide what to do with the child actor now. The default _supervisor strategy_ is to
|
||||||
|
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 for now in this simple experiment:
|
||||||
|
|
||||||
|
@@snip [Hello.scala](../../../test/scala/tutorial_1/ActorHierarchyExperiments.scala) { #supervise }
|
||||||
|
|
||||||
|
After running the snippet, we see the following output on the console:
|
||||||
|
|
||||||
|
```
|
||||||
|
supervised actor started
|
||||||
|
supervised actor fails now
|
||||||
|
supervised actor stopped
|
||||||
|
supervised actor started
|
||||||
|
[ERROR] [03/29/2017 10:47:14.150] [testSystem-akka.actor.default-dispatcher-2] [akka://testSystem/user/supervising-actor/supervised-actor] I failed!
|
||||||
|
java.lang.Exception: I failed!
|
||||||
|
at tutorial_1.SupervisedActor$$anonfun$receive$4.applyOrElse(ActorHierarchyExperiments.scala:57)
|
||||||
|
at akka.actor.Actor$class.aroundReceive(Actor.scala:513)
|
||||||
|
at tutorial_1.SupervisedActor.aroundReceive(ActorHierarchyExperiments.scala:47)
|
||||||
|
at akka.actor.ActorCell.receiveMessage(ActorCell.scala:519)
|
||||||
|
at akka.actor.ActorCell.invoke(ActorCell.scala:488)
|
||||||
|
at akka.dispatch.Mailbox.processMailbox(Mailbox.scala:257)
|
||||||
|
at akka.dispatch.Mailbox.run(Mailbox.scala:224)
|
||||||
|
at akka.dispatch.Mailbox.exec(Mailbox.scala:234)
|
||||||
|
at akka.dispatch.forkjoin.ForkJoinTask.doExec(ForkJoinTask.java:260)
|
||||||
|
at akka.dispatch.forkjoin.ForkJoinPool$WorkQueue.runTask(ForkJoinPool.java:1339)
|
||||||
|
at akka.dispatch.forkjoin.ForkJoinPool.runWorker(ForkJoinPool.java:1979)
|
||||||
|
at akka.dispatch.forkjoin.ForkJoinWorkerThread.run(ForkJoinWorkerThread.java:107)
|
||||||
|
```
|
||||||
|
|
||||||
|
We see that after failure the actor is stopped and immediately started again. We also see a log entry reporting the
|
||||||
|
exception that was handled, in this case our test exception. We only used here `preStart()` and `postStop()` hooks
|
||||||
|
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
|
||||||
|
how the output changes.
|
||||||
|
|
||||||
|
For the impatient, we also recommend looking into the supervision reference page (TODO: reference) for more in-depth
|
||||||
|
details.
|
||||||
|
|
||||||
|
### The first actor
|
||||||
|
|
||||||
|
Actors are organized into a strict tree, where the lifecylce of every child is tied to the parent and where parents
|
||||||
|
are responsible to decide on the fate of failed children. At first, it might not be evident how to map our problem
|
||||||
|
to such a tree, but in practice, this is easier than it looks. All we need to do is to rewrite our architecture diagram
|
||||||
|
that contained nested boxes into a tree:
|
||||||
|
|
||||||
|
ARCH_TREE_DIAGRAM
|
||||||
|
|
||||||
|
In simple terms, every component manages the lifecycle of the subcomponents. No subcomponent can outlive the parent
|
||||||
|
component. This is exactly how the actor hierarchy works. Also, it is desirable that a component handles the failure
|
||||||
|
of its subcomponents unless the failure leads to the failure of the whole component. Together, these two desirable
|
||||||
|
properties lead to the conclusion that the "contained-in" relationship of components should be mapped to the
|
||||||
|
"children-of" relationship of actors.
|
||||||
|
|
||||||
|
The only question left is how to map the top-level components to actors. It might be tempting to create the actors
|
||||||
|
representing the main components as top-level actors. We instead, recommend creating an explicit component that
|
||||||
|
represent the whole application. In other words, we will have a single top-level actor in our actor system and have
|
||||||
|
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 use instead the `ActorLogging` helper trait which allow us to use the
|
||||||
|
logging facility built into Akka directly:
|
||||||
|
|
||||||
|
@@snip [Hello.scala](../../../test/scala/tutorial_1/IotSupervisor.scala) { #iot-supervisor }
|
||||||
|
|
||||||
|
All we need now is to tie this up with a class with the `main` entry point:
|
||||||
|
|
||||||
|
@@snip [Hello.scala](../../../test/scala/tutorial_1/IotApp.scala) { #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.
|
||||||
|
|
||||||
|
## What is next?
|
||||||
|
|
||||||
|
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
|
||||||
|
3. We add query capabilities to device groups
|
||||||
|
4. We add the dashboard component
|
||||||
|
|
@ -0,0 +1,87 @@
|
||||||
|
package tutorial_1
|
||||||
|
|
||||||
|
import akka.actor.{ Actor, Props }
|
||||||
|
import akka.testkit.AkkaSpec
|
||||||
|
|
||||||
|
//#print-refs
|
||||||
|
class PrintMyActorRefActor extends Actor {
|
||||||
|
override def receive: Receive = {
|
||||||
|
case "printit" =>
|
||||||
|
val secondRef = context.actorOf(Props.empty, "second-actor")
|
||||||
|
println(secondRef)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//#print-refs
|
||||||
|
|
||||||
|
//#start-stop
|
||||||
|
class StartStopActor1 extends Actor {
|
||||||
|
override def receive: Receive = {
|
||||||
|
case "stop" => context.stop(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
override def preStart(): Unit = {
|
||||||
|
println("first started")
|
||||||
|
context.actorOf(Props[StartStopActor2], "second")
|
||||||
|
}
|
||||||
|
override def postStop(): Unit = println("first stopped")
|
||||||
|
}
|
||||||
|
|
||||||
|
class StartStopActor2 extends Actor {
|
||||||
|
// Actor.emptyBehavior is a useful placeholder when we don't
|
||||||
|
// want to handle any messages in the actor.
|
||||||
|
override def receive: Receive = Actor.emptyBehavior
|
||||||
|
|
||||||
|
override def preStart(): Unit = println("second started")
|
||||||
|
override def postStop(): Unit = println("second stopped")
|
||||||
|
}
|
||||||
|
//#start-stop
|
||||||
|
|
||||||
|
//#supervise
|
||||||
|
class SupervisingActor extends Actor {
|
||||||
|
val child = context.actorOf(Props[SupervisedActor], "supervised-actor")
|
||||||
|
|
||||||
|
override def receive: Receive = {
|
||||||
|
case "failChild" => child ! "fail"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class SupervisedActor extends Actor {
|
||||||
|
|
||||||
|
override def preStart(): Unit = println("supervised actor started")
|
||||||
|
override def postStop(): Unit = println("supervised actor stopped")
|
||||||
|
|
||||||
|
override def receive: Receive = {
|
||||||
|
case "fail" =>
|
||||||
|
println("supervised actor fails now")
|
||||||
|
throw new Exception("I failed!")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//#supervise
|
||||||
|
|
||||||
|
class ActorHierarchyExperiments extends AkkaSpec {
|
||||||
|
|
||||||
|
"create top and child actor" in {
|
||||||
|
|
||||||
|
//#print-refs
|
||||||
|
val firstRef = system.actorOf(Props[PrintMyActorRefActor], "first-actor")
|
||||||
|
println(firstRef)
|
||||||
|
firstRef ! "printit"
|
||||||
|
//#print-refs
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
"start and stop actors" in {
|
||||||
|
//#start-stop
|
||||||
|
val first = system.actorOf(Props[StartStopActor1], "first")
|
||||||
|
first ! "stop"
|
||||||
|
//#start-stop
|
||||||
|
}
|
||||||
|
|
||||||
|
"supervise actors" in {
|
||||||
|
//#supervise
|
||||||
|
val supervisingActor = system.actorOf(Props[SupervisingActor], "supervising-actor")
|
||||||
|
supervisingActor ! "failChild"
|
||||||
|
//#supervise
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
26
akka-docs-new/src/test/scala/tutorial_1/IotApp.scala
Normal file
26
akka-docs-new/src/test/scala/tutorial_1/IotApp.scala
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
/**
|
||||||
|
* Copyright (C) 2009-2017 Lightbend Inc. <http://www.lightbend.com>
|
||||||
|
*/
|
||||||
|
package tutorial_1
|
||||||
|
|
||||||
|
//#iot-app
|
||||||
|
import akka.actor.ActorSystem
|
||||||
|
import scala.io.StdIn
|
||||||
|
|
||||||
|
object IotApp {
|
||||||
|
|
||||||
|
def main(args: Array[String]): Unit = {
|
||||||
|
val system = ActorSystem("iot-system")
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Create top level supervisor
|
||||||
|
val supervisor = system.actorOf(IotSupervisor.props(), "iot-supervisor")
|
||||||
|
// Exit the system after ENTER is pressed
|
||||||
|
StdIn.readLine()
|
||||||
|
} finally {
|
||||||
|
system.terminate()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
//#iot-app
|
||||||
21
akka-docs-new/src/test/scala/tutorial_1/IotSupervisor.scala
Normal file
21
akka-docs-new/src/test/scala/tutorial_1/IotSupervisor.scala
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
/**
|
||||||
|
* Copyright (C) 2009-2017 Lightbend Inc. <http://www.lightbend.com>
|
||||||
|
*/
|
||||||
|
package tutorial_1
|
||||||
|
|
||||||
|
//#iot-supervisor
|
||||||
|
import akka.actor.{Actor, ActorLogging, Props}
|
||||||
|
|
||||||
|
object IotSupervisor {
|
||||||
|
def props(): Props = Props(new IotSupervisor)
|
||||||
|
}
|
||||||
|
|
||||||
|
class IotSupervisor extends Actor with ActorLogging {
|
||||||
|
override def preStart(): Unit = log.info("IoT Application started")
|
||||||
|
override def postStop(): Unit = log.info("IoT Application stopped")
|
||||||
|
|
||||||
|
// No need to handle any messages
|
||||||
|
override def receive = Actor.emptyBehavior
|
||||||
|
|
||||||
|
}
|
||||||
|
//#iot-supervisor
|
||||||
Loading…
Add table
Add a link
Reference in a new issue