Zum Inhalt springen
Deutsch

ActorSystem

Das ActorSystem ist der Top-Level-Container für Actors. Eines pro logischer Anwendung — manchmal eines pro Prozess, manchmal ein paar, die nebeneinander laufen (z.B. ein Worker-Thread-Isolations-Setup). Jeder Actor lebt innerhalb eines Systems; das System besitzt den Dispatcher (der die Nachrichtenverarbeitung plant), den Scheduler (der Timer ausführt), den Supervisionsbaum (der Actor-Fehler abfängt), den Event-Stream und alle Extensions, die du registriert hast.

import { ActorSystem } from 'actor-ts';
const system = ActorSystem.create('my-app');

Der String ist der Name des Systems — er erscheint in Actor-Pfaden (actor-ts://my-app/user/...), Log-Zeilen und der Cluster-Identifikation. Verschiedene Systeme können mit verschiedenen Namen koexistieren; derselbe Name in einem Cluster-Setup bedeutet “ich trete dem bestehenden Cluster bei”, ein anderer Name bedeutet “ich bin ein separater Cluster”.

create kehrt synchron zurück. Die Root-Guardians des Systems werden eifrig erzeugt; User-Actors existieren noch nicht — du spawnst sie über spawn (unten beschrieben).

ActorSystem.create nimmt ein optionales Settings-Objekt als zweites Argument:

const actorSystemOptions = ActorSystemOptions.create()
.withLogLevel(LogLevel.Info)
.withConfigFile('./application.conf');
const system = ActorSystem.create('my-app', actorSystemOptions);

Die vollständige Settings-Form:

FeldZweck
loggerEigene Logger-Instanz. Standardmäßig ein Console-Logger, der logLevel respektiert.
logLevelEiner von debug / info / warn / error / off.
dispatcherEigener Dispatcher. Standardmäßig der Immediate-Dispatcher; tausche einen Microtask- oder Throughput-basierten ein, um das Scheduling anzupassen.
schedulerEigener Scheduler. Standardmäßig ein Echtzeit-Scheduler; Tests injizieren ManualScheduler, um die Zeit zu kontrollieren.
configEntweder eine vorgefertigte Config oder ein einfaches Objekt mit HOCON-Overrides. Wird über die Referenz-Defaults + einer eventuellen application.conf gelegt.
configFileExpliziter Pfad zu einer application.conf-Datei. Überschreibt die ACTOR_TS_CONFIG-Env-Variable und das CWD-Lookup.

Konstruktor-Settings gewinnen immer gegenüber allem in der Config — sie sind die expliziten Code-Level-Overrides.

Für größere Anwendungen bevorzuge eine application.conf-Datei im Projekt-Root:

actor-ts {
logger {
level = "info"
}
dispatcher {
throughput = 100
}
cluster {
gossip-interval = 500ms
failure-detector.unreachable-after = 1500ms
}
}

Das Framework lädt sie automatisch, wenn vorhanden. ENV-Substitution (${?ENV_NAME}) funktioniert wie in der HOCON-Spec definiert — aus der Umgebung gezogene Werte fallen auf den Default zurück, wenn sie nicht gesetzt sind. Siehe Konfiguration für jeden Schlüssel, den das Framework liest.

Top-Level-Actors werden über system.spawn gespawnt:

const root = system.spawn(
MyRootActor,
'root', // optionaler Name; Framework wählt einen, wenn weggelassen
);

Die zurückgegebene ActorRef ist ein Handle, keine Instanz. Gib es weiter, speichere es, übergib es an andere Actors.

Innerhalb eines Actors werden Child-Actors über context.spawn gespawnt, nicht über system.spawn:

class Parent extends Actor<...> {
override onReceive(message) {
const child = this.context.spawn(Child, 'worker');
}
}

Kinder sind an den Lebenszyklus des Parents gebunden — wenn der Parent stoppt, stoppen zuerst alle Kinder. Fehler von Kindern eskalieren an die Supervisor-Strategie des Parents. Top-Level-Actors (aus system.spawn) eskalieren stattdessen an den Root-Guardian des Systems.

Jeder Actor hat einen Pfad unter dem System-Root. Drei “Guardian”-Top-Level-Actors sitzen direkt unter dem Root:

actor-ts://my-app/

/user

Actors deiner Anwendung

/system

framework-interne Actors

/deadLetters

Nachrichten an tote Refs

/system/cluster

Sharding, Singleton, PubSub, …

/system/devtools

Hub + Probes

Wenn du system.spawn(actor, name) aufrufst, wird der Actor unter /user erzeugt. /user enthält nur, was dein Code erzeugt hat; alles, was das Framework für sich selbst erzeugt, liegt unter /system — eine Gruppe pro Subsystem, siehe Actor-Pfade für das vollständige Layout. Es gibt keine öffentliche API, um in /system zu spawnen.

Beim Terminieren gehen die beiden Guardians nacheinander, nicht gleichzeitig: /user wird vollständig geleert, dann /system. Diese Reihenfolge ist es, die einem postStop eines User-Actors erlaubt, die Framework-Actors zu erreichen, von denen er abhängt — sich beim PubSub-Mediator abmelden, einen Shard zurückgeben — statt mit ihnen um Dead Letters zu rennen.

Der /deadLetters-”Actor” ist speziell — Nachrichten an ein tell auf einer gestoppten Ref oder an eine nie existierte Ref werden dorthin geleitet. Standardmäßig loggt das System Dead Letters auf debug-Level; abonniere den Event-Stream, wenn du programmatisch reagieren willst.

Extensions sind das Plugin-System des Frameworks. Cluster, Persistenz, DistributedData, DistributedPubSub, HTTP — sie sind alle Extensions. Du registrierst sie einmal auf System-Ebene und erreichst sie dann über system.extension(...):

import { Cluster, ClusterOptions, DistributedDataId } from 'actor-ts';
const cluster = await Cluster.join(system, ClusterOptions.create() /* ... */);
const dd = system.extension(DistributedDataId).start(cluster);

Extensions sind lazy: sie initialisieren sich nicht, bis du nach ihnen greifst. Eine App, die nie system.extension(DistributedDataId) aufruft, startet nie einen DD-Replicator. Das hält Single-Process-Apps klein; übernimm Features, indem du nach ihnen greifst, lass sie weg, indem du es nicht tust.

import { extensionId, type Extension, type ExtensionId } from 'actor-ts';
class MetricsCollector implements Extension {
constructor(private readonly system: ActorSystem) {}
incCounter(name: string): void { /* ... */ }
}
const MetricsCollectorId: ExtensionId<MetricsCollector> = extensionId(
'MetricsCollector',
(system) => new MetricsCollector(system),
);
// Lookup ist idempotent — der erste Aufruf erzeugt, folgende Aufrufe
// geben die gecachte Instanz zurück.
const metrics = system.extension(MetricsCollectorId);
metrics.incCounter('login.success');

Extensions sind nützlich, wenn:

  • Du übergreifenden Zustand brauchst, der von vielen Actors geteilt wird (ein Connection-Pool, ein Metrics-Collector).
  • Der Zustand teuer zu initialisieren ist und nicht existieren sollte, wenn niemand danach greift (ein Cluster-Join, ein DD-Replicator).
  • Du eine saubere Möglichkeit willst, Test-Doubles in Unit-Tests zu injizieren (überschreibe den ExtensionId-Resolver).
await system.terminate();

terminate führt einen geordneten Shutdown durch:

  1. Cluster benachrichtigen (falls beigetreten) — “ich verlasse” gossipen, damit Peers nicht mehr zu diesem Node routen.
  2. /user rekursiv stoppen — deine Actors bekommen postStop, Kinder zuerst. Actors mit laufenden async onReceives beenden ihre aktuelle Nachricht, bevor sie stoppen.
  3. /system stoppen — Framework-Internas wickeln sich ab. Das beginnt erst, wenn /user vollständig geleert ist, sodass ein postStop aus Schritt 2 noch mit den Sharding-, PubSub- oder Delivery-Actors reden kann, von denen es abhängt.
  4. Dispatcher und Scheduler schließen — keine neuen Nachrichten, keine neuen Timer.
  5. Das zurückgegebene Promise resolven.

Für Produktions-Apps wickelst du das typischerweise in einen SIGTERM-Handler:

process.on('SIGTERM', async () => {
await system.terminate();
process.exit(0);
});

…aber das Framework bietet ein reicheres Pattern dafür — siehe Coordinated Shutdown für das 12-phasige Ordered-Shutdown-DSL, das K8s-PreStop-Hooks, laufende HTTP-Requests, das Drainen von Brokern usw. handhabt.

Die übliche Antwort ist eins. Ein zweites System im selben Prozess bedeutet einen separaten Cluster, einen separaten Dispatcher, einen separaten Supervisionsbaum — typischerweise mehr Overhead, als der Use Case rechtfertigt.

Zwei Situationen, in denen ein zweites System Sinn macht:

  • Worker-Thread-Isolation: der Hauptthread läuft mit einem System, ein Worker-Thread mit einem anderen, beide spannen denselben Cluster über den MessageChannelTransport auf. Das ist das Worker-Mesh-Pattern — mehrere Systeme pro OS-Prozess, alle Teil desselben Clusters.
  • Test-Fixtures: ein TestActorSystem pro Testfall, damit das Cleanup garantiert ist. Siehe TestKit.
  • Actor — die Klasse, die du in das System spawnst.
  • Coordinated Shutdown — Graceful-Shutdown-DSL jenseits eines einfachen terminate.
  • Cluster-Überblick — wenn du von einem System pro Prozess zu vielen Systemen in einem Cluster gehst.
  • Konfiguration — jeder HOCON-Schlüssel, den das Framework liest, gruppiert nach Extension.

Die ActorSystem-Klassen-API-Referenz dokumentiert jede hier diskutierte öffentliche Methode.