Transports
Der Cluster-Transport ist die Leitung zwischen Cluster-Nodes —
er liefert Gossip-Nachrichten, Heartbeats und Anwendungs-Envelopes
(deine tells an Remote-Actors). Zwei Implementierungen liegen
dem Framework bei:
| Transport | Verwendung |
|---|---|
TcpTransport | Produktion. Echte TCP-Sockets, optional TLS. |
InMemoryTransport | Tests. Schleift Frames durch In-Process-JS-Strukturen — kein Networking. |
Beide implementieren dieselbe Transport-Schnittstelle, sodass das
Cluster-Verhalten unabhängig vom eingesteckten Transport identisch
ist.
Die Schnittstelle
Abschnitt betitelt „Die Schnittstelle“interface Transport { readonly self: NodeAddress; start(): Promise<void>; shutdown(): Promise<void>; setHandler(handler: (from: NodeAddress, message: WireMessage) => void): void; send(to: NodeAddress, message: WireMessage): void; disconnect(peer: NodeAddress): void; peers(): NodeAddress[];}Kleine Oberfläche — Bootstrap, Senden, Empfangen, Trennen. Der Cluster steckt einen Handler ein und bekommt einen Strom eingehender Wire-Nachrichten samt Absenderadresse.
TcpTransport (Standard)
Abschnitt betitelt „TcpTransport (Standard)“import { Cluster, ClusterOptions, TcpTransport } from 'actor-ts';
const clusterOptions = ClusterOptions.create() .withHost('0.0.0.0') .withPort(2552) .withSeeds(['...']);const cluster = await Cluster.join( system, clusterOptions, // transport defaultet auf TcpTransport — kein expliziter Pass nötig);Was er tut:
- Lauscht auf
host:portauf eingehende Verbindungen. - Verbindet sich nach Bedarf mit Peers (beim ersten Send oder zu Seeds bei Join-Zeit).
- Frame-Größen-Cap (Standard 16 MiB) — Frames, die größer sind, werden abgelehnt, um DoS via gefälschtem Längenpräfix zu verhindern.
- Auto-Reconnect — fällt eine Verbindung mitten im Cluster-Leben aus, wird beim nächsten Send neu verbunden.
Runtime-Backends
Abschnitt betitelt „Runtime-Backends“TcpTransport redet nicht direkt mit dem OS — er läuft über ein
TcpBackend-Interface mit jeweils einer Implementierung pro
Runtime:
| Runtime | Backend | Zugrundeliegende API |
|---|---|---|
| Bun | bunTcpBackend | Bun.listen / Bun.connect |
| Node | nodeTcpBackend | node:net |
| Deno | denoTcpBackend | Deno.listen / Deno.connect |
Automatisch erkannt über getTcpBackend(). Normalerweise denkst du
nicht darüber nach — derselbe TcpTransport läuft auf jeder
Runtime.
Optionales TLS
Abschnitt betitelt „Optionales TLS“import { Cluster, ClusterOptions, TcpTransport } from 'actor-ts';
const transport = new TcpTransport( NodeAddress.parse('actor-ts://my-app@10.0.0.5:2552'), system.log, { cert: '...', // PEM key: '...', // PEM ca: '...', // optionales CA-Bundle rejectUnauthorized: true, },);
const clusterOptions = ClusterOptions.create() .withHost(host) .withPort(port) .withSeeds(seeds) .withTransport(transport);await Cluster.join( system, clusterOptions,);TLS-gewrapptes TCP, alles-oder-nichts pro Cluster. Siehe Cluster-Sicherheit für das Produktionsrezept.
Frame-Größe
Abschnitt betitelt „Frame-Größe“new TcpTransport(self, log, null, 64 * 1024 * 1024); // 64 MiB max FrameÜberschreibe den Frame-Größen-Cap pro Frame. Standard 16 MiB reicht für typischen Cluster-Traffic (Gossip, Heartbeats, kleine Envelopes). Größere Werte verbessern nicht den allgemeinen Durchsatz — sie sind nur für einzelne große Nachrichten relevant.
InMemoryTransport (Tests)
Abschnitt betitelt „InMemoryTransport (Tests)“import { InMemoryTransport, NodeAddress, Cluster, ClusterOptions } from 'actor-ts';import { TestKit } from 'actor-ts/testkit';
// Kein Bus zu verdrahten — In-Memory-Transports finden einander über eine// prozessglobale Registry, indiziert nach der eigenen NodeAddress jedes Transports.const tk1 = TestKit.create('node-1');const tk2 = TestKit.create('node-2');
// Die self-Adresse jedes Transports muss dem system@host:port seines Knotens entsprechen.const clusterOptions = ClusterOptions.create() .withHost('1') .withPort(0) .withSeeds(['1:0']) .withTransport(new InMemoryTransport(new NodeAddress('node-1', '1', 0)));await Cluster.join( tk1.system, clusterOptions,);
const cluster2Options = ClusterOptions.create() .withHost('2') .withPort(0) .withSeeds(['1:0']) .withTransport(new InMemoryTransport(new NodeAddress('node-2', '2', 0)));await Cluster.join( tk2.system, cluster2Options,);Die Factory inMemoryTransport(system, host, port) ist eine Kurzform für
dieselbe Konstruktion — sie leitet die NodeAddress aus system.name ab.
Wie es funktioniert:
- Eine prozessglobale Registry routet Nachrichten zwischen In-Process-Transports.
- Jeder Transport registriert sich beim Start per Adresse in der Registry.
send(to, message)schlägt den Empfänger in der Registry nach und ruft dessen Handler direkt auf — keine Sockets, keine Serialisierung zu Bytes.
Wird von MultiNodeSpec — dem Multi-Node-Test-Harness — verwendet, um Multi-Node-Cluster in einem Prozess hochzufahren.
Was er nicht simuliert
Abschnitt betitelt „Was er nicht simuliert“- Netzwerkausfälle. Per Default liefert die Registry zuverlässig.
Für Fault Injection würdest du einen eigenen
Transportmit Drop-/Delay-/Reorder-Logik implementieren. - Latenz. Die Zustellung ist synchron innerhalb einer Event-Loop-Drehung.
- Serialisierung. Nachrichten werden per Referenz, nicht in
Bytes übergeben. Wenn dein Test tatsächlich Serialisierung üben
soll (z. B. CBOR-Codec testen), nutze stattdessen
TcpTransportüber Loopback.
Eigener Transport
Abschnitt betitelt „Eigener Transport“Transport gegen eine andere Drahtverbindung zu implementieren ist
selten, aber möglich. Beispiele:
- WebSocket-Transport — für Cluster-Teilnehmer im Browser (theoretisch; nicht implementiert).
- MessageChannel-Transport — für Cluster mit Worker-Threads in einem einzelnen OS-Prozess. Wird vom “Worker-Mesh”-Muster genutzt.
Die Schnittstelle ist klein genug, dass eine kompetente Implementierung ~200 Zeilen Code ist; die Schwierigkeit liegt darin, die Framing- und Heartbeat-Semantik zu treffen, die der Cluster erwartet.
Diagnostik
Abschnitt betitelt „Diagnostik“const peers = transport.peers(); // aktuell verbundene AdressenDer Transport legt keine Metriken pro Verbindung direkt offen — nutze die Metrik-Extension des Clusters, um Verbindungszahlen und gesendete/empfangene Bytes pro Peer zu bekommen.
Für tiefergehende Inspektion (spezifische Frame-Inhalte) aktiviere Debug-Logging am System:
const actorSystemOptions = ActorSystemOptions.create().withLogLevel(LogLevel.Debug);const system = ActorSystem.create('my-app', actorSystemOptions);// Suche nach [tcp-transport] Log-ZeilenMultiplexing
Abschnitt betitelt „Multiplexing“Eine einzige TCP-Verbindung zwischen zwei Nodes trägt:
- Gossip-Nachrichten — Cluster-Mitgliedschaftsaustausche.
- Heartbeat-Nachrichten — Failure Detection.
- Envelope-Nachrichten — deine
tells, mit Routing-Info kodiert. - Subsystem-Nachrichten — Sharding-Protokoll, PubSub-Gossip, DistributedData-Replikation.
Alles auf denselben TCP-Stream gemultiplext. Es gibt kein Prioritäts-Routing — Heartbeats und dein Bulk-Traffic teilen sich die Leitung. Für die meisten Workloads ist das in Ordnung; für explizite Isolation (Bandbreite für Cluster-Steuerung reservieren) bräuchte man einen eigenen Transport mit Per-Channel-Framing.
Wohin als Nächstes
Abschnitt betitelt „Wohin als Nächstes“- Cluster-Überblick — was auf dem Transport reitet.
- Refs über Nodes hinweg — wie Envelopes Actor-Refs für die Cross-Node-Zustellung kodieren.
- Cluster-Sicherheit — TLS + Auth.
- Worker-Mesh — MessageChannel-basierter Transport für In-Process-Worker.
- MultiNodeSpec —
nutzt
InMemoryTransportfür Multi-Node-Tests.
