Receptionist
Der Receptionist ist ein cluster-weites Service-Registry. Jeder Node hostet einen Receptionist-Actor unter einem wohldefinierten Pfad; Registrierungen sind lokal autoritativ (du vertraust den Registrierungen deines eigenen Nodes); Peers erfahren über Gossip von fremden Registrierungen.
import { Actor, ReceptionistId, ServiceKey, Register, Find, ReceptionistSubscribe, Listing,} from 'actor-ts';
const receptionist = system.extension(ReceptionistId).start(cluster);
// 1. Einen getypten Key für diesen Service definierenconst apiKey = ServiceKey.of<ApiMessage>('api-service');
// 2. Einen Actor unter dem Key registrierenconst api = system.spawn(ApiActor, 'api');receptionist.tell(new Register(apiKey, api));
// 3. Ein Consumer-Actor empfängt das Listing, mit dem Find / Subscribe antwortenclass Consumer extends Actor<Listing<ApiMessage>> { override onReceive(listing: Listing<ApiMessage>): void { console.log(`${listing.refs.length} API-Actors im Cluster`); }}const consumer = system.spawn(Consumer, 'consumer');
// 4. Von jedem Node — jeden registrierten Actor finden oder auf Änderungen subscribenreceptionist.tell(new Find(apiKey, consumer));receptionist.tell(new ReceptionistSubscribe(apiKey, consumer));Der Receptionist ist pro Cluster — jeder Node sieht dasselbe Listing (mit Gossip-Lag, siehe unten).
Service Keys
Abschnitt betitelt „Service Keys“const key = ServiceKey.of<ApiMessage>('api-service');// ^^^^^^^// getypter Payload — Consumer wissen, was sie senden müssenEin ServiceKey<T> trägt:
- Einen String-Identifier — der menschenlesbare Key-Name.
- Einen Typ-Parameter — der Nachrichtentyp, den der registrierte Actor akzeptiert.
Typsicheres Lookup: das Listing<ApiMessage>, das als Antwort auf
ein Find gesendet wird, trägt refs getypt als
ActorRef<ApiMessage>[].
Keys sind Werte — einmal definieren, überall importieren.
Konvention: in einem geteilten Keys.ts-Modul pro App.
Registrieren
Abschnitt betitelt „Registrieren“receptionist.tell(new Register(key, actorRef));Sagt dem lokalen Receptionist: “Dieser Actor auf diesem Node liefert den Service.” Der Receptionist:
- Fügt die Ref seiner lokalen Map unter
keyhinzu. - Gossipt das Hinzufügen an Peers (nächste Gossip-Runde).
Übergib ein optionales replyTo als drittes Argument
(new Register(key, actorRef, replyTo)), um eine
Registered-Nachricht zu erhalten, sobald die Registrierung
aufgezeichnet ist.
receptionist.tell(new Deregister(key, actorRef));Freiwilliges Entfernen. Nützlich, wenn ein Actor einen Service “verlässt”, ohne zu stoppen (vorübergehender Statuswechsel).
Der Receptionist watcht registrierte Refs nicht — wenn ein
Actor stoppt, bleibt seine Registrierung bestehen, bis du
Deregister sendest (oder, in einem Cluster, bis sein ganzer
Node den Cluster verlässt — siehe unten). Ein Find kann also
eine Ref auf einen gestoppten Actor zurückgeben; sichere dich auf
der Consumer-Seite dagegen ab. (Subscriber werden sehr wohl
gewatcht, siehe
Subscriptions sind begrenzt und gewatcht.)
Find ist ein einmaliges Lookup — das Ergebnis kommt als einzelne
Listing-Nachricht zurück, zugestellt an den replyTo-Actor, den
du benennst:
class ApiConsumer extends Actor<Listing<ApiMessage>> { override onReceive(listing: Listing<ApiMessage>): void { // listing.refs: jeder Actor, der unter dem Key registriert ist, cluster-weit console.log(`${listing.refs.length} Actors unter ${listing.key.id}`); }}const consumer = system.spawn(ApiConsumer, 'consumer');
receptionist.tell(new Find(key, consumer));Bei der Behandlung eines Find tut der Receptionist:
- Liest lokale Registrierungen unter
key. - Fügt bekannte Remote-Registrierungen aus Gossip hinzu.
- Antwortet
replyTomit einem einzelnenListingder kombinierten Liste.
Die Antwort trägt die aktuelle Sicht — keine synchrone Cluster-Abfrage. Bedeutet: Registrierungen auf anderen Nodes, die noch nicht gegossipt wurden, fehlen.
Innerhalb einer Gossip-Runde oder zwei (1-2 Sekunden Default) konvergiert jeder Node auf dieselbe Sicht.
Subscriben
Abschnitt betitelt „Subscriben“Subscribe ist kontinuierlich — replyTo erhält jetzt ein
Listing und danach bei jeder Änderung erneut. Es wird aus dem
Package-Root als ReceptionistSubscribe exportiert (und
Unsubscribe als ReceptionistUnsubscribe):
receptionist.tell(new ReceptionistSubscribe(key, consumer));
// Später — keine Updates mehr erhalten:receptionist.tell(new ReceptionistUnsubscribe(key, consumer));Ein frisches Listing wird an den Subscriber gesendet, wann
immer sich die Menge der Refs für key ändert — lokal
(register/deregister) oder via eingehendem Gossip und
Node-Abgängen.
Nutze es für dynamisches Routing: ein Actor, der auf einen Key subscribet und seine Routing-Entscheidungen anpasst, wenn Refs auftauchen / verschwinden.
Subscriptions sind begrenzt und gewatcht
Abschnitt betitelt „Subscriptions sind begrenzt und gewatcht“Zwei Mechanismen halten die Subscriber-Menge davon ab, unbegrenzt zu wachsen.
Death Watch. Der Receptionist watcht jeden Subscriber. Stoppt
einer, ohne Unsubscribe zu senden — Crash, vergessenes Cleanup,
ein pro Request gespawnter Actor —, wird sein Platz frei, sobald
das Terminated ankommt. Du musst in postStop nicht mehr
unsubscriben, es bleibt aber der schnellere Weg.
Obergrenzen. Zwei Limits begrenzen den Rest, die lebenden Subscriber:
| 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);Ein Subscribe über einer der beiden Grenzen wird abgelehnt,
nicht verworfen: replyTo erhält statt des ersten Listing ein
ReceptionistSubscribeRejected, und es folgen keine weiteren
Listings.
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: der Wert, auf den diese Grenze gesetzt ist this.log.warn(`discovery refused: ${message.reason} (${message.limit})`); return; } // …message.refs verwenden }}Die Antwort ist wichtiger, als sie aussieht: ein stillschweigend
verworfenes Subscribe ist nicht davon zu unterscheiden, dass der
Key schlicht noch keine Registrierungen hat — und zwischen beidem
liegt ein langer Nachmittag.
Die Klasse wird aus dem Package-Root als
ReceptionistSubscribeRejected exportiert — das unqualifizierte
SubscribeRejected gehört DistributedPubSub, genau wie Subscribe
zu ReceptionistSubscribe aliasiert ist.
Cluster-aware vs. Single-Node
Abschnitt betitelt „Cluster-aware vs. Single-Node“// In einem Cluster:const receptionist = system.extension(ReceptionistId).start(cluster);
// Ohne Cluster (Single Node):const receptionist = system.extension(ReceptionistId).start(null);Ohne Cluster funktioniert der Receptionist nur lokal — nützlich für Tests oder Single-Node-Apps, die trotzdem die ServiceKey-basierte Lookup-API wollen.
Für Cluster-Setups immer den Cluster übergeben — sonst sind Remote-Registrierungen unsichtbar.
Auto-Bereinigung beim Verlassen eines Nodes
Abschnitt betitelt „Auto-Bereinigung beim Verlassen eines Nodes“Wenn MemberRemoved für einen Peer feuert:
- Vergisst der Receptionist jede Registrierung, die dieser Node beigesteuert hat.
- Subscriber feuern mit dem aktualisierten (kleineren) Listing.
Das deckt den Fall “Node ist abgestürzt, hatte keine Chance zu deregistrieren” ab — Gossip + Cluster-Membership erledigen die Bereinigung.
Mehrere Actors pro Key
Abschnitt betitelt „Mehrere Actors pro Key“receptionist.tell(new Register(apiKey, instance1));receptionist.tell(new Register(apiKey, instance2));receptionist.tell(new Register(apiKey, instance3)); // alle drei unter demselben Key
receptionist.tell(new Find(apiKey, consumer)); // Listing.refs → [instance1, instance2, instance3]Ein häufiges Muster: N Worker registrieren sich alle unter demselben Key. Consumer sehen den ganzen Pool und können routen, wie sie wollen — Round-Robin, Broadcast, Zufallsauswahl.
Ein Actor pro Key (Konvention)
Abschnitt betitelt „Ein Actor pro Key (Konvention)“// Ein Singleton für den Cluster registriertreceptionist.tell(new Register(coordinatorKey, theCoordinator));receptionist.tell(new Find(coordinatorKey, consumer)); // Listing.refs → [theCoordinator]Für Singleton-artige Services hat der Key eine Ref. Consumer
nehmen refs[0] (mit Fallback für den leeren Fall).
Der Receptionist erzwingt keine einzelne Instanz — das ist die Aufgabe des Singleton Managers. Aber einen Singleton unter einem Key zu registrieren gibt Consumern einen Discovery-Pfad, der Leadership-Wechsel überlebt.
Wann verwenden vs. Alternativen
Abschnitt betitelt „Wann verwenden vs. Alternativen“| Bedarf | Werkzeug |
|---|---|
| Feste Routees pro Node, jeder Node hat sie | ClusterRouter mit einem wohldefinierten Pfad |
| Dynamische Registrierungen, Lookup per Service-Name | Receptionist |
| Genau ein Actor cluster-weit | ClusterSingleton (bei Bedarf zum Lookup im Receptionist registriert) |
| Per-Key-Actors mit Auto-Spawn | ClusterSharding |
Der Receptionist ist die flexibelste Discovery — du zahlst aber Gossip-Kosten für die Dynamik. Für statisches Routing ist der Cluster Router günstiger.
Wohin als Nächstes
Abschnitt betitelt „Wohin als Nächstes“- Discovery im Überblick — das Gesamtbild.
- Cluster im Überblick — das Membership-Modell darunter.
- Cluster Router — Static-Path-Alternative für “wohldefinierten Service”-Routing.
- Singleton im Überblick — oft im Receptionist für Discovery registriert.
Die Receptionist-API-Referenz
deckt die vollständige Oberfläche ab.
