You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
That's three concepts (RemoteActorRef construction, ask, timeout) for a common pattern. Erlang offers :rpc.call(Node, Module, Fn, Args) as a single primitive — same use case, single concept.
A cluster.rpc(node, path, msg) helper:
Reduces boilerplate.
Has a clear error type (RpcError) distinct from local errors.
Erlang's RPC is strictly to-an-actor: there's no general-purpose function call across nodes (functions are part of code modules, loaded per-node). actor-ts's natural equivalent is "tell-an-actor" — already what RemoteActorRef does. RPC is sugar.
Design sketch — actor-ts equivalent
// src/cluster/Rpc.ts (new) — or extension on ClusterexportclassRpcErrorextendsError{constructor(message: string,publicreadonlynode: NodeAddress,publicreadonlypath: string,publicreadonlycause?: unknown,){super(message);this.name='RpcError';}}exportinterfaceRpcOptions{/** Per-call timeout. Default: 5000 ms. */readonlytimeoutMs?: number;}/** * Synchronous cross-node call. Resolves with the reply or rejects with * RpcError on timeout / network failure / actor failure. * * const reply = await cluster.rpc<Reply>( * targetNode, '/user/services/orders', new GetOrder('42'), { timeoutMs: 3000 }); */asyncfunctionrpc<R=unknown>(cluster: Cluster,node: NodeAddress,path: string,msg: unknown,options?: RpcOptions,): Promise<R>;
Authenticated RPC — relies on existing cluster TLS / mTLS.
Open design questions
Sync vs async error semantics: ask-based RPC always rejects on timeout. Should cluster.rpc re-package as RpcError always, or pass through the original? Recommend: always wrap in RpcError for type consistency; original error in cause.
Path resolution: do we accept relative paths ('services/orders') or only absolute ('/user/services/orders')? Recommend: absolute only — explicit + matches ClusterClient.send.
rpcMulti parallelism: send to all nodes in parallel and aggregate? Or sequentially? Parallel is more useful. Recommend: parallel, with Promise.allSettled.
Local-node shortcut: cluster.rpc(selfAddress, path, msg) — does this short-circuit to local ask, or still go over the loopback transport? Recommend: short-circuit (faster + tracks the local-tell metric).
Test plan
rpc happy path — 2-node cluster; rpc from A to actor on B; reply received.
rpc timeout — actor on B never replies; rpc rejects with RpcError (cause: timeout) after timeoutMs.
rpc actor not found — path doesn't resolve on B; rpc rejects with RpcError.
rpc to self-node — local short-circuit; bypasses cluster transport.
rpc network partition — B unreachable; rpc rejects with RpcError (cause: network).
rpcMulti mixed success/failure — 3 nodes; 2 reply, 1 times out; result has succeeded.size === 2 and failed.size === 1.
cast fire-and-forget — no return value; doesn't await; metric increments.
MDC propagation — set MDC; call rpc; remote actor's log.info includes MDC.
Tracing propagation — active span; rpc; remote actor.receive span linked.
Size / Priority
:rpc.call/4.Rationale
Cross-node "call this actor at path X on node Y" is doable today via:
That's three concepts (RemoteActorRef construction, ask, timeout) for a common pattern. Erlang offers
:rpc.call(Node, Module, Fn, Args)as a single primitive — same use case, single concept.A
cluster.rpc(node, path, msg)helper:RpcError) distinct from local errors.Reference: what Erlang does
Erlang's RPC is strictly to-an-actor: there's no general-purpose function call across nodes (functions are part of code modules, loaded per-node). actor-ts's natural equivalent is "tell-an-actor" — already what
RemoteActorRefdoes. RPC is sugar.Design sketch — actor-ts equivalent
Method-on-Cluster ergonomic:
Integration with existing actor-ts subsystems
RemoteActorRef: existing primitive;rpcbuilds one internally.ask: existing primitive;rpcwraps it with explicitRpcErrortranslation.Cluster.transport: unchanged.cluster_rpc_total{node, outcome=ok|timeout|error}Counter +cluster_rpc_duration_seconds{node}Histogram. Subject to [Security] Prometheus cardinality attack via user-controlled label values #131's cardinality cap (node-label can grow with cluster size; bucketise viatruncateAddress).askalready propagates MDC; rpc inherits.ask; rpc inherits.Out of scope / non-goals
rpc:call(Node, Module, Fn, Args)) — not idiomatic in TS. Use actor-message pattern.pmap-style fan-out compute — out of scope; rpcMulti is the closest. For real parallel compute, use Streams ([Feature] Akka-Streams DSL subset (SourceQueue, MergeHub, BroadcastHub) #147) or a worker pool.Open design questions
cluster.rpcre-package asRpcErroralways, or pass through the original? Recommend: always wrap inRpcErrorfor type consistency; original error incause.'services/orders') or only absolute ('/user/services/orders')? Recommend: absolute only — explicit + matchesClusterClient.send.rpcMultiparallelism: send to all nodes in parallel and aggregate? Or sequentially? Parallel is more useful. Recommend: parallel, withPromise.allSettled.cluster.rpc(selfAddress, path, msg)— does this short-circuit to localask, or still go over the loopback transport? Recommend: short-circuit (faster + tracks the local-tell metric).Test plan
rpchappy path — 2-node cluster; rpc from A to actor on B; reply received.rpctimeout — actor on B never replies; rpc rejects withRpcError(cause: timeout) aftertimeoutMs.rpcactor not found — path doesn't resolve on B; rpc rejects withRpcError.rpcto self-node — local short-circuit; bypasses cluster transport.rpcnetwork partition — B unreachable; rpc rejects withRpcError(cause: network).rpcMultimixed success/failure — 3 nodes; 2 reply, 1 times out; result hassucceeded.size === 2andfailed.size === 1.castfire-and-forget — no return value; doesn't await; metric increments.log.infoincludes MDC.actor.receivespan linked.Acceptance criteria
cluster.rpc<R>(node, path, msg, opts?)exported.cluster.rpcMulti<R>(nodes, path, msg, opts?)exported.cluster.cast(node, path, msg)exported.RpcErrorclass withnode,path,causefields.