feat(serve): supervised listener pool (v0.2 chunk 1)
OneForOne supervisor on the root actor, Restart::Permanent per listener. ChildSpec factory owns its fd via Arc<OwnedFd> (OwnedFd gains Sync); restarts reuse the fd — no re-dup, no fd-less window. wait_readable failure now exits-and-restarts instead of being fatal. Conn actors stay unsupervised bare spawns. Test hook INJECT_LISTENER_PANICS panics before accept so a pending connection must survive into the restarted listener. Roadmap: chunk 1 marked done; chunk 3 fork resolved to smarm-native timed waits (RFC 008 landed at 393cdd0, reaper option removed); chunk 2 drain-then-stop decided; v0.4-3(b) unblocked.
This commit is contained in:
+57
-42
@@ -7,7 +7,8 @@
|
||||
|
||||
## State assessment (2026-06-11)
|
||||
|
||||
Verified against smarm master (`v0.8`, slot slab + park-epoch + select):
|
||||
Verified against smarm master (`393cdd0`: v0.8 slot slab + park-epoch +
|
||||
select, plus RFC 008 phase 1 fd arms / timed fd waits):
|
||||
|
||||
- **Builds clean, 24/24 tests pass.** The scheduler surface urus consumes
|
||||
(`spawn`, `wait_readable`, `wait_writable`, `sleep`, `init`, `Config`,
|
||||
@@ -27,10 +28,11 @@ Verified against smarm master (`v0.8`, slot slab + park-epoch + select):
|
||||
- **No streaming response path.** `RespBody` is `Empty | Bytes`. Blocks SSE
|
||||
(a spec v1 goal that slipped) and chunked responses.
|
||||
- **Full-duplex constraint (matters for WebSocket).** smarm's epoll model is
|
||||
one waiter per RawFd (`AlreadyExists` on the second registration) and one
|
||||
direction per parked actor. Concurrent read+write on one connection needs
|
||||
either the dup-fd trick (serve.rs already uses it for the listener pool)
|
||||
or a new smarm primitive. Decide in v0.2 design, before the frame codec.
|
||||
one waiter per RawFd (`AlreadyExists`, now surfaced as `Err` via the
|
||||
`try_select` twins) and one direction per parked actor. RFC 008 phase 1
|
||||
(landed `393cdd0`) gives first-of(fd, channel) via fd arms in select —
|
||||
the duplex options in v0.4-3 are both open now. Decide in v0.4 design,
|
||||
before the frame codec.
|
||||
|
||||
---
|
||||
|
||||
@@ -39,51 +41,60 @@ Verified against smarm master (`v0.8`, slot slab + park-epoch + select):
|
||||
Adopt the primitives urus predates. No protocol surface changes; `Plug`/`Conn`
|
||||
untouched. Chunks are independently landable, in order:
|
||||
|
||||
### 1. Supervised listener pool
|
||||
Replace bare spawns in `serve_with` with a supervisor:
|
||||
`Strategy::OneForOne`, one `ChildSpec` per dup'd listener fd,
|
||||
`Restart::Permanent`. A listener panic restarts that listener; the pool
|
||||
### 1. Supervised listener pool ✅
|
||||
Landed: `serve_with` runs a `Strategy::OneForOne` supervisor on the root
|
||||
actor, one `ChildSpec` per dup'd listener fd, `Restart::Permanent`. A
|
||||
listener panic (or a transient `wait_readable` failure, which now exits
|
||||
and restarts instead of being fatal) restarts that listener; the pool
|
||||
never silently shrinks. Connection actors stay unsupervised bare spawns
|
||||
(per spec: a connection is cheap and its failure is local — a 500 path,
|
||||
not a restart path).
|
||||
not a restart path); `spawn` parents them under the listener, which has no
|
||||
supervisor channel, so their deaths are invisible to the pool supervisor
|
||||
by construction.
|
||||
|
||||
Open question: should the restarted child re-dup from listener 0's fd, or
|
||||
own its fd across restarts via the ChildSpec closure? Leaning: closure
|
||||
captures the `OwnedFd`, re-registers on restart. No re-dup needed.
|
||||
Open question resolved as leaned: the ChildSpec factory owns the fd via
|
||||
`Arc<OwnedFd>` (`OwnedFd` gained `Sync` for this) and every (re)start
|
||||
reuses it. No re-dup, no window where a pool slot has no fd. Covered by
|
||||
`tests/integration.rs::panicking_listener_restarts` (pool of 1, panic
|
||||
injected before `accept` so the pending connection must survive into the
|
||||
restarted listener).
|
||||
|
||||
### 2. Graceful shutdown
|
||||
`serve_with` gains a shutdown signal (an `urus::Handle` with `.shutdown()`,
|
||||
backed by a channel + `request_stop` fan-out):
|
||||
1. Stop listeners (no new accepts; listener fds close).
|
||||
2. Stop connection actors — either drain-then-stop with a deadline, or
|
||||
immediate `request_stop` (unwind closes fds cleanly via `OwnedFd::drop`).
|
||||
2. **Decided: drain-then-stop with a deadline** — in-flight requests get
|
||||
to finish; `request_stop` after the deadline (unwind closes fds cleanly
|
||||
via `OwnedFd::drop`). Drain also keeps the door open for an
|
||||
apache-reload-style restart later.
|
||||
3. Supervisor winds down; `rt.run` returns; `serve_with` returns.
|
||||
|
||||
Requires tracking live connection pids. Cheapest honest version: a
|
||||
`gen_server` connection registry — listener casts `{Started, pid}`, conn
|
||||
actor monitors-free self-deregisters via a drop guard cast. This registry
|
||||
is also chunk 3's reaper and the future ws/channels introspection point,
|
||||
so build it once here.
|
||||
actor monitors-free self-deregisters via a drop guard cast. Pid set only
|
||||
(no activity stamps — chunk 3 went per-conn timed waits, no reaper); still
|
||||
the future ws/channels introspection point, so build it once here.
|
||||
|
||||
### 3. Enforce the timeouts we advertise
|
||||
**Decision fork — pick one before implementing:**
|
||||
**Decided: smarm-native timed waits** (the fork's option (a), a reaper in
|
||||
urus, died when RFC 008 landed at `393cdd0` — `wait_readable_timeout` /
|
||||
`wait_writable_timeout` exist upstream now, so there is nothing for the
|
||||
reaper to be the stopgap for):
|
||||
|
||||
- **(a) Reaper in urus.** The chunk-2 registry tracks per-conn
|
||||
`last_activity`; a ticker (`smarm::sleep` loop) sweeps and
|
||||
`request_stop`s expired conns. Works today, zero smarm changes.
|
||||
Granularity = sweep interval; conns must stamp activity (one cast per
|
||||
request — measurable but small).
|
||||
- **(b) `wait_readable_timeout` in smarm.** A first-of(fd, timer) wait.
|
||||
The v0.7 epoch-stamped consuming-unpark machinery exists precisely for
|
||||
multi-arm waits (`select_timeout` is the channel-flavoured proof), but
|
||||
fd arms aren't `Selectable` today — this is real smarm work in
|
||||
`wait_fd`/io thread. Cleaner per-conn semantics, benefits every future
|
||||
smarm io consumer, no reaper bookkeeping.
|
||||
`read_some` parks in `wait_readable_timeout` instead of `wait_readable`;
|
||||
`Ok(false)` means the deadline passed and the conn actor closes. Per-conn
|
||||
semantics, no activity stamping, no sweep ticker. Semantics note: a timed
|
||||
read wait enforces *idle-between-reads* — `keep_alive_timeout` between
|
||||
requests, and slowloris dies because each stalled read has a deadline.
|
||||
`request_timeout` as a whole-request wall clock is NOT what this gives;
|
||||
decide in implementation whether to track a per-request deadline across
|
||||
reads (cheap: one `Instant` in the conn loop, pass the min of both
|
||||
budgets to each wait) or redefine `request_timeout` as per-read. Leaning
|
||||
the `Instant`: it keeps the advertised config honest.
|
||||
|
||||
Leaning **(a) now, (b) later**: (a) ships the security fix this cycle and
|
||||
the registry exists anyway; (b) becomes a smarm roadmap item whose landing
|
||||
lets the reaper's stamps go away. But if you'd rather not build throwaway:
|
||||
(b) first is defensible — say the word.
|
||||
urus is the first real consumer of the timed wrappers — note the RFC 008
|
||||
reviewer flag (`wait_fd` wraps in `NoPreempt`, select arms don't); the
|
||||
crud example under load doubles as that confirmation.
|
||||
|
||||
### 4. Hygiene
|
||||
- Registry names: `urus.server`, `urus.listener.{n}` via `smarm::register`
|
||||
@@ -111,8 +122,9 @@ is "a response that never ends").
|
||||
cycle, the codec knowledge is fresh.
|
||||
3. SSE sugar: `Conn::sse()` → an `EventSender` (`.send(event, data)`),
|
||||
sets `content-type: text/event-stream`, disables buffering, heartbeats
|
||||
via comment lines on a timer. An SSE conn is just a Stream body that
|
||||
the reaper exempts from `request_timeout` (keep-alive ping instead).
|
||||
via comment lines on a timer. An SSE conn is just a Stream body with
|
||||
the per-read deadline disabled (or set to the heartbeat interval) —
|
||||
liveness comes from the keep-alive ping, not `request_timeout`.
|
||||
|
||||
---
|
||||
|
||||
@@ -130,10 +142,15 @@ is "a response that never ends").
|
||||
- **(a) Two actors, dup'd fd**: reader actor on the dup, writer actor
|
||||
on the original, writer owns a `Receiver<Frame>`; the user handler
|
||||
talks to both via channels. Proven trick (listener pool), works today.
|
||||
- **(b) One actor, smarm first-of(fd-readable, channel)**: needs fd
|
||||
arms in select — the same smarm work as v0.2-3(b). Simpler topology,
|
||||
no dup, but blocked on smarm.
|
||||
Default (a); revisit if (b) lands for timeouts anyway.
|
||||
- **(b) One actor, smarm first-of(fd-readable, channel)**: fd arms in
|
||||
select landed with RFC 008 (`393cdd0`) — no longer blocked. Simpler
|
||||
topology, no dup; use the fallible `try_select` twins (registration
|
||||
errors, incl. `AlreadyExists`, surface as `Err` and the wait fully
|
||||
retires).
|
||||
Was "default (a), revisit if (b) lands" — (b) landed, so this is a
|
||||
live choice for the v0.4 design pass. Note RFC 008 is phase 1:
|
||||
single-waiter-per-fd stands, `dup` remains the documented answer for
|
||||
true simultaneous read+write waits.
|
||||
4. Handler API sketch (settle in design pass): trait with
|
||||
`on_message/on_close` + a `WsSender` handle — gen_server-shaped, so a
|
||||
ws connection *is* an actor you can monitor, link, and register.
|
||||
@@ -182,8 +199,6 @@ PubSub underneath.
|
||||
- **SSE ergonomics round 2** — auto-reconnect support (`Last-Event-ID`),
|
||||
event-id bookkeeping, backpressure policy on slow consumers.
|
||||
- **TLS** — still deferred; acceptor design still must not preclude it.
|
||||
- **`wait_readable_timeout` / fd-in-select upstreaming** — the smarm-side
|
||||
item that simplifies v0.2-3 and v0.4-3 retroactively.
|
||||
- **Middleware ecosystem** — compression, static files, auth plugs.
|
||||
- **Bench suite** — `urus-bench-spec.md` exists in the artefact store;
|
||||
wire it up once v0.2 lands (supervision changes the hot path not at all,
|
||||
|
||||
Reference in New Issue
Block a user