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.
Eines erstellen
Abschnitt betitelt „Eines erstellen“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).
Konfiguration
Abschnitt betitelt „Konfiguration“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:
| Feld | Zweck |
|---|---|
logger | Eigene Logger-Instanz. Standardmäßig ein Console-Logger, der logLevel respektiert. |
logLevel | Einer von debug / info / warn / error / off. |
dispatcher | Eigener Dispatcher. Standardmäßig der Immediate-Dispatcher; tausche einen Microtask- oder Throughput-basierten ein, um das Scheduling anzupassen. |
scheduler | Eigener Scheduler. Standardmäßig ein Echtzeit-Scheduler; Tests injizieren ManualScheduler, um die Zeit zu kontrollieren. |
config | Entweder eine vorgefertigte Config oder ein einfaches Objekt mit HOCON-Overrides. Wird über die Referenz-Defaults + einer eventuellen application.conf gelegt. |
configFile | Expliziter 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.
HOCON-Config-Dateien
Abschnitt betitelt „HOCON-Config-Dateien“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.
Actors spawnen
Abschnitt betitelt „Actors spawnen“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.
Die Guardian-Hierarchie
Abschnitt betitelt „Die Guardian-Hierarchie“Jeder Actor hat einen Pfad unter dem System-Root. Drei “Guardian”-Top-Level-Actors sitzen direkt unter dem Root:
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. Jede davon wird als DeadLetter auf dem
Event-Stream veröffentlicht und benennt den Empfänger, den sie nicht
erreicht hat — und das ist alles, was standardmäßig passiert:
nichts loggt sie und nichts bewahrt sie auf, ohne Abonnent ist die
Nachricht also weg. Abonniere, um programmatisch zu reagieren, oder
schalte die begrenzte — optional dauerhafte — Queue ein, die sie
aufbewahrt und erneut zustellt; siehe
Dead Letters.
Extensions
Abschnitt betitelt „Extensions“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 } from 'actor-ts/cluster';import { DistributedDataId } from 'actor-ts/crdt';
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.
Eine eigene Extension schreiben
Abschnitt betitelt „Eine eigene Extension schreiben“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).
Terminieren
Abschnitt betitelt „Terminieren“await system.terminate();terminate führt einen geordneten Shutdown durch:
- Cluster benachrichtigen (falls beigetreten) — “ich verlasse” gossipen, damit Peers nicht mehr zu diesem Node routen.
/userleeren — warten, bis dort kein Actor mehr einen Turn laufen oder eine Nachricht in der Queue hat, sodassref.tell(x); await system.terminate()dasxnoch verarbeitet. Das Warten ist transitiv: eine Zelle gilt in dem Moment als beschäftigt, in dem ihr jemand etwas sendet, also halten Antworten, Router-Fan-outs und Restarts den Drain am Laufen, statt jede Mailbox nur einmal durchzuspülen./userrekursiv stoppen — deine Actors bekommenpostStop, Kinder zuerst. Actors mit laufenden asynconReceives beenden ihre aktuelle Nachricht, bevor sie stoppen./systemstoppen — Framework-Internas wickeln sich ab. Das beginnt erst, wenn/uservollständig geleert ist, sodass einpostStopaus Schritt 3 noch mit den Sharding-, PubSub- oder Delivery-Actors reden kann, von denen es abhängt.- Dispatcher und Scheduler schließen — keine neuen Nachrichten, keine neuen Timer.
- Das zurückgegebene Promise resolven.
Der Drain in Schritt 2 ist durch
actor-ts.system.shutdown-drain-timeout begrenzt (Standard 2 s) und
kehrt sofort zurück, sobald der Baum ruhig ist — ein untätiges System
zahlt einen Tick, nicht das Budget. Setze ihn auf 0, um das Leeren
ganz zu überspringen; was beim Ablauf des Budgets noch in der Queue
steht, landet über Schritt 3 in den Dead Letters — genau das passierte
vor dem Drain mit dem gesamten Rückstau.
Auf drei Dinge wird bewusst nicht gewartet:
- eine durch
context.throttle(...)geparkte Mailbox und eine, die suspendiert ist, während ein Supervisor entscheidet — keine von beiden leert sich in einem Tempo, auf das ein Shutdown warten kann, also gelten beide als ruhig; - alles unter
/system— Heartbeats, Failure Detectors und Broker-Reconnect-Schleifen sind konstruktionsbedingt nie ruhig; - Arbeit, die noch in keiner Mailbox liegt — ein noch nicht gefeuerter
context.timers-Tick oder eintellaus einem Promise, das ein Handler ohneawaitgestartet hat.
Um einen Actor nach dem Leeren seiner Mailbox zu stoppen und
darauf zu warten, nimm gracefulStop(ref, timeoutMs) — siehe
PoisonPill & Kill.
Laufen, bis der Prozess zum Stoppen aufgefordert wird
Abschnitt betitelt „Laufen, bis der Prozess zum Stoppen aufgefordert wird“Für einen Service ist terminate() nichts, was du aufrufst — es ist
das, was am Ende passiert. Die ganze main nach dem Verdrahten der
Actors lautet:
await system.runUntilTerminated();Das installiert SIGTERM/SIGINT-Handler, löst auf, sobald das System
unten ist, und hängt die Handler auf dem Weg hinaus wieder ab. Ein
handgeschriebenes process.on('SIGTERM', () => system.terminate())
sieht gleichwertig aus und ist es nicht: es stoppt zuerst die
Actors und gibt erst danach — falls du daran gedacht hast, es zu
schreiben — den HTTP-Port frei und verlässt den Cluster.
runUntilTerminated() führt die Pipeline aus
Coordinated Shutdown aus,
die Listener abbindet, Broker schließt und den Cluster verlässt,
bevor die Actors stoppen — ein Rolling Deploy nimmt den Knoten
also aus der Rotation, während seine Actors noch zu Ende bringen
können, was sie halten.
Es ist außerdem die Variante, die überall funktioniert: Deno stellt
Signale über Deno.addSignalListener zu, nicht über process.on, und
ein Listener hält dort die Event-Loop am Leben, bis er entfernt wird.
Wie viele Systeme pro Prozess?
Abschnitt betitelt „Wie viele Systeme pro Prozess?“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
MessageChannelTransportauf. Das ist das Worker-Mesh-Pattern — mehrere Systeme pro OS-Prozess, alle Teil desselben Clusters. - Test-Fixtures: ein
TestActorSystempro Testfall, damit das Cleanup garantiert ist. Siehe TestKit.
Häufige Fallstricke
Abschnitt betitelt „Häufige Fallstricke“Wie es weitergeht
Abschnitt betitelt „Wie es weitergeht“- 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.
