콘텐츠로 이동
한국어

Spawning actors

이 콘텐츠는 아직 번역되지 않았습니다.

Spawning takes two things: what to build, and — only when the defaults are wrong — how to run it.

import { ActorSystem } from 'actor-ts';
const system = ActorSystem.create('hello');
system.spawn(Greeter, 'greeter'); // the class itself
system.spawn(() => new Worker(database), 'worker-1'); // a factory

That is the whole common case. Defaults are an unbounded FIFO mailbox, the system dispatcher, and the parent’s supervision.

Pass the class when its constructor takes no arguments. Nothing else is needed — the framework constructs a fresh instance itself.

class Greeter extends Actor<string> {
override onReceive(name: string): void { console.log(`hello ${name}`); }
}
system.spawn(Greeter, 'greeter');

Pass a factory when the actor needs dependencies. The closure captures them, and every restart sees the same wiring:

class Worker extends Actor<WorkerMessage> {
constructor(private readonly database: ActorRef<DatabaseMessage>) { super(); }
}
system.spawn(() => new Worker(database), 'worker-1');

A class whose constructor does take arguments is rejected at the call site — passing it alone could only construct it with undefined dependencies:

system.spawn(Worker, 'worker-1');
// ✗ Worker needs a constructor argument, so the class alone is not
// enough to build it — pass a factory that supplies it:
// () => new Worker(database).

Neither form lets you hand over an actor instance, and that is deliberate: the actor is rebuilt on every restart.

// Rejected — an instance is neither a class nor a factory:
const counter = new Counter();
system.spawn(counter, 'counter');
// Fine, and fresh on every restart:
system.spawn(Counter, 'counter');

Reusing one instance across a restart means the actor never resets — it inherits the broken state from the failed run, which is the opposite of what “let it crash” is for.

The name is passed to spawn, never attached to the actor:

system.spawn(Greeter, 'greeter-1'); // → /user/greeter-1
system.spawn(Greeter, 'greeter-2'); // → /user/greeter-2
this.context.spawn(Greeter, 'sub-greeter'); // → /user/<this>/sub-greeter

Names must be unique among siblings. For an actor whose path does not matter — one-shot async work, throwaway helpers — use spawnAnonymous, which synthesizes $anonymous-<n>-<random>:

const ref = system.spawnAnonymous(Counter);

Everything beyond the defaults goes in a third argument. Build it with the fluent builder:

import { ActorOptions, stoppingStrategy, MicrotaskDispatcher, BoundedMailbox } from 'actor-ts';
const workerOptions = ActorOptions.create<WorkerMessage>()
.withSupervisorStrategy(stoppingStrategy)
.withDispatcher(new MicrotaskDispatcher())
.withMailbox(() => new BoundedMailbox({ capacity: 100, overflow: 'drop-head' }));
system.spawn(() => new Worker(database), 'worker-1', workerOptions);

A plain object is the shorthand alternative and reads identically:

system.spawn(() => new Worker(database), 'worker-1', { mailboxCapacity: 500 });
class ActorOptionsBuilder<TMessage> {
static create<TMessage>(): ActorOptionsBuilder<TMessage>;
withSupervisorStrategy(strategy: SupervisorStrategy): this;
withDispatcher(dispatcher: Dispatcher): this;
withMailboxCapacity(capacity: number): this;
withMailbox(factory: () => Mailbox<TMessage>): this;
withInternal(internal?: boolean): this;
withDisplayName(displayName: string): this;
withEntity(entity: EntityContext): this;
}
import { ActorOptions, OneForOneStrategy, Directive, stoppingStrategy } from 'actor-ts';
const workerOptions = ActorOptions.create().withSupervisorStrategy(stoppingStrategy);
const databaseOptions = ActorOptions.create()
.withSupervisorStrategy(new OneForOneStrategy(
(error) => error instanceof TransientError ? Directive.Resume : Directive.Restart,
{ maxRetries: 5, withinTimeRangeMs: 60_000 },
));

The strategy set here decides what happens when THIS actor fails. Two strategies are involved in every actor’s life:

  • The one in this actor’s spawn options — applied to this actor’s failures, by its parent.
  • The one inside this actor’s class (override supervisorStrategy) — applied to this actor’s children’s failures.

Most actors need neither: the parent’s default (or the system root’s defaultStrategy) restarts with a 10-per-minute cap. Reach for this when that is wrong — e.g. a worker that should be stopped rather than restarted because its parent will spawn a replacement.

The parent resolves the strategy in this order, first match wins:

  1. The failing child’s own spawn options.
  2. The parent actor’s supervisorStrategy() override.
  3. defaultStrategy.

So a single child can opt out of its parent’s policy without affecting its siblings. Two details fall out of the strategy being applied by the parent: an all-for-one strategy set on a child still widens to every sibling, and the restart budget (maxRetries / withinTimeRangeMs) is counted per parent, so siblings share one allowance.

See Supervision for the full semantics.

import { ActorOptions, MicrotaskDispatcher, ThroughputDispatcher } from 'actor-ts';
const crunchyOptions = ActorOptions.create().withDispatcher(new MicrotaskDispatcher());
const bulkOptions = ActorOptions.create().withDispatcher(new ThroughputDispatcher(100));

Override the system-wide dispatcher for one actor. Useful when a single actor is CPU-heavy and benefits from microtask scheduling, or is latency-sensitive while the rest of the system runs on a high-throughput dispatcher. For most apps the system-level dispatcher applies uniformly and you never need this — see Dispatchers.

import { ActorOptions } from 'actor-ts';
const consumerOptions = ActorOptions.create().withMailboxCapacity(500);

Bounds the mailbox, which is otherwise unbounded — so this is the call that introduces message loss. drop-head applies unless you say otherwise: a full mailbox discards the oldest queued message to make room, without throwing. Add withMailboxOverflow('drop-new') to discard the arriving message instead, or 'reject' to throw MailboxFullError at the tell site so the sender feels the pressure.

import { ActorOptions, BoundedMailbox, PriorityMailbox } from 'actor-ts';
const telemetryOptions = ActorOptions.create()
.withMailbox(() => new BoundedMailbox({ capacity: 1_000, overflow: 'drop-head' }));
const workerOptions = ActorOptions.create<Message>()
.withMailbox(() => new PriorityMailbox<Message>({
priorityFor: (message) => message.kind === 'urgent' ? 0 : 5,
}));

Full control — return any Mailbox subclass. The factory is called once, when the actor’s cell is constructed; the mailbox then outlives every restart. On restart only the Actor instance is rebuilt — the same mailbox is resumed in place, so messages queued before the crash are still there.

See Mailboxes for the overflow policies and priority semantics.

Marks an actor as belonging to tooling rather than to the application, and its children with it. Whole-system instrumentation skips it: without the mark, DevTools’ own hub — which publishes the spans it has just recorded — traces itself, and every batch comes back as the payload of the next one.

import { ActorOptions } from 'actor-ts';
const probeOptions = ActorOptions.create().withInternal();
system.spawn(MyProbe, 'my-probe', probeOptions);

You need it only when writing a tool that observes the system it runs in. Application actors should not be marked: hiding real work from the profiler is how a performance problem stays invisible.

Names the actor in log lines and in the DevTools actor tree. The spawn-site counterpart to overriding Actor.displayName() — reach for it when the actor has no subclass of yours to override: a Behaviors actor, a sharded entity, a singleton.

const workerOptions = ActorOptions.create().withDisplayName('ingest-worker');
system.spawn(IngestWorker, 'ingest', workerOptions);

Outranks the method, exactly as withSupervisorStrategy outranks Actor.supervisorStrategy(); context.setDisplayName(...) at runtime outranks both. Purely cosmetic — the path stays the identity everywhere that routes or correlates, so metric labels, tracing attributes and every cluster-wire identifier are unaffected.

See Logging for how the name reaches a log record.

Gives an actor a sharding identity, readable back off this.entityId / this.context.entity. ClusterSharding sets this itself for every entity a shard creates; it is public for the test bench, where an entity that derives its persistenceId from this.entityId would otherwise be unspawnable without a cluster standing behind it.

import { ActorOptions } from 'actor-ts';
const cartOptions = ActorOptions.create()
.withEntity({ entityId: 'user-42', typeName: 'cart', shardId: 3 });
const cart = system.spawn(CartEntity, 'cart-under-test', cartOptions);
import { ActorOptions, type ActorFactory } from 'actor-ts';
const workerOptions = ActorOptions.create<WorkerMessage>()
.withSupervisorStrategy(stoppingStrategy)
.withMailboxCapacity(500);
const worker = (database: ActorRef<DatabaseMessage>): ActorFactory<WorkerMessage> =>
() => new Worker(database);
for (let i = 0; i < 8; i++) {
this.context.spawn(worker(database), `worker-${i}`, workerOptions);
}

The options object is reusable across every actor spawned from it — it is snapshotted per spawn, so the eight workers do not share state.

The typed API spawns a Behavior instead of a class or factory:

const ref = system.spawnTyped(counter(0), 'counter');
const anonymous = system.spawnTypedAnonymous(counter(0));

To hand a Behavior to an API that takes an actor — Router, ClusterSharding.start — wrap it with typedActor(behavior). See Typed actors.

  • Actor — the base class whose constructor is invoked.
  • Supervision — the strategy you pass to withSupervisorStrategy.
  • Dispatchers — the scheduler you pass to withDispatcher.
  • Mailboxes — the queue you pass to withMailbox / withMailboxCapacity.
  • ActorSystem — the system.spawn(actor, name, options?) that consumes all of this.