ARTICLE

Exploring Akka’s Actors

From Akka in Action, Second Edition by Francisco Lopez-Sancho Abraham

This article discusses messaging in Akka and its new typed actors.

Take 35% off Akka in Action, Second Edition by entering fccabraham2 into the discount code box at checkout at manning.com.

Let’s start from the beginning. Long ago, the Fathers of the Actor Model gave three abilities to Akka’s Actors:

  1. Send a finite number of messages to other Actors
  2. Create a finite number of new Actors
  3. Designate the behavior to be used for the next message it receives

These are the three things that an actor can do upon receiving a message. There is not an assumed order in these actions, and they could be carried concurrently, which means interleaved with no prescribed order. You may also think of it as combined without repetition. When receiving a message, you may reason as if nothing else happens in the whole system. You would do some processing, maybe some storage, some communication, and in the end, you’d designate the next behavior. That’s it. An actor does one thing at a time, one message at a time.

Why finite you may ask? The actor can only process one message at a time. If it never stops sending messages or creating actors, when will it get the chance to process the next message?

Yep. Never again.

It’s also important to bear in mind that an Actor, as the fundamental unit of computation, has to embody three things. It has to be able to process, store, and communicate.

Sending messages

A fundamental property of the Actor model is that the only way to communicate with an actor is through message passing. Sending messages is the only way to provide or receive information from one actor to another.

Two messages sent concurrently can arrive in either order. The Actor model takes into account the environment as part of its essential properties and this makes this model non-deterministic.

Hello World

Without further ado, let’s have a look at a minimal Akka application of the project up.and.running — the famous Hello World. The only requirements we need in order to build these scala applications are a couple of libraries: ‘akka-actor-typed’ and ‘ch.qos.logback’.

The definition of the project is written in a .sbt file. Here we have it, in Listing 1—as simple as it gets.

Listing 1 build.sbt

lazy val `up-and-running` = project             #A
.in(file("up-and-running")) #B
.settings(
scalaVersion := "2.13.1",
libraryDependencies ++= Seq(
"com.typesafe.akka" %% "akka-actor-typed" % "2.6.9",
"ch.qos.logback" % "logback-classic" % "1.2.3"
)
)

… #C

#A back tilted to allow special characters in the name

#B folder where the project lives

#C sbt file continues

We typically assign the project to a val, in case we want to refer to it in any other part of the sbt file. The scala version and the library dependencies speak for themselves.

To make our first piece of code to run, you’ll need to have initiated the sbt server. We achieve that by executing sbt command in the root folder of the project. Now, we should enter project up-and-running to pick this project from all of the choices. Then, entering run will show all the apps you can run. Enter the number 7, which corresponds to the HelloWorldApp, and hit ‘Enter’. This piece of code is creating an actor and sending two messages to it.

In Listing 2 we see the output.

Listing 2 Output from HelloWorldApp

[HelloWorld$] - received message 'Hello'
[HelloWorld$] - received message 'Hello Again'

HelloWorld$ refers to the actor’s name and the rest is what we’re logging when we receive a message.

Let’s see this actor and the Main from which it gets instantiated and used. Actually, we are not strictly using Main here. Instead, we’ll extend from scala.App that is a bit simpler and will suffice, as long as we don’t need passing params to the application. Listing 3 shows us the App and the Actor.

Listing 3 HelloWorld Actor and HelloWorld App

import akka.actor.typed.scaladsl.Behaviors
import akka.actor.typed.{ActorSystem, Behavior}


object HelloWorld {

def apply(): Behaviors.Receive[String] = #A
Behaviors.receive { (context, message) =>
context.log.info(s"received message '$message'")
Behaviors.same
}

}

object HelloWorldApp extends App {

val guardian: ActorSystem[String] = ActorSystem(HelloWorld(), "HelloWorld") #B
guardian ! "Hello" #C
guardian ! "Hello Again" #C

}

#A behavior that defines how HelloWorld actor reacts to any message

#B Actor instantiation

#C messages of type String sent to the actor

Our Actor is the HelloWorld object, and ActorSystem is the constructor that uses this actor object to instantiate a value of its type. We usually call this actor the guardian to emphasize its role in the system. It’s the bearer of the utmost responsibility in your system, our highest supervisor, hence the name.

“Hello” (or “Hello Again”) are the messages we’re sending to the actor, of type String. And finally, ”!” means to ‘tell’ or to send. You could read the last two lines as ‘sending “Hello” to the guardian’ and ‘sending “Hello Again” to the guardian’. You might notice that the order of the terms that appear in the code don’t perfectly align with how you would say it. Otherwise, you would read “to the guardian send Hello.”

This is a very basic example, but there’s quite a bit of information already in the code. Let’s start from where the runtime starts in Listing 4.

Listing 4 First Akka line

val guardian: ActorSystem[String] = ActorSystem(HelloWorld(), "helloWorldMain")

Here’s our first actor — and it turns be a very special one. Apart from being an actor you can send messages to, it has the responsibility of making the whole system work in an orderly fashion. It will start the Dispatcher and the Scheduler. This is the greatest supervisor, and because of that, its logic should be restricted to the bare minimum. Anything else can fail and fall back in its supervisor, except for the ActorSystem.

The messages this guardian can work with are of the type String, and it is — almost inadvertently — one of, if not the greatest, improvements from previous versions of Akka: The type. You’ll find sometimes that this type and its subtypes, as a set, are called protocol. From version 2.6 onwards, Akka enforces the types of messages that an actor can receive. This will give you the ability to test your code at compile time. If, for any reason, you choose the wrong type, you will be alerted by the compiler. Also, while refactoring, if you alter the messages that an actor can receive and some other actors still point to the old protocol, the compiler will jump to the rescue. The importance of this new feature can be hardly overstated.

We should mention that there’s another kind of message, apart from the defined Type of the Actor, that an actor can receive. These are called Signals, and they are special messages sent from the system to the actor. We’ll see examples of this when we talk specifically about failure and supervision in Chapters X and Y.

Going back to our first line, ActorSystem(HelloWorld(), "helloWorldMain") corresponds to the signature, as seen Listing 5

Listing 5 Signature of method that creates an ActorSystem

ActorSystem(guardianBehavior: Behavior[T], name: String): ActorSystem[T]

In our case, [T] is of type [String] and we are passing in guardianBehavior the constructor of the actor HelloWorld(), which actually, without the syntactic sugar, is HelloWorld.apply(). Any Object in Scala with the method apply can be referenced through parenthesis for short.

Apart from the guardianBehavior, we are also passing to the parameter name, the String helloWorldMain. This name will become part of the address of each actor built under that actor system. Sort of a namespace: akka:/helloWorldMain/user/HelloWorld

But we aren’t finished explaining this “hello world” yet. You may be wondering now, “What are those ‘Behaviors’”?

Designating the next behavior

As we mentioned before, behaviors are one of the three pillars of the Actor model. A behavior is a function used to define how the Actor will react to the next message. This message may be of the type declared in the actor’s constructor, i.e. apply(), or of the type Signal. We’ll focus on this chapter in the former.

In previous versions, you had to extend from an actor class, but from now on, an actor is defined by its behavior or behaviors. You may have heard, “It’s not who you are underneath, it’s what you do that defines you.”. Apart from being very cool, this approach is more in line with the Actor Model.

In Akka, we build these behaviors through factories of behaviors as we can see in Listing 6

Listing 6 Usage of Behavior.receive factory

def apply(): Behaviors.Receive[String] =
Behaviors.receive { (context, message) => #A
context.log.info(s"received message '$message'")
Behaviors.same
}

#A Factory of behavior of type receive

Behavior.receive can be read as a factory that creates a Behavior, which is defined by a function that expects one context and one message. Then, it uses them to do some logging and return the next Behavior. We can see the signature in Listing 7

Listing 7 Signature of Behavior.receive factory

def receive(ctx: TypedActorContext[T], msg: T): Behavior[T]

Let’s not forget that in our example, [T] is [String].

Now the last bit: Behaviors.same is, again, syntactic sugar to refer to the outmost behavior that contains it. In this example, HelloWorld.apply() is the root of Behavior.same, the former contains the latter, and this shows an essential part of behaviors. They are built as Matryoshkas, or nested trees, for that matter. Once we get into a behavior, we can traverse as many behaviors as they may contain until the last node, the leaf. This last one will be the behavior for the next message.

We didn’t say anything about the context yet. Each Actor lives in its bubble, its context the relation of a behavior, unique in the sense that we cannot take a behavior and use it in another context. Some express this as a behavior closes over its context, and this makes it immobile. You can’t send the behavior from one actor to another. You may think that, if a behavior is function, you may pass it from one actor to another after all.

The context is also the instance from where the actor can find the reference to the system it belongs to, their children, and even its own reference. It’s also where it creates more actors, or executes some other actions, such as logging.

Proper Types

Now, let’s go over how to define the message type that an actor understands, also called its protocol. We will include as well another behavior similar to Behaviors.same, which is a Behavior.stopped. Do you understand the code in this Listing 8?

Listing 8 HallowingChild actor of type Treat

import akka.actor.typed.scaladsl.Behaviors
import akka.actor.typed.{ ActorSystem, Behavior }

object HalloweenChild {

sealed trait Treat #A

case class Candy(name: String) extends Treat #A
case object NonCandy extends Treat #A

def apply(): Behavior[Treat] =
Behaviors.receive { (context, message) =>
message match { #B
case NonCandy =>
context.log.info("this is not fun. I'm going home")
Behaviors.stopped #C

case Candy(name: String) =>
context.log.info(s"this is so much fun. I love $name")
Behaviors.same #C
}
}
}

# A protocol

# B different reactions to each subtype

# C next behavior

Candy or NonCandy—those are the possible messages. Both fall under the supertype Treat, and all three conform the actor’s protocol. It’s necessary using a sealed trait because this tells to the compiler that all its subtypes are fixed, known in advance. This allows the compiler to check whether we are covering all the subtypes when pattern matching, when we match.

Let’s now see how this is used in Listing 9.

Listing 9 Input of HalloweenChild

object HalloweenChildApp extends App {

val guardian: ActorSystem[HalloweenChild.Treat] =
ActorSystem(HalloweenChild(), "halloween")
guardian ! HalloweenChild.Candy("chocolate bar")
guardian ! HalloweenChild.NonCandy
guardian ! HalloweenChild.Candy("chocolate bar")
}

As with the Actor HelloWorld in Listing 3, this creates an ActorSystem and sends some messages to it. That’s all — but with a small difference. What’s going to happen to the second Candy we’re sending to that actor? You may want to look back to Listing 8 to figure that yourself, or just continue to the next listing, 10, where you’ll see the effects.

Listing 10 Output of HalloweenChild

[HalloweenChild$] - this is so much fun. I love chocolat bar
[HalloweenChild$] - this is not fun. I'm going home

Now, the output is very straightforward, except for the fact that we sent three messages and received information about just two of them. It’s understandable, I guess, that after the actor has stopped, it will not process any more messages. But still, if I send a message to an actor that has stopped, I’d like to get some information about it. Namely, that the message couldn’t be delivered. But all this is my fault actually; all the goodies of an ActorSystem lay inside the actor system, and our actor is not inside. It’s the ActorSystem itself.

We should not do this. Interacting with an ActorSystem from outside is not reliable, failure tolerant, nor scalable. We should reduce this outside interaction at the bare minimum, and let all the business logic we need be executed inside the system itself. We’ll come back to this point when we talk about the Error Kernel. But for now, we will do this a couple more times to simplify the assimilation of new concepts.

State

Let’s talk about the state now. We mentioned before that storage is one of the fundamental properties of an Actor. Storing is the means of an actor to keep its state and can be implemented through behaviors, that is, functions, but also can be done with simple variables. Depending on which implementation you’d like to use, you can pick the Actors functional style or the object oriented one. In the following case we’ll use the functional one.

In this example, we are going to write a Counter that will store the number of messages it receives and has a cap on the max messages it can handle. See the input in Listing 11.

Listing 11 Application that creates a Counter

object CounterApp extends App {

val guardian: ActorSystem[Counter.Command] =
ActorSystem(Counter(0, 2), "counter")
guardian ! Counter.Increase
guardian ! Counter.Increase
guardian ! Counter.Increase

}

This Counter will produce an output as Listing 12, increasing the initial value until the cap is reached.

Listing 12 Output of our Counter

INFO state.Counter$ - increasing to 1
INFO state.Counter$ - increasing to 2
INFO state.Counter$ - I'm overloaded. Counting '3' while max is '2’

In this example, we are using the behavior Counter(0, 2), same as Counter.apply(0,2), to pass the state from one message to the next. The state we keep track of is the current count and the max count. Because behaviors are just functions, we can use their input to pass state to the next behavior, and therefore, keep it inside the actor just like a variable would do. There’s a difference though: This state is immutable. The original value is not updated, but instead, a new value is created. See Listing 13.

Listing 13 Counter actor with state

import akka.actor.typed.{ ActorSystem, Behavior }
import akka.actor.typed.scaladsl.Behaviors

object Counter {

sealed trait Command
final case object Increase extends Command

def apply(init: Int, max: Int): Behavior[Command] = #A
Behaviors.receive { (context, message) =>
message match {
case Increase =>
val current = init + 1
if (current <= max) {
context.log.info(s"increasing to $current")
apply(current, max) #B
} else {
context.log.info(
s"I'm overloaded. Counting '$current' while max is '$max")
Behaviors.stopped #B
}
}
}
}

#A input as a val that represents state

#B output as a behavior, not a call to a method

As you can see, in the case current <= max, we are designating Counter.apply(current, max) with different parameters than when we started the counter. So instead of Counter(0,2), we are declaring that, after the first message, the behavior that will process the next message will be Counter.apply(0 + 1, 2).

You may notice that the protocol root has been named Command. This is a common practice.

Alternative to functional style

There is also possible to use an object-oriented style, which is a bit noisier in its definition, but easier to manage state when this gets complex. When you have many fields, you may prefer not to pass them from Behavior to Behavior. Let’s see OO style in Listing 14.

Listing 14 Object Oriented Counter equivalent to the previous functional one

import akka.actor.typed.{ ActorSystem, Behavior }
import akka.actor.typed.scaladsl.{ AbstractBehavior, ActorContext, Behaviors }

object Counter {


sealed trait Command
final case object Increase extends Command

def apply(init: Int, max: Int): Behavior[Command] =
Behaviors.setup((context) => new Counter(init, max, context))

class Counter(init: Int, max: Int, context: ActorContext[Command])
extends AbstractBehavior[Command](context) { #A
var current = init #B

override def onMessage(message: Command): Behavior[Command] = #C
message match {
case Increase =>
current += 1
context.log.info(s"increasing to $current")
if (current > max) {
context.log.info(
s"I'm overloaded. Counting '$current' while max is '$max")
Behaviors.stopped
} else {
this
}
}
}
}

#A Object-oriented through subtypying polimorphism

#B state

#B equivalent to Behaviors.receiveMessage

There are few differences here. When calling, as before, Counter(init, max), now we are instantiating a class that extends from AbstractBehavior[T]. This Abstract class forces you to override the onMessage(message: T) method to define the reaction of the actor to such a message. Other essential differences are the fact that we store the state in a variable var current, and finally, that when referring to the same behavior, we are no longer using Counter.apply(init, max), but instead we are using this.

We introduced a new behavior we didn’t mention before, Behaviors.setup. This is a behavior that runs only once. It’s usually used to initiate the state of the actor in the functional style, while in the OO, it’s necessary to instantiate the Actor itself. Here, Behaviors.setup defines a function that has a context as input and when provided, gives back a behavior—in our case, a newCounter(init, max, context). This context, an ActorContext, was provided at runtime when val guardian: ActorSystem[Counter.Command] = ActorSystem(Counter(0, 2), "counter").

As you probably assumed, OO or FP style hide behind the same interface, so creating Actors and sending them messages is transparent for the user. Listing 10 could be calling any of the two implementations.

That’s all for this article. Thanks for reading.

--

--

--

Follow Manning Publications on Medium for free content and exclusive discounts.

Love podcasts or audiobooks? Learn on the go with our new app.

Recommended from Medium

Docker with Node.js Application

Migrating HPC Workloads to the Cloud

Monitor image versions of components in k8s clusters

ANU #85 — New Version Launch and Layout Improvements

Why I added licenses to my Udacity projects

The Secrets to Learning Java Game Development with Beginner Coding Skills

‘Everything is awesome when you work in a team!’ Lego Scrum at the BHF — by Mugda Gadre

2020 or beyond, the path to becoming a web developer remains the same.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
Manning Publications

Manning Publications

Follow Manning Publications on Medium for free content and exclusive discounts.

More from Medium

No Dependency Fluent Logging in Scala

Scala/Akka Actors, CQRS/ES, and IoT

Cracking Scala Class with Bytecode

Dive into clojure.java.io