Receptionist
このコンテンツはまだ日本語訳がありません。
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 serviceconst apiKey = ServiceKey.of<ApiMessage>('api-service');
// 2. Register an actor under the keyconst api = system.spawn(ApiActor, 'api');receptionist.tell(new Register(apiKey, api));
// 3. A consumer actor receives the Listing that Find / Subscribe reply withclass 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 changesreceptionist.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).
Service keys
Section titled “Service keys”const key = ServiceKey.of<ApiMessage>('api-service');// ^^^^^^^// typed payload — consumers know what to tellA 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.
Registering
Section titled “Registering”receptionist.tell(new Register(key, actorRef));Tells the local receptionist: “this actor on this node provides the service.” The receptionist:
- Adds the ref to its local map under
key. - 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.)
Finding
Section titled “Finding”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:
- Reads local registrations under
key. - Adds known remote registrations from gossip.
- Replies to
replyTowith a singleListingof 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.
Subscribing
Section titled “Subscribing”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.
Subscriptions are bounded and watched
Section titled “Subscriptions are bounded and watched”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:
| Option | HOCON leaf | Default |
|---|---|---|
maxSubscribersPerKey | cluster.receptionist.max-subscribers-per-key | 1000 |
maxSubscribersTotal | cluster.receptionist.max-subscribers-total | 10000 |
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
ReceptionistSubscribeRejected — SubscribeRejected unqualified is
DistributedPubSub’s, exactly as Subscribe is aliased to
ReceptionistSubscribe.
Cluster-aware vs single-node
Section titled “Cluster-aware vs single-node”// 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.
Auto-cleanup on node leave
Section titled “Auto-cleanup on node leave”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.
Multiple actors per key
Section titled “Multiple actors per key”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.
Single actor per key (convention)
Section titled “Single actor per key (convention)”// One singleton registered for the clusterreceptionist.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.
When to use it vs alternatives
Section titled “When to use it vs alternatives”| Need | Tool |
|---|---|
| Fixed routees per node, every node has them | ClusterRouter with a well-known path |
| Dynamic registrations, lookup by service name | Receptionist |
| Exactly one actor cluster-wide | ClusterSingleton (registered in receptionist for lookup if desired) |
| Per-key actors with auto-spawn | ClusterSharding |
The receptionist is the most flexible discovery — but pay gossip cost for the dynamism. For static routing, the cluster router is cheaper.
Where to next
Section titled “Where to next”- Discovery overview — the bigger picture.
- Cluster overview — the membership underneath.
- Cluster router — static-path alternative for “well-known service” routing.
- Singleton overview — often registered in the receptionist for discovery.
The Receptionist API
reference covers the full surface.
