Actors erzeugen
Zum Erzeugen gehören zwei Dinge: was gebaut wird, und — nur wenn die Voreinstellungen nicht passen — wie es laufen soll.
import { ActorSystem } from 'actor-ts';
const system = ActorSystem.create('hello');
system.spawn(Greeter, 'greeter'); // die Klasse selbstsystem.spawn(() => new Worker(database), 'worker-1'); // eine FactoryDas ist der gesamte Normalfall. Voreingestellt sind eine unbeschränkte FIFO-Mailbox, der System-Dispatcher und die Supervision des Elternteils.
Klassenform und Factory-Form
Abschnitt betitelt „Klassenform und Factory-Form“Übergib die Klasse, wenn ihr Konstruktor keine Argumente nimmt. Mehr braucht es nicht — das Framework konstruiert selbst eine frische Instanz.
class Greeter extends Actor<string> { override onReceive(name: string): void { console.log(`hello ${name}`); }}
system.spawn(Greeter, 'greeter');Übergib eine Factory, wenn der Actor Abhängigkeiten braucht. Die Closure fängt sie ein, und jeder Neustart sieht dieselbe Verdrahtung:
class Worker extends Actor<WorkerMessage> { constructor(private readonly database: ActorRef<DatabaseMessage>) { super(); }}
system.spawn(() => new Worker(database), 'worker-1');Eine Klasse, deren Konstruktor Argumente verlangt, wird an der
Aufrufstelle abgelehnt — sie allein zu übergeben könnte sie nur mit
undefined-Abhängigkeiten konstruieren:
system.spawn(Worker, 'worker-1');// ✗ Worker braucht ein Konstruktorargument, die Klasse allein reicht// also nicht — übergib eine Factory, die es liefert:// () => new Worker(database).Warum eine Factory — und niemals eine Instanz
Abschnitt betitelt „Warum eine Factory — und niemals eine Instanz“Keine der beiden Formen lässt eine Actor-Instanz zu, und das ist Absicht: der Actor wird bei jedem Neustart neu gebaut.
// Abgelehnt — eine Instanz ist weder Klasse noch Factory:const counter = new Counter();system.spawn(counter, 'counter');
// Richtig, und bei jedem Neustart frisch:system.spawn(Counter, 'counter');Dieselbe Instanz über einen Neustart hinweg weiterzuverwenden heißt, dass der Actor sich nie zurücksetzt — er erbt den kaputten Zustand des gescheiterten Laufs, also genau das Gegenteil dessen, wofür „let it crash” da ist.
Benennung
Abschnitt betitelt „Benennung“Der Name geht an spawn, er hängt nie am Actor:
system.spawn(Greeter, 'greeter-1'); // → /user/greeter-1system.spawn(Greeter, 'greeter-2'); // → /user/greeter-2this.context.spawn(Greeter, 'sub-greeter'); // → /user/<this>/sub-greeterNamen müssen unter Geschwistern eindeutig sein. Für einen Actor, dessen
Pfad egal ist — einmalige asynchrone Arbeit, Wegwerf-Helfer — nimm
spawnAnonymous; es erzeugt $anonymous-<n>-<random>:
const ref = system.spawnAnonymous(Counter);ActorOptions
Abschnitt betitelt „ActorOptions“Alles jenseits der Voreinstellungen steht im dritten Argument. Gebaut wird es mit dem fluenten Builder:
import { ActorOptions, stoppingStrategy, MicrotaskDispatcher, BoundedMailbox } from 'actor-ts';
const workerOptions = ActorOptions.create<WorkerMessage>() .withSupervisorStrategy(stoppingStrategy) .withDispatcher(new MicrotaskDispatcher()) .withMailbox(() => new BoundedMailbox({ capacity: 100, overflow: 'drop-head' }));
system.spawn(() => new Worker(database), 'worker-1', workerOptions);Ein einfaches Objekt ist die Kurzform-Alternative und liest sich identisch:
system.spawn(() => new Worker(database), 'worker-1', { mailboxCapacity: 500 });class ActorOptionsBuilder<TMessage> { static create<TMessage>(): ActorOptionsBuilder<TMessage>;
withSupervisorStrategy(strategy: SupervisorStrategy): this; withDispatcher(dispatcher: Dispatcher): this; withMailboxCapacity(capacity: number): this; withMailbox(factory: () => Mailbox<TMessage>): this; withInternal(internal?: boolean): this; withDisplayName(displayName: string): this; withEntity(entity: EntityContext): this;}withSupervisorStrategy
Abschnitt betitelt „withSupervisorStrategy“import { ActorOptions, OneForOneStrategy, Directive, stoppingStrategy } from 'actor-ts';
const workerOptions = ActorOptions.create().withSupervisorStrategy(stoppingStrategy);
const databaseOptions = ActorOptions.create() .withSupervisorStrategy(new OneForOneStrategy( (error) => error instanceof TransientError ? Directive.Resume : Directive.Restart, { maxRetries: 5, withinTimeRangeMs: 60_000 }, ));Die hier gesetzte Strategie entscheidet, was passiert, wenn DIESER Actor scheitert. Im Leben jedes Actors sind zwei Strategien im Spiel:
- Die in den Spawn-Options dieses Actors — vom Elternteil auf dessen eigene Fehler angewandt.
- Die in der Klasse dieses Actors (
override supervisorStrategy) — auf die Fehler seiner Kinder angewandt.
Die meisten Actors brauchen keine von beiden: die Voreinstellung des
Elternteils (oder defaultStrategy an der Systemwurzel) startet mit
einer Obergrenze von 10 pro Minute neu. Greif hierzu, wenn das falsch
ist — etwa bei einem Worker, der eher gestoppt als neu gestartet werden
soll, weil sein Elternteil ohnehin Ersatz erzeugt.
Der Elternteil löst die Strategie in dieser Reihenfolge auf, der erste Treffer gewinnt:
- Die Spawn-Options des scheiternden Kindes.
- Der
supervisorStrategy()-Override des Eltern-Actors. defaultStrategy.
Ein einzelnes Kind kann sich so aus der Politik des Elternteils
ausklinken, ohne seine Geschwister zu berühren. Zwei Details folgen
daraus, dass die Strategie vom Elternteil angewandt wird: eine
all-for-one-Strategie an einem Kind weitet sich trotzdem auf alle
Geschwister aus, und das Neustart-Budget (maxRetries /
withinTimeRangeMs) wird pro Elternteil gezählt — Geschwister teilen
sich also ein Kontingent.
Die vollständige Semantik steht unter Supervision.
withDispatcher
Abschnitt betitelt „withDispatcher“import { ActorOptions, MicrotaskDispatcher, ThroughputDispatcher } from 'actor-ts';
const crunchyOptions = ActorOptions.create().withDispatcher(new MicrotaskDispatcher());const bulkOptions = ActorOptions.create().withDispatcher(new ThroughputDispatcher(100));Überschreibt den systemweiten Dispatcher für einen Actor. Nützlich, wenn ein einzelner Actor CPU-lastig ist und von Microtask-Scheduling profitiert, oder latenzempfindlich ist, während der Rest des Systems auf einem Durchsatz-Dispatcher läuft. In den meisten Anwendungen gilt der System-Dispatcher einheitlich und man braucht das nie — siehe Dispatcher.
withMailboxCapacity
Abschnitt betitelt „withMailboxCapacity“import { ActorOptions } from 'actor-ts';
const consumerOptions = ActorOptions.create().withMailboxCapacity(500);Beschränkt die Mailbox, die sonst unbeschränkt ist — dieser Aufruf ist
also der, der Nachrichtenverlust einführt. drop-head gilt, solange du
nichts anderes sagst: eine volle Mailbox verwirft die älteste
wartende Nachricht, um Platz zu schaffen, ohne zu werfen. Ergänze
withMailboxOverflow('drop-new'), um stattdessen die ankommende zu
verwerfen, oder 'reject', um an der tell-Stelle MailboxFullError
zu werfen, damit der Sender den Druck spürt.
withMailbox
Abschnitt betitelt „withMailbox“import { ActorOptions, BoundedMailbox, PriorityMailbox } from 'actor-ts';
const telemetryOptions = ActorOptions.create() .withMailbox(() => new BoundedMailbox({ capacity: 1_000, overflow: 'drop-head' }));
const workerOptions = ActorOptions.create<Message>() .withMailbox(() => new PriorityMailbox<Message>({ priorityFor: (message) => message.kind === 'urgent' ? 0 : 5, }));Volle Kontrolle — gib eine beliebige Mailbox-Unterklasse zurück. Die
Factory wird einmal aufgerufen, beim Konstruieren der Zelle des
Actors; die Mailbox überlebt danach jeden Neustart. Bei einem Neustart
wird nur die Actor-Instanz neu gebaut — dieselbe Mailbox läuft weiter,
vor dem Absturz eingereihte Nachrichten sind also noch da.
Die Überlaufstrategien und die Prioritätssemantik stehen unter Mailboxes.
withInternal
Abschnitt betitelt „withInternal“Markiert einen Actor als Werkzeug statt als Anwendung, und seine Kinder gleich mit. Systemweite Instrumentierung überspringt ihn: ohne die Markierung tracet der DevTools-Hub — der die eben aufgezeichneten Spans selbst publiziert — sich selbst, und jeder Batch kommt als Nutzlast des nächsten zurück.
import { ActorOptions } from 'actor-ts';
const probeOptions = ActorOptions.create().withInternal();
system.spawn(MyProbe, 'my-probe', probeOptions);Man braucht das nur, wenn man ein Werkzeug schreibt, das das System beobachtet, in dem es läuft. Anwendungs-Actors sollten nicht markiert werden: echte Arbeit vor dem Profiler zu verstecken ist der Weg, auf dem ein Performance-Problem unsichtbar bleibt.
withDisplayName
Abschnitt betitelt „withDisplayName“Benennt den Actor in Logzeilen und im DevTools-Actor-Baum. Das Gegenstück
an der Spawn-Stelle zum Überschreiben von Actor.displayName() — dafür da,
wenn der Actor keine eigene Unterklasse hat, die man überschreiben könnte:
ein Behaviors-Actor, eine Sharding-Entity, ein Singleton.
const workerOptions = ActorOptions.create().withDisplayName('ingest-worker');
system.spawn(IngestWorker, 'ingest', workerOptions);Sticht die Methode, genau wie withSupervisorStrategy Actor.supervisorStrategy()
sticht; context.setDisplayName(...) zur Laufzeit sticht beide. Rein
kosmetisch — der Pfad bleibt überall dort die Identität, wo geroutet oder
korreliert wird, Metrik-Labels, Tracing-Attribute und jeder Cluster-Wire-Bezeichner
bleiben also unberührt.
Wie der Name in einen Log-Record gelangt, steht unter Logging.
withEntity
Abschnitt betitelt „withEntity“Gibt einem Actor eine Sharding-Identität, die über this.entityId /
this.context.entity zurücklesbar ist. ClusterSharding setzt sie für
jede Entity selbst, die ein Shard erzeugt; öffentlich ist sie für den
Prüfstand, wo eine Entity, die ihre persistenceId aus this.entityId
ableitet, sonst ohne dahinterstehenden Cluster gar nicht erzeugbar wäre.
import { ActorOptions } from 'actor-ts';
const cartOptions = ActorOptions.create() .withEntity({ entityId: 'user-42', typeName: 'cart', shardId: 3 });
const cart = system.spawn(CartEntity, 'cart-under-test', cartOptions);Gängige Muster
Abschnitt betitelt „Gängige Muster“Ein Helfer für eine Actor-Familie
Abschnitt betitelt „Ein Helfer für eine Actor-Familie“import { ActorOptions, type ActorFactory } from 'actor-ts';
const workerOptions = ActorOptions.create<WorkerMessage>() .withSupervisorStrategy(stoppingStrategy) .withMailboxCapacity(500);
const worker = (database: ActorRef<DatabaseMessage>): ActorFactory<WorkerMessage> => () => new Worker(database);
for (let i = 0; i < 8; i++) { this.context.spawn(worker(database), `worker-${i}`, workerOptions);}Das Options-Objekt ist für jeden damit erzeugten Actor wiederverwendbar — es wird pro Spawn kopiert, die acht Worker teilen sich also keinen Zustand.
Typisierte Actors
Abschnitt betitelt „Typisierte Actors“Die typisierte API erzeugt ein Behavior statt einer Klasse oder
Factory:
const ref = system.spawnTyped(counter(0), 'counter');const anonymous = system.spawnTypedAnonymous(counter(0));Um ein Behavior an eine API zu geben, die einen Actor nimmt — Router,
ClusterSharding.start —, wickle es mit typedActor(behavior) ein.
Siehe Typisierte Actors.
Wie geht es weiter
Abschnitt betitelt „Wie geht es weiter“- Actor — die Basisklasse, deren Konstruktor aufgerufen wird.
- Supervision — die Strategie für
withSupervisorStrategy. - Dispatcher — der Scheduler für
withDispatcher. - Mailboxes — die Queue für
withMailbox/withMailboxCapacity. - ActorSystem — das
system.spawn(actor, name, options?), das all das verbraucht.
