Worker-Mesh
JavaScript ist pro Actor-System Single-Threaded. Für
Parallelität innerhalb eines einzelnen OS-Prozesses lässt das
Worker-Mesh des Frameworks mehrere ActorSystems laufen —
eines pro Worker-Thread — die alle am selben Cluster über einen
MessageChannel-Transport teilnehmen.
Jeder ist aus Sicht des Clusters ein separater Cluster-Node — Gossip + Mitgliedschaft + Sharding gelten alle. Die Kommunikation zwischen ihnen läuft über In-Process-MessageChannel (keine Serialisierung zu Bytes, kein TCP).
Wann verwenden
Abschnitt betitelt „Wann verwenden“Zwei Hauptszenarien:
- CPU-gebundene Parallelität in einem Prozess — actor-ts ist pro System Single-Threaded; Multi-Threading braucht mehrere Systeme. Worker-Mesh verteilt sie.
- Isolation innerhalb eines Prozesses — ein “Worker”, der ausfällt, reißt das Hauptsystem nicht mit.
Für Multi-Process-Parallelität (separate OS-Prozesse) nutze den regulären Cluster + TCP-Transport. Worker-Mesh ist speziell für den In-Process-Fall.
High-Level-API: WorkerCluster
Abschnitt betitelt „High-Level-API: WorkerCluster“Broker, Kanäle und den Per-Worker-Handshake von Hand zu verdrahten
(siehe Unter der Haube weiter
unten) ist genau das, was WorkerCluster automatisiert. Es spawnt
einen Worker-Pool aus einem Entrypoint-Modul, führt den
Hello/Init/Ready-Handshake aus, registriert jeden Worker an einem
gemeinsamen WorkerBroker und startet abgestürzte Worker gemäß einer
restartPolicy neu. Das zugrunde liegende Worker-Primitiv wird pro
Runtime gewählt — Web Workers auf Bun/Deno, node:worker_threads auf
Node — sodass derselbe Code überall läuft.
// main.ts — Hauptthreadimport { WorkerCluster, WorkerClusterOptions } from 'actor-ts';
const workerClusterOptions = WorkerClusterOptions.create() .withWorkers(4) .withBootstrap(new URL('./worker-node.js', import.meta.url)) .withSystemName('multi-core') .withBasePort(2552);const cluster = await WorkerCluster.spawn(workerClusterOptions);
console.log(`Spawned ${cluster.size} workers:`);for (const address of cluster.addresses) console.log(' -', address.toString());
// ...später, beim Shutdown:await cluster.terminate();Jeder Worker führt das Bootstrap-Modul aus. Es ruft
WorkerNode.join() auf, um den Handshake abzuschließen und seine
Adresse, den System-Namen, den Transport und initData zu erhalten,
und tritt dann dem Cluster bei wie jeder andere Knoten:
// worker-node.ts — läuft in jedem Workerimport { ActorSystem, Cluster, ClusterOptions, WorkerNode } from 'actor-ts';
async function main(): Promise<void> { const context = await WorkerNode.join<{ seedAddress?: string }>(); const system = ActorSystem.create(context.systemName); const clusterOptions = ClusterOptions.create() .withHost(context.self.host) .withPort(context.self.port) .withSeeds(context.initData.seedAddress ? [context.initData.seedAddress] : []) .withTransport(context.transport); await Cluster.join(system, clusterOptions);
system.spawn(MyActor, 'worker'); context.ready(); // dem Hauptthread signalisieren, dass der Knoten läuft}
void main();| Option | Was |
|---|---|
withBootstrap(url) | Worker-Entrypoint-Modul. Pflicht. |
withWorkers(n | 'auto') | Pool-Größe; 'auto' nutzt die Hardware-Concurrency. Default 'auto'. |
withSystemName(name) | ActorSystem-Name, den jeder Worker hostet. Default 'worker-cluster'. |
withHostname(host) | Hostname-Komponente der Adresse jedes Workers. Default 'worker'. |
withBasePort(port) | Port des ersten Workers; jeder weitere inkrementiert. Default 1. |
withInitData(data) | Payload, die jedem Worker im join()-Context zugestellt wird (initData). Default null. |
withRestartPolicy(p) | 'always' / 'on-failure' / 'never'. Default 'on-failure'. |
withReadyTimeoutMs(ms) | Handshake-Timeout pro Worker. Default 10000. |
withBackend(backend) | Startet über dieses WorkerBackend statt über das erkannte — für eine Runtime, die die Erkennung nicht kennt, oder ein In-Memory-Fake im Test. Default: erkannt. |
Das zurückgegebene WorkerCluster bietet size, addresses (die
NodeAddress jedes Workers), den gemeinsamen broker und
terminate(). Fehlkonfigurierte Optionen (ein falscher basePort,
ein nicht-positives workers) werfen OptionsError bei spawn.
Unter der Haube: manuelle Verdrahtung
Abschnitt betitelt „Unter der Haube: manuelle Verdrahtung“Das obige WorkerCluster übernimmt diese Verdrahtung für dich. Greife
zum manuellen Weg nur, wenn du volle Kontrolle brauchst — eine eigene
Topologie, einen maßgeschneiderten Handshake oder zusätzliches
Per-Node-Setup.
// main.ts — Hauptthreadimport { Worker } from 'node:worker_threads';import { ActorSystem, Cluster, ClusterOptions, MessageChannelTransport, NodeAddress } from 'actor-ts';
const channel = new MessageChannel();
const w1 = new Worker('./worker.js', { workerData: { mainPort: channel.port2 }, transferList: [channel.port2],});
const mainAddress = new NodeAddress('main', 'main', 0);const transport = new MessageChannelTransport(mainAddress, channel.port1);
const system = ActorSystem.create('main');const clusterOptions = ClusterOptions.create() .withHost('main') .withPort(0) .withSeeds(['main']) .withTransport(transport);await Cluster.join( system, clusterOptions,);
// worker.js — läuft im Worker-Threadimport { parentPort, workerData } from 'node:worker_threads';import { ActorSystem, Cluster, ClusterOptions, MessageChannelTransport, NodeAddress } from 'actor-ts';
const workerAddress = new NodeAddress('w1', 'w1', 0);const transport = new MessageChannelTransport(workerAddress, workerData.mainPort);
const system = ActorSystem.create('w1');const cluster2Options = ClusterOptions.create() .withHost('w1') .withPort(0) .withSeeds(['main']) .withTransport(transport);await Cluster.join( system, cluster2Options,);
// Ab hier ist w1 einfach ein weiterer Cluster-NodeDie Mesh-Form
Abschnitt betitelt „Die Mesh-Form“Der Transport ist ein Stern, kein voll vermaschtes Mesh. Ein
einzelner WorkerBroker im Hauptthread ist die Nabe: Er hält
einen MessagePort pro Node und leitet jeden Frame an den Node
weiter, den die to-Adresse des Envelopes benennt. Der
MessageChannelTransport jedes Nodes hält einen einzelnen
Port — sein eigenes Ende eines Channels zum Broker, niemals ein
Array:
import { MessageChannelTransport, NodeAddress, WorkerBroker } from 'actor-ts';
// Hauptthread: ein Broker ist die Nabe für jeden Node.const broker = new WorkerBroker();
// Ein MessageChannel pro Worker — der Broker behält das eine Ende und// der Worker erhält das andere (übertragen via workerData).const workerAddress = new NodeAddress('w1', 'w1', 0);const channel = new MessageChannel();broker.register(workerAddress, channel.port1);
// In diesem Worker hält sein Transport das einzelne ferne Ende:const transport = new MessageChannelTransport(workerAddress, channel.port2);Also brauchen N Worker N Channels (einen pro Worker) — nicht binomial(N,2). Der Broker leitet jeden Hop weiter, was die Verdrahtung linear hält, aber den Hauptthread zum Relay für den gesamten Node-übergreifenden Verkehr macht.
Wie es sich vom TCP-Cluster unterscheidet
Abschnitt betitelt „Wie es sich vom TCP-Cluster unterscheidet“TCP-Transport: MessageChannelTransport:- Sockets, Framing - postMessage zwischen Threads- Serialisierte Bytes - Structured Cloning (kein JSON)- Netzwerklatenz - Sub-Mikrosekunde- Cross-Host - Nur derselbe ProzessNachrichten zwischen Worker-Systemen gehen durch Structured Clone — schneller als JSON.stringify + parse und erhalten mehr Typen (Map, Set, Date etc.).
Anwendungsfälle
Abschnitt betitelt „Anwendungsfälle“Sharding über Cores
Abschnitt betitelt „Sharding über Cores“// 4-Worker-Mesh; Sharding verteilt Entities über sie:const startShardingOptions = StartShardingOptions.create() .withTypeName('order') .withEntityActor(...) .withExtractEntityId((message) => message.id) .withNumShards(16);sharding.start( startShardingOptions,);Der Koordinator (auf main) allokiert Shards auf die 4 Worker. CPU-gebundene Entity-Arbeit parallelisiert über Cores.
Per-Worker-Isolation
Abschnitt betitelt „Per-Worker-Isolation“// Worker, der GPU-gebundene Jobs handhabt:system.spawn(GpuJobActor, 'gpu-jobs');
// Abstürze in diesem Worker bleiben isoliert von main + anderen WorkernEin abstürzender Worker reißt das Hauptsystem nicht mit — separate Event-Loops.
Wann NICHT verwenden
Abschnitt betitelt „Wann NICHT verwenden“Wohin als Nächstes
Abschnitt betitelt „Wohin als Nächstes“- Cluster-Überblick — das Cluster-Modell, an dem das Worker-Mesh teilnimmt.
- Transports — die Transport-Schnittstelle, die MessageChannelTransport implementiert.
- Sharding — der Hauptnutzer der Mesh-Parallelität.
