Skip to content

Lifecycle and modes

Concepts

A node’s mode decides what each lifecycle transition does to it. Three modes and three cross-cutting flags cover everything.

modeat booton teardownon wake re-bring-up
Terminatespawnedexits and acksrespawned fresh
Pausespawned (or app-spawned)acks, then parks on wait_resume()resumed in place, keeps held resources
OnDemandnot startedstopped if runningleft down; the pool policy regrows it
stateDiagram-v2
accDescr: The states a node moves through, and the transition that causes each
direction LR
[*] --> Running : start (dep order)
Running --> Stopped : stop / teardown<br/>Terminate: exits
Stopped --> Running : respawn_terminate<br/>activate
Running --> Parked : stop / teardown<br/>Pause: acks then parks
Parked --> Running : resume_pausable<br/>resume_node (in place)
Running --> Exited : body returns<br/>mark_exited
Exited --> Running : Activate<br/>(control respawn)

Three flags cut across the modes:

  • disabled (the disabled; clause, or a control Deactivate on the node itself) is the “someone said stop” latch. Every bring-up path honors it, so a manual stop survives a wake respawn or an elastic regrow, until an Activate clears it.
  • collateral (TaskNode::is_collateral()) marks a node stopped only as a dependent of a deactivated node. It blocks bring-up exactly like disabled, but activate on the ancestor releases it once no disabled node remains among its transitive dependencies. start_node overrides the hold.
  • detached (set_detached(true)) is full hands-off. The supervisor starts a detached node once, then never drives it again: teardown, cascades, stop and respawn all skip it. Its deps: still order its first spawn.

The canonical behavior per operation. “Cascade” rows expand through the graph: activate expands dependencies upward, deactivate expands dependents downward.

operationTerminatePauseOnDemanddisableddetached
startspawn in dep order; idempotentspawn cold, or resume an instance parked by an earlier teardownskippedskippedfirst start spawns it; re-entry skips
teardownstop + ackstop + ack, parksstop if runningnothing to doskipped
deactivateseed: disabled + stopped; transitive dependents: collateral + stopped, dependents firstdisabled (or collateral) + stopped, parksdisabled + stopped, the whole pool when a member is the target; collateral as a dependentre-disabled (idempotent)skipped, even when targeted
activateenabled + started after its transitive deps; a collateral dependent with no disabled dep left is released and restarted in the same waveenabled + resumed in placeenabled/released only, policy regrowsclears the latchskipped
resume_nodeno-op (wrong mode)reset + resumed in placeno-opskippedno-op
restartcycle node + transitive dependents, re-gated on the way upresumed, never respawnedleft downskippedskipped
respawn_terminatereset + respawn in dep orderuntouchedleft downskippedskipped
resume_pausableuntouchedreset + resumed in placeuntouchedskippedleft parked

Worth knowing: the pair is symmetric over a subtree. deactivate(NET) stops NET’s dependents under the collateral hold, and activate(NET) brings the chain back: it clears the latch and releases every held dependent with no disabled node left in its dependencies. Released Terminate and Pause nodes restart in the same wave; released OnDemand pool members are left to the elastic policy. Overlapping deactivations compose: a node under two deactivated ancestors comes back on the second activate, and a node deactivated directly keeps its latch through an ancestor’s cycle. start_node overrides the hold by hand.

Teardown does not walk the order array one node at a time. It computes, at each moment, every node whose stopping dependents have all acked, and signals exactly those. The consequences are worth internalizing:

  • A dependency keeps serving while its dependents stop, because it is not even signalled until they are gone. A dependent may flush one last buffer over a link or drive a final ioctl through a runner it depends on, inside its own shutdown.
  • Nodes with no ordering relation to each other stop concurrently. Write stop paths against that contract: a node may be told to stop while an unrelated service is still running, so it frees what it owns as soon as it acks and must not assume unordered services still work.

A missed ack is an error, never a hang: every stop path awaits the ack with a 2 s timeout and returns ShutdownTimeout naming the node. The default is per-node overridable with ack_timeout:, for cleanup that legitimately takes longer; each node’s window runs from the moment it is signalled.

A stop that times out still releases the node’s divisible shares: a wedged holder cannot strand its claim. A Pause park is the exception, since the task is coming back. teardown aborts at the first timeout so a still-live dependent never has its dependencies stopped under it; teardown_continue is the best-effort variant for the “reset next anyway” path.

start, activate and restart’s up half spawn every node whose in-pass dependencies are up and whose gates test satisfied, on each round, parking between rounds on a gate-event signal (a slot was filled, an executor slot was set, readiness was asserted). Independent slow bring-ups overlap instead of queueing, and spawn ordering stays strict: a dependent never spawns before its in-pass deps. In a start() wave a node’s slot_timeout covers all its gates together, from the moment its dependencies resolve; the single-node verbs (start_node, and through them the cascades) budget per gate instead.

Shutdown-ack timeout 2 s (per node overridable with ack_timeout:; the window starts when the node is signalled). Pre-spawn gate wait 100 ms per node (override with slot_timeout:). Control mailbox depth 4. Trace tracks up to 4 executors; graphs register onto a linked chain, any number. Up to 256 nodes per graph, indices are u8.