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
|
### 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
|
```rust
|
||||||
use urus::{serve_with, Config};
|
use urus::{serve_with, Config};
|
||||||
@@ -130,7 +134,7 @@ use std::time::Duration;
|
|||||||
|
|
||||||
let cfg = Config {
|
let cfg = Config {
|
||||||
listener_pool: 2, // Supervised accept-loop actors
|
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
|
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
|
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())
|
..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:**
|
**Timeout semantics:**
|
||||||
@@ -162,10 +166,51 @@ serve_with(cfg, pipeline).unwrap();
|
|||||||
write stalls past the budget (the write-side twin of slowloris). Streamed
|
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.
|
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
|
The real API is `urus::endpoint(config, pipeline)`, which binds the socket
|
||||||
triggered from anywhere (another thread, a signal handler):
|
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
|
```rust
|
||||||
use urus::{serve_with_shutdown, shutdown_handle, Config};
|
use urus::{serve_with_shutdown, shutdown_handle, Config};
|
||||||
@@ -177,13 +222,13 @@ std::thread::spawn(move || {
|
|||||||
handle.shutdown();
|
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.
|
// Returns once the runtime has fully wound down.
|
||||||
```
|
```
|
||||||
|
|
||||||
`Handle::shutdown()` is idempotent and performs, in order:
|
`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.
|
2. Close idle keep-alive connections immediately.
|
||||||
3. Drain in-flight requests for up to `Config.drain_timeout`.
|
3. Drain in-flight requests for up to `Config.drain_timeout`.
|
||||||
4. Force-stop any stragglers past the deadline (sockets close cleanly on
|
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
|
Two composition rules that matter (both enforced by
|
||||||
`shutdown_with_open_chat_terminates` in the integration suite):
|
`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>>>`
|
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.
|
forever and graceful shutdown never returns.
|
||||||
2. Relay/producer actors hold the `Receiver` (plus e.g. a `WsSender`
|
2. Relay/producer actors hold the `Receiver` (plus e.g. a `WsSender`
|
||||||
clone) — **never a `PubSub` clone**, or relay and table keep each
|
clone) — **never a `PubSub` clone**, or relay and table keep each
|
||||||
|
|||||||
@@ -240,7 +240,7 @@ fn main() {
|
|||||||
let (handle, signal) = shutdown_handle();
|
let (handle, signal) = shutdown_handle();
|
||||||
let server = std::thread::spawn(move || {
|
let server = std::thread::spawn(move || {
|
||||||
let pipe = Pipeline::new().plug(Router::new().get("/order/:id", order));
|
let pipe = Pipeline::new().plug(Router::new().get("/order/:id", order));
|
||||||
serve_with_shutdown(Config::new(addr), pipe, signal).expect("serve");
|
serve_with_shutdown(Config::new(addr), smarm::Config::default(), pipe, signal).expect("serve");
|
||||||
});
|
});
|
||||||
|
|
||||||
// Wait until it's accepting.
|
// Wait until it's accepting.
|
||||||
|
|||||||
@@ -111,7 +111,12 @@ fn main() {
|
|||||||
handle.shutdown();
|
handle.shutdown();
|
||||||
});
|
});
|
||||||
|
|
||||||
serve_with_shutdown(Config::new("0.0.0.0:8080".parse().unwrap()), Pipeline::new().plug(router), signal)
|
serve_with_shutdown(
|
||||||
|
Config::new("0.0.0.0:8080".parse().unwrap()),
|
||||||
|
smarm::Config::default(),
|
||||||
|
Pipeline::new().plug(router),
|
||||||
|
signal,
|
||||||
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
println!("channels_chat: drained, bye");
|
println!("channels_chat: drained, bye");
|
||||||
}
|
}
|
||||||
|
|||||||
+88
-68
@@ -1,11 +1,18 @@
|
|||||||
//! CRUD example: a tiny user database with JSON persistence.
|
//! CRUD example: a tiny user database with JSON persistence.
|
||||||
//!
|
//!
|
||||||
//! Demonstrates urus and the actor model together:
|
//! Demonstrates urus and the actor model together, in the shape a real
|
||||||
//! - The pipeline is shared (Arc) across all connection actors.
|
//! application should use (v0.3):
|
||||||
//! - Handlers do NOT take a lock or share mutable state directly.
|
//! - The APP owns the smarm runtime and the root supervisor. urus is one
|
||||||
//! - A single "store" actor owns the data; handlers send it a request
|
//! child in that tree — `urus::endpoint(...)` — and the store actor is
|
||||||
//! via a channel and block on the reply. Serialization is structural —
|
//! an ordered sibling started BEFORE it, so the supervisor's
|
||||||
//! the store processes one request at a time, no Mutex needed.
|
//! reverse-order shutdown drains HTTP first and only then stops the
|
||||||
|
//! store. No handler can be mid-request against a store that is gone.
|
||||||
|
//! - Handlers do NOT take a lock or share mutable state directly. A
|
||||||
|
//! single "store" actor owns the data; handlers address it by
|
||||||
|
//! registered name and block on a reply channel. Serialization is
|
||||||
|
//! structural — one request at a time, no Mutex.
|
||||||
|
//! - Both actors are supervised: kill the store (or let it panic) and it
|
||||||
|
//! restarts from the JSON file, with the endpoint left alone.
|
||||||
//! - On every mutating request the store writes the JSON file. Read
|
//! - On every mutating request the store writes the JSON file. Read
|
||||||
//! requests don't touch disk.
|
//! requests don't touch disk.
|
||||||
//!
|
//!
|
||||||
@@ -23,9 +30,8 @@
|
|||||||
//! curl -s http://localhost:8080/users/1
|
//! curl -s http://localhost:8080/users/1
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use smarm::{channel, Sender};
|
use smarm::{channel, ChildSpec, Name, OneForOne, Restart, Sender};
|
||||||
use std::sync::OnceLock;
|
use urus::{Config, Conn, Next, Pipeline, Router};
|
||||||
use urus::{serve_with_shutdown, shutdown_handle, Config, Conn, Next, Pipeline, Router};
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Domain
|
// Domain
|
||||||
@@ -66,7 +72,12 @@ const DB_PATH: &str = "/tmp/urus-crud.json";
|
|||||||
// Store actor body
|
// Store actor body
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
fn store_loop(rx: smarm::Receiver<Request>) {
|
fn store_loop() {
|
||||||
|
let (tx, rx) = channel::<Request>();
|
||||||
|
// Self-registration: the name is bound before the first recv, and it
|
||||||
|
// is re-bound automatically on every restart.
|
||||||
|
smarm::register(STORE, tx).expect("crud.store name already taken");
|
||||||
|
|
||||||
// Load on start. Missing file = empty store. Corrupt file = panic; we
|
// Load on start. Missing file = empty store. Corrupt file = panic; we
|
||||||
// don't auto-rebuild because silently losing data is worse than failing
|
// don't auto-rebuild because silently losing data is worse than failing
|
||||||
// loud.
|
// loud.
|
||||||
@@ -78,19 +89,10 @@ fn store_loop(rx: smarm::Receiver<Request>) {
|
|||||||
let mut next_id: u64 = users.iter().map(|u| u.id).max().unwrap_or(0) + 1;
|
let mut next_id: u64 = users.iter().map(|u| u.id).max().unwrap_or(0) + 1;
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
// recv with a timeout rather than a bare recv: the store must be
|
// A plain park. The supervisor stops this actor at shutdown (after
|
||||||
// stoppable at shutdown, but a cross-thread Sender::send (from the
|
// the endpoint has drained), so there is nothing to poll for.
|
||||||
// stdin thread) can't wake a parked actor — same smarm limitation
|
let req = match rx.recv() {
|
||||||
// that motivates urus's SHUTDOWN_POLL. So we wake on our own timer
|
|
||||||
// and poll the flag. This poll dies with that limitation too.
|
|
||||||
let req = match rx.recv_timeout(std::time::Duration::from_millis(250)) {
|
|
||||||
Ok(r) => r,
|
Ok(r) => r,
|
||||||
Err(smarm::channel::RecvTimeoutError::Timeout) => {
|
|
||||||
if SHUTTING_DOWN.load(std::sync::atomic::Ordering::Relaxed) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
Err(_) => return, // all senders dropped
|
Err(_) => return, // all senders dropped
|
||||||
};
|
};
|
||||||
match req {
|
match req {
|
||||||
@@ -169,25 +171,29 @@ fn persist(users: &[User]) {
|
|||||||
// Handler helpers
|
// Handler helpers
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
//
|
//
|
||||||
// Once-cell trick: the store actor is spawned the first time a handler
|
// The store is a supervised child that registers its own inbox under a
|
||||||
// runs (smarm requires `spawn` to be called from inside an actor — which
|
// typed name; handlers resolve it per send. That replaces the old
|
||||||
// connection actors are). After that all handlers share the same Sender.
|
// `OnceLock<Sender>` spawn-on-first-use trick — which had no supervisor,
|
||||||
// Simpler than threading the Sender through the pipeline at startup.
|
// no restart, and no defined shutdown point — with a plain actor whose
|
||||||
|
// lifecycle the tree owns. A restart re-registers the same name, so
|
||||||
|
// in-flight handlers heal on their next send.
|
||||||
|
|
||||||
static STORE_TX: OnceLock<Sender<Request>> = OnceLock::new();
|
const STORE: Name<Request> = Name::new("crud.store");
|
||||||
|
|
||||||
// Set by the stdin thread at shutdown; the store actor polls it (see
|
/// Send to the store and wait for its reply. `Err` only if the store is
|
||||||
// store_loop). An always-on app actor that never returns would otherwise
|
/// between incarnations (restarting); the handler turns that into a 503
|
||||||
// block smarm's AllDone and keep serve_with_shutdown from returning.
|
/// rather than pretending.
|
||||||
static SHUTTING_DOWN: std::sync::atomic::AtomicBool =
|
fn ask(make: impl FnOnce(Sender<(u16, Vec<u8>)>) -> Request) -> Option<(u16, Vec<u8>)> {
|
||||||
std::sync::atomic::AtomicBool::new(false);
|
let (tx, rx) = channel::<(u16, Vec<u8>)>();
|
||||||
|
smarm::send(STORE, make(tx)).ok()?;
|
||||||
|
rx.recv().ok()
|
||||||
|
}
|
||||||
|
|
||||||
fn store() -> &'static Sender<Request> {
|
fn reply(conn: Conn, r: Option<(u16, Vec<u8>)>) -> Conn {
|
||||||
STORE_TX.get_or_init(|| {
|
match r {
|
||||||
let (tx, rx) = channel::<Request>();
|
Some((status, body)) => json(conn, status, body),
|
||||||
smarm::spawn(move || store_loop(rx));
|
None => json(conn, 503, b"{\"error\":\"store unavailable\"}".to_vec()),
|
||||||
tx
|
}
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn json(conn: Conn, status: u16, body: Vec<u8>) -> Conn {
|
fn json(conn: Conn, status: u16, body: Vec<u8>) -> Conn {
|
||||||
@@ -206,16 +212,13 @@ fn parse_id(s: &str) -> Option<u64> {
|
|||||||
|
|
||||||
/// SSE demo: `curl -N localhost:8080/ticker` streams a tick every second
|
/// SSE demo: `curl -N localhost:8080/ticker` streams a tick every second
|
||||||
/// (with `: keep-alive` comments if it ever goes quiet). The producer
|
/// (with `: keep-alive` comments if it ever goes quiet). The producer
|
||||||
/// exits on SseClosed (client gone / write timeout / shutdown drain) or
|
/// exits on SseClosed — client gone, write timeout, or the drain stopping
|
||||||
/// when the example is shutting down.
|
/// its connection actor. Nothing to flag: the tree's shutdown reaches it.
|
||||||
fn ticker(conn: Conn, _next: Next) -> Conn {
|
fn ticker(conn: Conn, _next: Next) -> Conn {
|
||||||
let (conn, events) = conn.sse();
|
let (conn, events) = conn.sse();
|
||||||
smarm::spawn(move || {
|
smarm::spawn(move || {
|
||||||
let mut n: u64 = 0;
|
let mut n: u64 = 0;
|
||||||
loop {
|
loop {
|
||||||
if SHUTTING_DOWN.load(std::sync::atomic::Ordering::Relaxed) {
|
|
||||||
return; // dropping `events` ends the stream cleanly
|
|
||||||
}
|
|
||||||
if events.send("tick", &n.to_string()).is_err() {
|
if events.send("tick", &n.to_string()).is_err() {
|
||||||
return; // SseClosed
|
return; // SseClosed
|
||||||
}
|
}
|
||||||
@@ -227,18 +230,14 @@ fn ticker(conn: Conn, _next: Next) -> Conn {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn list(conn: Conn, _next: Next) -> Conn {
|
fn list(conn: Conn, _next: Next) -> Conn {
|
||||||
let (tx, rx) = channel::<(u16, Vec<u8>)>();
|
let r = ask(|reply| Request::List { reply });
|
||||||
store().send(Request::List { reply: tx }).ok();
|
reply(conn, r)
|
||||||
let (status, body) = rx.recv().expect("store dropped");
|
|
||||||
json(conn, status, body)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn create(conn: Conn, _next: Next) -> Conn {
|
fn create(conn: Conn, _next: Next) -> Conn {
|
||||||
let body = conn.body.as_bytes().to_vec();
|
let body = conn.body.as_bytes().to_vec();
|
||||||
let (tx, rx) = channel::<(u16, Vec<u8>)>();
|
let r = ask(|reply| Request::Create { body, reply });
|
||||||
store().send(Request::Create { body, reply: tx }).ok();
|
reply(conn, r)
|
||||||
let (status, body) = rx.recv().expect("store dropped");
|
|
||||||
json(conn, status, body)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_one(conn: Conn, _next: Next) -> Conn {
|
fn get_one(conn: Conn, _next: Next) -> Conn {
|
||||||
@@ -246,10 +245,8 @@ fn get_one(conn: Conn, _next: Next) -> Conn {
|
|||||||
Some(id) => id,
|
Some(id) => id,
|
||||||
None => return json(conn, 400, b"{\"error\":\"bad id\"}".to_vec()),
|
None => return json(conn, 400, b"{\"error\":\"bad id\"}".to_vec()),
|
||||||
};
|
};
|
||||||
let (tx, rx) = channel::<(u16, Vec<u8>)>();
|
let r = ask(|reply| Request::Get { id, reply });
|
||||||
store().send(Request::Get { id, reply: tx }).ok();
|
reply(conn, r)
|
||||||
let (status, body) = rx.recv().expect("store dropped");
|
|
||||||
json(conn, status, body)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update(conn: Conn, _next: Next) -> Conn {
|
fn update(conn: Conn, _next: Next) -> Conn {
|
||||||
@@ -258,10 +255,8 @@ fn update(conn: Conn, _next: Next) -> Conn {
|
|||||||
None => return json(conn, 400, b"{\"error\":\"bad id\"}".to_vec()),
|
None => return json(conn, 400, b"{\"error\":\"bad id\"}".to_vec()),
|
||||||
};
|
};
|
||||||
let body = conn.body.as_bytes().to_vec();
|
let body = conn.body.as_bytes().to_vec();
|
||||||
let (tx, rx) = channel::<(u16, Vec<u8>)>();
|
let r = ask(|reply| Request::Update { id, body, reply });
|
||||||
store().send(Request::Update { id, body, reply: tx }).ok();
|
reply(conn, r)
|
||||||
let (status, body) = rx.recv().expect("store dropped");
|
|
||||||
json(conn, status, body)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn delete(conn: Conn, _next: Next) -> Conn {
|
fn delete(conn: Conn, _next: Next) -> Conn {
|
||||||
@@ -269,10 +264,8 @@ fn delete(conn: Conn, _next: Next) -> Conn {
|
|||||||
Some(id) => id,
|
Some(id) => id,
|
||||||
None => return json(conn, 400, b"{\"error\":\"bad id\"}".to_vec()),
|
None => return json(conn, 400, b"{\"error\":\"bad id\"}".to_vec()),
|
||||||
};
|
};
|
||||||
let (tx, rx) = channel::<(u16, Vec<u8>)>();
|
let r = ask(|reply| Request::Delete { id, reply });
|
||||||
store().send(Request::Delete { id, reply: tx }).ok();
|
reply(conn, r)
|
||||||
let (status, body) = rx.recv().expect("store dropped");
|
|
||||||
json(conn, status, body)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -305,20 +298,47 @@ fn main() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
let cfg = Config::new("127.0.0.1:8080".parse().unwrap());
|
let cfg = Config::new("127.0.0.1:8080".parse().unwrap());
|
||||||
|
// Bind here, on this thread: 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).expect("bind 127.0.0.1:8080");
|
||||||
|
|
||||||
println!("urus-crud: DB at {DB_PATH}");
|
println!("urus-crud: DB at {DB_PATH}");
|
||||||
println!("urus-crud: listening on 127.0.0.1:8080 — press Enter to shut down");
|
println!("urus-crud: listening on 127.0.0.1:8080 — press Enter to shut down");
|
||||||
|
|
||||||
|
let rt = smarm::init(smarm::Config::default());
|
||||||
|
let handle = rt.handle();
|
||||||
|
let (sup_tx, sup_rx) = std::sync::mpsc::channel();
|
||||||
|
|
||||||
// Graceful shutdown on stdin-Enter: a plain OS thread blocks on
|
// Graceful shutdown on stdin-Enter: a plain OS thread blocks on
|
||||||
// read_line and fires the handle. No signal handling crate needed.
|
// read_line and shuts the ROOT SUPERVISOR down. No signal-handling
|
||||||
let (handle, signal) = shutdown_handle();
|
// crate needed, and no urus-specific shutdown plumbing — this is
|
||||||
|
// exactly what a SIGTERM handler would do.
|
||||||
std::thread::spawn(move || {
|
std::thread::spawn(move || {
|
||||||
|
let sup: smarm::Pid = sup_rx.recv().expect("supervisor pid");
|
||||||
let mut line = String::new();
|
let mut line = String::new();
|
||||||
let _ = std::io::stdin().read_line(&mut line);
|
let _ = std::io::stdin().read_line(&mut line);
|
||||||
println!("urus-crud: shutting down (draining in-flight requests)…");
|
println!("urus-crud: shutting down (draining in-flight requests)…");
|
||||||
SHUTTING_DOWN.store(true, std::sync::atomic::Ordering::Relaxed);
|
handle.request_shutdown(sup);
|
||||||
handle.shutdown();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
serve_with_shutdown(cfg, pipeline, signal).unwrap();
|
rt.run(move || {
|
||||||
|
let sup = smarm::spawn(move || {
|
||||||
|
OneForOne::new()
|
||||||
|
// Store FIRST: reverse-order shutdown therefore stops it
|
||||||
|
// LAST, after the endpoint has finished draining.
|
||||||
|
.child(ChildSpec::new(Restart::Permanent, store_loop))
|
||||||
|
.child(
|
||||||
|
ChildSpec::new(Restart::Permanent, endpoint)
|
||||||
|
// The endpoint bounds its own drain with
|
||||||
|
// Config::drain_timeout; a shorter supervisor
|
||||||
|
// deadline would cut that drain in half.
|
||||||
|
.shutdown(smarm::supervisor::Shutdown::Infinity),
|
||||||
|
)
|
||||||
|
.run()
|
||||||
|
});
|
||||||
|
let _ = sup_tx.send(sup.pid());
|
||||||
|
let _ = sup.join();
|
||||||
|
});
|
||||||
println!("urus-crud: bye");
|
println!("urus-crud: bye");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ fn main() {
|
|||||||
let (handle, signal) = shutdown_handle();
|
let (handle, signal) = shutdown_handle();
|
||||||
let server = std::thread::spawn(move || {
|
let server = std::thread::spawn(move || {
|
||||||
let pipe = Pipeline::new().plug(Router::new().get("/json/:id", json_id));
|
let pipe = Pipeline::new().plug(Router::new().get("/json/:id", json_id));
|
||||||
serve_with_shutdown(Config::new(addr), pipe, signal).expect("serve");
|
serve_with_shutdown(Config::new(addr), smarm::Config::default(), pipe, signal).expect("serve");
|
||||||
});
|
});
|
||||||
|
|
||||||
// Readiness is the orchestrator's job (TCP probe); ours is to not
|
// Readiness is the orchestrator's job (TCP probe); ours is to not
|
||||||
|
|||||||
@@ -41,8 +41,13 @@ fn main() {
|
|||||||
// Audit line: lands in each bench cell's server.log so the effective
|
// Audit line: lands in each bench cell's server.log so the effective
|
||||||
// scheduler count is recorded per cell, same discipline as mode-verify.
|
// scheduler count is recorded per cell, same discipline as mode-verify.
|
||||||
eprintln!("plain_serve: scheduler_threads={sched_threads:?}");
|
eprintln!("plain_serve: scheduler_threads={sched_threads:?}");
|
||||||
let mut cfg = Config::new(addr);
|
// Scheduler threads are a RUNTIME knob, so they live in smarm::Config,
|
||||||
cfg.scheduler_threads = sched_threads;
|
// not urus's — an endpoint placed in someone else's tree could not
|
||||||
|
// honour them anyway.
|
||||||
|
let rt_cfg = match sched_threads {
|
||||||
|
Some(n) => smarm::Config::exact(n),
|
||||||
|
None => smarm::Config::default(),
|
||||||
|
};
|
||||||
let pipe = Pipeline::new().plug(Router::new().get("/json/:id", json_id));
|
let pipe = Pipeline::new().plug(Router::new().get("/json/:id", json_id));
|
||||||
serve_with(cfg, pipe).expect("serve");
|
serve_with(Config::new(addr), rt_cfg, pipe).expect("serve");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,5 +44,5 @@ fn main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let pipe = Pipeline::new().plug(Router::new().get("/json/:id", json_id));
|
let pipe = Pipeline::new().plug(Router::new().get("/json/:id", json_id));
|
||||||
serve_with(cfg, pipe).expect("serve");
|
serve_with(cfg, smarm::Config::default(), pipe).expect("serve");
|
||||||
}
|
}
|
||||||
|
|||||||
+17
-9
@@ -7,14 +7,16 @@
|
|||||||
//!
|
//!
|
||||||
//! - **One `PubSub<String>` for the whole app**, topics are rooms
|
//! - **One `PubSub<String>` for the whole app**, topics are rooms
|
||||||
//! (`room:{name}`). Built lazily on the first connection via a
|
//! (`room:{name}`). Built lazily on the first connection via a
|
||||||
//! NON-static `Arc<OnceLock<PubSub>>` captured by the route closure:
|
//! NON-static `Arc<OnceLock<PubSub>>` captured by the route closure,
|
||||||
//! `PubSub::new()` spawns the table actor, which smarm only allows
|
//! because `PubSub::new()` spawns the table actor and smarm only
|
||||||
//! in-runtime — and keeping the cell non-static means the table's
|
//! allows `spawn` in-runtime — while `serve_with_shutdown` owns the
|
||||||
//! last handle drops in-runtime when the drained pipeline drops, so
|
//! runtime, there is no in-runtime moment before the first request.
|
||||||
//! `serve_with_shutdown` actually returns. A `static` cell would pin
|
//! Keep the cell non-static so the table is dropped with the drained
|
||||||
//! the table forever and block smarm's all-done. (Same constraint
|
//! pipeline rather than pinned for the life of the process.
|
||||||
//! crud's store solves with its OnceLock; the non-static refinement
|
//!
|
||||||
//! is what makes graceful shutdown compose.)
|
//! If your app owns its own runtime and tree (see `crud`), skip this
|
||||||
|
//! entirely: start the table as a supervised sibling of
|
||||||
|
//! `urus::endpoint(...)` and address it by name.
|
||||||
//!
|
//!
|
||||||
//! - **`on_open` subscribes and spawns the relay** — a listen-only
|
//! - **`on_open` subscribes and spawns the relay** — a listen-only
|
||||||
//! client receives the room without ever sending. The subscription is
|
//! client receives the room without ever sending. The subscription is
|
||||||
@@ -123,6 +125,12 @@ fn main() {
|
|||||||
handle.shutdown();
|
handle.shutdown();
|
||||||
});
|
});
|
||||||
|
|
||||||
serve_with_shutdown(Config::new("0.0.0.0:8080".parse().unwrap()), pipeline, signal).unwrap();
|
serve_with_shutdown(
|
||||||
|
Config::new("0.0.0.0:8080".parse().unwrap()),
|
||||||
|
smarm::Config::default(),
|
||||||
|
pipeline,
|
||||||
|
signal,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
println!("ws_chat: drained, bye");
|
println!("ws_chat: drained, bye");
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -98,6 +98,6 @@ fn main() {
|
|||||||
handle.shutdown();
|
handle.shutdown();
|
||||||
});
|
});
|
||||||
|
|
||||||
serve_with_shutdown(cfg, pipeline, signal).unwrap();
|
serve_with_shutdown(cfg, smarm::Config::default(), pipeline, signal).unwrap();
|
||||||
println!("ws_echo: bye");
|
println!("ws_echo: bye");
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user