Cluster-Client
ClusterClient lässt einen Prozess außerhalb des Clusters mit
Actors innerhalb sprechen. Der externe Prozess ist kein
Cluster-Mitglied — kein Gossip, kein Mitgliedschaftsstatus — aber
er kann cluster-interne Actors per bekannten
Receptionist-Kontakten mit tell und ask ansprechen.
import { ClusterClient, ClusterClientOptions } from 'actor-ts/cluster';
const clusterClientOptions = ClusterClientOptions.create().withContactPoints(['actor-ts://my-app@10.0.0.5:2552/system/cluster/receptionist']);const client = new ClusterClient( clusterClientOptions,);
// Senden an einen bekannten Cluster-Actor:client.send('/user/api/orders', { kind: 'place', ... });
// Oder fragen und auf die Antwort warten:const orders = await client.ask('/user/api/orders', { kind: 'list-orders' });Wann verwenden
Abschnitt betitelt „Wann verwenden“Drei legitime Anwendungsfälle:
- Externe Services, die nicht in den Actor-Cluster gehören, aber mit ihm reden müssen — ein Python-ML-Service, der an einen Kafka-Actor innerhalb des Clusters postet.
- Mobile-/Desktop-Clients über eine Bridge — eine serverseitige Bridge hält einen ClusterClient + bietet eine REST/WS-API nach außen.
- Cross-Cluster-Föderation — zwei Cluster, bei denen einer mit bestimmten Actors im anderen sprechen muss, ohne zu mergen.
Für typische Setups ist alles Teil des Clusters — ClusterClient ist der Notausgang für Fälle, in denen das nicht möglich ist.
Wie es funktioniert
Abschnitt betitelt „Wie es funktioniert“Der Client:
- Verbindet sich mit einem oder mehreren Kontaktpunkten
(Cluster-Nodes, auf denen ein
ClusterClientReceptionistläuft). - Schickt jede Nachricht in einem Envelope, adressiert an einen
Ziel-Actor-Pfad —
sendfür Fire-and-Forget,askfür Request/Reply. - Der Receptionist auf dem Kontakt-Node löst diesen Pfad in seinem lokalen Actor-Baum auf und stellt die Nachricht zu.
- Behandelt Failover, wenn ein Kontaktpunkt unerreichbar wird — verbindet sich zu einem anderen.
Konfiguration
Abschnitt betitelt „Konfiguration“type ClusterClientOptionsType = { contactPoints: ReadonlyArray<string>; // mindestens ein Node: host:port oder <system>@host:port systemName?: string; // synthetischer System-Name im Hello des Clients clientIdentity?: { host: string; port: number }; // im Hello des Clients angekündigte Identität askTimeoutMs?: number; // Standard-Ask-Timeout (ms; Standard 5000) tls?: TlsTransportOptionsType; // muss zum Cluster passen logger?: Logger; // Standard: ConsoleLogger auf WARN};contactPoints ist die Liste der zu wählenden Cluster-Nodes —
jeweils ein host:port- oder <system>@host:port-String. Der
Client probiert sie der Reihe nach; bei Ausfall fällt er auf den
nächsten zurück.
Für stabile Kontaktadressen betreibt die Cluster-Seite typisch
einen ClusterClientReceptionist auf einem festen Satz von Nodes,
deren Adressen zu den Kontaktpunkten des Clients werden.
Serverseite — ClusterClientReceptionist
Abschnitt betitelt „Serverseite — ClusterClientReceptionist“import { ClusterClientReceptionistId } from 'actor-ts/cluster';
// Der Receptionist ist eine Per-System-Extension — starte ihn auf// jedem Node, der Outside-in-Client-Verbindungen annehmen soll:system.extension(ClusterClientReceptionistId).start(cluster);Es gibt keine Service-Registry zu befüllen. Der Receptionist löst
den Ziel-Pfad jedes Envelopes im lokalen Actor-Baum auf und stellt
die Nachricht zu — jeder bereits unter /user laufende Actor ist
über den Pfad erreichbar, an den der Client sendet (z. B.
client.send('/user/api/orders', ...)).
Seine einzige Option, askTimeoutMs, begrenzt die Wartezeit, wenn
ein Client-Envelope ein ask trägt; setze sie über
ClusterClientReceptionistOptions.
An wen die Antwort geht
Abschnitt betitelt „An wen die Antwort geht“Ein ask wird über die Verbindung beantwortet, auf der das
Envelope ankam — nie an eine Adresse, die das Envelope nennt.
Ein Client sagt genau einmal, wer er ist: im hello-Handshake.
Der Transport bindet diese Angabe an den Socket, und jede
Antwort, jede Redaktion und jedes „path not found“ folgt dieser
Bindung.
Deshalb hat das Envelope überhaupt kein Absenderfeld mehr. Früher trug es eines, und der Receptionist routete darauf — was jede verbundene Gegenstelle die Adresse wählen ließ, an die dieser Node antwortete und zu der er eine Verbindung aufbaute. Das Feld zu löschen statt es zu prüfen, lässt nichts übrig, das beim nächsten Mal falsch gemacht werden kann, wenn jemand zur billigeren von zwei Antworten auf dieselbe Frage greift.
Ein Client, der alt genug ist, das Feld noch zu senden, wird
normal bedient; der Wert wird ignoriert. Nennt er etwas
anderes als die Verbindung, zählt der Node es auf
cluster_envelope_from_mismatch_total{frame="cluster-client-envelope"}
und macht weiter. Das ist entweder ein Client, der sich über
seine eigene Adresse irrt, oder jemand, der testet, ob dieser
Node auf Payload routet — eine Zahl wert, keine Ablehnung, denn
das Envelope selbst ist in Ordnung.
Was ein Fehlschlag dem Client verrät
Abschnitt betitelt „Was ein Fehlschlag dem Client verrät“Ein ClusterClient ist kein Peer. Er spricht dieselbe Leitung
wie ein Cluster-Mitglied — und genau deshalb lohnt es, die
Unterscheidung auszusprechen: Er ist nie dem Membership-Ring
beigetreten, trägt keine Gossip- oder Heartbeat-Pflicht, und ein
Kontaktpunkt ist per Entwurf von außerhalb der Grenze erreichbar,
die die clustereigenen Verbindungen schützt. Ein abgeschlossenes
hello berechtigt die Gegenstelle nicht zu den Interna des
Clusters.
Schlägt ein ask im Cluster fehl, reicht der Receptionist den Fehlertext daher nicht weiter. Diesen Text schreibt der Actor, der zufällig gescheitert ist — dieselbe Klasse von String wie bei einem HTTP-500, mit Dateipfaden, SQL-Fragmenten oder einem Stack. Der Client bekommt stattdessen einen festen Satz und eine Korrelations-Id:
ask failed on the cluster node (correlationId=6f1c…-…) — the reason is in that node's logDer Volltext wird auf dem Node geloggt, der das ask ausgeführt
hat — unter derselben Id, auf warn. Ein externer Aufrufer
nennt die Id, ein Operator greppt danach. Der Grund geht nie
über die Leitung.
Ein unbekannter Pfad wird anders gemeldet, denn dort erfährt der Client nur etwas über seinen eigenen Request:
path not found: user/api/ordersBeachte, was nicht darin steht — die Adresse des Nodes. Ein Kontaktpunkt steht oft hinter einem Load Balancer oder NAT, die Bind-Adresse ist also nicht die gewählte, und es gibt keinen Grund, sie herauszugeben.
Muss ein Client wirklich auf einen bestimmten Fehlschlag reagieren, modelliere ihn als Antwort, die der Actor selbst verfasst, statt als Wurf:
class OrderActor extends Actor<OrderCommand> { override onReceive(message: OrderCommand): void { if (!this.stock.has(message.sku)) { // A deliberate, authored answer — it reaches the client verbatim. this.sender.forEach((s) => s.tell({ kind: 'rejected', reason: 'out-of-stock' })); return; } // … }}Eine erfolgreiche Antwort wird unverändert durchgereicht; redigiert werden nur Fehlschläge. So bleibt der Domänenvertrag explizit, statt das, was eine Exception zufällig sagt, zur API zu machen.
Vergleich mit Cluster-fähigem ActorRef
Abschnitt betitelt „Vergleich mit Cluster-fähigem ActorRef“// Innerhalb des Clusters — Actors sprechen direkt:const ref = await system.actorSelection('actor-ts://my-app@host:2552/user/api').resolveOne();ref.tell(...);
// Außerhalb des Clusters — ClusterClient:const clusterClientOptions = ClusterClientOptions.create().withContactPoints([...]);const client = new ClusterClient(clusterClientOptions);client.send('/user/api', ...);Unterschiede:
- Innen: erfordert Cluster-Mitgliedschaft. Refs propagieren per Gossip; du kannst langlebige Refs halten.
- ClusterClient: keine Mitgliedschaft; Refs werden pro Nachricht über den Receptionist aufgelöst.
Wann NICHT verwenden
Abschnitt betitelt „Wann NICHT verwenden“Wohin als Nächstes
Abschnitt betitelt „Wohin als Nächstes“- Refs über Nodes hinweg — Cluster-interne Ref-Semantik zum Vergleich.
- Receptionist — die cluster-interne Service-Registry (eine eigenständige Komponente mit ähnlichem Namen).
- HTTP-Überblick — die häufigere extern zugängliche Alternative.
