Zum Inhalt springen
Deutsch

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' });

Drei legitime Anwendungsfälle:

  1. 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.
  2. Mobile-/Desktop-Clients über eine Bridge — eine serverseitige Bridge hält einen ClusterClient + bietet eine REST/WS-API nach außen.
  3. 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.

ClusterExterner ProzessClusterExterner Prozessverbinden + Hello-HandshakeEnvelope mit Ziel-Pfad + NachrichtAntwort (bei ask)

Der Client:

  1. Verbindet sich mit einem oder mehreren Kontaktpunkten (Cluster-Nodes, auf denen ein ClusterClientReceptionist läuft).
  2. Schickt jede Nachricht in einem Envelope, adressiert an einen Ziel-Actor-Pfadsend für Fire-and-Forget, ask für Request/Reply.
  3. Der Receptionist auf dem Kontakt-Node löst diesen Pfad in seinem lokalen Actor-Baum auf und stellt die Nachricht zu.
  4. Behandelt Failover, wenn ein Kontaktpunkt unerreichbar wird — verbindet sich zu einem anderen.
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.

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.

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 log

Der 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/orders

Beachte, 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.

// 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.
  • 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.