Found benching with ab -k: a 1.0 request with Connection: Keep-Alive is
kept alive (second pipelined request answered on the same socket) but
the response never echoes the header. 1.0 clients only reuse on explicit
server opt-in, so they wait for EOF that never comes; ab -k deadlocks to
apr_pollset_poll timeout on the first response. Recorded with repro,
fix direction (echo header in serialise_response on live 1.0 conns, or
force-close 1.0), and the no-keep-alive baseline: GET /users, release,
loopback, logging silenced, c=50 -> ~18.9k req/s, p50 3ms / p99 5ms,
0 failed. Fix deliberately not started: scope (error responses, HEAD)
is a user call.
README: write_timeout in the Config sample, read/write timeout-split
semantics spelled out, new Streaming Responses and Server-Sent Events
sections (producer contract, 1.0 fallback, heartbeat liveness), roadmap
summary bumped. ROADMAP: v0.3 marked done with the as-landed decisions
(pull shape per the user's fork answer; Q3 timeout split as proposed)
and verified exit criteria. crud example: GET /ticker SSE route — one
event/second, producer exits on SseClosed or the example's shutdown
flag (so an open ticker doesn't block AllDone past the drain).
Registration:
- Each listener registers "urus.listener.{i}" from inside its ChildSpec
factory. On a restart the stale binding points at a dead pid; smarm's
registry evicts those lazily on register/whereis, so re-registering
the same name is safe. Result ignored — a registry hiccup must not
take the listener down.
- "urus.server" -> supervisor pid, registered via the JoinHandle's
.pid() immediately after spawn rather than inside the closure: the
binding exists before the supervisor runs an instruction, so an early
whereis can't observe None.
Test: server_and_listeners_are_registered probes whereis from a route
handler — whereis must run inside the runtime, and the test thread is a
foreign OS thread with no runtime in its TLS.
Docs:
- README: Config documented in full (timeout semantics: keep_alive =
idle-before-first-byte, request = Instant deadline first-byte ->
head+body, slowloris-proof), Handle/shutdown_handle/serve_with_shutdown
section with the 4-step shutdown sequence, named-actors note, test
coverage list refreshed, Roadmap section now points at ROADMAP.md.
- ROADMAP: v0.2 chunks 1-4 marked landed; design detail stays in the
chunk commit bodies.
Verified: cargo build --features smarm-trace clean; full suite (32
tests) green 3x; debug-grep clean.
This closes v0.2. Exit criteria all verified across chunks 1-3: crud
drains on stdin-Enter (~100ms), idle keep-alive reaped at
keep_alive_timeout, panicking listener restarts under load.
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.
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.