1. Introduction

In this tutorial, we’ll look at how to discover Actors in Akka. We’ll start by registering two actors and then retrieving them using ServiceKeys instead of using direct actor references.

2. Dependencies

Before we start, we’ll add Akka to the dependencies in the build.sbt file:

libraryDependencies += "com.typesafe.akka" %% "akka-actor-typed" % "2.6.15"
libraryDependencies += "org.slf4j" % "slf4j-simple" % "1.7.21"

3. Why Use Actor Discoverability?

Normally, Akka actors interact with other actors using references obtained in one of five ways:

  • Creating an actor inside another actor
  • Passing an actor reference into an actor constructor
  • Retrieving an actor reference from the actorSelection function using the actor name
  • Sending an actor reference in a message
  • Sending a message as a reply to another message using the sender reference

Actor discoverability gives us a centralized solution for retrieving any actor existing in the actor system, as long as the service key and its corresponding actor have been registered.

4. How Does Actor Discoverability Work?

In Akka, we have a built-in actor called a Receptionist, which receives registration requests from actors and sends actor references to actors that requested an actor reference. It works as a lookup table mapping from a ServiceKey to an actor reference.

The Receptionist is an actor itself, so we communicate with it using messages. The Receptionist‘s response is also a message. Therefore, the requesting actor must be prepared to receive the actor listing (response from the Receptionist).

5. Example

In our example, we’ll implement an actor system to perform math calculations such as addition and multiplication. We’ll implement both operations as separate actors that receive a message of type Operations.Calculate and log the result of the calculation.

5.1. Defining Actors

Let’s define our Addition and Multiplication actors:

object Addition {
  def apply(): Behavior[Operations.Calculate] = Behaviors.setup { context =>
    Behaviors.receiveMessage[Operations.Calculate] {
      message =>
        context.log.info(s"${message.a} + ${message.b} = ${message.a + message.b}")
        Behaviors.same
    }
  }
}

object Multiplication {
  def apply(): Behavior[Operations.Calculate] = Behaviors.setup { context =>
    Behaviors.receiveMessage[Operations.Calculate] {
      message =>
        context.log.info(s"${message.a} * ${message.b} = ${message.a * message.b}")
        Behaviors.same
    }
  }
}

5.2. Registering Actors

Now, we’ll create an instance of those actors. We’ll use an additional actor that defines the service keys, spawns actor instances, and registers them in the Receptionist actor. Note that we could move the registration code to the Addition and Multiplication actors because an actor can register itself.

Let’s define the Operations object to create the actors:

object Operations {
  final case class Setup()
  final case class Calculate(a: Int, b: Int)

  val AdditionKey = ServiceKey[Operations.Calculate]("addition")
  val MultiplicationKey = ServiceKey[Operations.Calculate]("multiplication")

  def apply(): Behavior[Setup] = Behaviors.setup{ context =>
    Behaviors.receiveMessage[Setup]{ _ =>
      context.log.info("Registering operations...")

      val addition = context.spawnAnonymous(Addition())
      context.system.receptionist ! Receptionist.Register(Operations.AdditionKey, addition)
      context.log.info("Registered addition")

      val multiplication = context.spawnAnonymous(Multiplication())
      context.system.receptionist ! Receptionist.Register(Operations.MultiplicationKey, multiplication)
      context.log.info("Registered multiplication")

      Behaviors.same
    }
  }
}

In the above code, we first defined two case classes which we used as messages to be passed between the actors. Then, we define the actor service keys. Later, we’ll use the service keys to retrieve actor references from the Receptionist.

In the actor body, we’ll wait until we get the Setup request, then we’ll spawn the actor instances and register them in the Receptionist.

5.3. Retrieving and Using Actors

Now, we’re ready to implement the Calculator actor, which will use the actors we defined earlier:

object Calculator {
  sealed trait Command
  final case class Calculate(operation: String, a: Int, b: Int) extends Command
  final case class CalculateUsingOperation(operation: ActorRef[Operations.Calculate], a: Int, b: Int) extends Command

  ...

}

In the first step, we have to create a hierarchy of messages retrieved by the actor. The actor receives the Calculate message with the operation name, requests the actor reference from the Receptionist, and receives it as a message sent to itself. Hence, the second message contains the actor reference instead of the operation name.

To simplify the code, we’ll implement a helper function mapping operation names into ServiceKeys:

private def getKey = (operationName: String) => {
  operationName match {
    case "addition" => Operations.AdditionKey
    case "multiplication" => Operations.MultiplicationKey
  }
}

Now, we’ll implement the logic to send the Setup request to the Operations actor. This will spawn the Addition and Multiplication and register them in the Receptionist, handle the Calculate request by requesting an actor reference, and use the actor reference to request the calculation.

Let’s add this implementation to the apply function:

def apply(): Behavior[Command] = Behaviors.setup{ context =>
  context.spawn(Operations(), "operations") ! Operations.Setup()

  implicit val timeout: Timeout = Timeout.apply(100, TimeUnit.MILLISECONDS)

  Behaviors.receiveMessagePartial[Command] {
    case Calculate(operation, a, b) => {
      context.log.info(s"Looking for implementation of ${operation}")
      val operationKey = getKey(operation)
      context.ask(
        context.system.receptionist,
        Receptionist.Find(operationKey)
      ) {
           case Success(listing) => {
             val instances = listing.serviceInstances(operationKey)
             val firstImplementation = instances.iterator.next()
             CalculateUsingOperation(firstImplementation, a, b)
           }
        }

       Behaviors.same
    }

    case CalculateUsingOperation(operation, a, b) => {
      context.log.info("Calculating...")
      operation ! Operations.Calculate(a, b)
      Behaviors.same
    }
  }
}

5.4. Starting the Actor System

Now, we can start the actor system and request two calculations:

object MathActorDiscovery extends App {
  val system: ActorSystem[Calculator.Calculate] = ActorSystem(Calculator(), "calculator")

  system ! Calculator.Calculate("addition", 3, 5)
  system ! Calculator.Calculate("multiplication", 3, 5)

  system.terminate()
}

6. Subscribing to Actor Registrations

In addition to requesting actor references from the Receptionist, we can also get a message every time an actor registers with a specified service key. To use this feature, we have to send the Receptionist.Subscribe message to the Receptionist.

Let’s implement a RegistrationListener actor which subscribes to both ServiceKeys and logs the actor paths:

object RegistrationListener {
  def apply(): Behavior[Receptionist.Listing] = Behaviors.setup {
    context =>
      context.system.receptionist ! Receptionist.Subscribe(Operations.AdditionKey, context.self)
      context.system.receptionist ! Receptionist.Subscribe(Operations.MultiplicationKey, context.self)

      Behaviors.receiveMessage[Receptionist.Listing] {
        listing =>
        val key = listing.key
        listing.getServiceInstances(key).forEach{ reference =>
        context.log.info(s"Registered: ${reference.path}")
      }
      Behaviors.same
    }
  }
}

Remember to spawn the actor in the Calculator setup:

context.spawnAnonymous(RegistrationListener())

We should see two additional log messages:

[calculator-akka.actor.default-dispatcher-7] INFO com.baeldung.akka.RegistrationListener$ - Registered: akka://calculator/user/operations/$a
[calculator-akka.actor.default-dispatcher-7] INFO com.baeldung.akka.RegistrationListener$ - Registered: akka://calculator/user/operations/$b

7. Deregistering Actors

When we no longer need an actor, we should deregister it before removing the actor instance from the actor system:

context.system.receptionist ! Receptionist.Deregister(Operations.AdditionKey, context.self)

Note that the Receptionist does not deregister the actor immediately, so it may still return the actor reference of the deregistered actor for some time.

8. Conclusion

In this article, we learned how to use the Actor Discovery feature in Akka, register actor references, subscribe to the registrations, retrieve the actors by the ServiceKey, and deregister an actor.

As usual, the source code for our application is available over on GitHub.

Comments are closed on this article!