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:
@@ -0,0 +1,120 @@
|
||||
//! RFC 010 c6 — the per-peer connection actor.
|
||||
//!
|
||||
//! One actor per established control connection. It owns the whole
|
||||
//! [`FramedConn`] and, in a single [`select`](crate::select), waits on two
|
||||
//! things at once: its command inbox and the connection becoming readable (the
|
||||
//! [`FdArm`](crate::scheduler::FdArm) the transport hands back). That is why it
|
||||
//! is a plain select-loop actor rather than a `gen_server` or `gen_statem` —
|
||||
//! neither of those can fold fd-readiness into its wait, and folding it in is
|
||||
//! the whole job. The single owner sends and receives on the one `FramedConn`,
|
||||
//! so no read/write split is needed.
|
||||
//!
|
||||
//! The handshake completes *before* this actor exists (on the accept/connect
|
||||
//! path — c6b) and produces the [`Peer`]; the actor registers that peer with
|
||||
//! the [`manager`](crate::cluster::manager), which monitors it so any exit
|
||||
//! deregisters the connection. Heartbeat send and fixed-timeout liveness join
|
||||
//! the loop in c6c (the timeout arm of the same `select`).
|
||||
|
||||
use crate::channel::{channel, Receiver, Selectable, Sender};
|
||||
use crate::cluster::handshake::Peer;
|
||||
use crate::cluster::manager::{Call, Registered, Reply, MANAGER};
|
||||
use crate::cluster::transport::FramedConn;
|
||||
use crate::gen_server;
|
||||
use crate::scheduler::{self, spawn};
|
||||
|
||||
/// Commands to a running connection actor.
|
||||
enum Cmd {
|
||||
Shutdown,
|
||||
}
|
||||
|
||||
/// A handle to a running connection actor.
|
||||
pub struct ConnHandle {
|
||||
cmd_tx: Sender<Cmd>,
|
||||
}
|
||||
|
||||
impl ConnHandle {
|
||||
/// Ask the connection to close and exit. Idempotent, and a no-op if the
|
||||
/// actor has already gone.
|
||||
pub fn shutdown(&self) {
|
||||
let _ = self.cmd_tx.send(Cmd::Shutdown);
|
||||
}
|
||||
}
|
||||
|
||||
/// Spawn a connection actor for an **already-established** connection: the
|
||||
/// handshake has completed elsewhere and produced `peer`. Returns as soon as
|
||||
/// the actor is spawned; the actor's first act is to register with the manager.
|
||||
pub fn spawn_established(framed: FramedConn, peer: Peer) -> ConnHandle {
|
||||
let (cmd_tx, cmd_rx) = channel();
|
||||
spawn(move || run(framed, peer, cmd_rx));
|
||||
ConnHandle { cmd_tx }
|
||||
}
|
||||
|
||||
fn run(mut framed: FramedConn, peer: Peer, cmd_rx: Receiver<Cmd>) {
|
||||
let me = scheduler::self_pid();
|
||||
match gen_server::call(
|
||||
MANAGER,
|
||||
Call::Register {
|
||||
name: peer.node_name.clone(),
|
||||
pid: me,
|
||||
},
|
||||
) {
|
||||
Ok(Reply::Registered(Registered::Ok)) => {}
|
||||
// Duplicate name, or the manager is unreachable: do not run. The
|
||||
// connection drops (closing the socket) as `framed` falls out of scope.
|
||||
_ => return,
|
||||
}
|
||||
|
||||
loop {
|
||||
match framed.readable_arm() {
|
||||
Some(arm) => {
|
||||
let arms: [&dyn Selectable; 2] = [&cmd_rx, &arm];
|
||||
match crate::channel::try_select(&arms) {
|
||||
Ok(0) => {
|
||||
if should_stop(&cmd_rx) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Ok(_) => {
|
||||
if pump_readable(&mut framed) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
// The fd arm failed to register — the connection is gone.
|
||||
Err(_) => break,
|
||||
}
|
||||
}
|
||||
None => {
|
||||
// No fd to select on (loopback): only a command can end the
|
||||
// wait. Liveness over such a transport is out of scope.
|
||||
let arms: [&dyn Selectable; 1] = [&cmd_rx];
|
||||
let _ = crate::channel::select(&arms);
|
||||
if should_stop(&cmd_rx) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
framed.close();
|
||||
}
|
||||
|
||||
/// Drain the command arm. Returns `true` when the actor should exit — a
|
||||
/// shutdown was requested, or the last handle was dropped.
|
||||
fn should_stop(cmd_rx: &Receiver<Cmd>) -> bool {
|
||||
match cmd_rx.try_recv() {
|
||||
Ok(Some(Cmd::Shutdown)) => true,
|
||||
Ok(None) => false, // spurious wake
|
||||
Err(_) => true, // all senders dropped
|
||||
}
|
||||
}
|
||||
|
||||
/// Consume whatever is readable now. Returns `true` when the connection has
|
||||
/// ended (clean EOF or an unrecoverable stream error). This chunk does not
|
||||
/// interpret frames; c6c handles heartbeats and resets the liveness timer here.
|
||||
fn pump_readable(framed: &mut FramedConn) -> bool {
|
||||
match framed.recv() {
|
||||
Ok(Some(_frame)) => false,
|
||||
Ok(None) => true, // clean EOF at a frame boundary
|
||||
Err(_) => true, // corrupt / truncated / io
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user