Introduction to Akka
Update 26.03.2023: In September 2022, Lightbend changed the license of Akka from Apache 2.0 to the source-available Business Source License (BSL) 1.1. Akka was forked as Pekko.
There are several models of concurrent computing, the actor model is one of them. I am going to give a glimpse of this model and one of its implementation - Akka toolkit.
The actor model
In the actor model, actors are objects that have state and behavior and communicate to each other by message passing. This sounds like good old objects from OOP, but the crucial difference is that message passing is one-way and asynchronous: an actor sends a message to another actor and continues its work. In fact, actors are totally reactive, all theirs activity is happening as reaction to incoming messages, which are processed one by one. However, it is not a limitation because messages can be of any sort including scheduled messages (by timer) and network messages.
Such design makes actors’ activity naturally and conveniently concurrent. Messages are processed in order, one by one and there should be no shared mutable state between actors so no synchronization is needed. A number of actors can be distributed across multiple processor cores and even across multiple computers.
You can find more information about the actor model in Wikipedia (including a bit of formal theory).
There are several actor model implementations (see the Wikipedia page), the most widely used one is in Erlang programming language in which many lightweight processes can be spawned and interact by messages. In this article, I want to tell about another implementation called Akka. Akka is an actor toolkit created by Typesafe Inc. and the community, free and open-source (Apache 2.0 license), written mostly in Scala (Java API is also available).
Akka basics
Let us create a very small Akka application. It is possible to do it using a template from Typesafe activator, but I prefer simple build.sbt
:
name := "akka-example"
version := "1.0"
scalaVersion := "2.11.5"
libraryDependencies ++= Seq(
"com.typesafe.akka" %% "akka-actor" % "2.3.9",
"com.typesafe.akka" %% "akka-testkit" % "2.3.9" % "test",
"org.scalatest" %% "scalatest" % "2.2.1" % "test"
)
Create directory hierarchy src/main/scala/
and put the code inside it.
SimpleActor.scala
, actor class:
import akka.actor.Actor
import akka.actor.Actor.Receive
class SimpleActor extends Actor {
override def receive: Receive = {
case x =>
println("Received message: " + x)
}
}
An actor is a class that has Actor
trait that requires implementing receive
method. The purpose of the method is to receive incoming messages one by one, usually using pattern matching. Each actor has a mail box (unbounded by default; can be made bounded, with priorities etc.) for incoming messages. The messages are extracted by an actor system and passed to receive
method. Messages that cannot be received go to special “Dead letter office” actor.
Akka gives at-most-once delivery (i.e. no delivery guarantee). This might make it seem useless, but there are explanations: 1) Akka is really targeting distributed computing and built-in delivery guarantees in distributed systems might be leaky abstraction; 2) there are design patterns that help achieve needed guarantees. These reasons are quite universal in distributed systems, so another similar products are basically the same in this case. Delivery guarantees are stronger in a local JVM (but it is not always recommended to rely on them because it may bring difficulties when you want to make already written system distributed). See documentation for details.
In Akka, actors are very lightweight for the CPU and have small memory footprint (about 300 bytes per actor) so a very big number of actors can be created. It’s OK to create a separate actor for quite a small task, which life cycle will consist of receiving/sending a couple of messages only.
Main.scala
, program entry point:
import akka.actor.{Props, ActorRef, ActorSystem}
object Main extends App {
val system = ActorSystem("the-actor-system")
val simpleActor: ActorRef = system.actorOf(Props[SimpleActor])
simpleActor ! "Hello world"
// Wait for message processing and shutdown
// (not needed in normal application)
java.lang.Thread.sleep(1000)
system.shutdown()
}
Here we can see ActorSystem
- the central part of any Akka application, normally there should be only one of them. An instance of SimpleActor
cannot be created directly (via a constructor), instead it is created by the actor system. Props
is a convenient abstraction for representing actor object creation properties.
ActorRef
is a reference to an actor. Actors are not accessible directly but through special reference objects that help make message passing logic more advanced and convenient, for instance, by letting actors be remote (yes, Akka supports remote actors out of the box). In other words, Akka actors are location transparent. There is another distributed ability (which will be introduced in the following release 2.4): the ability of Akka applications to form a fault-tolerant cluster.
Actors are addressed by path. Path can be local or remote, absolute or relative. For instance:
val localAbsolute = context.actorSelection(
"akka://the-actor-system/user/hello-actor")
val remoteAbsolute = context.actorSelection(
"akka.tcp://[email protected]:5678/user/hello-actor")
// Select all siblings (zero or more)
val localRelative = context.actorSelection("../*")
!
is an alias for ActorRef
method tell
, which is used to send a message to an actor behind a reference.
When we run this program with sbt run
we will get something like Received message: Hello world.
Actors form hierarchy with parenthood relationship shown on the picture
Every actor has one parent and zero or more children. All user actors are descendants of user
actor. Also, there are several system actors that play various service roles.
Supervision
Actor hierarchy introduces the idea of supervision in an actor system. According to it, every actor is responsible for errors that happen in its children actors. When unhandled exception is thrown inside a child actor, its parent-superviser can choose a strategy how to deal with it: resume the failed child, restart it, stop it permanently or propagate the failure (fail itself). Selected strategy can be applied to all the children instead of one (the failed).
This supervision scheme brings us to let-it-crash design principle and error kernel design pattern. The main idea of let-it-crash principle is not to try totally prevent program crashes (which is nearly impossible) but to be ready for crashes and have mechanisms to easily restore system’s ability to work in case of them (for instance, by restarting failed subsystem). Error kernel is a design pattern that implies to put potentially faulty code in actors lower in the hierarchy, away from valuable state which should be placed higher.
For example, we are creating a web crawling application. There is an actor that aggregates crawled pages’ content (in memory). The crawling logic is potentially faulty (anything can happen during downloading and parsing a web page) and we do not want to lose already crawled content in case of error. We place this logic in one or more child actors to prevent the parent from fail (also we will have concurrency in case of multiple children). If a child actor fails, the parent applies selected supervision strategy (e.g. restart the failed child).
Consider the example from the documentation:
import akka.actor.OneForOneStrategy
import akka.actor.SupervisorStrategy._
import scala.concurrent.duration._
override val supervisorStrategy =
OneForOneStrategy(maxNrOfRetries = 10, withinTimeRange = 1 minute) {
case _: ArithmeticException => Resume
case _: NullPointerException => Restart
case _: IllegalArgumentException => Stop
case _: Exception => Escalate
}
Actor work
Actors can (and should) work concurrently and possibly parallel. Messages are delivered to actors by dispatchers that are (besides other things) Scala ExecutionContext
s (think of them as thread pools). ExecutionContext
creates one or more threads to run its actors on. It is a bad idea to block a thread inside an actor because this might make other actors on this ExecutionContext
get stuck. Actors should be programmed in non-blocking way and Akka and Scala gives plenty of opportunities for this.
Nevertheless, sometimes it is necessary to block inside actors. For instance, we have a group of actors that interacts with database. In this case, we can create a special dispatcher with enough threads and place all this blocking actors doing the same job on it.
There is BalancingDispatcher
that enables its actors to do work sharing.
When to use Akka
Ignoring some possible exceptions, Akka is suitable in every application where Erlang is (network services, distributed applications, for instance). Also, it is good to bring concurrency to complex not network-centric applications. For simple programs, Akka could be an overkill: sometimes it is more easy to make program concurrent using ExecutionContext
+ Future
s / thread pools or other concurrency models.
In the documentation, there are some examples of use-cases of Akka and success stories. There is an interesting discussion when not to use actor model. Also, on , and talk “Akka in Production: Our Story” from Pacific Northwest Scala 2013.
Conclusion
I have described Akka very briefly. However, the topic is such broad that I would have to write a book to cover all the Akka features (such as IO, remoting, FSM, testing, upcoming Akka Cluster, Akka Streams, Akka Http) and patterns :) Nevertheless, it is highly likely that I will do some posts on the most interesting parts.
I can recommend this materials to get more familiar with Akka:
- Official documentation and FAQ.
- Principles of Reactive Programming course on Coursera taught by M. Odersky, E. Meijer, R. Kuhn.
- Akka Concurrency book by D. Wyatt.
- Akka in Action book by R. Roestenburg, R. Bakker, and R. Williams
Possibly, you already use Akka or another actor model implementations in production or just playing around with them. You are welcome to share your experience in comments.