Files
urus/ROADMAP.md
T
Claude 3da553b9b2 docs(roadmap): state assessment vs smarm v0.8 + v0.2-v0.6 plan
urus builds clean and passes 24/24 against smarm master; the refresh is
OTP adoption, not breakage. v0.2 chunks: supervised listener pool,
graceful shutdown via request_stop, enforce the (currently decorative)
timeouts, registry naming. Then: streaming bodies + SSE (v0.3),
WebSocket upgrade (v0.4), PubSub (v0.5), Channels (v0.6). HTTP/2 stays
iceboxed per spec §2.2. Two decision forks flagged inline: timeout
mechanism (reaper vs smarm timed fd waits) and ws duplex topology
(dup'd fds vs fd-arms-in-select) — both hinge on smarm RFC 008.
2026-06-11 10:48:50 +00:00

9.4 KiB
Raw Blame History

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 (v0.8, slot slab + park-epoch + select):

  • Builds clean, 24/24 tests pass. The scheduler surface urus consumes (spawn, wait_readable, wait_writable, sleep, init, Config, run, JoinHandle) survived the v0.5v0.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 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.

v0.2 — OTP refresh

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 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).

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.

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).
  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.

3. Enforce the timeouts we advertise

Decision fork — pick one before implementing:

  • (a) Reaper in urus. The chunk-2 registry tracks per-conn last_activity; a ticker (smarm::sleep loop) sweeps and request_stops 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.

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.

4. Hygiene

  • 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<Vec<u8>>; 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 that the reaper exempts from request_timeout (keep-alive ping instead).

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<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.
  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<Topic, Vec<(Pid, Sender<Arc<M>>)>>; 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<M> 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.
  • 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 suiteurus-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).