Akka Actors Problem: You know that an actor that “blocking” is bad for your Akka system, so you want to create actors that don’t block.
Solution
When using Akka actors, the mantra is always, “Delegate, delegate, delegate.” It’s important that high-level actors delegate work to lower-level actors, so the high-level actors can be free to receive new messages and respond to them immediately. This example shows how to implement non-blocking actors.
(Note: This tutorial is written for Akka 2.6.)
Import statements
These import statements are required for the code that follow:
import akka.actor.typed.{ActorRef, ActorSystem, Behavior, PostStop}
import akka.actor.typed.scaladsl.{ActorContext, Behaviors}
Example
To demonstrate delegation, in this example I create a functional programming (FP) style “parent” actor, whose only responsibilities are to (a) create two children, (b) receive messages from others, and (c) pass those messages on to their children, who do the actual work:
object Parent {
// messages that Parent can handle
sealed trait MessageToParent
final case object HelloParent extends MessageToParent
final case object TakeOutTheTrash extends MessageToParent
final case object WashTheDishes extends MessageToParent
// the factory method that lets other create new Parent instances
def apply(): Behavior[MessageToParent] = Behaviors.setup { context: ActorContext =>
// create two children
val child1: ActorRef[Child.MessageToChild] = context.spawn(
Child(), "Child_1"
)
val child2: ActorRef[Child.MessageToChild] = context.spawn(
Child(), "Child_2"
)
// pass all the long-running work to the children
Behaviors.receiveMessage { message: MessageToParent =>
message match {
case HelloParent =>
println("Parent: Hi. I work as little as possible!")
child1 ! Child.HelloChild
Behaviors.same
case TakeOutTheTrash =>
child1 ! Child.TakeOutTheTrash
println("Parent: LOL, the Child is taking out the trash.")
Behaviors.same
case WashTheDishes =>
child2 ! Child.WashTheDishes
println("Parent: LOL, the Child is washing the dishes.")
Behaviors.same
}
}
}
}
Next, I create an FP-style “child” actor. The child actor does the usual things:
-
Defines the messages it can receive
-
Defines an
apply
method that serves as a factory method to create newChild
instances -
Defines a match expression inside
Behaviors.receive
to handle the messages that are sent to it -
In this example I also add two slow-running methods in
takeOutTheTrash
andwashTheDishes
to help demonstrate the need for delegation
Here’s the Child
source code:
object Child {
// message that Child can handle
sealed trait MessageToChild
final case object HelloChild extends MessageToChild
final case object TakeOutTheTrash extends MessageToChild
final case object WashTheDishes extends MessageToChild
// the factory method that lets other create new Child instances.
// this line of code is long, so it is wrapped onto two lines here.
def apply(): Behavior[MessageToChild] = {
Behaviors.receive[MessageToChild] { (context, message) =>
message match {
case HelloChild =>
println("Child: I’m the child. I do all the work.")
Behaviors.same
case TakeOutTheTrash =>
println("Child: *sigh* I’m taking out the trash.")
takeOutTheTrash
Behaviors.same
case WashTheDishes =>
println("Child: Yeah, yeah, I’m washing the dishes.")
washTheDishes
Behaviors.same
}
}}
// simulate some long-running-tasks
private def takeOutTheTrash(): Unit = Thread.sleep(100)
private def washTheDishes(): Unit = Thread.sleep(200)
}
In this example, takeOutTheTrash
and washTheDishes
are two simulated slow-running functions, but in the real world, functions like these may access microservices, web services, databases, and perform other long-running algorithms.
As with the previous recipes, all we need now is an App
to test the Parent
and Child
actors:
object ChildrenDoTheWork extends App {
val actorSystem: ActorSystem[Parent.MessageToParent] = ActorSystem(
Parent(),
"ParentChildSystem"
)
actorSystem ! Parent.HelloParent
actorSystem ! Parent.TakeOutTheTrash
actorSystem ! Parent.WashTheDishes
Thread.sleep(500)
// shut down the system
actorSystem.terminate()
}
There’s nothing too new in this App
. It starts the ActorSystem
, sends three messages to it, and sleeps for a few moments. The only thing I added to the App
for this recipe is that I show how to shut down the ActorSystem
with its terminate
method.
Here’s the output of this App
:
Parent: Hi. I work as little as possible! Child: I’m the child. I do all the work. Parent: LOL, the Child is taking out the trash. Child: *sigh* I’m taking out the trash. Parent: LOL, the Child is washing the dishes. Child: Yeah, yeah, I’m washing the dishes.
Discussion
In an application, actors form hierarchies, like a family, or a business organization:
-
When Akka was first created, the Typesafe team (now Lightbend) recommended that you think of an actor as being like a person, such as a person in a business organization.
-
An actor has one parent (supervisor): the actor that created it.
-
An actor may have children. If you think of this as a business, a president may have a number of vice presidents (VPs). Those VPs will have many subordinates, and so on.
-
An actor may have siblings. For instance, there may be 10 VPs in an organization, at the same level under its president.
-
A best practice of developing actor systems is to “delegate, delegate, delegate,” especially if behavior will block. In a business, the president may want something done, so he delegates that work to a VP. That VP delegates work to a manager, and so on, until the work is eventually performed by one or more subordinates.
-
Delegation is important. Imagine that some work takes several man-years, such as starting a new electric car business. If the president had to handle that work himself, he couldn’t respond to other needs, while all of the other VPs might be idle.
Although this example is simple, it showed the basics of delegation:
-
Create a parent actor that responds to incoming messages, and delegates the actual work to child actors. This keeps the parent free to respond to new messages.
-
Create as many child actors as necessary to handle the workload.
Although I didn’t show it in this example, if the child needs to communicate back to the parent, pass the parent ActorRef
to the child’s factory method. You’ll just need to add a few more messages to the system, and then the child can send messages back to the parent:
// in the Child match expression
parent ! ChildFinishedTheDishes
parent ! ChildFinishedTakingOutTheTrash
this post is sponsored by my books: | |||
#1 New Release |
FP Best Seller |
Learn Scala 3 |
Learn FP Fast |