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';
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 }; // Identität für das Reply-Routing 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';
// 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.
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.
