Router
Ce contenu n’est pas encore disponible dans votre langue.
Router is the local pool-router — the routees are children of the
router actor, created when the router is spawned, supervised by it.
The API has five one-shot factories:
Router.roundRobin(size, routee)Router.random(size, routee)Router.broadcast(size, routee)Router.smallestMailbox(size, routee)Router.custom(size, routee, strategy)Each returns an ActorFactory<TMessage | Broadcast<TMessage>> — pass it
to system.spawn or context.spawn like any other actor. The
type parameter from routee flows through, so the resulting
ref is typed.
A sixth factory does not fit that shape, because it intercepts the routees’ replies instead of forwarding and forgetting:
Router.scatterGatherFirstCompleted(size, routee, options?, routeeOptions?)It asks every routee and answers the caller with the first reply — the hedged-request pattern. It has its own page: Scatter/gather.
A minimal example
Section titled “A minimal example”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',);
// One message per routee, cycling:pool.tell({ payload: 'a' }); // → routee-1pool.tell({ payload: 'b' }); // → routee-2pool.tell({ payload: 'c' }); // → routee-3pool.tell({ payload: 'd' }); // → routee-4
// Override the strategy for a single message — send to ALL routees:pool.tell(new Broadcast({ payload: 'announce' }));The pool’s path is actor-ts://demo/user/workers; the routees are
actor-ts://demo/user/workers/routee-1 through routee-4.
What happens at spawn time
Section titled “What happens at spawn time”When you spawn a router factory, the runtime:
- Creates one
RouterActorinstance. It’s the actor with the path you provided ('workers'in the example). - Inside
RouterActor.preStart, it spawnssizechildren usingroutee, namedroutee-1throughroutee-N. - It
watches every routee so it can react if one stops.
The router is now ready. Any tell to the router ref runs the
strategy and forwards.
Broadcast — override the strategy per-message
Section titled “Broadcast — override the strategy per-message”import { Broadcast } from 'actor-ts';
pool.tell({ payload: 'a' }); // normal: one routeepool.tell(new Broadcast({ payload: 'announce' })); // every routeeBroadcast<T> wraps a payload. The router unwraps it, ignores the
strategy, and sends the inner message to every routee. Useful
for occasional fan-out messages (cache invalidation, schema
update notifications) that don’t fit the routine routing pattern.
The router accepts both TMessage and Broadcast<TMessage> — the type
parameter on the returned factory reflects that.
Router.smallestMailbox — balance by backlog
Section titled “Router.smallestMailbox — balance by backlog”const pool = system.spawn( Router.smallestMailbox(4, Worker), 'workers',);Each message goes to the routee with the shortest queue at that moment. Reach for it when per-message cost varies a lot: round-robin keeps feeding a routee that is still grinding through a heavy job, while smallest-mailbox skips it until it catches back up.
Three things worth knowing before you switch the default:
- It costs a depth read per routee per message. With a 4-routee pool that is four reads; with a 200-routee pool it is two hundred. Round-robin is one modulo. For uniform workloads the extra work buys nothing.
- Ties rotate. An idle pool has every mailbox at zero, so the
strategy falls back to round-robin order rather than pinning
everything to
routee-1. - The strategy never refuses to route. On the unbounded default
there is no “does not fit”, so depth stays a truthful reading of
backlog however far behind the pool falls. A pool of bounded
routees does saturate, and then the same tie-break answers it: all
depths are equal again, so the overflow spreads evenly instead of
piling onto one routee. What happens to a message that does not fit
is the mailbox’s own overflow policy (
drop-head/drop-new/reject), not the router’s call to make.
Local only. Mailbox depth is in-process state, so ClusterRouter has
no smallest-mailbox mode — see
Cluster router.
Router.custom — bring your own strategy
Section titled “Router.custom — bring your own strategy”import { Router, type RoutingStrategy } from 'actor-ts';
// Send to the FIRST routee for the first 100 messages, then 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',);A RoutingStrategy is a function from (routees, state) to an
Iterable<ActorRef> — return one ref for single-target routing,
multiple for fan-out. Empty iterable means “drop this message”
(silent, no dead-letter routing — your responsibility to log if
needed).
See Strategies for the full strategy type and built-in implementations.
Spawning the routees yourself
Section titled “Spawning the routees yourself”Router.roundRobin(size, routee) is a pool — the router creates
the routees itself from routee. If you want to route to existing actors
instead (e.g. shard regions, specific named workers), the local
Router doesn’t support that; you’d write a custom router actor.
For cluster setups, ClusterRouter does have a “find existing
actors by path” mode — see
Pool vs group for the
distinction and
Cluster router for the
cluster API.
Stopping the pool
Section titled “Stopping the pool”pool.stop() (or pool.tell(PoisonPill.instance)) stops the
router, which cascade-stops every routee. Or stop a single routee
by addressing it directly:
const oneRoutee = (await system.actorSelection( '/user/workers/routee-2').resolveOne()) as ActorRef<Message>;
oneRoutee.stop();When a routee stops, the router watches it and… does nothing by default in the framework’s current router. Its ref stays in the pool — the router never prunes it — so round-robin keeps handing it roughly 1-in-N messages, which land in dead letters until the whole router restarts. For self-healing pools, wrap the routee in a supervisor strategy that restarts on Stop (or wrap the whole pool in a BackoffSupervisor).
Where to next
Section titled “Where to next”- Strategies — round-robin, random, broadcast, smallest-mailbox, custom; plus the cluster-only consistent-hashing.
- Scatter/gather — the sixth factory: ask every routee, answer with the first reply.
- Pool vs group — when you want existing routees instead of pool-spawned ones.
- Cluster router — the membership-driven cluster equivalent.
- Spawning actors — the routee options
shape;
withSupervisorStrategy,withDispatcher, etc., all apply to routees individually.
The Router and
Broadcast API references
cover the full surface.
