MultiNodeSpec
Это содержимое пока не доступно на вашем языке.
MultiNodeSpec runs N cluster nodes inside one test process.
Each is a real ActorSystem; they communicate via an
in-process transport (no TCP). Useful for testing cluster
behavior — sharding, singletons, gossip convergence,
failover — without Docker.
import { MultiNodeSpec, TestProbe } from 'actor-ts/testkit';
// Each role name is one node — and doubles as that node's system name.const spec = new MultiNodeSpec({ roles: ['a', 'b', 'c'] });await spec.start();await spec.awaitMembers('a', 3); // wait for the cluster to converge
// Reach any node's ActorSystem with systemFor(role):const node2 = spec.systemFor('b');
// Spawn actors on a specific node and assert with a probe:const probe = new TestProbe(spec.systemFor('a'));const remote = node2.spawnAnonymous(Worker);
remote.tell({ kind: 'do', replyTo: probe });await probe.expectMessage({ kind: 'done' });
await spec.stop();When to use it
Section titled “When to use it”Three primary cases:
- Testing cluster behavior — sharding rebalances, singleton failover, gossip convergence — verifying these without real network setup.
- Reproducing distributed bugs — easier to isolate when the entire cluster runs in one process.
- CI-friendly cluster tests — fast (sub-second), no Docker, no ports.
For tests that need true parallelism (real OS threads — concurrent journal writes, scheduler interleaving), use ParallelMultiNodeSpec. For real TCP/TLS or cross-host latency, use an external Docker-Compose cluster.
Configuration
Section titled “Configuration”type MultiNodeSpecOptionsType = { roles: ReadonlyArray<string>; // one node per role; must be unique seedRoles?: ReadonlyArray<string>; // bootstrap seeds — defaults to [roles[0]] addresses?: Record<string, { host: string; port: number }>; failureDetector?: ClusterOptionsType['failureDetector']; gossipIntervalMs?: number; // default 100 (vs. 1 s in production) awaitTimeoutMs?: number; // default 10_000 — how long await* helpers wait logLevel?: LogLevel; // defaults to a quiet NoopLogger downing?: (role: string) => DowningProvider | undefined;};| Field | Purpose |
|---|---|
roles | Node list — each entry is one node, and the string doubles as its system name. Must be unique. |
seedRoles | Which roles act as bootstrap seeds. Defaults to the first role. |
addresses | Per-role host/port overrides. Auto-allocated if omitted. |
failureDetector | Failure-detector overrides — tests usually tighten these so crashes are detected quickly. |
gossipIntervalMs | Gossip round interval. Default 100 ms. |
awaitTimeoutMs | Default timeout for the await* helpers. Default 10 s. |
logLevel | Log level — quiet by default. |
downing | Per-role split-brain resolver factory. |
Pass a plain object as shown, or build it fluently with
MultiNodeSpecOptions.create().withRoles(['a', 'b', 'c']).
Roles, seeds, and addresses
Section titled “Roles, seeds, and addresses”Each entry in roles is a distinct node — the string is that
node’s identity and its ActorSystem name, so it must be unique
(the constructor throws on duplicates or an empty list). By
default the first role is the sole bootstrap seed; override
that with seedRoles:
const spec = new MultiNodeSpec({ roles: ['seed', 'worker-1', 'worker-2'], seedRoles: ['seed'],});await spec.start();Ports are auto-allocated on 127.0.0.1; pin specific ones with
addresses when a test needs fixed endpoints:
const spec = new MultiNodeSpec({ roles: ['a', 'b'], addresses: { a: { host: '127.0.0.1', port: 2551 } },});What’s shared
Section titled “What’s shared”All nodes share:
- A shared in-process message bus — they can talk to each other.
- The same gossip protocol — membership converges.
- Independent actor systems — separate dispatchers, schedulers, supervisor trees.
This means:
- Real cluster semantics — members come up, gossip converges, failure detector observes, sharding rebalances.
- No serialization — messages between “nodes” pass by reference (in-process). Not a fit for testing serialization-dependent behavior.
Failover testing
Section titled “Failover testing”Two ways to take a node down: crash(role) yanks its transport
out abruptly (peers detect it via the failure detector), and
leave(role) performs a graceful cluster exit.
// Verify singleton failover:const spec = new MultiNodeSpec({ roles: ['a', 'b', 'c'], failureDetector: { heartbeatIntervalMs: 50, unreachableAfterMs: 200, downAfterMs: 400 },});await spec.start();await Promise.all([ spec.awaitMembers('a', 3), spec.awaitMembers('b', 3), spec.awaitMembers('c', 3),]);
// ... start the singleton on every node ...
// Identify the current host, then crash it:const host = spec.clusterFor('a').leader().toNullable();await spec.crash('a');
// Wait for the survivors to down 'a' and re-converge:await spec.awaitMembers('b', 2);await spec.awaitMemberStatus('b', 'a', 'down');
// ... assert the singleton has moved to a survivor ...
await spec.stop();crash(role) removes the node abruptly; the others observe the
unreachable status, gossip the change, then trigger downing +
failover. The await* helpers replace fragile setTimeout
sleeps — they poll until the condition holds (or throw after
awaitTimeoutMs).
Per-node TestProbe
Section titled “Per-node TestProbe”MultiNodeSpec has no probe factory — construct a TestProbe
against whichever node’s system you want to observe:
const probeA = new TestProbe(spec.systemFor('a')); // probe on node 'a'const probeB = new TestProbe(spec.systemFor('b')); // probe on node 'b'Each probe is bound to one node’s actor system. Useful when testing routing — verify that a message ends up on the expected node.
Sharding tests
Section titled “Sharding tests”Start a sharding region on every node via its Cluster — reach
each one with clusterFor(role):
const spec = new MultiNodeSpec({ roles: ['a', 'b', 'c'] });await spec.start();await spec.awaitMembers('a', 3);
const startShardingOptions = StartShardingOptions.create<Command>() .withTypeName('entity') .withEntityActor(Entity) .withExtractEntityId((message) => message.id);const regions = spec.allRoles().map((role) => spec.clusterFor(role).sharding.start(startShardingOptions));
// Spawn entities; verify they spread across nodes:for (const id of ['e1', 'e2', 'e3', 'e4', 'e5']) { regions[0].tell({ id, kind: 'wake-up' });}
await spec.stop();The framework’s coordinator distributes shards across the nodes just like a real cluster.
What MultiNodeSpec doesn’t test
Section titled “What MultiNodeSpec doesn’t test”Where to next
Section titled “Where to next”- Testing overview — the bigger picture.
- TestKit — single-system testing.
- ParallelMultiNodeSpec — worker-thread variant.
- Cluster overview — what MultiNodeSpec simulates.
