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.
9.4 KiB
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.5–v0.8 runtime rework unchanged. - Timeouts are decorative.
Config::{request_timeout, keep_alive_timeout}are threaded intoConnLimitsand never read.read_someparks inwait_readablewith 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/ChildSpecand uses barespawn+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 inwait_readablesafe. TheOwnedFd-drops-on-unwind discipline inconn_actor.rsalready composes with this. - No streaming response path.
RespBodyisEmpty | 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 (
AlreadyExistson 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):
- Stop listeners (no new accepts; listener fds close).
- Stop connection actors — either drain-then-stop with a deadline, or
immediate
request_stop(unwind closes fds cleanly viaOwnedFd::drop). - Supervisor winds down;
rt.runreturns;serve_withreturns.
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::sleeploop) sweeps andrequest_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_timeoutin smarm. A first-of(fd, timer) wait. The v0.7 epoch-stamped consuming-unpark machinery exists precisely for multi-arm waits (select_timeoutis the channel-flavoured proof), but fd arms aren'tSelectabletoday — this is real smarm work inwait_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}viasmarm::register(debuggability;whereisin tests). - README: rewrite Roadmap section to point here; document
Handle. smarm-tracefeature 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").
RespBody::Stream(...)— handler hands back a writer callback or aReceiver<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.- Chunked request bodies (
parser.rscurrently returns 411) — same cycle, the codec knowledge is fresh. - SSE sugar:
Conn::sse()→ anEventSender(.send(event, data)), setscontent-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 fromrequest_timeout(keep-alive ping instead).
v0.4 — WebSocket upgrade path
- Handshake: detect
Connection: Upgrade+Upgrade: websocketin the pipeline;Conn::upgrade(handler)short-circuits — conn actor validatesSec-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.) - 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.
- 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.
- (a) Two actors, dup'd fd: reader actor on the dup, writer actor
on the original, writer owns a
- Handler API sketch (settle in design pass): trait with
on_message/on_close+ aWsSenderhandle — 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_serverhandle_downwas 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.
Channeltrait: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 suite —
urus-bench-spec.mdexists in the artefact store; wire it up once v0.2 lands (supervision changes the hot path not at all, but prove it).