docs: record the supervised-bus cycle (v0.8) and retire the OnceLock idiom
ROADMAP gains a v0.8 entry: the smarm 415effb lifetime change as root cause, the description/instantiation split, the tree shape, the accepted per-call resolution cost, and what stays open (RegistryName newtype, PubSub's unprotectable const, dynamic session actors, hammer.sh's missing feature matrix). The v0.7 "unreproduced test failure" open item is closed out and pointed at it — it was this hang, hidden because hammer.sh builds with default features while the failure needs --all-features load. README and the two module headers still taught the v0.5 rules: in-runtime-only construction, the non-static Arc<OnceLock<..>> cell, and "a relay must never hold a PubSub clone". None of those are true any more; they are replaced by what actually holds now, with a note on what changed for anyone who learnt the old shape. session.rs also gains an honest note that its session actors are the last lifetime in urus implied by a drop rather than stated — they are dynamic, so a fixed ChildSpec list cannot hold them, and the trigger is at least a command now rather than a refcount.
This commit is contained in:
+83
-6
@@ -413,15 +413,92 @@ your root sup
|
||||
on `serve*`.
|
||||
|
||||
**Open after this cycle:**
|
||||
- One unreproduced test failure seen once in ~120 full-suite runs (output
|
||||
discarded by the loop that caught it; not reproduced in 60x lib + 20x
|
||||
integration + 10x concurrent + 15x full since). Candidates: the
|
||||
`free_port()` bind race the harness already documents, or a timing margin
|
||||
in the new endpoint tests. Grab the test name next time it fires.
|
||||
- ~~One unreproduced test failure seen once in ~120 full-suite runs~~
|
||||
**DIAGNOSED AND FIXED** in v0.8 below — it was the pubsub/channels drain
|
||||
hang, not the `free_port()` race. It hid because `hammer.sh` builds with
|
||||
default features while the failure needs `--all-features` load.
|
||||
- `endpoint()` returns `impl Fn()`, so calling it twice = two endpoints
|
||||
contending for one `Config.name` (second panics on the clash). Honest
|
||||
failure, but the type doesn't prevent the mistake; a consume-on-first-use
|
||||
newtype would.
|
||||
newtype would. `ChannelHub::new` returning `(hub, children)` in v0.8 is
|
||||
the same lesson applied: make the type refuse the mistake.
|
||||
|
||||
## v0.8 — Bus, hub and session registries are supervised — DONE (2026-08-20)
|
||||
|
||||
Same crate version (**0.3.0**); this cycle and v0.7 ship together and are
|
||||
both breaking.
|
||||
|
||||
v0.7 put the endpoint under a supervisor. This puts everything else there
|
||||
too, because smarm 0.7 left it no choice: commit `415effb` ("lifetime is
|
||||
the actor's — refs are addresses") removed the rule the pubsub table,
|
||||
channel hub bus and session registries were built on. A `GenServerRef` no
|
||||
longer owns its server; the loop holds its own inbox sender and the inbox
|
||||
never closes when the last ref drops. Nothing commanded these three to
|
||||
stop, and what still terminated a run was the root-exit sweep racing the
|
||||
drain.
|
||||
|
||||
That is the flake above: `shutdown_with_open_chat_terminates` and
|
||||
`channels_wire::shutdown_with_open_channel_terminates`, 4 failures in 25
|
||||
`--all-features` integration runs, all "serve did not return: ... outlived
|
||||
the drain: Timeout". 0 in 10 full-suite runs after the fix.
|
||||
|
||||
**Description separated from instantiation.** The gotchas here all
|
||||
descended from constructors that spawn — `PubSub::new()`,
|
||||
`ChannelHub::new()` and `PrefixRouter::channel_session()` all started
|
||||
actors, which forced in-runtime-only construction, which forced the
|
||||
`Arc<OnceLock<..>>` lazy-init from the first handler, which forced the
|
||||
"cell must not be `static`" and "a relay must never hold a `PubSub` clone"
|
||||
rules. Five documented rules propping up one inverted dependency.
|
||||
|
||||
```
|
||||
your root sup (RestForOne)
|
||||
├── ChildSpec(Permanent, PubSub::child) <- bus table, named
|
||||
├── ChildSpec(Permanent, session registry) <- one per channel_session
|
||||
└── ChildSpec(Permanent, urus::endpoint(..)) <- starts last, drains first
|
||||
```
|
||||
|
||||
- **`PubSub<M>` is a name, not a ref.** `const`-constructible, `Copy`,
|
||||
spawns nothing, legal outside the runtime and in a `static`. Operations
|
||||
resolve through the registry per call, so a supervisor-restarted table is
|
||||
reached transparently. `PubSub::new()` is gone: `PubSub::new(name)` +
|
||||
`PubSub::child()`.
|
||||
- **`ChannelHub::new(bus, router) -> (hub, Vec<ChildSpec>)`**, `#[must_use]`.
|
||||
Both halves come back together because a hub whose children were never
|
||||
started compiles and fails on the first join. There is no `children()`
|
||||
method to forget to call.
|
||||
- **`channel_session` takes a registry name**; each registry is separately
|
||||
named and supervised.
|
||||
- **`serve_with` / `serve_with_shutdown` take a `Vec<ChildSpec>`** of app
|
||||
children and build `RestForOne[..app children, endpoint]`. App children
|
||||
start before the endpoint and — shutdown being ordered in reverse — stop
|
||||
after it has drained, so a request still in flight can still reach the
|
||||
bus. `RestForOne` because a bus crash leaves live sockets addressing a
|
||||
table that no longer knows them.
|
||||
- Deleted: the `Arc<OnceLock>` idiom from both examples and both test
|
||||
pipelines, and every module rule that existed only to hand-manage a
|
||||
refcount. `ws_chat` lost 21 lines and a struct field; examples net -25.
|
||||
|
||||
**Cost accepted:** one registry resolution per operation, including per
|
||||
broadcast. Caching a `GenServerRef` in the handle would save it and go
|
||||
stale across exactly the restart the supervisor exists to perform. Bench
|
||||
before optimising.
|
||||
|
||||
**Open after this cycle:**
|
||||
- `channel_session("session:*", "chat-sessions", f)` puts two unrelated
|
||||
string literals side by side and nothing catches a transposition. A
|
||||
`RegistryName` newtype makes it a type error. Not done.
|
||||
- `PubSub` cannot be protected the way `ChannelHub` was: a `const` is
|
||||
copied at each use, so it can't be consume-on-first-use. Forgetting
|
||||
`BUS.child()` compiles and fails at runtime with `PubSubDown`. Judged
|
||||
worth it — the `const` is what makes the handle pleasant.
|
||||
- Session actors are still plain-spawned by their registry, not supervised
|
||||
(they're dynamic, one per key — OTP would want `simple_one_for_one`).
|
||||
Their exit chain is now commanded rather than refcounted: the registry is
|
||||
shut down, its state drops, control senders drop, parked sessions wake on
|
||||
the closed arm. Sound, but it is the last place a lifetime is implied by
|
||||
a drop rather than stated.
|
||||
- `hammer.sh` still passes no feature flags, which is why this hid for a
|
||||
whole cycle. A feature matrix is the obvious fix.
|
||||
|
||||
## Known bugs
|
||||
|
||||
|
||||
Reference in New Issue
Block a user