Actor discovery

Dependency

To use Akka Actor Typed, you must add the following dependency in your project:

sbt
libraryDependencies += "com.typesafe.akka" %% "akka-actor-typed" % "2.5.32"
Maven
<dependencies>
  <dependency>
    <groupId>com.typesafe.akka</groupId>
    <artifactId>akka-actor-typed_2.12</artifactId>
    <version>2.5.32</version>
  </dependency>
</dependencies>
Gradle
dependencies {
  implementation "com.typesafe.akka:akka-actor-typed_2.12:2.5.32"
}

Introduction

With untyped actors you would use ActorSelection to “lookup” actors. Given an actor path with address information you can get hold of an ActorRef to any actor. ActorSelection does not exist in Akka Typed, so how do you get the actor references? You can send refs in messages but you need something to bootstrap the interaction.

Receptionist

For this purpose there is an actor called the Receptionist. You register the specific actors that should be discoverable from other nodes in the local Receptionist instance. The API of the receptionist is also based on actor messages. This registry of actor references is then automatically distributed to all other nodes in the cluster. You can lookup such actors with the key that was used when they were registered. The reply to such a Find request is a Listing, which contains a Set of actor references that are registered for the key. Note that several actors can be registered to the same key.

The registry is dynamic. New actors can be registered during the lifecycle of the system. Entries are removed when registered actors are stopped or a node is removed from the cluster. To facilitate this dynamic aspect you can also subscribe to changes with the Receptionist.Subscribe message. It will send Listing messages to the subscriber when entries for a key are changed.

The primary scenario for using the receptionist is when an actor needs to be discovered by another actor but you are unable to put a reference to it in an incoming message.

These imports are used in the following example:

Scala
sourceimport akka.actor.typed.ActorRef
import akka.actor.typed.Behavior
import akka.actor.typed.receptionist.Receptionist
import akka.actor.typed.receptionist.Receptionist.Listing
import akka.actor.typed.receptionist.ServiceKey
import akka.actor.typed.scaladsl.Behaviors
Java
sourceimport akka.actor.typed.ActorRef;
import akka.actor.typed.Behavior;
import akka.actor.typed.javadsl.ActorContext;
import akka.actor.typed.javadsl.Behaviors;
import akka.actor.typed.receptionist.Receptionist;
import akka.actor.typed.receptionist.ServiceKey;

First we create a pingServicePingService actor and register it with the Receptionist against a ServiceKey that will later be used to lookup the reference:

Scala
sourceval PingServiceKey = ServiceKey[Ping]("pingService")

final case class Ping(replyTo: ActorRef[Pong.type])
final case object Pong

val pingService: Behavior[Ping] =
  Behaviors.setup { ctx =>
    ctx.system.receptionist ! Receptionist.Register(PingServiceKey, ctx.self)
    Behaviors.receive { (context, msg) =>
      msg match {
        case Ping(replyTo) =>
          context.log.info("Pinged by {}", replyTo)
          replyTo ! Pong
          Behaviors.same
      }
    }
  }
Java
sourcepublic static class PingService {

  static final ServiceKey<Ping> pingServiceKey = ServiceKey.create(Ping.class, "pingService");

  public static class Pong {}

  public static class Ping {
    private final ActorRef<Pong> replyTo;

    public Ping(ActorRef<Pong> replyTo) {
      this.replyTo = replyTo;
    }
  }

  static Behavior<Ping> createBehavior() {
    return Behaviors.setup(
        context -> {
          context
              .getSystem()
              .receptionist()
              .tell(Receptionist.register(pingServiceKey, context.getSelf()));

          return Behaviors.receive(Ping.class).onMessage(Ping.class, PingService::onPing).build();
        });
  }

  private static Behavior<Ping> onPing(ActorContext<Ping> context, Ping msg) {
    context.getLog().info("Pinged by {}", msg.replyTo);
    msg.replyTo.tell(new Pong());
    return Behaviors.same();
  }
}

Then we have another actor that requires a pingServicePingService to be constructed:

Scala
sourcedef pinger(pingService: ActorRef[Ping]): Behavior[Pong.type] =
  Behaviors.setup[Pong.type] { ctx =>
    pingService ! Ping(ctx.self)
    Behaviors.receive { (context, _) =>
      context.log.info("{} was ponged!!", context.self)
      Behaviors.stopped
    }
  }
Java
sourcepublic static class Pinger {
  static Behavior<PingService.Pong> createBehavior(ActorRef<PingService.Ping> pingService) {
    return Behaviors.setup(
        (ctx) -> {
          pingService.tell(new PingService.Ping(ctx.getSelf()));
          return Behaviors.receive(PingService.Pong.class)
              .onMessage(PingService.Pong.class, Pinger::onPong)
              .build();
        });
  }

  private static Behavior<PingService.Pong> onPong(
      ActorContext<PingService.Pong> context, PingService.Pong msg) {
    context.getLog().info("{} was ponged!!", context.getSelf());
    return Behaviors.stopped();
  }
}

Finally in the guardian actor we spawn the service as well as subscribing to any actors registering against the ServiceKey. Subscribing means that the guardian actor will be informed of any new registrations via a Listing message:

Scala
sourceval guardian: Behavior[Nothing] =
  Behaviors
    .setup[Listing] { context =>
      context.spawnAnonymous(pingService)
      context.system.receptionist ! Receptionist.Subscribe(PingServiceKey, context.self)
      Behaviors.receiveMessagePartial[Listing] {
        case PingServiceKey.Listing(listings) =>
          listings.foreach(ps => context.spawnAnonymous(pinger(ps)))
          Behaviors.same
      }
    }
    .narrow
Java
sourcepublic static Behavior<Void> createGuardianBehavior() {
  return Behaviors.setup(
          context -> {
            context
                .getSystem()
                .receptionist()
                .tell(
                    Receptionist.subscribe(
                        PingService.pingServiceKey, context.getSelf().narrow()));
            context.spawnAnonymous(PingService.createBehavior());
            return Behaviors.receive(Object.class)
                .onMessage(
                    Receptionist.Listing.class,
                    (c, msg) -> {
                      msg.getServiceInstances(PingService.pingServiceKey)
                          .forEach(
                              pingService ->
                                  context.spawnAnonymous(Pinger.createBehavior(pingService)));
                      return Behaviors.same();
                    })
                .build();
          })
      .narrow();
}

Each time a new (which is just a single time in this example) pingServicePingService is registered the guardian actor spawns a pingerPinger for each currently known PingService. The pingerPinger sends a Ping message and when receiving the Pong reply it stops.

Cluster Receptionist

The Receptionist also works in a cluster, an actor registered to the receptionist will appear in the receptionist of the other nodes of the cluster.

The state for the receptionist is propagated via distributed data which means that each node will eventually reach the same set of actors per ServiceKey.

One important difference from a local only receptions is the serialisation concerns, all messages sent to and back from an actor on another node must be serializable, see clustering.

Found an error in this documentation? The source code for this page can be found here. Please feel free to edit and contribute a pull request.