docs: README streaming/SSE sections, ROADMAP v0.3 done, crud SSE ticker (v0.3 chunk 4)
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).
This commit is contained in:
@@ -132,7 +132,8 @@ let cfg = Config {
|
||||
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
|
||||
request_timeout: Duration::from_secs(30), // Whole-request READ deadline
|
||||
write_timeout: Duration::from_secs(30), // Per-write response budget
|
||||
max_header_count: 64,
|
||||
read_buf_size: 8 * 1024,
|
||||
max_body_bytes: 1 << 20,
|
||||
@@ -152,7 +153,14 @@ serve_with(cfg, pipeline).unwrap();
|
||||
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.
|
||||
reset its budget by sending one byte at a time. Covers the **read phase
|
||||
only** — a streaming response may legitimately outlive any whole-request
|
||||
clock.
|
||||
- `write_timeout` — a per-write budget for response bytes: the fixed
|
||||
head+body write, and *each* streamed chunk, must complete within it. A
|
||||
client that stops reading is dropped once its socket buffer fills and a
|
||||
write stalls past the budget (the write-side twin of slowloris). Streamed
|
||||
chunks each get a fresh budget; a stream as a whole has no deadline.
|
||||
|
||||
### Graceful Shutdown
|
||||
|
||||
@@ -185,6 +193,62 @@ If every `Handle` is dropped, shutdown can never be signalled and the
|
||||
server runs forever — exactly `serve_with`'s semantics (it does this
|
||||
internally).
|
||||
|
||||
### Streaming Responses
|
||||
|
||||
A handler can return a body it doesn't have yet: hand urus the read end of
|
||||
a channel and feed it from a producer actor (the conn actor owns the
|
||||
socket and all write deadlines; the producer never touches the fd):
|
||||
|
||||
```rust
|
||||
use urus::{Conn, Next};
|
||||
|
||||
|c: Conn, _n: Next| {
|
||||
let (tx, rx) = smarm::channel::<Vec<u8>>();
|
||||
smarm::spawn(move || {
|
||||
for part in ["chunk one", "chunk two"] {
|
||||
if tx.send(part.as_bytes().to_vec()).is_err() {
|
||||
return; // connection died — stop producing
|
||||
}
|
||||
}
|
||||
// tx drops: end of stream.
|
||||
});
|
||||
c.put_status(200).put_body(rx)
|
||||
}
|
||||
```
|
||||
|
||||
On HTTP/1.1 the response uses `Transfer-Encoding: chunked` and the
|
||||
connection is reusable afterwards; on HTTP/1.0 (no chunked framing) bytes
|
||||
go out raw and the connection closes to delimit the body. End of stream is
|
||||
signalled by dropping every `Sender`. The producer contract: a `send`
|
||||
error means the connection is gone — exit. Chunked **request** bodies are
|
||||
decoded transparently; handlers see `conn.body` either way.
|
||||
|
||||
### Server-Sent Events
|
||||
|
||||
`Conn::sse()` is the streaming body with the SSE headers and a heartbeat
|
||||
wired up:
|
||||
|
||||
```rust
|
||||
|c: Conn, _n: Next| {
|
||||
let (c, events) = c.sse(); // or c.sse_with_heartbeat(interval)
|
||||
smarm::spawn(move || {
|
||||
loop {
|
||||
if events.send("tick", "data").is_err() {
|
||||
return; // SseClosed: client gone or server draining
|
||||
}
|
||||
smarm::sleep(std::time::Duration::from_secs(1));
|
||||
}
|
||||
});
|
||||
c
|
||||
}
|
||||
```
|
||||
|
||||
While the producer is silent the connection actor emits `: keep-alive`
|
||||
comment chunks every `HEARTBEAT_INTERVAL` (15s default). Liveness comes
|
||||
from that ping: a vanished client is detected when a write stalls past
|
||||
`write_timeout`. No request clock applies to an open SSE stream, and a
|
||||
graceful shutdown force-stops it at the drain deadline.
|
||||
|
||||
### Named Actors
|
||||
|
||||
For introspection and debugging, the server registers itself in smarm's
|
||||
@@ -262,9 +326,9 @@ This avoids the complexity of async ecosystems while maintaining full concurrenc
|
||||
## Roadmap
|
||||
|
||||
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).
|
||||
graceful shutdown, enforced timeouts, registry names) and v0.3 (streaming
|
||||
bodies, chunked requests, SSE) are done; next up are 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.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user