# urus — Roadmap > Companion to `urus-spec.md` (artefact store, URUS project). v1 shipped > 2026-05-26, built against pre-v0.4 smarm — before cancellation, supervisors, > links, gen_server, registry, select, and the slot-slab runtime existed. > smarm is now at v0.8. This roadmap covers catching up, then extending. ## State assessment (2026-06-11) 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`, `run`, `JoinHandle`) survived the v0.5–v0.8 runtime rework unchanged. - **Timeouts are decorative.** `Config::{request_timeout, keep_alive_timeout}` are threaded into `ConnLimits` and never read. `read_some` parks in `wait_readable` with no deadline → an idle keep-alive connection pins its actor (and its 64 KiB+ of buffers) forever. Slowloris-shaped. - **No supervision.** Spec §2.1 calls for a supervised listener pool; v1 predates `Strategy`/`ChildSpec` and uses bare `spawn` + `join`. A panicking listener silently shrinks the accept pool. - **No graceful shutdown.** Spec deferred it; smarm now has the pieces: `request_stop` + sentinel unwind, and the 06-10 io fix (*deregister a stopped actor's fd wait on the unwind path*) makes stopping an actor parked in `wait_readable` safe. The `OwnedFd`-drops-on-unwind discipline in `conn_actor.rs` already composes with this. - **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`, 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. --- ## v0.2 — OTP refresh ✅ (landed 2026-06, chunks 1-4 / commits 5fe6969..HEAD) Adopt the primitives urus predates. No protocol surface changes; `Plug`/`Conn` untouched. Chunks are independently landable, in order: ### 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); `spawn` parents them under the listener, which has no supervisor channel, so their deaths are invisible to the pool supervisor by construction. Open question resolved as leaned: the ChildSpec factory owns the fd via `Arc` (`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 ✅ Landed (c658ac0): `serve_with_shutdown(config, pipeline, signal)` + `shutdown_handle()`. Drain-then-stop as decided below; design details in the commit body. `serve_with` keeps v1 run-forever semantics by dropping its own Handle. `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. **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. 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 ✅ Landed (a60687b): timed waits as decided below; the leaning held — a per-request `Instant` deadline keeps `request_timeout` an honest whole-request wall clock (mid-head expiry: best-effort non-parking 408; mid-body: close). Details in the commit body. **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): `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. 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 ✅ Landed: registry names + whereis test (in-runtime probe via a handler — tests are foreign threads), README rewrite, smarm-trace compile verified. - Registry names: `urus.server`, `urus.listener.{n}` via `smarm::register` (debuggability; `whereis` in tests). - README: rewrite Roadmap section to point here; document `Handle`. - `smarm-trace` feature passthrough already exists — verify it still compiles under `--features smarm-trace`. **Exit criteria:** kill -TERM equivalent drains cleanly in the crud example; an idle keep-alive conn is reaped at `keep_alive_timeout`; a deliberately panicking listener restarts under load without dropped accepts (test each). --- ## v0.3 — Streaming bodies + SSE The slipped v1 goal, and the WebSocket prerequisite (an upgraded connection is "a response that never ends"). 1. `RespBody::Stream(...)` — handler hands back a writer callback or a `Receiver>`; conn actor switches to chunked transfer-encoding (HTTP/1.1) and pumps until the stream closes. Keep-alive after a completed chunked response. 2. Chunked **request** bodies (`parser.rs` currently returns 411) — same 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 with the per-read deadline disabled (or set to the heartbeat interval) — liveness comes from the keep-alive ping, not `request_timeout`. --- ## v0.4 — WebSocket upgrade path 1. **Handshake**: detect `Connection: Upgrade` + `Upgrade: websocket` in the pipeline; `Conn::upgrade(handler)` short-circuits — conn actor validates `Sec-WebSocket-Key`, emits 101, exits the HTTP loop. (SHA-1 + base64 needed; vendor a tiny SHA-1 rather than pulling a crate — urus has two deps and it should stay that way.) 2. **Frame codec**: RFC 6455 framing — fragmentation, masking (client frames are always masked), control frames (ping/pong/close), 125-byte control-payload rule, close-code handshake. 3. **Duplex topology** (the design decision from the assessment): - **(a) Two actors, dup'd fd**: reader actor on the dup, writer actor on the original, writer owns a `Receiver`; the user handler talks to both via channels. Proven trick (listener pool), works today. - **(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. ## v0.5 — PubSub `urus::pubsub`, phoenix_pubsub-shaped, local node only (no distribution — smarm has none): - `subscribe(topic)`, `unsubscribe(topic)`, `broadcast(topic, msg)`, `broadcast_from(self, topic, msg)`. - Topic table: gen_server owning `HashMap>)>>`; shard N ways by topic hash if the single server measures hot (don't pre-shard; bench first — smarm channel sends are cheap). - Dead subscriber cleanup via `monitor` + `handle_down` (this is exactly what 06-10's gen_server `handle_down` was built for). - `Arc` payloads: one allocation per broadcast, not per subscriber — the shared-heap advantage over BEAM, take it. Deliberately independent of HTTP — usable from any smarm app. Separate module now, candidate for crate extraction later. ## v0.6 — Channels Phoenix channels: the join/leave/event protocol over WebSocket transport, PubSub underneath. - `Channel` trait: `join(topic, payload, socket)`, `handle_in(event, payload, socket)`, `handle_out`, `terminate`. - Topic router (`"room:*"` patterns) → channel actor per (conn, topic), spawned under the ws connection, linked so conn death reaps channels. - Wire format: Phoenix V2 JSON serializer (`[join_ref, ref, topic, event, payload]`) — free interop with phoenix.js clients is the killer feature; needs a JSON dep or a hand-rolled mini-codec (decision point — this would be dep #3). - Presence: explicitly out of scope until distribution exists somewhere. --- ## Later / icebox - **HTTP/2** — spec §2.2 already designs the stream-actor demux (zero-copy ownership transfer, conn actor owns HPACK + flow control, stream actors run the pipeline). Big: HPACK, flow control, priorities. The Stressgrid constraint (no stream-actor cost on HTTP/1.1) stays law. - **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. - **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, but prove it).