docs+examples: v0.3 endpoint API; crud converted to the app-owns-the-tree shape
- crud is now the demonstrator: app owns smarm runtime + root supervisor,
store actor and urus::endpoint as ordered siblings (store first, so
reverse-order shutdown drains HTTP before stopping the store),
Shutdown::Infinity on the endpoint, shutdown via
rt.handle().request_shutdown(root_sup) from the stdin thread.
Deletes two kludges the old shape forced:
* static OnceLock<Sender> spawn-on-first-use -> a supervised child
that self-registers a typed Name; handlers use smarm::send per
request and turn 'between incarnations' into a 503 instead of
panicking on a dropped store.
* static SHUTTING_DOWN AtomicBool + 250ms recv_timeout poll in the
store loop and in the SSE ticker -> a plain park; the tree stops
both. Smoke-tested live: CRUD round-trips, SSE stream, clean drain
with the stream open, port closed after.
- Other examples stay short and on serve*, updated for the split config
(serve_with(cfg, smarm::Config, pipe) / serve_with_shutdown(..., signal)).
plain_serve's URUS_SCHED_THREADS now builds a smarm::Config.
ws_chat's doc block explains the OnceLock is a serve*-only workaround
and points at crud for the clean shape.
- README: new 'Your Own Supervision Tree' section (endpoint as the real
API), graceful shutdown reframed as the serve*-only path, Config table
loses scheduler_threads and gains name, PubSub rule 1 notes the
supervised-sibling alternative.
111 lib + 50 integration + 2 doc tests green; clippy clean.
This commit is contained in:
@@ -122,7 +122,11 @@ Handlers are closures taking `(Conn, Next) -> Conn`. Call `Next::call(c)` to con
|
||||
|
||||
### Server Configuration
|
||||
|
||||
`serve(addr, pipeline)` binds and listens on the given address. For more control, use `serve_with(config, pipeline)`:
|
||||
`serve(addr, pipeline)` binds and listens on the given address. For more
|
||||
control, use `serve_with(config, runtime_config, pipeline)` — the urus
|
||||
`Config` holds endpoint knobs, the `smarm::Config` holds runtime knobs
|
||||
(they are separate because an endpoint placed in someone else's tree
|
||||
cannot dictate the runtime):
|
||||
|
||||
```rust
|
||||
use urus::{serve_with, Config};
|
||||
@@ -130,7 +134,7 @@ use std::time::Duration;
|
||||
|
||||
let cfg = Config {
|
||||
listener_pool: 2, // Supervised accept-loop actors
|
||||
scheduler_threads: Some(2), // smarm worker threads (None = one per CPU)
|
||||
name: "urus", // Endpoint registry name (unique per endpoint)
|
||||
keep_alive_timeout: Duration::from_secs(60), // Idle budget between requests
|
||||
request_timeout: Duration::from_secs(30), // Whole-request READ deadline
|
||||
write_timeout: Duration::from_secs(30), // Per-write response budget
|
||||
@@ -141,7 +145,7 @@ let cfg = Config {
|
||||
..Config::new("127.0.0.1:8080".parse().unwrap())
|
||||
};
|
||||
|
||||
serve_with(cfg, pipeline).unwrap();
|
||||
serve_with(cfg, smarm::Config::exact(2), pipeline).unwrap();
|
||||
```
|
||||
|
||||
**Timeout semantics:**
|
||||
@@ -162,10 +166,51 @@ serve_with(cfg, pipeline).unwrap();
|
||||
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
|
||||
### Your Own Supervision Tree
|
||||
|
||||
`serve_with_shutdown` takes a `ShutdownSignal`; the paired `Handle` can be
|
||||
triggered from anywhere (another thread, a signal handler):
|
||||
The real API is `urus::endpoint(config, pipeline)`, which binds the socket
|
||||
and hands back a supervisable child body. Your application owns the
|
||||
runtime and the root supervisor; urus is one child among your own actors:
|
||||
|
||||
```rust
|
||||
use smarm::{ChildSpec, OneForOne, Restart, supervisor::Shutdown};
|
||||
|
||||
// Binds here: an address-in-use error is a startup failure, not an actor
|
||||
// crash. The fds outlive any restart of the endpoint child.
|
||||
let endpoint = urus::endpoint(cfg, pipeline)?;
|
||||
|
||||
let rt = smarm::init(smarm::Config::default());
|
||||
rt.run(move || {
|
||||
let sup = smarm::spawn(move || {
|
||||
OneForOne::new()
|
||||
// Your state actor FIRST: reverse-order shutdown therefore
|
||||
// stops it LAST, after HTTP has finished draining.
|
||||
.child(ChildSpec::new(Restart::Permanent, my_store))
|
||||
.child(ChildSpec::new(Restart::Permanent, endpoint)
|
||||
// The endpoint bounds its own drain with drain_timeout;
|
||||
// a shorter supervisor deadline would cut it in half.
|
||||
.shutdown(Shutdown::Infinity))
|
||||
.run()
|
||||
});
|
||||
let _ = sup.join();
|
||||
});
|
||||
```
|
||||
|
||||
Shutdown is then whatever your app already does — `request_shutdown` on
|
||||
the root supervisor (from a SIGTERM handler via `rt.handle()`, say). The
|
||||
endpoint's `handle_shutdown` stops the listener pool, closes idle
|
||||
keep-alive connections, drains in-flight requests up to `drain_timeout`,
|
||||
force-stops stragglers past it, and exits only when the last connection is
|
||||
gone — so "the endpoint child has stopped" *is* "every connection is
|
||||
gone". See `examples/crud.rs` for a complete app in this shape.
|
||||
|
||||
Address a running endpoint by name for introspection:
|
||||
`urus::endpoint::whereis("urus")`.
|
||||
|
||||
### Graceful Shutdown Without a Tree
|
||||
|
||||
If serving is all your process does, let `serve*` own the runtime and use
|
||||
`Handle`, which triggers the same sequence from any thread:
|
||||
|
||||
```rust
|
||||
use urus::{serve_with_shutdown, shutdown_handle, Config};
|
||||
@@ -177,13 +222,13 @@ std::thread::spawn(move || {
|
||||
handle.shutdown();
|
||||
});
|
||||
|
||||
serve_with_shutdown(cfg, pipeline, signal).unwrap();
|
||||
serve_with_shutdown(cfg, smarm::Config::default(), 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.
|
||||
1. Stop accepting — the listener pool is stopped; 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
|
||||
@@ -331,9 +376,12 @@ by the two cleanup paths above.
|
||||
Two composition rules that matter (both enforced by
|
||||
`shutdown_with_open_chat_terminates` in the integration suite):
|
||||
|
||||
1. `PubSub::new()` spawns an actor, so it must run in-runtime — build it
|
||||
1. `PubSub::new()` spawns an actor, so it must run in-runtime. If your
|
||||
app owns its tree, start the table as a supervised sibling of the
|
||||
endpoint and address it by name — the clean shape. Under `serve*`
|
||||
there is no in-runtime moment before the first request, so build it
|
||||
lazily from a handler via a **non-static** `Arc<OnceLock<PubSub<M>>>`
|
||||
captured by the route closure. A `static` cell pins the table actor
|
||||
captured by the route closure; a `static` cell pins the table actor
|
||||
forever and graceful shutdown never returns.
|
||||
2. Relay/producer actors hold the `Receiver` (plus e.g. a `WsSender`
|
||||
clone) — **never a `PubSub` clone**, or relay and table keep each
|
||||
|
||||
Reference in New Issue
Block a user