Zum Inhalt springen
Deutsch

Router

Router ist der lokale Pool-Router — die Routees sind Kinder des Router-Actors, beim Spawnen des Routers erstellt und von ihm supervised.

Die API hat fünf One-Shot-Factories:

Router.roundRobin(size, routee)
Router.random(size, routee)
Router.broadcast(size, routee)
Router.smallestMailbox(size, routee)
Router.custom(size, routee, strategy)

Jede gibt eine ActorFactory<TMessage | Broadcast<TMessage>> zurück — übergib sie an system.spawn oder context.spawn wie jeden anderen Actor. Der Typ-Parameter von routee fließt durch, die resultierende Ref ist also typisiert.

Eine sechste Factory passt nicht in diese Form, weil sie die Antworten der Routees abfängt, statt weiterzuleiten und zu vergessen:

Router.scatterGatherFirstCompleted(size, routee, options?, routeeOptions?)

Sie fragt jeden Routee und antwortet dem Aufrufer mit der ersten Antwort — das Hedged-Request-Pattern. Sie hat eine eigene Seite: Scatter/Gather.

import { ActorSystem, Router, Actor, Broadcast } from 'actor-ts';
type Message = { payload: string };
class Worker extends Actor<Message> {
override onReceive(message: Message): void {
this.log.info(`processed ${message.payload}`);
}
}
const system = ActorSystem.create('demo');
const pool = system.spawn(
Router.roundRobin(4, Worker),
'workers',
);
// Eine Nachricht pro Routee, zyklisch:
pool.tell({ payload: 'a' }); // → routee-1
pool.tell({ payload: 'b' }); // → routee-2
pool.tell({ payload: 'c' }); // → routee-3
pool.tell({ payload: 'd' }); // → routee-4
// Strategie für eine einzelne Nachricht überschreiben — an ALLE Routees senden:
pool.tell(new Broadcast({ payload: 'announce' }));

Der Pfad des Pools ist actor-ts://demo/user/workers; die Routees sind actor-ts://demo/user/workers/routee-1 bis routee-4.

Wenn du eine Router-Factory mit spawn ausführst, tut die Runtime:

  1. Erstellt eine RouterActor-Instanz. Das ist der Actor mit dem Pfad, den du angegeben hast ('workers' im Beispiel).
  2. In RouterActor.preStart spawnt es size Kinder mit routee, benannt routee-1 bis routee-N.
  3. Es watcht jeden Routee, damit es reagieren kann, wenn einer stoppt.

Der Router ist jetzt bereit. Jedes tell an die Router-Ref führt die Strategie aus und leitet weiter.

Broadcast — Strategie pro Nachricht überschreiben

Abschnitt betitelt „Broadcast — Strategie pro Nachricht überschreiben“
import { Broadcast } from 'actor-ts';
pool.tell({ payload: 'a' }); // normal: ein Routee
pool.tell(new Broadcast({ payload: 'announce' })); // jeder Routee

Broadcast<T> wickelt eine Payload. Der Router packt sie aus, ignoriert die Strategie und sendet die innere Nachricht an jeden Routee. Nützlich für gelegentliche Fan-Out-Nachrichten (Cache-Invalidierung, Schema-Update-Notifications), die nicht ins Routine-Routing-Pattern passen.

Der Router akzeptiert sowohl TMessage als auch Broadcast<TMessage> — der Typ-Parameter auf der zurückgegebenen Factory reflektiert das.

Router.smallestMailbox — nach Rückstau balancieren

Abschnitt betitelt „Router.smallestMailbox — nach Rückstau balancieren“
const pool = system.spawn(
Router.smallestMailbox(4, Worker),
'workers',
);

Jede Nachricht geht an den Routee mit der kürzesten Queue in diesem Moment. Greif dazu, wenn die Kosten pro Nachricht stark schwanken: Round-Robin füttert einen Routee weiter, der noch an einem schweren Job kaut, Smallest-Mailbox überspringt ihn, bis er aufgeholt hat.

Drei Dinge, die du wissen solltest, bevor du den Default wechselst:

  • Es kostet eine Tiefenabfrage pro Routee pro Nachricht. Bei einem 4er-Pool sind das vier Abfragen, bei einem 200er-Pool zweihundert. Round-Robin ist ein Modulo. Bei uniformen Workloads kauft die Mehrarbeit nichts.
  • Gleichstände rotieren. In einem leerlaufenden Pool steht jede Mailbox auf null, die Strategie fällt also auf die Round-Robin-Reihenfolge zurück, statt alles auf routee-1 zu pinnen.
  • Die Strategie verweigert das Routen nie. Auf dem unbounded Default gibt es kein „passt nicht mehr”, die Tiefe bleibt also ein echter Messwert des Rückstaus, egal wie weit der Pool zurückfällt. Ein Pool aus begrenzten Routees sättigt sich irgendwann, und dann antwortet derselbe Gleichstand: alle Tiefen sind wieder gleich, der Überlauf verteilt sich also gleichmäßig, statt sich bei einem Routee aufzutürmen. Was mit einer Nachricht passiert, die nicht mehr passt, entscheidet die Overflow-Policy der Mailbox (drop-head / drop-new / reject), nicht der Router.

Nur lokal. Die Mailbox-Tiefe ist In-Process-Zustand, ClusterRouter hat deshalb keinen Smallest-Mailbox-Modus — siehe Cluster-Router.

import { Router, type RoutingStrategy } from 'actor-ts';
// Sende für die ersten 100 Nachrichten an den ERSTEN Routee, dann Round-Robin.
const warmupStrategy: RoutingStrategy = (routees, state) => {
if (state.messageIndex < 100) return [routees[0]];
return [routees[state.messageIndex % routees.length]];
};
const pool = system.spawn(
Router.custom(4, Worker, warmupStrategy),
'workers',
);

Eine RoutingStrategy ist eine Funktion von (routees, state) zu einem Iterable<ActorRef> — gib eine Ref für Single-Target-Routing zurück, mehrere für Fan-Out. Leeres Iterable bedeutet “verwirf diese Nachricht” (still, kein Dead-Letter-Routing — deine Verantwortung zu loggen, wenn nötig).

Siehe Strategien für den vollen Strategie-Typ und eingebaute Implementierungen.

Router.roundRobin(size, routee) ist ein Pool — der Router erstellt die Routees selbst aus routee. Wenn du stattdessen an existierende Actors routen willst (z.B. Shard-Regionen, spezifische benannte Workers), unterstützt der lokale Router das nicht; du würdest einen eigenen Router-Actor schreiben.

Für Cluster-Setups hat ClusterRouter einen “finde existierende Actors per Pfad”-Modus — siehe Pool vs Group für die Unterscheidung und Cluster-Router für die Cluster-API.

pool.stop() (oder pool.tell(PoisonPill.instance)) stoppt den Router, der jeden Routee kaskadiert stoppt. Oder stoppe einen einzelnen Routee, indem du ihn direkt adressierst:

const oneRoutee = (await system.actorSelection(
'/user/workers/routee-2'
).resolveOne()) as ActorRef<Message>;
oneRoutee.stop();

Wenn ein Routee stoppt, beobachtet der Router ihn und… tut standardmäßig nichts im aktuellen Router des Frameworks. Seine Ref bleibt im Pool — der Router entfernt sie nie — sodass Round-Robin ihm weiterhin etwa jede N-te Nachricht zuteilt, die bis zum Neustart des ganzen Routers in Dead Letters landet. Für selbstheilende Pools wickle den Routee in eine Supervisor-Strategie, die bei Stop restartet (oder wickle den ganzen Pool in einen BackoffSupervisor).

  • Strategien — Round-Robin, Random, Broadcast, Smallest-Mailbox, Custom; plus das cluster-only Consistent-Hashing.
  • Scatter/Gather — die sechste Factory: jeden Routee fragen, mit der ersten Antwort antworten.
  • Pool vs Group — wenn du bestehende Routees statt pool-gespawnte willst.
  • Cluster-Router — das mitgliedschafts-getriebene Cluster-Äquivalent.
  • Actors erzeugen — die Routee-Options; withSupervisorStrategy, withDispatcher, etc., gelten alle individuell für Routees.

Die Router- und Broadcast-API-Referenzen decken die volle Schnittstelle ab.