docs(roadmap): supervisor start order is not start readiness

Filed from the urus v0.3 endpoint work. start_child spawns and moves on,
so a later sibling can whereis an earlier named child before that child's
actor has run. Notes why blocking spawn is not the fix ('has begun
executing' != 'has bound its name', plus a per-accept round-trip tax and
every spawn becoming a context-switch point), that OTP has the same async
spawn and synchronises one level up in gen_server:start_link, the
readiness-ack shape if scheduled, and the structural workaround urus uses
today (registrar spawns its own consumers).
This commit is contained in:
Claude (sandbox)
2026-08-20 13:20:42 +00:00
parent 415effb2e9
commit e570138da5
2 changed files with 251 additions and 0 deletions
+22
View File
@@ -283,6 +283,28 @@ outright — `join` what you need finished. No forcing sweep follows.
- A root-exit shutdown reaches only actors live *at that instant*; a
non-trapping forest root that spawns before it unwinds leaves that spawn
to itself (Erlang: an unlinked spawn is nobody's child).
- **Supervisor start *order* is not start *readiness*.** `start_child`
spawns and moves straight on, so an earlier child is merely *scheduled*,
not initialised, when a later sibling starts. A later child that resolves
an earlier one by name (`whereis_server`) can therefore miss it — the
classic "named registry sibling, then its consumers" tree. Ordered
`OneForOne`/`RestForOne` shutdown is unaffected (reverse order is honoured
and each stop *is* awaited); this is a start-side gap only.
Making `spawn` itself block does NOT fix it — it would only shrink the
window to "child has begun executing", while the property callers need is
"child has bound its name / opened its socket", which only the child can
declare. It would also tax the hot path (one round-trip per accepted
connection) and turn every spawn into a context-switch point. OTP has the
same async `spawn` and puts the synchronisation one level up:
`gen_server:start_link` blocks the caller until `init/1` returns.
Fix shape when scheduled: a readiness ack in the supervisor's child-start
path (`ChildSpec` variant whose factory receives a ready-signal;
`NamedGenServerBuilder::run` acks after its name bind, gen_server default
acks after `init`; plain closures ack at spawn as today, i.e. opt-in with
no cost to existing children). Until then the workaround is structural:
have the registrar spawn its own consumers so the ordering is program
order inside one actor, not a cross-actor guarantee (urus v0.3 endpoint
does exactly this).
## Invariants & gotchas (respect these across all cycles)