docs(roadmap): v0.8 plan — handle_info/handle_down on select; io fd hygiene

This commit is contained in:
Claude
2026-06-10 14:53:10 +00:00
parent a69b296190
commit e545f818fa
+47 -8
View File
@@ -244,6 +244,53 @@ API-design question, not a runtime question — see Later).
--- ---
## v0.8 — gen_server: handle_info / handle_down + io fd hygiene
Goal: spend the `select` primitive on the server loop — out-of-band messages
(`handle_info`) and monitor `Down`s (`handle_down`) dispatched alongside
calls and casts — and close the v0.2 fd-hygiene hole in `io.rs`.
Design decisions (settled up front):
- **Info channels are static.** `type Info: Send + 'static` on the trait; a
`Vec<Receiver<G::Info>>` handed over at start. Users enum their own Info.
Dynamic info subscription is deliberately out: revisit against a consumer.
- **Down forwarding is dynamic, because monitors are.** You monitor a worker
you just spawned inside a handler, so down arms cannot ride the static
list. Mechanism: the loop owns a control arm (`Receiver<Monitor>`);
`init` grows a `&ServerCtx` parameter (breaking, no-op default body) whose
clonable `Watcher` the state stores and calls `watch(monitor)` from any
handler thereafter. Handler signatures otherwise untouched.
- **Priority order: downs → control → infos → inbox.** System messages
preempt; a hot inbox cannot starve a death notice. Within each class,
declaration order (select's documented priority semantics).
- **Closed arms drop silently** (the closed-arm-is-ready-forever gotcha).
Closed inbox still means graceful shutdown, unchanged.
### 1. handle_info — static info arms in the server loop
`type Info` + `handle_info` (no-op default) on `GenServer`; `ServerBuilder`
(`Server::new(state).with_info(rx).under(sup).start()`) so start variants
don't multiply; `start`/`start_under` stay as wrappers. Loop becomes
`select` over `[infos…, inbox]`, dispatching by arm index.
### 2. handle_down — ServerCtx::watcher + control arm
`handle_down` (no-op default), `ServerCtx`/`Watcher`, the control arm, and
the loop's live `Vec<Monitor>`. A `Down` retires its arm (monitors are
one-shot). Integration test: the motivating worker-pool pattern — a server
that spawns workers in a handler, watches them, and reacts to their deaths.
### 3. io fd hygiene — DEL on the unwind path
A stopped actor unwinding out of `wait_fd`'s park leaks its `waiters` entry
and the kernel registration; the stale entry then fails every future
`wait_*` on that fd with `AlreadyExists` (the defensive bare-DEL in
`epoll_register` sits behind the `contains_key` check, so it never runs).
Fix where the invariant breaks: a drop guard in `wait_fd`, armed after a
successful register, disarmed on normal wake; on unwind it removes the
waiters entry iff it is still `(me, epoch)` and only then DELs the fd
(an entry consumed by a racing `FdReady` means the fd may already be
re-registered by someone else — leave it alone).
---
## Later ## Later
### Tunable scheduler idle policy (RFC 004) ### Tunable scheduler idle policy (RFC 004)
@@ -266,15 +313,7 @@ Fixed slab with a loud assert (`Config::max_actors(n)`, default 16 384).
Revisit with a segmented slab (array of `AtomicPtr<Segment>`, doubling segment Revisit with a segmented slab (array of `AtomicPtr<Segment>`, doubling segment
sizes, append-only) once the cap is actually hit. Do not let it calcify. sizes, append-only) once the cap is actually hit. Do not let it calcify.
### gen_server: handle_info
Runtime prerequisite landed in v0.7 (`select`). What remains is API design:
the `Info` message type, who owns the extra channels, whether monitor `Down`s
auto-forward into the server loop. Decide against real consumers, not
speculatively.
### IO fd hygiene on actor death
Pre-existing v0.2 TODO in `io.rs`: fds registered with epoll for a dead actor
are not cleaned up. Audit and fix.
### arm-port validation & merge ### arm-port validation & merge
`arm-port` branch carries an AAPCS64 context-switch backend, never run on `arm-port` branch carries an AAPCS64 context-switch backend, never run on