feat(ws+pubsub): on_open hook + ws_chat — v0.5 chunk 2
WsHandler::on_open(&mut self, sender) — defaulted, non-breaking,
added mid-chunk for veto-by-diff (the v0.3 "push" precedent for
defaults-level decisions): without it a listen-only ws client can
never be subscribed, since on_message never fires for a client that
doesn't send. Runs exactly once, first, in the conn actor, before any
buffered pipelined frame is decoded. Panic contract identical to
on_message: check_cancelled re-raise dance, else 1011 and no
on_close.
DISCOVERY 1 (documented, not fixed — inherent): the 101 reaches the
client BEFORE on_open runs, so "is my subscription live yet" is a
race client-side. App-level ack is the answer (any reply proves
on_open completed; callbacks are sequential). The integration tests
hit this immediately (first read of a join notice flaked into a 5s
read-timeout) and use a sync/synced ack; same reason Phoenix joins
reply.
DISCOVERY 2 (the structural one): PubSub::new() is in-runtime only,
but pipelines are built pre-runtime. crud's static-OnceLock bootstrap
DOES NOT COMPOSE with serve_with_shutdown here: a static pins the
table actor's last ServerRef forever, its inbox never closes, the
gen_server never exits, AllDone is unreachable — serve hangs (the
cross-thread-unpark limitation closes the workaround routes). The
pattern that works: NON-static Arc<OnceLock<PubSub<M>>> captured by
the route closure. Drain stops conns+listeners -> last Arc<Pipeline>
drops IN-runtime -> cell+handle drop -> inbox closes -> table exits.
Corollary: relays hold the Receiver only, NEVER a PubSub clone
(relay holds table's inbox open, table holds relay's receiver open:
mutual keepalive, shutdown hangs). Both rules in module docs, README,
and enforced by the new shutdown_with_open_chat_terminates test:
two open chat sockets + live relays + live table, shutdown must
return within 5s.
examples/ws_chat.rs: rooms as topics (room:{name}), on_open
subscribes (conn-actor pid: the monitor scopes cleanup to the
session) + spawns the relay (rx -> WsSender clone; exits on prune or
WsClosed), on_message broadcast_from(self_pid()) so the speaker is
never echoed, on_close broadcasts the leave notice and relies on the
monitor for unsubscribe.
Integration: ws_chat_broadcast_reaches_other_client_not_sender
(no-echo proven orderingly, no timeout reads: B speaks after A, A's
next frame must be B's) + shutdown_with_open_chat_terminates.
hammer.sh default subset now includes ws_ (v0.4 ran it ad hoc; ws IS
conn-lifecycle). Hammered: 35x subset (incl. both new tests) + 3x
full + 1x full under smarm-trace, all green. dbg!-grep clean. smarm
PRISTINE. README pub/sub + on_open sections; ROADMAP v0.5 as-landed.
Suite: 67 unit + 42 integration + 2 doc.
This commit is contained in:
@@ -38,6 +38,23 @@
|
||||
//! but stays alive keeps its (one-shot, inert) monitor until death;
|
||||
//! that's a bounded bookkeeping entry, not a leak.
|
||||
//!
|
||||
//! # Construction must happen in-runtime
|
||||
//!
|
||||
//! [`PubSub::new`] spawns the table actor, which smarm only permits from
|
||||
//! inside `Runtime::run`. Pipelines are built *before* `serve*` boots the
|
||||
//! runtime, so the working pattern (same constraint crud's store hits) is
|
||||
//! lazy init from the first handler — but with a **non-static**
|
||||
//! `Arc<OnceLock<PubSub<M>>>` captured by the route closure, NOT a
|
||||
//! `static`: when the drained pipeline drops at graceful shutdown, the
|
||||
//! cell (and thus the last handle) drops in-runtime, the table's inbox
|
||||
//! closes, and the table exits — `serve_with_shutdown` returns. A
|
||||
//! `static` pins the table forever and blocks smarm's all-done. The same
|
||||
//! reasoning forbids long-lived consumer actors (relays, producers) from
|
||||
//! holding a `PubSub` clone: relay holds table's inbox open, table holds
|
||||
//! relay's receiver open, neither exits. Relays take the `Receiver` only.
|
||||
//! See `examples/ws_chat.rs` and the `shutdown_with_open_chat_terminates`
|
||||
//! integration test for the full chain.
|
||||
//!
|
||||
//! # Why there is no `register(name)` helper (yet)
|
||||
//!
|
||||
//! smarm's registry maps `name → Pid`, but a `Pid` cannot be turned back
|
||||
|
||||
@@ -55,6 +55,22 @@ use std::time::Instant;
|
||||
/// error` close frame and skips `on_close` (the handler is not re-entered
|
||||
/// after it panicked).
|
||||
pub trait WsHandler: Send + 'static {
|
||||
/// The connection is up: the 101 has been written and the frame loop
|
||||
/// is about to start. Runs exactly once, first, inside the
|
||||
/// connection actor. This is the place to subscribe / spawn
|
||||
/// producers for sessions where the client may never send (a
|
||||
/// listen-only chat member, a live feed) — `on_message` never fires
|
||||
/// for those. NOTE the 101 reaches the client *before* `on_open`
|
||||
/// runs: a client that needs to know its subscriptions are live must
|
||||
/// use an application-level ack (send something, await the reply —
|
||||
/// any reply proves `on_open` completed, since callbacks are
|
||||
/// sequential in the conn actor). A panic here closes `1011` and
|
||||
/// skips `on_close`, exactly like a panicking `on_message`.
|
||||
/// Default: no-op.
|
||||
fn on_open(&mut self, sender: &WsSender) {
|
||||
let _ = sender;
|
||||
}
|
||||
|
||||
/// A complete data message (fragments already reassembled, text
|
||||
/// already UTF-8 validated) arrived from the client.
|
||||
fn on_message(&mut self, msg: Message, sender: &WsSender);
|
||||
@@ -158,6 +174,16 @@ pub(crate) fn run_duplex(
|
||||
let sender = WsSender { tx };
|
||||
let mut asm = Assembler::new(limits.max_message_bytes);
|
||||
|
||||
// on_open before anything is decoded — even a pipelined first frame
|
||||
// is observed by a handler that knows the connection exists. Same
|
||||
// panic contract as on_message: re-raise a stop sentinel, else 1011.
|
||||
let r = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| handler.on_open(&sender)));
|
||||
if r.is_err() {
|
||||
smarm::preempt::check_cancelled();
|
||||
let _ = close_and_drain_code(raw, 1011, "", &mut buf, limits);
|
||||
return;
|
||||
}
|
||||
|
||||
loop {
|
||||
// ----- 1. Decode everything already buffered. -----
|
||||
// Runs before the first select so a pipelined first frame is
|
||||
|
||||
Reference in New Issue
Block a user