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.