diff --git a/README.md b/README.md index 058bdb8..b5f8b34 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ convenience wrapper around `runtime::init(Config::exact(1)).run(f)`. | `supervisor` | `Signal::Exit`/`Panic`/`Stopped` funnelled to a parent; `OneForOne`/`OneForAll`/`RestForOne` strategies + restart-intensity cap | | `monitor` | `monitor(pid)` → `Monitor { id, target, rx }`; one-shot `Down` via `rx`; `demonitor(&m)` tears one registration down; unidirectional death notice | | `link` | bidirectional `link`/`unlink`; abnormal death propagates (cooperative stop, or an `ExitSignal` message under `trap_exit`) | -| `gen_server` | `call`/`call_timeout` (sync request-reply) / `cast` (async) over one inbox; `ServerRef` + `init`/`terminate` hooks; server-down via channel closure | +| `gen_server` | `call`/`call_timeout` (sync request-reply) / `cast` (async) over one inbox; `handle_info` over static info arms + `handle_down` via `Watcher`-fed monitors, selected ahead of the inbox; `ServerRef`/`ServerBuilder` + `init`/`terminate` hooks; server-down via channel closure | | `registry` | `register`/`whereis`/`name_of`: name ↔ pid bimap; lazy generation-checked cleanup | ## Quick taste diff --git a/ROADMAP.md b/ROADMAP.md index 723dc86..94b9783 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -244,7 +244,7 @@ API-design question, not a runtime question — see Later). --- -## v0.8 — gen_server: handle_info / handle_down + io fd hygiene +## v0.8 — gen_server: handle_info / handle_down + io fd hygiene ✅ DONE Goal: spend the `select` primitive on the server loop — out-of-band messages (`handle_info`) and monitor `Down`s (`handle_down`) dispatched alongside @@ -266,19 +266,19 @@ Design decisions (settled up front): - **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 +### 1. handle_info — static info arms in the server loop ✅ (`e5d1b3b`) `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 +### 2. handle_down — ServerCtx::watcher + control arm ✅ (`24b95c9`) `handle_down` (no-op default), `ServerCtx`/`Watcher`, the control arm, and the loop's live `Vec`. 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 +### 3. io fd hygiene — DEL on the unwind path ✅ (`f6969e5`) 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 @@ -289,6 +289,17 @@ 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). +### Deviations from the plan +- **The plain-inbox fast path narrowed.** It now also requires the control + arm closed, so every server pays one select on its first iteration (the + ctx drops after `init`; a state that didn't clone the Watcher closes the + arm there and then). Servers that DO hold a Watcher select forever — the + price of dynamic watch. +- **The fd leak was worse than the v0.2 caveat admitted.** Not "at most one + stale wakeup": the stale `waiters` entry failed every future `wait_*` on + that fd with AlreadyExists, permanently (the defensive DEL sat behind the + `contains_key` check). The regression test exhibits it as a hang. + --- ## Later @@ -348,8 +359,10 @@ hardware. Build + run full test suite on an aarch64 device; check - **`select` exists; a unified per-process mailbox still does not.** The supervisor keeps its single `supervisor_channel` funnel; `recv_match` stays per-channel. `select` composes channels at the wait, not into one - queue — the mailbox-merge decision remains open (see Later: - gen_server handle_info). + queue — gen_server's `handle_info`/`handle_down` (v0.8) are built on + exactly that composition, with documented arm priority (downs → control + → infos → inbox) instead of mailbox FIFO. A hot higher-priority arm + starves lower ones by design; that's the contract. - **Cooperative-only.** Preemption and cancellation both depend on the actor reaching `check!()`/yield/alloc/blocking points. - **Lock order is Leaf → Channel, one of each at most** (debug-asserted in