feat(serve): registry names + docs (v0.2 chunk 4)

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.
This commit is contained in:
Claude
2026-06-11 22:24:02 +00:00
parent a60687bfec
commit 8065ea561e
4 changed files with 115 additions and 17 deletions
+68 -13
View File
@@ -126,17 +126,72 @@ Handlers are closures taking `(Conn, Next) -> Conn`. Call `Next::call(c)` to con
```rust
use urus::{serve_with, Config};
use std::net::SocketAddr;
use std::time::Duration;
let cfg = Config {
listener_pool: 2, // Number of OS threads handling accept()
scheduler_threads: Some(2), // Number of smarm worker threads
listener_pool: 2, // Supervised accept-loop actors
scheduler_threads: Some(2), // smarm worker threads (None = one per CPU)
keep_alive_timeout: Duration::from_secs(60), // Idle budget between requests
request_timeout: Duration::from_secs(30), // Whole-request read deadline
max_header_count: 64,
read_buf_size: 8 * 1024,
max_body_bytes: 1 << 20,
drain_timeout: Duration::from_secs(30), // Graceful-shutdown drain budget
..Config::new("127.0.0.1:8080".parse().unwrap())
};
serve_with(cfg, pipeline).unwrap();
```
**Timeout semantics:**
- `keep_alive_timeout` — how long a connection may sit idle waiting for the
*first byte* of a request. Expiry closes the socket silently (nothing was
in flight). Pipelined leftover bytes count as a started request, not idle.
- `request_timeout` — a wall-clock deadline from a request's first byte
until its head and body are fully read. Expiry mid-head gets a
best-effort `408 Request Timeout`; expiry mid-body just closes. Deadlines
are absolute instants, so a trickling (slowloris-style) client can't
reset its budget by sending one byte at a time.
### Graceful Shutdown
`serve_with_shutdown` takes a `ShutdownSignal`; the paired `Handle` can be
triggered from anywhere (another thread, a signal handler):
```rust
use urus::{serve_with_shutdown, shutdown_handle, Config};
let (handle, signal) = shutdown_handle();
std::thread::spawn(move || {
// e.g. wait for SIGTERM / stdin / an admin endpoint...
handle.shutdown();
});
serve_with_shutdown(cfg, pipeline, signal).unwrap();
// Returns once the runtime has fully wound down.
```
`Handle::shutdown()` is idempotent and performs, in order:
1. Stop accepting — every listener exits; no new connections.
2. Close idle keep-alive connections immediately.
3. Drain in-flight requests for up to `Config.drain_timeout`.
4. Force-stop any stragglers past the deadline (sockets close cleanly on
unwind via `OwnedFd::drop`).
If every `Handle` is dropped, shutdown can never be signalled and the
server runs forever — exactly `serve_with`'s semantics (it does this
internally).
### Named Actors
For introspection and debugging, the server registers itself in smarm's
process registry: `urus.server` (the listener-pool supervisor) and
`urus.listener.{i}` (each accept loop). `smarm::whereis(name)` resolves
them from any actor inside the runtime.
## Examples
### CRUD with Actor Ownership
@@ -165,11 +220,12 @@ cargo test
```
Tests in `tests/integration.rs` cover:
- Basic routing (GET, POST, PUT, DELETE)
- Request body echoing
- HTTP header parsing
- Path parameter extraction
- Status code responses
- Basic routing, request bodies, headers, path parameters, status codes
- Keep-alive and pipelining
- Listener supervision (a panicking listener restarts without dropping a pending accept)
- Graceful shutdown (idle close, in-flight drain, force-stop at the drain deadline)
- Timeouts (idle keep-alive reaping, slowloris-style slow headers/body)
- Registry names (`urus.server`, `urus.listener.{i}` resolve via `whereis`)
## Architecture
@@ -205,11 +261,10 @@ This avoids the complexity of async ecosystems while maintaining full concurrenc
## Roadmap
v1 covers HTTP/1.1, the plug pipeline, and a built-in router. Future versions may include:
- Middleware ecosystem (auth, logging, compression, etc.).
- WebSocket support.
- Benchmarking suite (see `urus-bench-spec.md`).
- Additional utilities and examples.
See [`ROADMAP.md`](ROADMAP.md). Summary: v0.2 (supervised listener pool,
graceful shutdown, enforced timeouts, registry names) is done; next up are
streaming bodies + SSE (v0.3), WebSocket (v0.4), PubSub (v0.5), and
Phoenix-style channels (v0.6).
Refer to `urus-spec.md` and `urus-v1-build-notes.md` in the artifact persistence for the original design and implementation notes.