ParallelMultiNodeSpec
Это содержимое пока не доступно на вашем языке.
ParallelMultiNodeSpec is the worker-thread variant of
MultiNodeSpec. Each
“node” runs in its own worker_threads Worker (or a Web Worker
on Bun/Deno), connected through an in-process broker. Same test
shape as MultiNodeSpec, but with real OS threads so
concurrency bugs the single-threaded variant papers over can
surface.
import { ParallelMultiNodeSpec } from 'actor-ts/testkit';
const spec = new ParallelMultiNodeSpec({ roles: ['a', 'b', 'c'], scenarioModule: new URL('./my-scenario.ts', import.meta.url),});await spec.start();await spec.awaitMembers('a', 3);
// Drive the worker-side scenario over an RPC:const result = await spec.runIn('a', 'compute', { x: 42 });
await spec.stop();When to use it
Section titled “When to use it”For tests that need true parallelism:
- Real threads — catches races the in-process variant serialises away on a single event loop (concurrent journal writes, scheduler-thread interleaving).
- Serialization — every message crosses a worker boundary via structured clone, so non-serializable payloads (functions, class instances) fail here just as they would on a real wire.
- Thread isolation — each node has its own heap and event loop.
For most cluster tests, MultiNodeSpec is faster + simpler — use ParallelMultiNodeSpec only when single-threaded tests don’t cover the case. Neither variant uses TCP; for real network conditions, use an external Docker-Compose cluster.
Configuration
Section titled “Configuration”type ParallelMultiNodeSpecOptionsType = { roles: ReadonlyArray<string>; // one worker per role; must be unique seedRoles?: ReadonlyArray<string>; // bootstrap seeds — defaults to [roles[0]] scenarioModule?: URL; // module each worker loads (setup + commands) scenarioInitDataFor?: (role: string) => unknown; // per-role data passed to setup() addresses?: Record<string, { host: string; port: number }>; failureDetector?: Partial<FailureDetectorOptionsType>; gossipIntervalMs?: number; awaitTimeoutMs?: number; // default 30_000 (worker bootstrap is slower) logLevel?: LogLevel; bootstrapModule?: URL; // override the bundled worker bootstrap};| Field | Purpose |
|---|---|
roles | Node list — one worker per role; the string is the worker’s system name. Must be unique. |
seedRoles | Which roles act as bootstrap seeds. Defaults to the first role. |
scenarioModule | URL of the module each worker imports — it owns the actor-shaped setup and the commands map runIn dispatches to. |
scenarioInitDataFor | Per-role data forwarded to the scenario’s setup(context). |
failureDetector | Failure-detector overrides. |
awaitTimeoutMs | Default await* timeout. Default 30 s — worker bootstrap is slower than in-process. |
bootstrapModule | Override the bundled worker entry point (advanced). |
The scenario module
Section titled “The scenario module”Each worker joins the cluster automatically — the harness owns
the ActorSystem + Cluster bootstrap. Your test-specific
actor code lives in a scenario module: a plain module that
exports an optional setup(context) hook and a commands map.
The worker imports it by URL, runs setup once after the cluster
joins, then dispatches runIn(role, command, args) calls to the
matching command.
import type { ScenarioModule } from 'actor-ts/testkit';import { type ActorRef } from 'actor-ts';
export const setup: ScenarioModule['setup'] = (context) => { // context = { role, system, cluster, initData, state } context.state.counter = context.system.spawnAnonymous(Counter);};
export const commands: ScenarioModule['commands'] = { increment(args, context): void { (context.state.counter as ActorRef<Command>).tell({ kind: 'increment' }); }, // Commands may return any JSON-serialisable value back to the harness: ping(): string { return 'pong'; },};The module owns everything actor-shaped (entity classes, sharding regions, …). The harness only ever exchanges JSON-serialisable command/response pairs with it — closures can’t cross the worker boundary, which is why scenario code lives in its own file loaded by URL rather than inline in the test.
Driving the workers
Section titled “Driving the workers”The test can’t touch a worker’s in-memory actors directly, so it drives them through the harness:
// Invoke a scenario command on a specific role and await its result:await spec.runIn('a', 'increment');const reply = await spec.runIn<string>('a', 'ping'); // → 'pong'
// Inspect each worker's cluster view (JSON snapshots, not live objects):const members = await spec.getMembers('a');const leader = await spec.getLeader('a');runIn(role, command, args?) calls into the scenario module’s
commands map and returns whatever that handler returns. Because
systemFor / clusterFor can’t hand back objects that live in
another thread, membership is exposed as getMembers(role) /
getLeader(role) snapshots instead.
When to use which
Section titled “When to use which”| Test concept | MultiNodeSpec | ParallelMultiNodeSpec |
|---|---|---|
| Cluster membership semantics | ✓ | ✓ |
| Sharding distribution | ✓ | ✓ |
| Singleton failover | ✓ | ✓ |
| Gossip convergence | ✓ | ✓ |
| Serialization round-trip | ✗ | ✓ |
| Per-worker heap isolation | ✗ | ✓ |
| True parallelism (OS threads) | ✗ | ✓ |
| Real TCP semantics | ✗ | ✗ |
| CI speed | very fast | slow |
For 90 % of tests, MultiNodeSpec. For the 10 % where fidelity matters, ParallelMultiNodeSpec.
Spinning up worker threads:
- Per-node startup: 200-500 ms (worker spawn + cluster handshake).
- 3-node spec: ~1.5 s total.
- Compared to MultiNodeSpec: sub-100 ms total.
For tight test loops (many test cases), the cost adds up. Use ParallelMultiNodeSpec sparingly — one or two key tests for real parallelism / serialization, MultiNodeSpec for the rest.
Cleanup
Section titled “Cleanup”await spec.stop();// → terminates every worker thread (awaited, so none leak)// → unblocks the test process to exitAlways call stop() — orphaned worker threads leak and can
starve later tests. The test framework may clean them up if the
test crashes, but explicit teardown is safer.
Where to next
Section titled “Where to next”- Testing overview — the bigger picture.
- MultiNodeSpec — the faster in-process variant.
- TestKit — for single-system tests.
