Zum Inhalt springen
Deutsch

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.

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.

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.

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.

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.

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:

  • Nachrichten — die replyTo: ActorRef<T>-Feld-Konvention im Detail.
  • Actorthis.sender und 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-PatternspipeTo, after, sequence — async-Ergebnisse zurück in die Actor-Welt komponieren, ohne mit await zu blockieren.

Die ask-Funktions-API-Referenz dokumentiert die volle Signatur und die Error-Typen.