Ask-Pattern
tell ist Fire-and-Forget — es kehrt sofort zurück und du siehst die
Antwort des Actors nie. Die meiste Actor-zu-Actor-Kommunikation ist
damit gut bedient. Aber manchmal brauchst du eine Antwort: ein
HTTP-Handler, der einen Actor nach Zustand fragt und ihn als Response
serialisiert; eine Saga, die das Ergebnis von Schritt N braucht, bevor
sie Schritt N+1 anstößt; ein Test, der einen Rückgabewert assertieren
will.
ask ist der Helfer für diese Fälle. Es sendet eine Nachricht an
einen Actor, hängt unter der Haube eine temporäre “Reply-to”-Ref an
und gibt ein Promise zurück, das mit der ersten Antwort resolved oder
bei Timeout rejected.
Ein minimales Beispiel
Abschnitt betitelt „Ein minimales Beispiel“import { match } from 'ts-pattern';import { Actor, ActorSystem, type ActorRef } from 'actor-ts';
type IncrementCommand = { readonly kind: 'increment' };type GetCommand = { readonly kind: 'get'; readonly replyTo: ActorRef<number> };type Command = IncrementCommand | GetCommand;
class Counter extends Actor<Command> { private count = 0; override onReceive(command: Command): void { match(command) .with({ kind: 'increment' }, () => this.onIncrement()) .with({ kind: 'get' }, (c) => this.onGet(c)) .exhaustive(); }
private onIncrement(): void { this.count++; } private onGet(command: GetCommand): void { command.replyTo.tell(this.count); }}
const system = ActorSystem.create('demo');const counter = system.spawnAnonymous(Counter);
counter.tell({ kind: 'increment' });counter.tell({ kind: 'increment' });
// Methode-auf-Ref — knapp, leitet den Reply-Typ ab:const current = await counter.ask<number>({ kind: 'get' }, 5_000);console.log(current); // 2
await system.terminate();ask ist eine Methode auf jeder ActorRef:
// Methode auf jeder ActorRef — leitet Reply aus dem Type-Argument ab:ref.ask<Reply>(message, timeoutMs?): Promise<Reply>Der Parameter-Typ OmitReplyTo<TMessage> zieht replyTo aus jeder
Variante der Message-Union ab, die es deklariert — Aufrufer schreiben
das Feld selbst nie hin. Das Framework allokiert eine
One-Shot-Reply-Ref, hängt sie sowohl als context.sender als auch
(wenn die Message-Shape es erwartet) als message.replyTo an und
löst das Promise mit der ersten Antwort auf.
Die replyTo-Feld-Konvention
Abschnitt betitelt „Die replyTo-Feld-Konvention“ask funktioniert, indem es deine Nachricht mit einer angehängten,
temporären Reply-to-Ref sendet. Der Empfänger antwortet, indem er
dieser Ref tellt.
Per Konvention verwendet das Framework replyTo als Feldnamen in
der Request-Nachricht, und der Empfänger pattern-matcht darauf wie auf
jedes andere Feld. Schau dir den Command-Typ oben an: die
'get'-Variante hat replyTo: ActorRef<number>.
Für den Empfänger ist es einfach ein normales tell auf einer
normalen Ref:
private onGet(command: GetCommand): void { command.replyTo.tell(this.count);}Innerhalb des Empfängers kannst du dieselbe Ref auch über
this.sender erreichen — es ist ein Option<ActorRef>, und ask
hängt die temporäre Ref auch über diesen Kanal an:
private onGet(command: GetCommand): void { this.sender.forEach((replyTo) => replyTo.tell(this.count));}Beides funktioniert. Ein explizites replyTo-Feld zu verwenden,
macht den Typvertrag offensichtlicher (die Request-Form deklariert,
dass eine Antwort vom Typ ActorRef<number> erwartet wird);
this.sender zu verwenden ist impliziter, funktioniert aber für
Actors, die sowohl ask-artige als auch Fire-and-Forget-Nachrichten
behandeln.
Timeouts + der AskTimeoutError
Abschnitt betitelt „Timeouts + der AskTimeoutError“import { AskTimeoutError } from 'actor-ts';
try { const result = await counter.ask({ kind: 'get' }, 1_000); console.log(result);} catch (e) { if (e instanceof AskTimeoutError) { console.log('counter did not reply in time'); } else { throw e; }}Das Timeout ist im Geiste verpflichtend — der Default von 5 Sekunden fängt den Fall, in dem du vergessen hast, darüber nachzudenken. Tune pro Aufrufstelle:
- Günstige In-Process-Antworten: 100-500 ms.
- Cross-Cluster-Lookups: 1-5 s, je nach Netzwerkerwartungen.
- Alles, was potenziell ein langsames Downstream trifft (DB-Query, HTTP-Call innerhalb des Actors): das Timeout des unterliegenden Calls + ein kleiner Puffer.
Wenn der Timeout feuert, rejected der AskTimeoutError das Promise.
Die temporäre Reply-to-Ref ist ein One-Shot: sobald sie einmal
abgeschlossen ist — durch den Timeout oder die erste Antwort — wird
jede spätere Antwort still an Ort und Stelle verworfen (sie geht nicht
in die Dead Letters). Der Actor selbst ist nicht betroffen — er hat
keine Ahnung, dass der Asker aufgegeben hat.
Mit einem Error antworten
Abschnitt betitelt „Mit einem Error antworten“Ein Empfänger kann einen Ask ablehnen, indem er mit einer
Error-Instanz antwortet. Das ask-Promise wird dann mit diesem
Fehler rejected, statt mit der Nachricht resolved.
class Service extends Actor<Command> { override onReceive(command: Command): void { match(command) .with({ kind: 'fetch' }, (c) => this.onFetch(c)) .exhaustive(); }
private onFetch(command: FetchCommand): void { if (this.unavailable) { command.replyTo.tell(new Error('service unavailable')); return; } command.replyTo.tell(this.data); }}
try { const data = await service.ask({ kind: 'fetch' });} catch (e) { // e ist der "service unavailable"-Error des Actors.}Das ist eine bewusste Konvention — das Framework erkennt
Error-typisierte Antworten und rejected das Promise. Trage
Domain-Fehler den normalen Weg (typisiert über die Reply-Union) für
alles andere; reserviere den Error-Reply-Pfad für echte Fehlerzustände.
Wann ask NICHT verwendet werden sollte
Abschnitt betitelt „Wann ask NICHT verwendet werden sollte“ask ist bequem, hat aber Overhead — es allokiert eine temporäre Ref,
registriert sie beim System und räumt sie auf, wenn sie resolved/timeout.
Drei Situationen, in denen einfaches tell der richtige Aufruf ist:
Wie es weitergeht
Abschnitt betitelt „Wie es weitergeht“- Nachrichten — die
replyTo: ActorRef<T>-Feld-Konvention im Detail. - Actor —
this.senderund wie das Framework es verdrahtet. - ActorSystem — wo temporäre Ask-Refs registriert und wieder abgebaut werden.
- CircuitBreaker — wenn der Downstream-Call des Asks flakey ist und du lieber schnell fehlschlagen als timen willst.
- Future-Patterns —
pipeTo,after, sequence — async-Ergebnisse zurück in die Actor-Welt komponieren, ohne mitawaitzu blockieren.
Die ActorRef-Klassen-API-Referenz
dokumentiert die volle Signatur der ask-Methode und die Error-Typen.
