Files
fips/src/proto/discovery/state.rs
T
Johnathan Corgan e03b206f62 proto/discovery: sans-IO state-machine migration + no_std reductions
Migrate the FMP discovery decision logic out of the async handlers into
synchronous, runtime-agnostic sans-IO state machines owned by the protocol
structs, with I/O pushed to the edges. Pulls the full decision surface into a
pure core (backoff, rate-limit, planners, response routing), consolidates the
tests into a per-module tree with a shared crate testutil, and injects a u64
wall-clock so the core is free of Instant and std time.

Also brings the module toward no_std+alloc: the four discovery maps use
alloc::collections::BTreeMap (HashMap's RandomState is std-only), Arc is spelled
alloc::sync::Arc, the backoff-reset log lives in the shell (the core returns the
cleared count so observability stays out of the pure core), and the crate root
names alloc directly. The one remaining tether is ProtocolError's
std::error::Error coupling in the wire codec.

First subsystem of the broader sans-IO refactor; establishes the extraction
patterns and conventions carried forward to the remaining protocols.
2026-07-05 21:59:21 +00:00

111 lines
3.9 KiB
Rust

//! Discovery-subsystem state owned by [`Node`](crate::node::Node).
//!
//! Groups the four discovery-related state fields (recent-request dedup
//! cache, in-flight lookups, originator-side backoff, transit-side forward
//! rate limiter) behind a single struct so the discovery handlers can
//! evolve toward a sans-IO core without threading four fields through
//! `Node`.
use alloc::collections::BTreeMap;
use super::limits::{DiscoveryBackoff, DiscoveryForwardRateLimiter};
use crate::NodeAddr;
/// Recent request tracking for dedup and reverse-path forwarding.
///
/// When a LookupRequest is forwarded through a node, the node stores the
/// request_id and which peer sent it. When the corresponding LookupResponse
/// arrives, it's forwarded back to that peer (reverse-path forwarding).
/// The `response_forwarded` flag prevents response routing loops.
#[derive(Clone, Debug)]
pub(crate) struct RecentRequest {
/// The peer who sent this request to us.
pub(crate) from_peer: NodeAddr,
/// When we received this request (Unix milliseconds).
pub(crate) timestamp_ms: u64,
/// Whether we've already forwarded a response for this request.
/// Prevents response routing loops when convergent request paths
/// create bidirectional entries in recent_requests.
pub(crate) response_forwarded: bool,
}
impl RecentRequest {
pub(crate) fn new(from_peer: NodeAddr, timestamp_ms: u64) -> Self {
Self {
from_peer,
timestamp_ms,
response_forwarded: false,
}
}
/// Check if this entry has expired (older than expiry_ms).
pub(crate) fn is_expired(&self, current_time_ms: u64, expiry_ms: u64) -> bool {
current_time_ms.saturating_sub(self.timestamp_ms) > expiry_ms
}
}
/// Tracks a pending discovery lookup with retry state.
pub struct PendingLookup {
/// When the lookup was first initiated.
pub initiated_ms: u64,
/// When the last attempt was sent.
pub last_sent_ms: u64,
/// Current attempt number (1 = initial, 2 = first retry, ...).
pub attempt: u8,
}
impl PendingLookup {
pub fn new(now_ms: u64) -> Self {
Self {
initiated_ms: now_ms,
last_sent_ms: now_ms,
attempt: 1,
}
}
}
/// Discovery-subsystem state.
pub(crate) struct Discovery {
/// Recent discovery requests (dedup + reverse-path forwarding).
/// Maps request_id → RecentRequest.
pub(crate) recent_requests: BTreeMap<u64, RecentRequest>,
/// Tracks in-flight discovery lookups. Maps target NodeAddr to the
/// initiation timestamp (Unix ms). Prevents duplicate flood queries.
pub(crate) pending_lookups: BTreeMap<NodeAddr, PendingLookup>,
/// Backoff for failed discovery lookups (originator-side).
pub(crate) backoff: DiscoveryBackoff,
/// Rate limiter for forwarded discovery requests (transit-side).
pub(crate) forward_limiter: DiscoveryForwardRateLimiter,
}
impl Discovery {
/// Create discovery state with the given backoff and forward limiter.
///
/// The two limiters are constructed by the caller so each `Node`
/// constructor can supply its own configured/default variant, matching
/// the pre-refactor initialization exactly.
pub(crate) fn new(
backoff: DiscoveryBackoff,
forward_limiter: DiscoveryForwardRateLimiter,
) -> Self {
Self {
recent_requests: BTreeMap::new(),
pending_lookups: BTreeMap::new(),
backoff,
forward_limiter,
}
}
/// Reset discovery backoff on topology changes. Returns the number of
/// entries cleared (0 if already empty) so the shell can log the reset —
/// observability stays out of the pure core.
pub(crate) fn reset_backoff(&mut self) -> usize {
if self.backoff.is_empty() {
return 0;
}
let cleared = self.backoff.entry_count();
self.backoff.reset_all();
cleared
}
}