Zum Inhalt springen
Deutsch

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 selbst
system.spawn(() => new Worker(database), 'worker-1'); // eine Factory

Das ist der gesamte Normalfall. Voreingestellt sind eine unbeschränkte FIFO-Mailbox, der System-Dispatcher und die Supervision des Elternteils.

Ü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).

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.

Der Name geht an spawn, er hängt nie am Actor:

system.spawn(Greeter, 'greeter-1'); // → /user/greeter-1
system.spawn(Greeter, 'greeter-2'); // → /user/greeter-2
this.context.spawn(Greeter, 'sub-greeter'); // → /user/<this>/sub-greeter

Namen 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);

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;
}
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:

  1. Die Spawn-Options des scheiternden Kindes.
  2. Der supervisorStrategy()-Override des Eltern-Actors.
  3. 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.

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.

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.

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.

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.

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.

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);
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.

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.

  • 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.