feat(cluster): RFC 010 c6a — connection actor + manager subtree

Per-peer connection actor as a single select-loop plain actor owning the
whole FramedConn: one select folds its command inbox and the transport's
readable arm, so reads and control share one execution context — no reader
thread, no read/write split. The handshake is bypassed here (c6b wires it);
the actor is spawned already-established and self-registers with the manager.

Manager gen_server: the peer-name -> conn-pid registry and the uniqueness
source the handshake's NameTaken depends on. It monitors each connection, so
the table self-heals on any exit path. Explicit supervision subtree keeps the
manager up; connections are dynamic and monitored, never restarted (c7
re-dials).

Transport gains an additive Conn::readable_arm -> Option<FdArm> (default None;
TCP returns its fd's arm, loopback stays None). Existing c3 transport tests
unchanged.

Lifecycle test over localhost TCP: up reflected in the table, commanded
shutdown reaps exactly one, peer EOF reaps the other.
This commit is contained in:
Claude
2026-08-14 19:23:39 +00:00
parent 9e49038474
commit bbaaa062e3
6 changed files with 410 additions and 2 deletions
+55 -2
View File
@@ -2,9 +2,62 @@
//!
//! c1: feature flag + optional deps. c2: the owned envelope. c3: the
//! transport trait (control connection), framed codec, and the TCP +
//! loopback impls. c5: the handshake state machine. Everything above them
//! lands in later chunks.
//! loopback impls. c5: the handshake state machine. c6: the connection
//! [`manager`] (registry) and per-peer connection actors ([`conn`]), started
//! as an explicit supervision subtree. Everything above them lands in later
//! chunks.
pub mod conn;
pub mod envelope;
pub mod handshake;
pub mod manager;
pub mod transport;
use std::time::Duration;
use crate::gen_server::{self, GenServerBuilder};
use crate::monitor::monitor;
use crate::scheduler::{sleep, spawn, JoinHandle};
use crate::supervisor::{ChildSpec, OneForOne, Restart};
pub use conn::{spawn_established, ConnHandle};
pub use manager::{Manager, MANAGER};
/// A running cluster subtree: an explicitly-started supervisor over the
/// connection [`Manager`]. Roles will eventually mount this subtree; until the
/// role mechanism lands it is started by hand (RFC 010 §7). Dropping the handle
/// detaches the subtree, which keeps running for the life of the runtime.
pub struct Cluster {
_sup: JoinHandle,
}
/// Start the cluster subtree and block until the manager is registered and
/// ready to answer. The manager is a supervised child (restarted on crash);
/// per-peer connection actors are dynamic and monitored by the manager rather
/// than statically supervised — a lost connection is re-established by dialing
/// (c7), never resurrected onto a stale socket.
pub fn start() -> Cluster {
let sup = spawn(|| {
OneForOne::new()
.child(ChildSpec::new(Restart::Permanent, manager_child))
.run()
});
while gen_server::whereis_server(MANAGER).is_none() {
sleep(Duration::from_millis(1));
}
Cluster { _sup: sup }
}
/// The supervised manager child body. It *is* the child actor: it starts the
/// named manager, then parks on the manager's own termination so this actor's
/// lifetime tracks the manager's — the supervisor's restart accounting keys off
/// this actor exiting.
fn manager_child() {
let m = match GenServerBuilder::new(Manager::new()).named(MANAGER).start() {
Ok(m) => m,
// Name still held by a not-yet-reaped prior instance: return and let
// the supervisor retry under its restart policy.
Err(_) => return,
};
let _ = monitor(m.pid()).rx.recv();
}