peer: route peer-loss reports by kind

Add a LostKind discriminator to the ReportLost action so the executor can
route an un-promoted handshake failure to the connected-guarded reconnect
reflex (note_handshake_timeout) and an established peer's link-death to the
unconditional one (note_link_dead), instead of collapsing every loss to
note_link_dead.

The two loss producers dispatched today, the liveness reap and the
inbound-restart-then-promote arm, both keep the link-dead routing, so
behavior is unchanged. The handshake-timeout and dial-failure producers are
tagged accordingly but stay dormant until their events are dispatched.
This commit is contained in:
Johnathan Corgan
2026-07-15 03:37:31 +00:00
parent a70c725e48
commit e064c96df3
2 changed files with 72 additions and 21 deletions
+15 -11
View File
@@ -19,14 +19,14 @@
//! realized as those planes are wired).
//! `RegisterDecryptSession` is a deliberate no-op — see its arm for the note.
use crate::PeerIdentity;
use crate::node::Node;
use crate::node::reject::{HandshakeReject, RejectReason};
use crate::peer::machine::{PeerAction, PeerEvent};
use crate::peer::machine::{LostKind, PeerAction, PeerEvent};
use crate::proto::fmp::PromotionResult;
use crate::proto::fmp::wire::build_msg2;
use crate::transport::{LinkId, TransportAddr, TransportId};
use crate::utils::index::SessionIndex;
use crate::{NodeAddr, PeerIdentity};
use std::collections::VecDeque;
use tracing::{debug, trace, warn};
@@ -449,18 +449,22 @@ impl Node {
// INERT — the legacy tick timers still run, so driving
// these would double-schedule.
}
PeerAction::ReportLost { peer } => {
// The single loss token the reconciler reflex (`driver.rs:48`).
self.report_peer_lost(peer, ambient.now_ms);
PeerAction::ReportLost { peer, kind } => {
// The single loss token, routed to the reconciler reflex the
// `kind` names: an un-promoted handshake attempt takes the
// connected-guarded `note_handshake_timeout` (`driver.rs:28`),
// an established peer's link-death takes the unconditional
// `note_link_dead` (`driver.rs:48`).
match kind {
LostKind::HandshakeTimeout => {
self.note_handshake_timeout(peer, ambient.now_ms);
}
LostKind::LinkDead => {
self.note_link_dead(peer, ambient.now_ms);
}
}
}
}
}
/// `ReportLost` → `note_link_dead` (kept as a named seam so the ambient clock
/// source is explicit and the reconciler-computed backoff can be threaded later).
#[allow(dead_code)]
fn report_peer_lost(&mut self, peer: NodeAddr, now_ms: u64) {
self.note_link_dead(peer, now_ms);
}
}
+56 -9
View File
@@ -257,6 +257,21 @@ pub(crate) enum PeerEvent {
Tick,
}
/// Why a peer was reported lost. Selects the reconciler reflex the executor
/// routes the `ReportLost` token to: an un-promoted handshake attempt that
/// failed (`HandshakeTimeout`, connected-guarded like the old `schedule_retry`)
/// versus an established peer whose link died (`LinkDead`, unconditional like
/// the old `schedule_reconnect`).
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum LostKind {
/// An outbound handshake attempt timed out or its dial failed before the
/// peer promoted — routes to the connected-guarded reflex.
HandshakeTimeout,
/// An established peer's link went dead or is being replaced — routes to
/// the unconditional reconnect reflex.
LinkDead,
}
/// An effect the driver executes on the machine's behalf. Runtime-agnostic
/// plain data — no tokio handles, time only as `at_ms` fields.
#[derive(Clone, Debug, PartialEq, Eq)]
@@ -303,8 +318,9 @@ pub(crate) enum PeerAction {
/// Cancel a scheduled timer.
CancelTimer { kind: TimerKind },
/// Report the peer lost to the reconciler (the single loss token — there is
/// deliberately no `ScheduleRetry` machine action).
ReportLost { peer: NodeAddr },
/// deliberately no `ScheduleRetry` machine action). `kind` selects the
/// reflex (handshake-timeout vs link-dead) the executor routes to.
ReportLost { peer: NodeAddr, kind: LostKind },
}
// ============================================================================
@@ -527,7 +543,13 @@ impl PeerMachine {
}
let mut actions = Vec::new();
if let Some(peer) = self.addr() {
actions.push(PeerAction::ReportLost { peer });
// Dial failure on an un-promoted leg routes like a handshake timeout
// (the connected-guarded reflex). Dormant today — no `TransportFailed`
// event is dispatched until the connection-oriented cutover (C5).
actions.push(PeerAction::ReportLost {
peer,
kind: LostKind::HandshakeTimeout,
});
}
self.state = PeerState::Closed {
backoff_deadline_ms: now + CLOSED_BACKOFF_MS,
@@ -641,7 +663,12 @@ impl PeerMachine {
if let Some(idx) = self.our_index.take() {
actions.push(PeerAction::UnregisterDecryptSession { index: idx });
}
actions.push(PeerAction::ReportLost { peer });
// An established peer being replaced by a fresh inbound leg — the
// unconditional reconnect reflex (a live, cut-over producer).
actions.push(PeerAction::ReportLost {
peer,
kind: LostKind::LinkDead,
});
actions.extend(self.inbound_classify(link, &wire));
actions
}
@@ -1017,7 +1044,12 @@ impl PeerMachine {
PeerAction::TeardownConnectedUdp,
];
if let Some(peer) = self.addr() {
actions.push(PeerAction::ReportLost { peer });
// An established peer whose link died — the unconditional reconnect
// reflex (the live liveness-reap producer).
actions.push(PeerAction::ReportLost {
peer,
kind: LostKind::LinkDead,
});
}
self.state = PeerState::Closed {
backoff_deadline_ms: now + CLOSED_BACKOFF_MS,
@@ -1088,7 +1120,13 @@ impl PeerMachine {
for act in Fmp::new().poll_timeouts(vec![snap]) {
match act {
ConnAction::ScheduleRetry { peer } => {
lost.push(PeerAction::ReportLost { peer });
// Handshake timeout on an un-promoted leg — the connected-
// guarded reflex. Dormant today (no `Timeout` event is
// dispatched until the timeout fold in C5).
lost.push(PeerAction::ReportLost {
peer,
kind: LostKind::HandshakeTimeout,
});
}
ConnAction::Teardown { .. } => {
if let Some(idx) = self.conn.our_index() {
@@ -1338,7 +1376,10 @@ mod tests {
PeerAction::CancelTimer {
kind: TimerKind::Liveness,
},
PeerAction::ReportLost { peer },
PeerAction::ReportLost {
peer,
kind: LostKind::LinkDead,
},
];
for a in &sample {
// Exhaustiveness guard: no `_` wildcard, so adding a variant without
@@ -1587,7 +1628,10 @@ mod tests {
PeerAction::UnregisterDecryptSession {
index: SessionIndex::new(0xDEAD)
},
PeerAction::ReportLost { peer: peer_addr },
PeerAction::ReportLost {
peer: peer_addr,
kind: LostKind::LinkDead,
},
]
);
assert!(matches!(
@@ -1988,7 +2032,10 @@ mod tests {
vec![
PeerAction::InvalidateSendState,
PeerAction::TeardownConnectedUdp,
PeerAction::ReportLost { peer: addr },
PeerAction::ReportLost {
peer: addr,
kind: LostKind::LinkDead,
},
]
);
assert!(matches!(m.state(), PeerState::Closed { .. }));