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.
Ein minimales Beispiel
Abschnitt betitelt „Ein minimales Beispiel“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-1pool.tell({ payload: 'b' }); // → routee-2pool.tell({ payload: 'c' }); // → routee-3pool.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.
Was zur Spawn-Zeit passiert
Abschnitt betitelt „Was zur Spawn-Zeit passiert“Wenn du eine Router-Factory mit spawn ausführst, tut die Runtime:
- Erstellt eine
RouterActor-Instanz. Das ist der Actor mit dem Pfad, den du angegeben hast ('workers'im Beispiel). - In
RouterActor.preStartspawnt essizeKinder mitroutee, benanntroutee-1bisroutee-N. - 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 Routeepool.tell(new Broadcast({ payload: 'announce' })); // jeder RouteeBroadcast<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-1zu 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.
Router.custom — eigene Strategie mitbringen
Abschnitt betitelt „Router.custom — eigene Strategie mitbringen“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.
Routees selbst spawnen
Abschnitt betitelt „Routees selbst spawnen“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.
Den Pool stoppen
Abschnitt betitelt „Den Pool stoppen“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).
Wie es weitergeht
Abschnitt betitelt „Wie es weitergeht“- 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.
