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 { Actor, ActorSystem, Props, type ActorRef } from 'actor-ts';
type Cmd = | { readonly kind: 'inc' } | { readonly kind: 'get'; readonly replyTo: ActorRef<number> };
class Counter extends Actor<Cmd> { private count = 0; override onReceive(cmd: Cmd): void { if (cmd.kind === 'inc') this.count++; else if (cmd.kind === 'get') cmd.replyTo.tell(this.count); }}
const system = ActorSystem.create('demo');const counter = system.spawnAnonymous(Props.create(() => new Counter()));
counter.tell({ kind: 'inc' });counter.tell({ kind: 'inc' });
// 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();Zwei äquivalente Schreibweisen:
// Methode auf jeder ActorRef — leitet Reply aus dem Type-Argument ab:ref.ask<Reply>(message, timeoutMs?): Promise<Reply>
// Freie Funktion — nützlich, wo du die explizite Form willst:function target: ActorRef<TReq>.ask<TRes = unknown>(message: OmitReplyTo<TReq>, timeoutMs: number = 5_000,): Promise<TRes>Der Parameter-Typ OmitReplyTo<TReq> 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 Cmd-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:
if (cmd.kind === 'get') { cmd.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:
if (cmd.kind === 'get') { 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, wird die temporäre Reply-to-Ref gestoppt.
Eine späte Antwort vom Actor geht in die Dead Letters, und der
AskTimeoutError rejected das Promise. 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<Cmd> { override onReceive(cmd: Cmd): void { if (cmd.kind === 'fetch') { if (this.unavailable) { cmd.replyTo.tell(new Error('service unavailable')); return; } cmd.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 ask-Funktions-API-Referenz dokumentiert
die volle Signatur und die Error-Typen.