Ir al contenido
Español

Receptionist

Esta página aún no está disponible en tu idioma.

The Receptionist is a cluster-wide service registry. Each node hosts one Receptionist actor at a well-known path; registrations are local-authoritative (you trust your own node’s registrations); peers learn about foreign registrations through gossip.

import {
Actor,
ReceptionistId,
ServiceKey,
Register,
Find,
ReceptionistSubscribe,
Listing,
} from 'actor-ts';
const receptionist = system.extension(ReceptionistId).start(cluster);
// 1. Define a typed key for this service
const apiKey = ServiceKey.of<ApiMessage>('api-service');
// 2. Register an actor under the key
const api = system.spawn(ApiActor, 'api');
receptionist.tell(new Register(apiKey, api));
// 3. A consumer actor receives the Listing that Find / Subscribe reply with
class Consumer extends Actor<Listing<ApiMessage>> {
override onReceive(listing: Listing<ApiMessage>): void {
console.log(`${listing.refs.length} api actors in the cluster`);
}
}
const consumer = system.spawn(Consumer, 'consumer');
// 4. From any node — find every registered actor, or subscribe to changes
receptionist.tell(new Find(apiKey, consumer));
receptionist.tell(new ReceptionistSubscribe(apiKey, consumer));

The receptionist is per-cluster — every node sees the same listing (with gossip lag, see below).

const key = ServiceKey.of<ApiMessage>('api-service');
// ^^^^^^^
// typed payload — consumers know what to tell

A ServiceKey<T> carries:

  • A string identifier — the human-readable key name.
  • A type parameter — the message type the registered actor accepts.

Type-safe lookup: the Listing<ApiMessage> sent in reply to a Find carries refs typed as ActorRef<ApiMessage>[].

Keys are values — define them once, import them everywhere. Convention: in a shared Keys.ts module per app.

receptionist.tell(new Register(key, actorRef));

Tells the local receptionist: “this actor on this node provides the service.” The receptionist:

  1. Adds the ref to its local map under key.
  2. Gossips the addition to peers (next gossip round).

Pass an optional replyTo as the third argument (new Register(key, actorRef, replyTo)) to be sent a Registered message once it’s recorded.

receptionist.tell(new Deregister(key, actorRef));

Voluntary removal. Useful when an actor “leaves” a service without stopping (transient state change).

The receptionist does not watch registered refs — if an actor stops, its registration lingers until you send Deregister (or, in a cluster, until its whole node leaves — see below). So a find can return a ref to a stopped actor; guard for that on the consumer side. (Subscribers are watched; see Subscriptions are bounded and watched.)

Find is a one-shot lookup — the result comes back as a single Listing message, delivered to the replyTo actor you name:

class ApiConsumer extends Actor<Listing<ApiMessage>> {
override onReceive(listing: Listing<ApiMessage>): void {
// listing.refs: every actor registered under the key, across the cluster
console.log(`${listing.refs.length} actors under ${listing.key.id}`);
}
}
const consumer = system.spawn(ApiConsumer, 'consumer');
receptionist.tell(new Find(key, consumer));

Handling a Find, the receptionist:

  1. Reads local registrations under key.
  2. Adds known remote registrations from gossip.
  3. Replies to replyTo with a single Listing of the combined list.

The reply carries the current view — no synchronous cluster query. Means: registrations on other nodes that haven’t gossiped yet are missing.

Within a gossip round or two (1-2 seconds default), every node converges on the same view.

Subscribe is continuous — replyTo receives a Listing now and again on every change. It’s exported from the package root as ReceptionistSubscribe (and Unsubscribe as ReceptionistUnsubscribe):

receptionist.tell(new ReceptionistSubscribe(key, consumer));
// Later — stop receiving updates:
receptionist.tell(new ReceptionistUnsubscribe(key, consumer));

A fresh Listing is sent to the subscriber whenever the set of refs for key changes — locally (register/deregister) or via incoming gossip and node departures.

Use for dynamic routing: an actor that subscribes to a key and updates its routing decisions when refs appear / disappear.

Two things keep the subscriber set from growing without end.

Death watch. The receptionist watches every subscriber. When one stops without sending Unsubscribe — a crash, a forgotten cleanup, an actor spawned per request — its slot is released as soon as the Terminated lands. You do not have to unsubscribe in postStop, though it is still the faster path.

Caps. Two limits bound what is left, the live subscribers:

OptionHOCON leafDefault
maxSubscribersPerKeycluster.receptionist.max-subscribers-per-key1000
maxSubscribersTotalcluster.receptionist.max-subscribers-total10000
const receptionistOptions = ReceptionistOptions.create()
.withMaxSubscribersPerKey(200)
.withMaxSubscribersTotal(2_000);
const receptionist = system.extension(ReceptionistId).start(cluster, receptionistOptions);

A Subscribe over either cap is refused, not dropped: the replyTo receives a ReceptionistSubscribeRejected instead of the first Listing, and no further listings follow.

import { ReceptionistSubscribeRejected } from 'actor-ts';
class Consumer extends Actor<Listing<ApiMessage> | ReceptionistSubscribeRejected<ApiMessage>> {
override onReceive(message: Listing<ApiMessage> | ReceptionistSubscribeRejected<ApiMessage>): void {
if (message instanceof ReceptionistSubscribeRejected) {
// message.reason: 'maxSubscribersPerKey' | 'maxSubscribersTotal'
// message.limit: the value that cap is set to
this.log.warn(`discovery refused: ${message.reason} (${message.limit})`);
return;
}
// …use message.refs
}
}

Answering matters more than it looks: a silently discarded Subscribe is indistinguishable from a key that simply has no registrations yet, and the two are a long afternoon apart.

The class is exported from the package root as ReceptionistSubscribeRejectedSubscribeRejected unqualified is DistributedPubSub’s, exactly as Subscribe is aliased to ReceptionistSubscribe.

// In a cluster:
const receptionist = system.extension(ReceptionistId).start(cluster);
// Without a cluster (single node):
const receptionist = system.extension(ReceptionistId).start(null);

Without a cluster, the receptionist works locally only — useful for tests or single-node apps that still want the ServiceKey-based lookup API.

For clustered setups, always pass the cluster — otherwise remote registrations are invisible.

When MemberRemoved fires for a peer:

  • The receptionist forgets every registration that node contributed.
  • Subscribers fire with the updated (smaller) listing.

This handles the “node crashed, didn’t get a chance to deregister” case — gossip + cluster membership do the cleanup.

receptionist.tell(new Register(apiKey, instance1));
receptionist.tell(new Register(apiKey, instance2));
receptionist.tell(new Register(apiKey, instance3)); // all three under the same key
receptionist.tell(new Find(apiKey, consumer)); // Listing.refs → [instance1, instance2, instance3]

A common pattern: N workers all registering under the same key. Consumers see the whole pool and can route however they like — round-robin, broadcast, random pick.

// One singleton registered for the cluster
receptionist.tell(new Register(coordinatorKey, theCoordinator));
receptionist.tell(new Find(coordinatorKey, consumer)); // Listing.refs → [theCoordinator]

For singleton-style services, the key has one ref. Consumers pick refs[0] (with a fallback for the empty case).

The receptionist doesn’t enforce single-instance — that’s the singleton manager’s job. But registering a singleton under a key gives consumers a discovery path that survives leadership changes.

NeedTool
Fixed routees per node, every node has themClusterRouter with a well-known path
Dynamic registrations, lookup by service nameReceptionist
Exactly one actor cluster-wideClusterSingleton (registered in receptionist for lookup if desired)
Per-key actors with auto-spawnClusterSharding

The receptionist is the most flexible discovery — but pay gossip cost for the dynamism. For static routing, the cluster router is cheaper.

The Receptionist API reference covers the full surface.