node: add a debug-build peer-map coherence check

Assert, once per tick in debug builds, that the peer control-machine map
and its carriers stay coherent: every control machine has a live
handshake leg, an active peer, or a pending connect (a machine with none
is a leak the stale reaper can never see), and every handshake leg has a
machine. The second direction sits behind a file-local const so a branch
where handshake-window legs legitimately run machine-less can gate it
off without weakening the leak tripwire.

Teach the add_connection test seam to seed a control machine for the leg
it inserts (derived from the connection's direction and identity), and
remove_connection to dispose it, so the seam-built topologies satisfy
the check. Three unit tests: seam coherence, coherence through a real
promotion, and the orphaned-machine panic.
This commit is contained in:
Johnathan Corgan
2026-07-17 02:09:28 +00:00
parent 119b85d28e
commit 5ccd95cf3f
3 changed files with 128 additions and 1 deletions

View File

@@ -354,6 +354,11 @@ impl Node {
self.sample_transport_congestion();
#[cfg(any(target_os = "linux", target_os = "macos"))]
self.activate_connected_udp_sessions().await;
// Debug-build sweep of the peer-lifecycle map invariant
// (leaked machines / machine-less legs); two map scans,
// compiled out of release builds.
#[cfg(debug_assertions)]
self.debug_assert_peer_maps_coherent();
}
// Shutdown signal → enter the bounded drain in place, ONCE.
// Gated on `is_none()` so it only fires while serving; after

View File

@@ -2273,6 +2273,53 @@ impl Node {
self.peer_timers.remove(&link);
}
/// Gates the leg→machine direction of `debug_assert_peer_maps_coherent`.
/// Asserting it requires the convention that every `connections` insert is
/// paired with a `peer_machines` insert before the next await point —
/// which holds here: inbound msg1 births a machine alongside the window
/// leg, and dials birth one before the leg exists. Where handshake-window
/// legs legitimately run machine-less, flip this to `false` rather than
/// weakening the machine→carrier direction.
#[cfg(debug_assertions)]
const CHECK_LEGS_HAVE_MACHINES: bool = true;
/// Debug-build coherence sweep over the peer-lifecycle maps, run once per
/// rx-loop tick and invoked directly by unit tests.
///
/// Machine→carrier: every `peer_machines` entry must have a live carrier —
/// a pending connection on the same link, an active peer on it, or a
/// pending connect still resolving toward it. A machine with none of these
/// is unreachable by every teardown path and has leaked.
///
/// Leg→machine: every pending connection carries a control machine (gated
/// by `CHECK_LEGS_HAVE_MACHINES` above).
#[cfg(debug_assertions)]
pub(in crate::node) fn debug_assert_peer_maps_coherent(&self) {
for link in self.peer_machines.keys() {
let has_carrier = self.connections.contains_key(link)
|| self.peers.values().any(|peer| peer.link_id() == *link)
|| self
.peering
.pending_connects
.iter()
.any(|pending| pending.link_id == *link);
assert!(
has_carrier,
"control machine for link {link} has no live carrier \
(no pending connection, active peer, or pending connect)"
);
}
if Self::CHECK_LEGS_HAVE_MACHINES {
for link in self.connections.keys() {
assert!(
self.peer_machines.contains_key(link),
"pending connection on link {link} has no control machine"
);
}
}
}
/// Operator-visible msg1 resend count for a pending handshake `link`, read
/// from the per-peer machine (the counter's home once the resend drive moved
/// off the shell connection). Machine-less connections (inbound legs that
@@ -2335,6 +2382,11 @@ impl Node {
// === Connection Management (Handshake Phase) ===
/// Add a pending connection.
///
/// Also seeds a control machine for the leg when none exists yet, keeping
/// the leg→machine invariant (`debug_assert_peer_maps_coherent`) intact
/// for callers that insert a connection directly rather than through the
/// dial or inbound-msg1 paths, which pair the two inserts themselves.
pub fn add_connection(&mut self, connection: PeerConnection) -> Result<(), NodeError> {
let link_id = connection.link_id();
@@ -2348,6 +2400,16 @@ impl Node {
});
}
self.peer_machines.entry(link_id).or_insert_with(|| {
let now = connection.started_at();
match connection.expected_identity() {
Some(identity) if connection.is_outbound() => {
PeerMachine::new_outbound(link_id, *identity, now)
}
_ => PeerMachine::new_inbound(link_id, now),
}
});
self.connections.insert(link_id, connection);
Ok(())
}
@@ -2362,8 +2424,10 @@ impl Node {
self.connections.get_mut(link_id)
}
/// Remove a connection.
/// Remove a connection, disposing its control machine alongside
/// (the disposal complement of `add_connection`'s machine seeding).
pub fn remove_connection(&mut self, link_id: &LinkId) -> Option<PeerConnection> {
self.remove_peer_machine(*link_id);
self.connections.remove(link_id)
}

View File

@@ -364,6 +364,64 @@ fn test_node_connection_duplicate() {
assert!(matches!(result, Err(NodeError::ConnectionAlreadyExists(_))));
}
#[cfg(debug_assertions)]
#[test]
fn test_peer_maps_coherent_after_add_connection() {
let mut node = make_node();
let identity = make_peer_identity();
let link_id = LinkId::new(1);
let conn = PeerConnection::outbound(link_id, identity, 1000);
node.add_connection(conn).unwrap();
assert!(
node.peer_machines.contains_key(&link_id),
"add_connection seeds a control machine for its leg"
);
node.debug_assert_peer_maps_coherent();
}
#[cfg(debug_assertions)]
#[test]
fn test_peer_maps_coherent_through_establish() {
let mut node = make_node();
let transport_id = TransportId::new(1);
let link_id = LinkId::new(1);
let (conn, identity) = make_completed_connection(&mut node, link_id, transport_id, 1000);
node.add_connection(conn).unwrap();
node.debug_assert_peer_maps_coherent();
let result = node.promote_connection(link_id, identity, 2000).unwrap();
assert!(matches!(result, PromotionResult::Promoted(_)));
// The leg's connection entry is consumed by the promote; the active peer
// on the same link is now the machine's carrier.
node.debug_assert_peer_maps_coherent();
}
#[cfg(debug_assertions)]
#[test]
fn test_peer_maps_coherence_detects_orphaned_machine() {
let mut node = make_node();
// A machine with no connection, no active peer, and no pending connect
// has no live carrier; the check must panic on it.
let link_id = LinkId::new(7);
let machine = crate::peer::machine::PeerMachine::new_inbound(link_id, 1000);
node.peer_machines.insert(link_id, machine);
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
node.debug_assert_peer_maps_coherent();
}));
assert!(
result.is_err(),
"coherence check accepts a machine with no live carrier"
);
}
#[test]
fn test_node_promote_connection() {
let mut node = make_node();