feat(pubsub,channels)!: handles are addresses — bus, hub and session registries are supervised children
smarm 0.7 (415effb, "lifetime is the actor's — refs are addresses") removed the
rule these three actors were built on: a GenServerRef no longer owns the server,
the loop holds its own inbox sender, and the inbox never closes when the last ref
drops. urus's pubsub table, channel hub bus and session registries were still
governed by that deleted rule — PubSub::new() spawned the table and the handle
owned its life — so nothing commanded them to stop. What still terminated a run
was the root-exit sweep, racing the drain: shutdown_with_open_chat_terminates and
channels_wire::shutdown_with_open_channel_terminates failed 4 times in 25
--all-features runs with "serve did not return: ... outlived the drain: Timeout".
Zero in 10 full-suite runs after this change.
The fix is not a supervisor wrapped around the old shape. Every gotcha in this
area descended from constructors that spawn: PubSub::new(), ChannelHub::new() and
PrefixRouter::channel_session() all started actors, which forced in-runtime-only
construction, which forced the Arc<OnceLock<..>> lazy-init from the first handler,
which forced the "cell must not be static" and "a relay must never hold a PubSub
clone" rules. Five documented rules propping up one inverted dependency. So:
description is separated from instantiation.
- PubSub<M> is a name, not a GenServerRef: const-constructible, Copy, spawns
nothing, valid outside the runtime and in a static. Operations resolve through
the registry per call, so a table restarted by its supervisor is reached
transparently (one lookup per broadcast — bench before caching a ref, which
would go stale across exactly the restart the supervisor exists to perform).
PubSub::new() is gone; PubSub::new(name) + PubSub::child() replace it.
- ChannelHub::new(bus, router) returns (hub, Vec<ChildSpec>) — the bus table plus
one registry per session route. Returning both is the point: a hub whose
children were never started compiles and fails on the first join, so the vec is
not left behind a method you can forget to call. #[must_use].
- channel_session gains a registry name; each session registry is separately
named and separately supervised.
- serve_with/serve_with_shutdown take a Vec<ChildSpec> of app children and build
the root as RestForOne[..app children, endpoint]. They start before the
endpoint and, shutdown being ordered in reverse, stop after it has drained, so
a request still in flight can reach the bus. RestForOne because a bus crash
leaves live sockets addressing a table that no longer knows them.
- Deleted: the Arc<OnceLock> idiom from both examples and both test pipelines,
and the module rules that existed only to hand-manage a refcount.
Known cost, not fixed here: channel_session("session:*", "chat-sessions", f) puts
two unrelated string literals side by side and nothing catches a transposition —
a RegistryName newtype is the obvious follow-up.
Tests: 111 lib + 50 integration + 2 doc green, clippy clean, 10/10 full-suite
runs. Unit tests poll for name binding before use — smarm's start-order-is-not-
start-readiness gap; real apps don't hit it, since a handler only runs once a
connection has been accepted.
This commit is contained in:
+58
-22
@@ -48,8 +48,8 @@
|
||||
|
||||
use super::*;
|
||||
|
||||
use smarm::gen_server::{self, GenServer, GenServerCtx};
|
||||
use smarm::{Down, GenServerRef, Watcher};
|
||||
use smarm::gen_server::{self, GenServer, GenServerBuilder, GenServerCtx, GenServerName};
|
||||
use smarm::{Down, Restart, Watcher};
|
||||
|
||||
use std::collections::VecDeque;
|
||||
use std::hash::Hash;
|
||||
@@ -87,7 +87,14 @@ pub trait ChannelSession<P>: Send + 'static {
|
||||
pub(super) struct SessionFactory<P: Encode + Decode + Send + Sync + 'static, K: SessionKey> {
|
||||
inner: Arc<dyn ChannelFactory<P>>,
|
||||
keyfn: fn(&str, &P) -> K,
|
||||
registry: GenServerRef<Registry<P, K>>,
|
||||
/// The registry's registered name. Resolved per deploy, so a
|
||||
/// registry restarted by the supervisor is reached transparently —
|
||||
/// with its session map empty, which is the honest outcome: the
|
||||
/// session actors it tracked died with their control senders.
|
||||
registry: GenServerName<Registry<P, K>>,
|
||||
/// Everything the registry's `ChildSpec` needs to build it again.
|
||||
cap: usize,
|
||||
ttl: Duration,
|
||||
}
|
||||
|
||||
/// The registry's working bounds for a session key.
|
||||
@@ -95,17 +102,19 @@ pub(super) trait SessionKey: Eq + Hash + Clone + Send + 'static {}
|
||||
impl<K: Eq + Hash + Clone + Send + 'static> SessionKey for K {}
|
||||
|
||||
impl<P: Encode + Decode + Send + Sync + 'static, K: SessionKey> SessionFactory<P, K> {
|
||||
/// **In-runtime only** — spawns the registry gen_server.
|
||||
pub(super) fn new<S: ChannelSession<P, Key = K>>(inner: Arc<dyn ChannelFactory<P>>) -> Self {
|
||||
let registry = gen_server::start(Registry {
|
||||
factory: inner.clone(),
|
||||
/// Describe the session route. Spawns nothing; the registry actor is
|
||||
/// started from [`children`](ChannelFactory::children).
|
||||
pub(super) fn new<S: ChannelSession<P, Key = K>>(
|
||||
registry: &'static str,
|
||||
inner: Arc<dyn ChannelFactory<P>>,
|
||||
) -> Self {
|
||||
SessionFactory {
|
||||
inner,
|
||||
keyfn: S::session_key,
|
||||
registry: GenServerName::new(registry),
|
||||
cap: S::buffer_cap(),
|
||||
ttl: S::ttl(),
|
||||
sessions: HashMap::new(),
|
||||
pid_key: HashMap::new(),
|
||||
watcher: None,
|
||||
});
|
||||
SessionFactory { inner, keyfn: S::session_key, registry }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -116,6 +125,23 @@ impl<P: Encode + Decode + Send + Sync + 'static, K: SessionKey> ChannelFactory<P
|
||||
self.inner.create(topic)
|
||||
}
|
||||
|
||||
fn children(&self) -> Vec<ChildSpec> {
|
||||
let (name, factory, cap, ttl) = (self.registry, self.inner.clone(), self.cap, self.ttl);
|
||||
vec![ChildSpec::new(Restart::Permanent, move || {
|
||||
let state = Registry::<P, K> {
|
||||
factory: factory.clone(),
|
||||
cap,
|
||||
ttl,
|
||||
sessions: HashMap::new(),
|
||||
pid_key: HashMap::new(),
|
||||
watcher: None,
|
||||
};
|
||||
if let Err(e) = GenServerBuilder::new(state).named(name).run() {
|
||||
panic!("urus session registry '{}': name already taken: {e:?}", name.as_str());
|
||||
}
|
||||
})]
|
||||
}
|
||||
|
||||
fn deploy(&self, hs: JoinHandshake<P>) -> ChannelInbox<P>
|
||||
where
|
||||
P: Encode + Decode,
|
||||
@@ -125,7 +151,7 @@ impl<P: Encode + Decode + Send + Sync + 'static, K: SessionKey> ChannelFactory<P
|
||||
// Cast: the actor acks the join straight to the socket; the
|
||||
// conn actor has nothing to wait for. A dead registry can only
|
||||
// mean shutdown — the failed join is moot.
|
||||
let _ = self.registry.cast(Join { key, hs, inbound: rx });
|
||||
let _ = gen_server::cast(self.registry, Join { key, hs, inbound: rx });
|
||||
ChannelInbox(tx)
|
||||
}
|
||||
}
|
||||
@@ -568,10 +594,14 @@ mod tests {
|
||||
fn session_hub<S: ChannelSession<TP, Key = String>>(
|
||||
term: &Arc<AtomicBool>,
|
||||
reject_rejoin: bool,
|
||||
) -> ChannelHub<TP> {
|
||||
) -> (ChannelHub<TP>, Vec<smarm::ChildSpec>) {
|
||||
ChannelHub::new(
|
||||
PrefixRouter::new()
|
||||
.channel_session::<S>("room:*", counter_factory(term, reject_rejoin)),
|
||||
"sess-unit-bus",
|
||||
PrefixRouter::new().channel_session::<S>(
|
||||
"room:*",
|
||||
"sess-unit-registry",
|
||||
counter_factory(term, reject_rejoin),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -580,7 +610,8 @@ mod tests {
|
||||
let out = Arc::new(Mutex::new(Vec::<String>::new()));
|
||||
let (out2, term) = (out.clone(), Arc::new(AtomicBool::new(false)));
|
||||
smarm::run(move || {
|
||||
let hub = session_hub::<Cfg>(&term, false);
|
||||
let (hub, children) = session_hub::<Cfg>(&term, false);
|
||||
let _sup = crate::channels::tests::start_hub(&hub, children);
|
||||
let mut h1 = Harness::new(&hub);
|
||||
h1.send("j1|r1|room:a|event|phx_join|u");
|
||||
out2.lock().unwrap().push(h1.recv());
|
||||
@@ -611,7 +642,8 @@ mod tests {
|
||||
let (out2, term) = (out.clone(), Arc::new(AtomicBool::new(false)));
|
||||
let term2 = term.clone();
|
||||
smarm::run(move || {
|
||||
let hub = session_hub::<CfgTtl>(&term2, false);
|
||||
let (hub, children) = session_hub::<CfgTtl>(&term2, false);
|
||||
let _sup = crate::channels::tests::start_hub(&hub, children);
|
||||
let mut h1 = Harness::new(&hub);
|
||||
h1.send("j1|r1|room:a|event|phx_join|u");
|
||||
out2.lock().unwrap().push(h1.recv());
|
||||
@@ -632,7 +664,8 @@ mod tests {
|
||||
let out = Arc::new(Mutex::new(Vec::<String>::new()));
|
||||
let (out2, term) = (out.clone(), Arc::new(AtomicBool::new(false)));
|
||||
smarm::run(move || {
|
||||
let hub = session_hub::<CfgCap>(&term, false);
|
||||
let (hub, children) = session_hub::<CfgCap>(&term, false);
|
||||
let _sup = crate::channels::tests::start_hub(&hub, children);
|
||||
let mut h1 = Harness::new(&hub);
|
||||
h1.send("j1|r1|room:a|event|phx_join|u");
|
||||
out2.lock().unwrap().push(h1.recv());
|
||||
@@ -657,7 +690,8 @@ mod tests {
|
||||
let out = Arc::new(Mutex::new(Vec::<String>::new()));
|
||||
let (out2, term) = (out.clone(), Arc::new(AtomicBool::new(false)));
|
||||
smarm::run(move || {
|
||||
let hub = session_hub::<Cfg>(&term, false);
|
||||
let (hub, children) = session_hub::<Cfg>(&term, false);
|
||||
let _sup = crate::channels::tests::start_hub(&hub, children);
|
||||
let mut h = Harness::new(&hub);
|
||||
h.send("j1|r1|room:a|event|phx_join|u");
|
||||
out2.lock().unwrap().push(h.recv());
|
||||
@@ -681,7 +715,8 @@ mod tests {
|
||||
let out = Arc::new(Mutex::new(Vec::<String>::new()));
|
||||
let (out2, term) = (out.clone(), Arc::new(AtomicBool::new(false)));
|
||||
smarm::run(move || {
|
||||
let hub = session_hub::<Cfg>(&term, false);
|
||||
let (hub, children) = session_hub::<Cfg>(&term, false);
|
||||
let _sup = crate::channels::tests::start_hub(&hub, children);
|
||||
let mut h1 = Harness::new(&hub);
|
||||
h1.send("j1|r1|room:a|event|phx_join|u");
|
||||
out2.lock().unwrap().push(h1.recv());
|
||||
@@ -714,7 +749,8 @@ mod tests {
|
||||
let out = Arc::new(Mutex::new(Vec::<String>::new()));
|
||||
let (out2, term) = (out.clone(), Arc::new(AtomicBool::new(false)));
|
||||
smarm::run(move || {
|
||||
let hub = session_hub::<Cfg>(&term, true);
|
||||
let (hub, children) = session_hub::<Cfg>(&term, true);
|
||||
let _sup = crate::channels::tests::start_hub(&hub, children);
|
||||
let mut h1 = Harness::new(&hub);
|
||||
h1.send("j1|r1|room:a|event|phx_join|u");
|
||||
out2.lock().unwrap().push(h1.recv());
|
||||
|
||||
Reference in New Issue
Block a user