Перейти к содержимому
Русский

Cluster client

Это содержимое пока не доступно на вашем языке.

ClusterClient lets a process outside the cluster talk to actors inside. The external process isn’t a cluster member — no gossip, no membership state — but it can tell and ask cluster-internal actors via known receptionist contacts.

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,
);
// Send to a well-known cluster actor:
client.send('/user/api/orders', { kind: 'place', ... });
// Or ask and await the reply:
const orders = await client.ask('/user/api/orders', { kind: 'list-orders' });

Three legitimate use cases:

  1. External services that don’t belong in the actor cluster but need to talk to it — a Python ML service that posts to a Kafka actor inside the cluster.
  2. Mobile / desktop clients via a bridge — a server-side bridge holds a ClusterClient + exposes a REST/WS API externally.
  3. Cross-cluster federation — two clusters where one needs to talk to specific actors in the other without merging.

For typical setups, everything is part of the cluster — ClusterClient is the escape hatch for cases where it can’t be.

ClusterExternal processClusterExternal processconnect + hello handshakeenvelope with target path + messagereply (for ask)

The client:

  1. Connects to one or more contact points (cluster nodes running a ClusterClientReceptionist).
  2. Sends each message in an envelope addressed to a target actor pathsend for fire-and-forget, ask for request/reply.
  3. The receptionist on the contact node resolves that path in its local actor tree and delivers the message.
  4. Handles failover when a contact point becomes unreachable — reconnects to another.
type ClusterClientOptionsType = {
contactPoints: ReadonlyArray<string>; // at least one node: host:port or <system>@host:port
systemName?: string; // synthetic system name in the client's hello
clientIdentity?: { host: string; port: number }; // identity used for reply routing
askTimeoutMs?: number; // default ask timeout (ms; default 5000)
tls?: TlsTransportOptionsType; // must match the cluster's
logger?: Logger; // default: ConsoleLogger at WARN
};

contactPoints is the list of cluster nodes to dial — each a host:port or <system>@host:port string. The client tries them in order; on failure, it falls back to the next.

For stable contact addresses, the cluster side typically runs a ClusterClientReceptionist on a fixed set of nodes, whose addresses become the client’s contact points.

import { ClusterClientReceptionistId } from 'actor-ts';
// The receptionist is a per-system extension — start it on each
// node that should accept outside-in client connections:
system.extension(ClusterClientReceptionistId).start(cluster);

There’s no service registry to populate. The receptionist resolves each envelope’s target path against the local actor tree and delivers the message — any actor already running under /user is reachable by the path the client sends to (e.g. client.send('/user/api/orders', ...)).

Its one option, askTimeoutMs, bounds the wait when a client envelope carries an ask; set it via ClusterClientReceptionistOptions.

A ClusterClient is not a peer. It speaks the same wire as a cluster member, which is exactly why the distinction is worth stating: it never joined the membership ring, carries no gossip or heartbeat duty, and a contact point is by design reachable from outside whatever boundary protects the cluster’s own links. Completing a hello does not entitle the party on the other end to the cluster’s internals.

So when an ask fails inside the cluster, the receptionist does not forward the rejection text. That text is written by whatever actor happened to fail — the same class of string as an HTTP 500’s, carrying file paths, SQL fragments or a stack. The client gets a fixed sentence and a correlation id instead:

ask failed on the cluster node (correlationId=6f1c…-…) — the reason is in that node's log

The full text is logged on the node that ran the ask, under the same id, at warn. An outside caller quotes the id; an operator greps for it. Nothing carries the reason over the wire.

An unknown path is reported differently, because there the client only learns about its own request:

path not found: user/api/orders

Note what is not in it — the node’s own address. A contact point is often behind a load balancer or NAT, so the address it binds on is not the one the client dialled, and there is no reason to hand it out.

If a client genuinely needs to act on a specific failure, model it as a reply the actor authors rather than as a throw:

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;
}
// …
}
}

A successful reply is passed through untouched; only failures are redacted. That keeps the domain contract explicit instead of turning whatever an exception happened to say into an API.

// Inside the cluster — actors talk directly:
const ref = await system.actorSelection('actor-ts://my-app@host:2552/user/api').resolveOne();
ref.tell(...);
// Outside the cluster — ClusterClient:
const clusterClientOptions = ClusterClientOptions.create().withContactPoints([...]);
const client = new ClusterClient(clusterClientOptions);
client.send('/user/api', ...);

Differences:

  • Inside: requires cluster membership. Refs propagate via gossip; you can hold long-lived refs.
  • ClusterClient: no membership; refs are resolved per message via the receptionist.
  • Refs across nodes — in-cluster ref semantics for comparison.
  • Receptionist — the in-cluster service registry (a distinct component with a similar name).
  • HTTP overview — the more common external-facing alternative.