コンテンツにスキップ
日本語

Router

このコンテンツはまだ日本語訳がありません。

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.

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-1
pool.tell({ payload: 'b' }); // → routee-2
pool.tell({ payload: 'c' }); // → routee-3
pool.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.

When you spawn a router factory, the runtime:

  1. Creates one RouterActor instance. It’s the actor with the path you provided ('workers' in the example).
  2. Inside RouterActor.preStart, it spawns size children using routee, named routee-1 through routee-N.
  3. 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 routee
pool.tell(new Broadcast({ payload: 'announce' })); // every routee

Broadcast<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.

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.

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.

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).

  • 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.