port(smarm): track HEAD efbc254 — RFC 014/015 API sync

- gen_server rename (RFC 015, 3e31606): ServerRef/ServerCtx/ServerBuilder
  -> GenServerRef/GenServerCtx/GenServerBuilder across conn_actor,
  conn_registry, serve, pubsub, channels::session.
- type Timer = () on the three GenServer impls (RFC 015, 57eadb5);
  handle_timer/handle_idle/tick_every stay defaulted — opt-in later.
- Watcher and GenServerCtx are now generic over the server type: watcher
  fields typed Watcher<Table<M>> / Watcher<Registry<P, K>>; Registry's
  struct bounds strengthened to its GenServer impl bounds
  (P: Encode + Decode + Send + Sync + 'static, K: SessionKey) so the
  Watcher field's G: GenServer bound is satisfiable at the declaration.
- RFC 014 (a866e34) registry: register is (Name<M>, Sender<M>), self-only
  — a name is a typed messaging endpoint, not a pid tag. The
  introspection-only urus.server / urus.listener.{i} bindings are dropped
  rather than faked with unit channels; the whereis integration test is
  deleted; a proper messageable-name design is icebox'd in ROADMAP.md.
- Audit vs smarm 6c2b7e9 (queued messages dropped when Receiver drops):
  pubsub's prune-on-send-failure retain still holds — send Errs once
  receiver_alive is false, so a dropped rx prunes on next broadcast,
  exactly what subscriber_count's doc already promised. Freeing stranded
  Arc<M> broadcasts is strictly good. No change needed.
- The 7x E0283 in channels/mod.rs were cascade fallout of the generics
  changes; dissolved with the port, as discovery predicted.

Suite: 90 lib + 45 integration + 2 doc, green under default, smarm-trace,
phoenix, and all-features. 3 pre-existing clippy lints (conn.rs,
parser.rs, conn_actor.rs; clippy 1.97 strictness) deferred to a follow-up
chore commit to keep this diff pure.
This commit is contained in:
Claude
2026-07-13 08:22:40 +00:00
parent 86c8d31b93
commit 078072b527
7 changed files with 41 additions and 54 deletions
+6
View File
@@ -378,3 +378,9 @@ reconnect cycles.
- **Bench suite** — `urus-bench-spec.md` exists in the artefact store; - **Bench suite** — `urus-bench-spec.md` exists in the artefact store;
wire it up once v0.2 lands (supervision changes the hot path not at all, wire it up once v0.2 lands (supervision changes the hot path not at all,
but prove it). but prove it).
- **Operator introspection via typed names** — the v0.2-era
`urus.server` / `urus.listener.{i}` pid tags were dropped in the
RFC 014 port (names are messageable endpoints now, self-registered
with a real `Sender<M>`). If wanted back, do it properly: register the
serve loop's shutdown/control channel under a typed `urus.server`
name instead of faking pid tags with unit channels.
+7 -6
View File
@@ -48,8 +48,8 @@
use super::*; use super::*;
use smarm::gen_server::{self, GenServer, ServerCtx}; use smarm::gen_server::{self, GenServer, GenServerCtx};
use smarm::{Down, ServerRef, Watcher}; use smarm::{Down, GenServerRef, Watcher};
use std::collections::VecDeque; use std::collections::VecDeque;
use std::hash::Hash; use std::hash::Hash;
@@ -87,7 +87,7 @@ pub trait ChannelSession<P>: Send + 'static {
pub(super) struct SessionFactory<P: Encode + Decode + Send + Sync + 'static, K: SessionKey> { pub(super) struct SessionFactory<P: Encode + Decode + Send + Sync + 'static, K: SessionKey> {
inner: Arc<dyn ChannelFactory<P>>, inner: Arc<dyn ChannelFactory<P>>,
keyfn: fn(&str, &P) -> K, keyfn: fn(&str, &P) -> K,
registry: ServerRef<Registry<P, K>>, registry: GenServerRef<Registry<P, K>>,
} }
/// The registry's working bounds for a session key. /// The registry's working bounds for a session key.
@@ -140,14 +140,14 @@ pub(super) struct Join<P: Send + Sync + 'static, K> {
inbound: Receiver<In<P>>, inbound: Receiver<In<P>>,
} }
struct Registry<P: Send + Sync + 'static, K> { struct Registry<P: Encode + Decode + Send + Sync + 'static, K: SessionKey> {
factory: Arc<dyn ChannelFactory<P>>, factory: Arc<dyn ChannelFactory<P>>,
cap: usize, cap: usize,
ttl: Duration, ttl: Duration,
sessions: HashMap<K, (Pid, Sender<Ctl<P>>)>, sessions: HashMap<K, (Pid, Sender<Ctl<P>>)>,
/// Reverse index for `handle_down` — the reason `Key: Clone`. /// Reverse index for `handle_down` — the reason `Key: Clone`.
pid_key: HashMap<Pid, K>, pid_key: HashMap<Pid, K>,
watcher: Option<Watcher>, watcher: Option<Watcher<Registry<P, K>>>,
} }
impl<P: Encode + Decode + Send + Sync + 'static, K: SessionKey> GenServer for Registry<P, K> { impl<P: Encode + Decode + Send + Sync + 'static, K: SessionKey> GenServer for Registry<P, K> {
@@ -155,8 +155,9 @@ impl<P: Encode + Decode + Send + Sync + 'static, K: SessionKey> GenServer for Re
type Reply = (); type Reply = ();
type Cast = Join<P, K>; type Cast = Join<P, K>;
type Info = (); type Info = ();
type Timer = ();
fn init(&mut self, ctx: &ServerCtx) { fn init(&mut self, ctx: &GenServerCtx<Self>) {
self.watcher = Some(ctx.watcher()); self.watcher = Some(ctx.watcher());
} }
+2 -2
View File
@@ -19,7 +19,7 @@ use crate::net::OwnedFd;
use crate::parser::{self, ParseError}; use crate::parser::{self, ParseError};
use crate::plug::Pipeline; use crate::plug::Pipeline;
use smarm::ServerRef; use smarm::GenServerRef;
use std::io::{self, ErrorKind}; use std::io::{self, ErrorKind};
use std::os::fd::RawFd; use std::os::fd::RawFd;
@@ -92,7 +92,7 @@ pub fn run_connection(
fd: OwnedFd, fd: OwnedFd,
pipeline: Pipeline, pipeline: Pipeline,
limits: ConnLimits, limits: ConnLimits,
registry: ServerRef<ConnRegistry>, registry: GenServerRef<ConnRegistry>,
) { ) {
// The OwnedFd cleans up via Drop on any exit path (panic, error, or // The OwnedFd cleans up via Drop on any exit path (panic, error, or
// normal close). No explicit close calls below. // normal close). No explicit close calls below.
+6 -5
View File
@@ -33,7 +33,7 @@
//! (roadmap v0.4+), which is why it exists as its own module rather than //! (roadmap v0.4+), which is why it exists as its own module rather than
//! being inlined into `serve`. //! being inlined into `serve`.
use smarm::{GenServer, Pid, ServerBuilder, ServerRef}; use smarm::{GenServer, Pid, GenServerBuilder, GenServerRef};
use std::collections::HashMap; use std::collections::HashMap;
@@ -86,6 +86,7 @@ impl GenServer for ConnRegistry {
type Reply = Reply; type Reply = Reply;
type Cast = Cast; type Cast = Cast;
type Info = (); type Info = ();
type Timer = ();
fn handle_call(&mut self, request: Call) -> Reply { fn handle_call(&mut self, request: Call) -> Reply {
match request { match request {
@@ -137,8 +138,8 @@ impl GenServer for ConnRegistry {
} }
} }
pub fn start() -> ServerRef<ConnRegistry> { pub fn start() -> GenServerRef<ConnRegistry> {
ServerBuilder::new(ConnRegistry::default()).start() GenServerBuilder::new(ConnRegistry::default()).start()
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -149,13 +150,13 @@ pub fn start() -> ServerRef<ConnRegistry> {
/// unwind, and panic unwind alike; the cast is infallible from the /// unwind, and panic unwind alike; the cast is infallible from the
/// guard's perspective (a dead registry just returns an ignored Err). /// guard's perspective (a dead registry just returns an ignored Err).
pub struct DeregisterGuard { pub struct DeregisterGuard {
registry: ServerRef<ConnRegistry>, registry: GenServerRef<ConnRegistry>,
pid: Pid, pid: Pid,
make: fn(Pid) -> Cast, make: fn(Pid) -> Cast,
} }
impl DeregisterGuard { impl DeregisterGuard {
pub fn new(registry: ServerRef<ConnRegistry>, pid: Pid, make: fn(Pid) -> Cast) -> Self { pub fn new(registry: GenServerRef<ConnRegistry>, pid: Pid, make: fn(Pid) -> Cast) -> Self {
Self { registry, pid, make } Self { registry, pid, make }
} }
} }
+7 -6
View File
@@ -58,7 +58,7 @@
//! # Why there is no `register(name)` helper (yet) //! # Why there is no `register(name)` helper (yet)
//! //!
//! smarm's registry maps `name → Pid`, but a `Pid` cannot be turned back //! smarm's registry maps `name → Pid`, but a `Pid` cannot be turned back
//! into a `ServerRef` (the ref *is* the inbox sender). A useful named //! into a `GenServerRef` (the ref *is* the inbox sender). A useful named
//! lookup therefore needs either smarm support (registry-held senders) //! lookup therefore needs either smarm support (registry-held senders)
//! or a process-global type-erased map here — both against the grain of //! or a process-global type-erased map here — both against the grain of
//! the ratified design. Deferred; pass the handle. //! the ratified design. Deferred; pass the handle.
@@ -67,8 +67,8 @@ use std::collections::{HashMap, HashSet};
use std::fmt; use std::fmt;
use std::sync::Arc; use std::sync::Arc;
use smarm::gen_server::{self, GenServer, ServerCtx}; use smarm::gen_server::{self, GenServer, GenServerCtx};
use smarm::{channel, Down, Pid, Receiver, Sender, ServerRef, Watcher}; use smarm::{channel, Down, Pid, Receiver, Sender, GenServerRef, Watcher};
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Public handle // Public handle
@@ -92,7 +92,7 @@ impl std::error::Error for PubSubDown {}
/// Payloads are broadcast as `Arc<M>`: one allocation per broadcast, not /// Payloads are broadcast as `Arc<M>`: one allocation per broadcast, not
/// per subscriber. /// per subscriber.
pub struct PubSub<M: Send + Sync + 'static> { pub struct PubSub<M: Send + Sync + 'static> {
server: ServerRef<Table<M>>, server: GenServerRef<Table<M>>,
} }
impl<M: Send + Sync + 'static> Clone for PubSub<M> { impl<M: Send + Sync + 'static> Clone for PubSub<M> {
@@ -226,7 +226,7 @@ struct Table<M: Send + Sync + 'static> {
/// retires the entry so a reused-slot pid (fresh generation) gets a /// retires the entry so a reused-slot pid (fresh generation) gets a
/// fresh monitor. /// fresh monitor.
monitored: HashSet<Pid>, monitored: HashSet<Pid>,
watcher: Option<Watcher>, watcher: Option<Watcher<Table<M>>>,
} }
impl<M: Send + Sync + 'static> Table<M> { impl<M: Send + Sync + 'static> Table<M> {
@@ -240,8 +240,9 @@ impl<M: Send + Sync + 'static> GenServer for Table<M> {
type Reply = Reply; type Reply = Reply;
type Cast = Cast<M>; type Cast = Cast<M>;
type Info = (); type Info = ();
type Timer = ();
fn init(&mut self, ctx: &ServerCtx) { fn init(&mut self, ctx: &GenServerCtx<Self>) {
self.watcher = Some(ctx.watcher()); self.watcher = Some(ctx.watcher());
} }
+10 -17
View File
@@ -17,7 +17,7 @@ use crate::conn_registry::{self, Call, Cast, ConnRegistry, Reply};
use crate::net::{accept_nonblocking, bind_and_listen, OwnedFd}; use crate::net::{accept_nonblocking, bind_and_listen, OwnedFd};
use crate::plug::Pipeline; use crate::plug::Pipeline;
use smarm::{ChildSpec, OneForOne, Restart, ServerRef, Strategy}; use smarm::{ChildSpec, OneForOne, Restart, GenServerRef, Strategy};
use std::io::{self, ErrorKind}; use std::io::{self, ErrorKind};
use std::net::{SocketAddr, ToSocketAddrs}; use std::net::{SocketAddr, ToSocketAddrs};
@@ -125,7 +125,7 @@ fn listener_loop(
listener: Arc<OwnedFd>, listener: Arc<OwnedFd>,
pipeline: Pipeline, pipeline: Pipeline,
limits: ConnLimits, limits: ConnLimits,
registry: ServerRef<ConnRegistry>, registry: GenServerRef<ConnRegistry>,
shutdown: Arc<AtomicBool>, shutdown: Arc<AtomicBool>,
) { ) {
let fd = listener.as_raw(); let fd = listener.as_raw();
@@ -327,15 +327,6 @@ pub fn serve_with_shutdown(
let sf = shutdown_flag.clone(); let sf = shutdown_flag.clone();
sup = sup.child(ChildSpec::new(Restart::Transient, move || { sup = sup.child(ChildSpec::new(Restart::Transient, move || {
println!("urus: listener {} starting", i); println!("urus: listener {} starting", i);
// Named for whereis-style introspection. On a restart the
// old binding points at a dead pid; smarm's registry
// evicts stale bindings lazily, so re-registering the
// same name is fine. Ignore the result — a registry
// hiccup must not take the listener down.
let _ = smarm::register(
format!("urus.listener.{i}"),
smarm::self_pid(),
);
listener_loop(lfd.clone(), p.clone(), limits, r.clone(), sf.clone()); listener_loop(lfd.clone(), p.clone(), limits, r.clone(), sf.clone());
})); }));
} }
@@ -343,11 +334,13 @@ pub fn serve_with_shutdown(
// faster than that trips the cap and tears the pool down — loud // faster than that trips the cap and tears the pool down — loud
// failure over a zombie server. // failure over a zombie server.
let sup_h = smarm::spawn(move || sup.run()); let sup_h = smarm::spawn(move || sup.run());
// Register via the JoinHandle's pid rather than inside the // The old `urus.server` / `urus.listener.{i}` name registrations
// closure: the binding exists before the supervisor body runs a // are gone with smarm's RFC 014 registry rework: `register` is now
// single instruction, so an early `whereis("urus.server")` can't // `(Name<M>, Sender<M>)`, self-only — a name is a typed messaging
// race a None. Result ignored for the same reason as listeners. // endpoint, not a pid tag. urus's bindings were introspection-only
let _ = smarm::register("urus.server", sup_h.pid()); // with no channel behind them, so they were dropped rather than
// faked with a unit channel. A real messageable `urus.server`
// name is in the icebox (ROADMAP.md).
// Block until told to shut down. We poll `try_recv` + `sleep` // Block until told to shut down. We poll `try_recv` + `sleep`
// rather than parking in `recv`: a smarm `Sender::send` from a // rather than parking in `recv`: a smarm `Sender::send` from a
@@ -399,7 +392,7 @@ pub fn serve_with_shutdown(
} }
} }
// 5. Our ServerRef drops here. The registry's inbox closes once // 5. Our GenServerRef drops here. The registry's inbox closes once
// the last conn's clone drops with it, and the runtime winds // the last conn's clone drops with it, and the runtime winds
// down when the last actor exits. // down when the last actor exits.
}); });
+3 -18
View File
@@ -89,24 +89,9 @@ fn hello_world() {
assert_eq!(http_body(&resp), b"hello urus"); assert_eq!(http_body(&resp), b"hello urus");
} }
#[test] // `server_and_listeners_are_registered` was deleted with the RFC 014 port:
fn server_and_listeners_are_registered() { // urus no longer binds `urus.server` / `urus.listener.{i}` names (see the
// `whereis` must run inside the runtime (the test itself is a foreign // note in serve.rs and the icebox entry in ROADMAP.md).
// OS thread with no runtime in its TLS), so probe from a handler —
// connection actors live in the runtime by construction.
let pipe = Pipeline::new().plug(
Router::new().get("/whereis", |c: Conn, _n: Next| {
let ok = smarm::whereis("urus.server").is_some()
&& smarm::whereis("urus.listener.0").is_some()
&& smarm::whereis("urus.listener.1").is_some(); // pool of 2
c.put_status(200).put_body(if ok { "registered" } else { "missing" })
})
);
let port = spawn_server(pipe);
let resp = send_request(port, b"GET /whereis HTTP/1.1\r\nHost: x\r\nConnection: close\r\n\r\n");
assert_eq!(http_status(&resp), 200);
assert_eq!(http_body(&resp), b"registered");
}
#[test] #[test]
fn echo_body() { fn echo_body() {