1. Overview

In this tutorial, we’ll explain how actors can be discovered or found using Akka Typed.

In Akka classic, we could easily just call the actorSelection method on the ActorSystem, which will try to give us a reference to an Actor. But with Akka Typed, as we’ll soon learn, we achieve this differently.

2. Introduction

When building systems using the Actor model with Akka, it’s necessary to be able to identify individual Actors in order to have them perform specific tasks. Sometimes, we can achieve this by passing a reference to specific Actors in messages.

In some cases where that’s not possible, we need to be able to locate or discover the specific Actor we care about.

As an example, let’s imagine we have to build a system that follows the Master-Worker architecture, where we have one master node or Actor, but multiple Worker nodes or Actors. If for some reason, we want to change how specific workers behave based on a factor, we may need to identify a single worker, either to have that worker do something different or shut it down.

How do we go about doing that using Akka Typed?

With the introduction of Akka Typed, the most important piece to understanding how Actors are found is the Receptionist.

A Receptionist is a service provided by the Actor System that’s used when an actor needs to be discovered by another actor but it’s not possible to put a reference to it in an incoming message.

For an Actor to be discovered, it has to be registered with the receptionist at the beginning of its lifecycle. 

To explain how we can use the receptionist to find Actors, we’ll design a simple application that uses the Master-Worker architecture. In this system, we’ll try to randomly find workers, and for simplicity, our worker will simply print something out to the console.

3. Actor Registration

Let’s see how we create an Actor and register it with the receptionist.

We start by creating our Worker Actor and sending a Register message to the receptionist:

object Worker {
   sealed trait WorkerMessage
   object WorkerMessage {
    // simple message for workers to identify themselves
    case object IdentifyYourself extends WorkerMessage
   }
    
    // key to uniquely identify Worker actors
    val key  : ServiceKey[WorkerMessage] = ServiceKey("Worker")
    import WorkerMessage._
    def apply(id : Int) : Behavior[WorkerMessage] = Behaviors.setup { context =>
    
    // register actor with receptionist using the key and passing itself
    context.system.receptionist ! Receptionist.Register(key, context.self)
    Behaviors.receiveMessage {
      case IdentifyYourself =>
        println(s"Hello, I am worker $id")
        Behaviors.same
    }
  }
}

Let’s go through what we’re doing here. First, we define the kind of messages that this Worker Actor can receive, defined by the trait WorkerMessage. We also define a Service Key and register it with the receptionist. The receptionist then uses this key to identify the Actor or group of Actors.

When our Worker receives an IdentifyYourself message, all it does is print its worker id to the console.

4. Actor Discovery

We’ve explained how an Actor can register itself with the receptionist. Now, let’s see how we can use the same receptionist to find Actors.

To do this, we’ll create our Master Actor, whose job is to spin up Worker Actors and also be able to find or discover these workers:

 object Master {
   sealed trait MasterMessage
    
  object MasterMessage {
    case class StartWorkers(numWorker: Int) extends MasterMessage
    case class IdentifyWorker(id: Int) extends MasterMessage
    case object Done extends MasterMessage
    case object Failed extends MasterMessage
  }

  import MasterMessage._

  def workerName(id: Int) = {
    s"Worker-$id"
  }

  def apply(): Behavior[MasterMessage] = Behaviors.setup { context =>

    Behaviors.receiveMessage {
      case StartWorkers(numWorker) =>

        // spin up new workers
        for (id <- 0 to numWorker) {
          context.spawn(Worker(id), workerName(id))
        }
        Behaviors.same


      case IdentifyWorker(id) =>
        implicit val timeout: Timeout = 1.second
        context.ask(
          context.system.receptionist,
          Find(Worker.key) // ask the receptionist for actors with the key defined by Worker.key
        ) {
          case Success(listing: Listing) =>
            val workerInstances = listing.serviceInstances(Worker.key)

            // find worker with the correct id
            val maybeWorker = workerInstances.find { worker =>
              worker.path.name contentEquals workerName(id)
            }
            maybeWorker match {
              case Some(worker) =>
                worker ! Worker.WorkerMessage.IdentifyYourself
              case None =>
                println("worker not found ): ")
            }
            MasterMessage.Done

          case Failure(_) =>
            MasterMessage.Failed
        }

        Behaviors.same
    }
  }
}

As with Actor Registration, we created our Master Actor, which has the job of either spawning Worker Actors or trying to find worker Actors by their id. When we’re asked to identify a worker, the first thing we do is ask the receptionist for all the Actors that were registered with the Worker Service Key. We get back a set of Worker Actors.

When we get back this set of Worker Actors, we can then find the Actor we want by checking the names of the Actors from the resulting set. If we don’t find the Actor, we can then do something else — in our case, we only print a message to the console.

If we find the Actor we care about, we send an IdentifyYourself message to that Actor.

This is what our driver program then looks like:

// create the ActorSystem
val master: ActorSystem[MasterMessage] = ActorSystem(
  Master(),
  "master"
)

// send the StartWorker message to the Master Actor
master ! MasterMessage.StartWorkers(10)

// prints "Hello, I am worker 5"
master ! MasterMessage.IdentifyWorker(5)

5. Conclusion

In this article, we’ve seen how Actors can be registered and found by the receptionist. When designing systems with Actors, this is a powerful tool to have under our belt, as we can take advantage of the receptionist to identify actors when we don’t have the ability to pass the actor as a message.

As usual, the source code can be found over on GitHub.