mirror of
https://github.com/jmcorgan/fips.git
synced 2026-07-30 19:46:15 +00:00
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:
@@ -354,6 +354,11 @@ impl Node {
|
|||||||
self.sample_transport_congestion();
|
self.sample_transport_congestion();
|
||||||
#[cfg(any(target_os = "linux", target_os = "macos"))]
|
#[cfg(any(target_os = "linux", target_os = "macos"))]
|
||||||
self.activate_connected_udp_sessions().await;
|
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.
|
// Shutdown signal → enter the bounded drain in place, ONCE.
|
||||||
// Gated on `is_none()` so it only fires while serving; after
|
// Gated on `is_none()` so it only fires while serving; after
|
||||||
|
|||||||
+65
-1
@@ -2273,6 +2273,53 @@ impl Node {
|
|||||||
self.peer_timers.remove(&link);
|
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
|
/// 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
|
/// from the per-peer machine (the counter's home once the resend drive moved
|
||||||
/// off the shell connection). Machine-less connections (inbound legs that
|
/// off the shell connection). Machine-less connections (inbound legs that
|
||||||
@@ -2335,6 +2382,11 @@ impl Node {
|
|||||||
// === Connection Management (Handshake Phase) ===
|
// === Connection Management (Handshake Phase) ===
|
||||||
|
|
||||||
/// Add a pending connection.
|
/// 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> {
|
pub fn add_connection(&mut self, connection: PeerConnection) -> Result<(), NodeError> {
|
||||||
let link_id = connection.link_id();
|
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);
|
self.connections.insert(link_id, connection);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -2362,8 +2424,10 @@ impl Node {
|
|||||||
self.connections.get_mut(link_id)
|
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> {
|
pub fn remove_connection(&mut self, link_id: &LinkId) -> Option<PeerConnection> {
|
||||||
|
self.remove_peer_machine(*link_id);
|
||||||
self.connections.remove(link_id)
|
self.connections.remove(link_id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -364,6 +364,64 @@ fn test_node_connection_duplicate() {
|
|||||||
assert!(matches!(result, Err(NodeError::ConnectionAlreadyExists(_))));
|
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]
|
#[test]
|
||||||
fn test_node_promote_connection() {
|
fn test_node_promote_connection() {
|
||||||
let mut node = make_node();
|
let mut node = make_node();
|
||||||
|
|||||||
Reference in New Issue
Block a user