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 system = ActorSystem.create('my-app', {
logLevel: 'info',
configFile: './application.conf',
});

Die vollständige Settings-Form:

FeldZweck
loggerEigene Logger-Instanz. Standardmäßig ein Console-Logger, der logLevel respektiert.
logLevelEiner von debug / info / warn / error / silent.
dispatcherEigener Dispatcher. Standardmäßig ein Microtask-basierter Dispatcher; Tests tauschen typischerweise einen Immediate- oder Manual-Dispatcher ein.
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 {
log-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:

import { Props } from 'actor-ts';
const root = system.spawn(
Props.create(() => new 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(msg) {
const child = this.context.spawn(
Props.create(() => new 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

Wenn du system.spawnAnonymous(props) aufrufst, wird der Actor unter /user erzeugt. Wenn das System terminiert, stoppen die Guardians in umgekehrter Reihenfolge nacheinander: User-Actors zuerst (damit sie ihre Arbeit beenden können), dann die System-Internas.

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, DistributedDataId } from 'actor-ts';
const cluster = await Cluster.join(system, { /* ... */ });
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 { type Extension, type ExtensionId } from 'actor-ts';
class MetricsCollector implements Extension {
constructor(private readonly system: ActorSystem) {}
incCounter(name: string): void { /* ... */ }
}
const MetricsCollectorId: ExtensionId<MetricsCollector> = {
name: 'MetricsCollector',
create: (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.
  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.