Tree snapshot of d9c62a8 (2026-08-18). The 20 source commits between
16ef583 (c9) and d9c62a8 were never pushed and the clone that held them
was lost; this commit carries their combined tree verbatim so the build
history stays auditable from the c1–c9 commits below it. Original
hashes as recorded in the session handoff:
c10 f03e94d pid targeting + auto-serialization (RemotePid, D14 name
on the wire); Phase 3 gate
c11 7ef4bad DownReason::Disconnected, wire tag 5
c12 d124162 remote monitors (Monitor/Demonitor/Down frames)
c13 9de967b connection-loss synthesis (A+B: Monitors::teardown +
unread-command Disconnected); Phase 4 gate
c14 7e822b7 eager pg eviction (reaper actor, ReaperInboxes)
dbe1a22 InboundVerdict::label(), trace::Event::ClusterInbound
31a9877 tests/channel.rs monitor-churn target gated on `go`
653559e Discovery::Withdrawn{name, addr}
c15 b41d76e distributed pg: Sync on NodeUp, Join/Leave broadcast,
NodeDown sweep, members_all; PgMsg wire type
c16 fafa881 pick_any / dispatch_any; Phase 5 complete
Phase 6 Tier A:
195c73e p4 NodeEvent::NodeDown(NodeInfo)
48fd766 p1 connector Candidate{name, addr, state}
ce8cf99 p2+p7 conn.rs select arms as Vec<Arm>; Outbound::Drained
bf24988 p6 RemotePid::from_local -> Option
9ae0380 p3 PeerStanding{Free, Claimed, Dialing}
c7d62a1 p11 cluster::Timing knobs, threaded by value
46f171d p11 cluster_disconnect un-ignored on SMARM_FAST_TIMING
Phase 6 Tier B:
391a9ae p5 cluster::RemoteDownReason{Local, Disconnected};
DownReason::Disconnected removed from core
7ddd908 p9 pg ctl channel unconditional, one cfg seam at spawn
d9c62a8 PeerNameMismatch parks the candidate; ClusterDial trace
Verified at d9c62a8: default 361/0, cluster 448/0, clippy --lib on
default / cluster / cluster+smarm-trace, fmt, 10x flake on
cluster_dial_mismatch, 5x on cluster_pg.
118 lines
4.4 KiB
Rust
118 lines
4.4 KiB
Rust
//! RFC 010 c6a — connection-actor lifecycle against the manager table.
|
|
//!
|
|
//! The handshake is bypassed here (c6b wires it): each connection is
|
|
//! constructed already-established over a real localhost TCP pair, handed a
|
|
//! fabricated `Peer`, and spawned. `spawn_established` registers it with the
|
|
//! manager, which takes its handle and monitors it, so the table reflects the
|
|
//! connection while it lives and reaps it on any exit path. This proves three
|
|
//! things at once: a live connection shows up, a commanded `Disconnect`
|
|
//! removes exactly that one, and a peer close (EOF, no command) removes the
|
|
//! other.
|
|
//!
|
|
//! TCP parks the calling actor, so everything runs inside `smarm::run`; the
|
|
//! single-threaded runtime is fine because every wait is a cooperative fd park.
|
|
#![cfg(feature = "cluster")]
|
|
|
|
use std::time::Duration;
|
|
|
|
use smarm::cluster::envelope::NodeMeta;
|
|
use smarm::cluster::handshake::Peer;
|
|
use smarm::cluster::manager::{Call, Manager, Reply, MANAGER};
|
|
use smarm::cluster::spawn_established;
|
|
use smarm::cluster::transport::tcp::TcpTransport;
|
|
use smarm::cluster::transport::{Conn, FramedConn, Transport};
|
|
use smarm::cluster::Timing;
|
|
use smarm::gen_server::{self, GenServerBuilder};
|
|
use smarm::pg::Incarnation;
|
|
use smarm::{run, sleep};
|
|
|
|
/// A fabricated post-handshake peer identity. Only `node_name` matters to the
|
|
/// manager table; the rest is filler until c7 consumes it.
|
|
fn peer(name: &str) -> Peer {
|
|
Peer {
|
|
node_name: name.to_string(),
|
|
incarnation: Incarnation::new(1),
|
|
meta: NodeMeta {
|
|
role: "test".to_string(),
|
|
region: "test".to_string(),
|
|
},
|
|
}
|
|
}
|
|
|
|
/// One established transport pair over localhost. Relies on TCP backlog so the
|
|
/// sequential dial-then-accept needs no concurrent acceptor (same assumption as
|
|
/// the c3 conformance suite).
|
|
fn pair(t: &dyn Transport) -> (Box<dyn Conn>, Box<dyn Conn>) {
|
|
let mut l = t.listen("127.0.0.1:0").unwrap();
|
|
let a = t.dial(&l.local_addr()).unwrap();
|
|
let b = l.accept().unwrap();
|
|
(a, b)
|
|
}
|
|
|
|
/// Poll the manager until its peer set matches `expected` (sorted), or fail.
|
|
/// The bound is generous against a sub-millisecond real cost.
|
|
fn wait_peers(expected: &[&str]) {
|
|
let want: Vec<String> = expected.iter().map(|s| s.to_string()).collect();
|
|
for _ in 0..2000 {
|
|
if let Ok(Reply::Peers(got)) = gen_server::call(MANAGER, Call::Peers) {
|
|
if got == want {
|
|
return;
|
|
}
|
|
}
|
|
sleep(Duration::from_millis(1));
|
|
}
|
|
let got = gen_server::call(MANAGER, Call::Peers);
|
|
panic!("timed out waiting for peers == {want:?}; last = {got:?}");
|
|
}
|
|
|
|
#[test]
|
|
fn connection_up_commanded_shutdown_and_eof_all_reflected_in_table() {
|
|
run(|| {
|
|
// The manager, started plainly and reachable at its well-known name.
|
|
// (The supervised subtree in `cluster::start` is permanent by design;
|
|
// a plainly-started manager lets this test terminate cleanly.)
|
|
let mgr = GenServerBuilder::new(Manager::new())
|
|
.named(MANAGER)
|
|
.start()
|
|
.expect("manager name is free");
|
|
|
|
let t = TcpTransport;
|
|
let (a1, b1) = pair(&t);
|
|
let (a2, b2) = pair(&t);
|
|
|
|
// Manage the `a` ends as peers node-b and node-c; keep the `b` far ends
|
|
// open so neither socket is closed from the far side yet.
|
|
spawn_established(FramedConn::new(a1), peer("node-b"), Timing::default())
|
|
.expect("node-b registers");
|
|
spawn_established(FramedConn::new(a2), peer("node-c"), Timing::default())
|
|
.expect("node-c registers");
|
|
|
|
// Up: both connections register and the table shows them.
|
|
wait_peers(&["node-b", "node-c"]);
|
|
|
|
// A commanded disconnect reaps exactly its own connection: the
|
|
// manager drops that entry's handle and the actor stops.
|
|
assert!(matches!(
|
|
gen_server::call(
|
|
MANAGER,
|
|
Call::Disconnect {
|
|
name: "node-b".to_string()
|
|
}
|
|
),
|
|
Ok(Reply::Disconnected)
|
|
));
|
|
wait_peers(&["node-c"]);
|
|
|
|
// A peer close (EOF) reaps the other with no command at all.
|
|
drop(b2);
|
|
wait_peers(&[]);
|
|
|
|
// node-b's far end stayed open until here, so its removal above was the
|
|
// disconnect command and not an EOF.
|
|
drop(b1);
|
|
|
|
// All connection actors have exited; stop the manager so `run` returns.
|
|
mgr.shutdown();
|
|
});
|
|
}
|