Akka Actors: How to delegate the work to the children

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 new Child 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 and washTheDishes 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