Become und Stash
Das onReceive eines Actors ist normalerweise eine einzige Funktion,
die jeden Nachrichten-Kind behandelt. Zwei Context-APIs lassen ihn
diese Verarbeitung im Fluge umformen:
context.become(handler)— den aktuellen Receive-Handler temporär durch einen anderen ersetzen. Mitunbecome()wiederherstellen.context.stash()+unstashAll()— Nachrichten, für die ein Actor noch nicht bereit ist, parken und sie dann erneut ausliefern, wenn er bereit ist.
Zusammen sind sie die Antwort des Actor-Modells auf “dieser Actor hat Phasen, in denen er keinen normalen Traffic akzeptiert” — Initialisierung, Recovery von einem Checkpoint, Drainen vor Shutdown, Warten auf einen async-Config-Load.
become — das Verhalten austauschen
Abschnitt betitelt „become — das Verhalten austauschen“import { Actor, ActorSystem, Props } from 'actor-ts';
type Msg = | { readonly kind: 'on' } | { readonly kind: 'off' } | { readonly kind: 'press' };
class Switch extends Actor<Msg> { override onReceive(msg: Msg): void { this.offState(msg); }
private offState = (msg: Msg): void => { if (msg.kind === 'press') { this.log.info('turning on'); this.context.become(this.onState); } };
private onState = (msg: Msg): void => { if (msg.kind === 'press') { this.log.info('turning off'); this.context.become(this.offState); } };}
const system = ActorSystem.create('demo');const sw = system.spawnAnonymous(Props.create(() => new Switch()));
sw.tell({ kind: 'press' }); // → "turning on"sw.tell({ kind: 'press' }); // → "turning off"sw.tell({ kind: 'press' }); // → "turning on"become(handler) tauscht die Funktion aus, die das Framework bei der
nächsten Nachricht aufruft. Keine Flag-Felder, keine inline
if (this.state === 'on')-Leiter — das Verhalten ist der Zustand.
Die Signatur:
become(behavior: Receive<TMsg>, discardOld?: boolean): void;unbecome(): void;discardOld ist standardmäßig true (ersetzen). Übergib false,
um das neue Verhalten stattdessen auf einen Stack zu pushen —
unbecome() poppt es dann und stellt das vorherige wieder her.
Gestackte Verhaltensweisen
Abschnitt betitelt „Gestackte Verhaltensweisen“class Loader extends Actor<Cmd> { override onReceive(msg: Cmd): void { this.idle(msg); }
private idle = (msg: Cmd): void => { if (msg.kind === 'load') { this.context.become(this.loading, /* discardOld */ false); this.startLoading(); } };
private loading = (msg: Cmd): void => { if (msg.kind === 'done') { this.context.unbecome(); // poppt loading, idle kehrt zurück } };}Stacking ist das richtige Werkzeug, wenn du ein transientes
Sub-Verhalten hast (“gerade ladend”, “gerade authentifizierend”), das
zu einem stabilen Basis-Verhalten zurückkehrt, sobald es fertig ist.
Für Toggles wie das Switch-Beispiel oben ist discardOld = true (der
Default) sauberer.
Verglichen mit einem schlichten Zustands-Feld
Abschnitt betitelt „Verglichen mit einem schlichten Zustands-Feld“Ohne become würde derselbe Switch so aussehen:
class Switch extends Actor<Msg> { private isOn = false; override onReceive(msg: Msg): void { if (msg.kind === 'press') { this.isOn = !this.isOn; this.log.info(this.isOn ? 'turning on' : 'turning off'); } }}Zwei Zustände — die Plain-Field-Variante ist kürzer. Wo become
gewinnt, ist bei N Zuständen: mit drei oder vier Phasen wächst
die Inline-if/else-Kette auf isOn zu einer State-Machine-Leiter,
bei der das Typsystem dir nicht helfen kann. become macht jede
Phase zu ihrer eigenen Funktion, mit ihrem eigenen
Valid-Message-Set, natürlich verengt.
Für eine formellere Version derselben Idee siehe das FSM-Pattern — explizite Zustands- + Transition-Deklarationen auf derselben Engine.
stash — parken, dann replayen
Abschnitt betitelt „stash — parken, dann replayen“import { Actor, ActorSystem, Props } from 'actor-ts';import { match } from 'ts-pattern';
type Msg = | { readonly kind: 'configure'; readonly url: string } | { readonly kind: 'request'; readonly payload: string };
class Worker extends Actor<Msg> { private url?: string;
override onReceive(msg: Msg): void { match(msg) .with({ kind: 'configure' }, (m) => { this.url = m.url; // Jetzt drainen, was sich angesammelt hat, während wir unkonfiguriert waren. this.context.unstashAll(); }) .with({ kind: 'request' }, (m) => { if (!this.url) { // Noch nicht bereit — diese Nachricht parken. this.context.stash(); return; } this.log.info(`POST ${this.url}: ${m.payload}`); }) .exhaustive(); }}Der Fluss: Nachrichten, die vor configure ankommen, werden gestasht.
Sobald configure läuft, fügt unstashAll() sie in der
Ankunftsreihenfolge wieder vorne in die Mailbox ein, und der Actor
verarbeitet sie mit nun gesetzter URL.
Die Signaturen:
stash(): void;unstashAll(): void;readonly stashSize: number;Drei Details, die zählen:
stash()muss aus einem User-Nachrichten-Handler heraus aufgerufen werden. Es parkt die gerade behandelte Nachricht. Sie auspreStart, einem Timer-Callback, der keine Actor-Nachricht ist, oder außerhalb vononReceiveaufzurufen, wirftStashOutsideHandlerError.unstashAll()ist FIFO. Nachrichten kommen in der Reihenfolge zurück, in der sie gestasht wurden. Für einePriorityMailboxwerden sie über die Priority-Funktion erneut eingefügt — eine unstashed Nachricht reiht sich wieder in ihre Priority-Schicht ein, nicht an die absolute Front der Queue.- Der Stash hat eine Kapazität. Standardmäßig ist der Buffer
gebunden, um Memory-Leaks durch ausuferndes Stashing zu verhindern;
Overflow wirft
StashOverflowError. Konfiguriere über System-Settings, wenn du größere / kleinere brauchst; siehe Konfiguration.
stash + become — die kanonische Kombination
Abschnitt betitelt „stash + become — die kanonische Kombination“Die zwei APIs komponieren für das “Actor mit Init-Phase”-Pattern:
class Worker extends Actor<Msg> { private url?: string;
override preStart(): void { // Sofort in das "loading config"-Verhalten wechseln. this.context.become(this.loading); void this.loadConfig(); }
private loading = (msg: Msg): void => { if (msg.kind === 'configure') { this.url = msg.url; this.context.become(this.ready); this.context.unstashAll(); } else { // Alles andere, was während des Ladens ankommt — stashen. this.context.stash(); } };
private ready = (msg: Msg): void => { if (msg.kind === 'request') { this.log.info(`POST ${this.url}: ${msg.payload}`); } };
override onReceive(msg: Msg): void { // Initial-Verhalten — das preStart wechselt vor jeder // User-Nachricht in 'loading'. Das hier befriedigt die Signatur. this.loading(msg); }
private async loadConfig(): Promise<void> { const cfg = await fetchConfig(); this.context.self.tell({ kind: 'configure', url: cfg.url }); }}Zwei Verhaltensweisen: loading (stasht alles außer configure),
ready (behandelt Requests). Das become macht den Übergang
explizit; das stash + unstashAll stellt sicher, dass während des
Initialisierungs-Fensters kein Traffic verloren geht.
Wann zu welchem greifen
Abschnitt betitelt „Wann zu welchem greifen“| Situation | Werkzeug |
|---|---|
| Zwei oder mehr diskrete Phasen mit verschiedenen Valid-Message-Sets | become |
| Eine Phase hat eine transiente Sub-Phase, die bei Done wiederherstellen soll | become(..., discardOld=false) + unbecome() |
| Boolean-Flag — “ready”/“not ready” — mit gleicher Handler-Form | Ein einfaches Feld (greife nicht zu become) |
| Manche Nachrichten müssen geordnet bis später verschoben werden | stash + unstashAll |
| Init-Phase, die allen Non-Init-Traffic puffert | become(loading) + stash in loading + unstashAll beim Übergang |
Wie es weitergeht
Abschnitt betitelt „Wie es weitergeht“- Actor — die Basisklasse, deren
onReceivedu mitbecomeaustauschst. - Mailboxes —
unstashAllfügt in die Mailbox ein; die Mailbox entscheidet die Reihenfolge. - FSM — ein formellerer
State-Machine-Stil auf derselben
become-Engine. - Typed Behaviors — die typed-API
drückt dieselbe Idee als funktionale
Behavior<T>-Werte aus, statt Context zu mutieren.
Die ActorContext-API-Referenz
deckt become, stash und die vollständige Schnittstelle ab.