Skip to content

[Feature] ref.stopNow() — immediate external stop that bypasses the user queue #1192

Description

@pathosDev

Problem

Both public ways to stop another actor queue behind its backlog:

  • ref.stop() / system.stop(ref) / context.stop(ref) send PoisonPill as an ordinary user message (src/ActorRef.ts:97, src/internal/ActorCell.ts:527-529) — stop-after-drain, by design.
  • ref.kill() enqueues Kill the same way (src/ActorRef.ts:100), so even the "hard" variant waits until everything already queued has been processed before it raises ActorKilledError.

The immediate path exists: the terminate system command jumps the queue, because ActorCell.run() drains system messages before each user message (src/internal/ActorCell.ts:801-811) and finalizeTermination dead-letters the remainder (src/internal/ActorCell.ts:984-986). But it is reachable only for the actor itself (context.stopSelf(), src/internal/ActorCell.ts:531-533) or via the hierarchy (parent teardown / system.terminate()).

So an overloaded or runaway actor with, say, 10k queued messages cannot be taken down from outside without processing all 10k first — or tearing down its parent.

The framework itself hits this gap: the cluster-singleton handover stops the old instance with a PoisonPill queued behind that child's whole mailbox — one half of #949.

Proposed API

// ActorRef — sibling of stop() and kill()
/**
 * Stop this actor immediately.  The currently running handler finishes
 * (processing is cooperative, never preemptive), the rest of the user
 * queue goes to dead letters, children stop first, watchers receive
 * Terminated — the same path context.stopSelf() takes today.
 */
stopNow(): void;
  • LocalActorRef.stopNow()getCell().enqueueSystem({ kind: 'terminate' }) — the existing TerminateCommand, no new mechanism.
  • Pass-throughs where stop already exists: ActorSystem.stopNow(ref) (src/ActorSystem.ts:410-412), ActorContext.stopNow(ref) (src/ActorContext.ts:122-126).
  • Remote refs: out of scope for the first cut. A cross-node immediate stop needs a wire-level system command; until that exists, RemoteActorRef.stopNow() should fail loudly (throw) rather than silently degrade to drain semantics.

Alternatives considered

  • ref.kill() — still a user message; with a deep backlog it acts as late as stop().
  • Stopping the parent — works via the system-command cascade, but takes down siblings and is the wrong altitude for "remove this one actor".
  • Reaching into LocalActorRef.getCell().enqueueSystem(...) from user code — internal API, not a supported surface.

Acceptance criteria

  • An actor with N pending user messages terminates after at most the one in-flight message once stopNow() is called; the N pending messages arrive in dead letters.
  • Children stop before the parent finalizes; watchers receive Terminated — identical to stopSelf().
  • stop() / kill() semantics unchanged (drain).
  • Lifecycle docs (EN + DE) document the three flavours side by side: drain (stop), supervised kill (kill), immediate (stopNow).

Related: #663 (draining shutdown — the complementary direction), #949 (singleton handover queues PoisonPill behind a full mailbox).

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions