mirror of
https://github.com/jmcorgan/fips.git
synced 2026-08-09 08:14:42 +00:00
759 lines
28 KiB
Rust
759 lines
28 KiB
Rust
//! Read-side state snapshots published from the node's natural mutators so
|
|
//! pure-snapshot `show_*` queries render off the rx_loop hot path.
|
|
//!
|
|
//! [`StatsSnapshot`] is the R2 reference implementation of the canonical
|
|
//! snapshot pattern (see `design/fast-path-refactoring-r0-read-handle.md`): a
|
|
//! read-only data bundle published via `ArcSwap` from the tick after
|
|
//! `StatsHistory::tick()`. It carries
|
|
//!
|
|
//! - the `stats_history` read-side rings (the "dual-ring": the live mutable
|
|
//! ring stays on the tick; this is the cloned read copy), and
|
|
//! - the cheap scalar gauges `show_status` needs (`estimated_mesh_size`,
|
|
//! node `state`, `tun_state`, `tun_name`, `effective_ipv6_mtu`, and the
|
|
//! peer / session / link / connection / transport counts), plus
|
|
//! `peer_aliases` (effectively immutable after construction).
|
|
//!
|
|
//! The snapshot holds *data*, not rendered `Response` envelopes (Q1-d):
|
|
//! rendering happens in the control task off the rx_loop. Staleness is bounded
|
|
//! by the tick interval and is never staler than the underlying data, which
|
|
//! also advances only on the tick (Q1-b).
|
|
|
|
use std::collections::HashMap;
|
|
use std::sync::Arc;
|
|
|
|
use crate::identity::NodeAddr;
|
|
use crate::node::NodeState;
|
|
use crate::node::acl::PeerAclStatus;
|
|
use crate::node::stats_history::StatsHistory;
|
|
use crate::upper::tun::TunState;
|
|
|
|
/// Read-only snapshot of the stats-history rings plus the scalar gauges and
|
|
/// counts `show_status` reports. Published from the tick (Q1-b).
|
|
#[derive(Clone)]
|
|
pub(crate) struct StatsSnapshot {
|
|
/// Cloned read copy of the history rings (the dual-ring read side).
|
|
pub history: Arc<StatsHistory>,
|
|
/// Cached estimated mesh size, or `None` when no estimate is available.
|
|
pub estimated_mesh_size: Option<u64>,
|
|
/// Node operational state.
|
|
pub state: NodeState,
|
|
/// TUN device state.
|
|
pub tun_state: TunState,
|
|
/// TUN interface name, if active.
|
|
pub tun_name: Option<String>,
|
|
/// Effective IPv6 MTU over the mesh.
|
|
pub effective_ipv6_mtu: u16,
|
|
/// Number of pending connections (handshake in progress).
|
|
pub connection_count: usize,
|
|
/// Number of authenticated peers.
|
|
pub peer_count: usize,
|
|
/// Number of active links.
|
|
pub link_count: usize,
|
|
/// Number of active transports.
|
|
pub transport_count: usize,
|
|
/// Number of active sessions.
|
|
pub session_count: usize,
|
|
/// Current spanning-tree root `NodeAddr` (rendered as hex by `show_status`).
|
|
pub root: NodeAddr,
|
|
/// Whether this node is the spanning-tree root.
|
|
pub is_root: bool,
|
|
/// Per-configured-transport-type count of peers whose active link rides that
|
|
/// transport type. Configured-but-idle types appear with a zero count.
|
|
/// Keyed by the transport type name (`"udp"`, `"tcp"`, `"tor"`, ...).
|
|
pub transport_peer_counts: std::collections::BTreeMap<String, usize>,
|
|
/// Configured peer aliases, keyed by `NodeAddr`. Effectively immutable
|
|
/// after construction; shared to avoid a per-tick map clone.
|
|
pub peer_aliases: Arc<HashMap<NodeAddr, String>>,
|
|
/// Loaded peer-ACL status (`show_acl`). The ACL itself is an
|
|
/// `arc_swap::ArcSwap<PeerAcl>` mutated only by the tick's `reload_peer_acl`;
|
|
/// the human-readable status is a cheap projection of it (R5).
|
|
pub acl_status: PeerAclStatus,
|
|
/// Per-stats-history-peer metadata resolved against the live peer/session
|
|
/// tables and host map at publish time (`show_stats_peers` /
|
|
/// `show_stats_history_all_peers`), keyed by `NodeAddr`. The lifecycle
|
|
/// timestamps and per-peer metric rings stay in `history`; this map carries
|
|
/// only the cross-subsystem fields a renderer can't derive from the rings
|
|
/// alone (`is_active`, resolved `npub`, resolved `display_name`) (R5).
|
|
pub peer_meta: Arc<HashMap<NodeAddr, StatsPeerMeta>>,
|
|
}
|
|
|
|
/// Cross-subsystem metadata for one peer tracked in the stats-history rings,
|
|
/// resolved at publish time. Joined against `StatsSnapshot::history`'s rings
|
|
/// (lifecycle timestamps + metric series) by the off-loop `show_stats_peers` /
|
|
/// `show_stats_history_all_peers` renderers.
|
|
#[derive(Clone)]
|
|
pub(crate) struct StatsPeerMeta {
|
|
/// Whether this peer is currently in the live authenticated-peer table.
|
|
pub is_active: bool,
|
|
/// Resolved npub (live peer npub, or `node_addr` hex when not a live peer),
|
|
/// matching the on-loop `show_stats_peers` fallback.
|
|
pub npub: String,
|
|
/// Display name resolved via `Node::peer_display_name` at publish time.
|
|
pub display_name: String,
|
|
}
|
|
|
|
impl StatsSnapshot {
|
|
/// Build an empty snapshot for seeding the `ArcSwap` cell at construction,
|
|
/// before the first tick has published real state.
|
|
pub(crate) fn empty() -> Self {
|
|
Self {
|
|
history: Arc::new(StatsHistory::new()),
|
|
estimated_mesh_size: None,
|
|
state: NodeState::Created,
|
|
tun_state: TunState::Disabled,
|
|
tun_name: None,
|
|
effective_ipv6_mtu: 0,
|
|
connection_count: 0,
|
|
peer_count: 0,
|
|
link_count: 0,
|
|
transport_count: 0,
|
|
session_count: 0,
|
|
root: zero_addr(),
|
|
is_root: false,
|
|
transport_peer_counts: std::collections::BTreeMap::new(),
|
|
peer_aliases: Arc::new(HashMap::new()),
|
|
acl_status: empty_acl_status(),
|
|
peer_meta: Arc::new(HashMap::new()),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// An empty/default [`PeerAclStatus`] for seeding the snapshot before the first
|
|
/// tick publishes the real ACL status. `PeerAclStatus` does not derive
|
|
/// `Default`, so this spells out the inert "no ACL loaded" shape.
|
|
fn empty_acl_status() -> PeerAclStatus {
|
|
PeerAclStatus {
|
|
allow_file: String::new(),
|
|
deny_file: String::new(),
|
|
enforcement_active: false,
|
|
effective_mode: String::new(),
|
|
default_decision: String::new(),
|
|
allow_all: false,
|
|
deny_all: false,
|
|
allow_file_entries: Vec::new(),
|
|
deny_file_entries: Vec::new(),
|
|
allow_entries: Vec::new(),
|
|
deny_entries: Vec::new(),
|
|
}
|
|
}
|
|
|
|
// =====================================================================
|
|
// RoutingSnapshot (R3 — Category-D derived/routing/cache read view)
|
|
// =====================================================================
|
|
|
|
/// Read-only snapshot of the Category-D derived/routing/cache subsystems that
|
|
/// the pure-snapshot `show_tree` / `show_bloom` / `show_cache` / `show_routing`
|
|
/// / `show_identity_cache` queries render. Published via `ArcSwap`.
|
|
///
|
|
/// The R0 stub (`design/fast-path-refactoring-r0-read-handle.md`) names a
|
|
/// single combined `ArcSwap<RoutingSnapshot>` for R3. This is that cell: one
|
|
/// cohesive routing view holding the four subsystems (tree / bloom / coord
|
|
/// cache / identity cache) plus the F-queue summary scalars.
|
|
///
|
|
/// **Publisher placement (Q1).** The four subsystems mutate at many scattered
|
|
/// handler sites (28 `coord_cache_mut` call sites, 16 `tree_state_mut`, ~32
|
|
/// identity-cache touches), and every projected row needs a *display name*
|
|
/// resolved against the live peer/session tables and host map — Category-E
|
|
/// state reachable only with `&Node`. Wiring an on-change `publish_*` at each
|
|
/// mutation site would be large, error-prone surgery, and each call would still
|
|
/// need `&Node` to resolve names across subsystem boundaries. So this snapshot
|
|
/// is published from the **tick** (Q1-b acceptable-at-mutator / the documented
|
|
/// interim the spec permits, mirroring R2's stats publish): the tick is the one
|
|
/// site with coherent `&Node` access to resolve all display names together. A
|
|
/// single combined cell is the natural shape because there is exactly one
|
|
/// publisher — the multi-mutator "rebuild the whole snapshot N times" hazard
|
|
/// that Q1-c warns against does not arise.
|
|
///
|
|
/// The snapshot holds *data* (typed rows + scalars), not rendered `Response`
|
|
/// envelopes (Q1-d); rendering happens off the rx_loop in the control task. The
|
|
/// counter-family `stats` blocks the queries also emit come from the
|
|
/// `MetricsRegistry` (already `Arc`-shared in the handle) at render time, not
|
|
/// from this snapshot.
|
|
///
|
|
/// Time-relative fields (`age_ms`, `idle_ms`) are derived at render time from
|
|
/// the captured absolute timestamps, so the rendered age stays fresh relative
|
|
/// to the read, exactly as the on-loop queries computed it.
|
|
///
|
|
/// Forward-compat: when step 5 structurally extracts the Category-D subsystems
|
|
/// into typed types, these projections become thin views over them without
|
|
/// changing the read-handle interface or this publisher placement.
|
|
#[derive(Clone)]
|
|
pub(crate) struct RoutingSnapshot {
|
|
/// Spanning-tree read view (`show_tree`).
|
|
pub tree: TreeView,
|
|
/// Bloom-filter read view (`show_bloom`).
|
|
pub bloom: BloomView,
|
|
/// Coordinate-cache read view (`show_cache`, `show_routing`).
|
|
pub cache: CacheView,
|
|
/// F-queue / discovery routing scalars + rows (`show_routing`).
|
|
pub routing: RoutingView,
|
|
/// Identity-cache read view (`show_identity_cache`, `show_routing`).
|
|
pub identity: IdentityView,
|
|
}
|
|
|
|
impl RoutingSnapshot {
|
|
/// Build an empty snapshot for seeding the `ArcSwap` cell at construction,
|
|
/// before the first tick has published real state.
|
|
pub(crate) fn empty() -> Self {
|
|
Self {
|
|
tree: TreeView::default(),
|
|
bloom: BloomView::default(),
|
|
cache: CacheView::default(),
|
|
routing: RoutingView::default(),
|
|
identity: IdentityView::default(),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Zero `NodeAddr` for empty/seed views (all-zero 16 bytes).
|
|
fn zero_addr() -> NodeAddr {
|
|
NodeAddr::from_bytes([0u8; 16])
|
|
}
|
|
|
|
/// Spanning-tree read view for `show_tree`.
|
|
#[derive(Clone)]
|
|
pub(crate) struct TreeView {
|
|
pub my_node_addr: NodeAddr,
|
|
pub root: NodeAddr,
|
|
/// Resolved npub of the root node, when discoverable (self when root, a live
|
|
/// peer's attested npub, or an identity-cache hit); `None` otherwise.
|
|
pub root_npub: Option<String>,
|
|
pub is_root: bool,
|
|
pub depth: usize,
|
|
/// `my_coords` entries as `NodeAddr`s (rendered as hex).
|
|
pub my_coords: Vec<NodeAddr>,
|
|
pub parent: NodeAddr,
|
|
pub parent_display_name: String,
|
|
pub declaration_sequence: u64,
|
|
pub declaration_signed: bool,
|
|
pub peer_tree_count: usize,
|
|
pub peers: Vec<TreePeerRow>,
|
|
}
|
|
|
|
impl Default for TreeView {
|
|
fn default() -> Self {
|
|
Self {
|
|
my_node_addr: zero_addr(),
|
|
root: zero_addr(),
|
|
root_npub: None,
|
|
is_root: false,
|
|
depth: 0,
|
|
my_coords: Vec::new(),
|
|
parent: zero_addr(),
|
|
parent_display_name: String::new(),
|
|
declaration_sequence: 0,
|
|
declaration_signed: false,
|
|
peer_tree_count: 0,
|
|
peers: Vec::new(),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// One peer's tree position in `show_tree`.
|
|
#[derive(Clone)]
|
|
pub(crate) struct TreePeerRow {
|
|
pub node_addr: NodeAddr,
|
|
pub display_name: String,
|
|
/// Present only when the peer's coordinates are known.
|
|
pub coords: Option<TreePeerCoords>,
|
|
}
|
|
|
|
/// Coordinate detail for a tree peer (present only when known).
|
|
#[derive(Clone)]
|
|
pub(crate) struct TreePeerCoords {
|
|
pub depth: usize,
|
|
pub root: NodeAddr,
|
|
pub coord_path: Vec<NodeAddr>,
|
|
pub distance_to_us: usize,
|
|
}
|
|
|
|
/// Bloom-filter read view for `show_bloom`.
|
|
#[derive(Clone)]
|
|
pub(crate) struct BloomView {
|
|
pub own_node_addr: NodeAddr,
|
|
pub is_leaf_only: bool,
|
|
pub sequence: u64,
|
|
pub leaf_dependents: Vec<NodeAddr>,
|
|
pub peer_filters: Vec<BloomPeerRow>,
|
|
/// Fill ratio of the last filter actually sent uptree (to the tree parent).
|
|
/// `None` for a root node (nothing sent uptree) or before the first
|
|
/// announce has been sent.
|
|
pub uptree_fill_ratio: Option<f64>,
|
|
/// Estimated cardinality of the last filter sent uptree — this node's whole
|
|
/// subtree (self + tree-descendants, parent excluded), since bloom is
|
|
/// split-horizon. `None` for root, pre-first-announce, or when the estimate
|
|
/// is undefined for the saturation.
|
|
pub uptree_estimated_count: Option<f64>,
|
|
}
|
|
|
|
impl Default for BloomView {
|
|
fn default() -> Self {
|
|
Self {
|
|
own_node_addr: zero_addr(),
|
|
is_leaf_only: false,
|
|
sequence: 0,
|
|
leaf_dependents: Vec::new(),
|
|
peer_filters: Vec::new(),
|
|
uptree_fill_ratio: None,
|
|
uptree_estimated_count: None,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// One peer's bloom-filter state in `show_bloom`.
|
|
#[derive(Clone)]
|
|
pub(crate) struct BloomPeerRow {
|
|
pub peer: NodeAddr,
|
|
pub display_name: String,
|
|
pub has_filter: bool,
|
|
pub filter_sequence: u64,
|
|
/// Present only when the peer has supplied an inbound filter.
|
|
pub filter: Option<BloomPeerFilter>,
|
|
}
|
|
|
|
/// Inbound-filter statistics for a bloom peer (present only when known).
|
|
#[derive(Clone)]
|
|
pub(crate) struct BloomPeerFilter {
|
|
/// Estimated cardinality (`None` when undefined for the saturation),
|
|
/// matching `BloomFilter::estimated_count`'s `Option<f64>`.
|
|
pub estimated_count: Option<f64>,
|
|
pub set_bits: usize,
|
|
pub fill_ratio: f64,
|
|
}
|
|
|
|
/// Coordinate-cache read view for `show_cache` (and the cache scalars in
|
|
/// `show_routing`).
|
|
#[derive(Clone, Default)]
|
|
pub(crate) struct CacheView {
|
|
pub count: usize,
|
|
pub max_entries: usize,
|
|
pub fill_ratio: f64,
|
|
pub default_ttl_ms: u64,
|
|
pub expired: usize,
|
|
pub avg_age_ms: u64,
|
|
pub entries: Vec<CacheEntryRow>,
|
|
}
|
|
|
|
/// One coordinate-cache entry in `show_cache`.
|
|
#[derive(Clone)]
|
|
pub(crate) struct CacheEntryRow {
|
|
pub node_addr: NodeAddr,
|
|
pub display_name: String,
|
|
pub depth: usize,
|
|
pub coord_path: Vec<NodeAddr>,
|
|
/// Absolute creation time (Unix ms); `age_ms` derived at render time.
|
|
pub created_at: u64,
|
|
pub last_used_ms: u64,
|
|
pub path_mtu: Option<u16>,
|
|
}
|
|
|
|
/// F-queue / discovery routing read view for `show_routing`.
|
|
#[derive(Clone, Default)]
|
|
pub(crate) struct RoutingView {
|
|
pub pending_lookups: Vec<PendingLookupRow>,
|
|
pub pending_tun_destinations: usize,
|
|
pub pending_tun_packets: usize,
|
|
pub recent_requests: usize,
|
|
pub retries: Vec<RetryRow>,
|
|
}
|
|
|
|
/// One in-flight discovery lookup in `show_routing`.
|
|
#[derive(Clone)]
|
|
pub(crate) struct PendingLookupRow {
|
|
pub target: NodeAddr,
|
|
pub display_name: String,
|
|
/// Absolute initiation time (Unix ms); `age_ms` derived at render time.
|
|
pub initiated_ms: u64,
|
|
pub last_sent_ms: u64,
|
|
pub attempt: u8,
|
|
}
|
|
|
|
/// One connection-retry entry in `show_routing`.
|
|
#[derive(Clone)]
|
|
pub(crate) struct RetryRow {
|
|
pub node_addr: NodeAddr,
|
|
pub display_name: String,
|
|
pub retry_count: u32,
|
|
pub retry_after_ms: u64,
|
|
pub auto_reconnect: bool,
|
|
}
|
|
|
|
/// Identity-cache read view for `show_identity_cache` (and the
|
|
/// `identity_cache_entries` scalar in `show_routing`).
|
|
#[derive(Clone, Default)]
|
|
pub(crate) struct IdentityView {
|
|
pub entries: Vec<IdentityRow>,
|
|
pub max_entries: usize,
|
|
}
|
|
|
|
/// One identity-cache entry in `show_identity_cache`.
|
|
#[derive(Clone)]
|
|
pub(crate) struct IdentityRow {
|
|
pub node_addr: NodeAddr,
|
|
pub npub: String,
|
|
pub display_name: String,
|
|
pub ipv6_addr: String,
|
|
pub last_seen_ms: u64,
|
|
}
|
|
|
|
// =====================================================================
|
|
// EntitySnapshot (R4 — Category-E per-entity table read views)
|
|
// =====================================================================
|
|
|
|
/// Read-only snapshot of the Category-E per-entity tables that the
|
|
/// pure-snapshot `show_peers` / `show_sessions` / `show_links` /
|
|
/// `show_connections` / `show_transports` / `show_mmp` queries render.
|
|
/// Published via `ArcSwap`.
|
|
///
|
|
/// The R0 stub (`design/fast-path-refactoring-r0-read-handle.md`) pre-scopes
|
|
/// R4 as `entities — ArcSwap<EntitySnapshot>: peers / sessions / links /
|
|
/// connections / transports, published per-entity with `Vec<Arc<Row>>`
|
|
/// structural sharing`. This is that cell.
|
|
///
|
|
/// **Structural sharing (the umbrella mandate).** Every entity table is a
|
|
/// `Vec<Arc<Row>>`, so a republish in which only one row changed re-allocates
|
|
/// only that one `Arc<Row>` — the unchanged rows are reused by pointer from the
|
|
/// previous snapshot (`Arc::ptr_eq`-stable). The publisher diffs each freshly
|
|
/// projected row against the prior published row by value (`PartialEq`) and
|
|
/// keeps the old `Arc` when they are equal. A clone of the snapshot for each
|
|
/// accepted control connection is then a vector of cheap pointer clones, not a
|
|
/// deep table copy. This is what keeps the per-tick publish cost off the hot
|
|
/// path at scale, as the umbrella requires for R4.
|
|
///
|
|
/// **Publisher placement (Q1).** Like R3, this is published from the **tick**,
|
|
/// not per-mutator. Two reasons, both stronger than for R3:
|
|
///
|
|
/// 1. Every projected row needs a *display name* resolved against the live
|
|
/// peer/session tables and host map (`&Node`), and `show_peers` additionally
|
|
/// needs the live tree state to derive `is_parent` / `is_child` and the
|
|
/// Nostr-discovery failure-state map — cross-subsystem reads available only
|
|
/// with `&Node`.
|
|
/// 2. Most of the projected fields (link/session traffic counters, MMP
|
|
/// metrics, `last_seen`, noise counters, replay/decrypt counters) are
|
|
/// mutated continuously on the **data plane / rx_loop**, not at the discrete
|
|
/// peer/session/link lifecycle mutators. Per-lifecycle-mutator publication
|
|
/// (Q1-a) would therefore not even capture freshness for those fields; the
|
|
/// tick is the natural cadence at which this read view advances.
|
|
///
|
|
/// The diff-and-reuse therefore satisfies the structural-sharing goal the
|
|
/// umbrella mandates (only changed rows re-allocate) while keeping a single
|
|
/// coherent `&Node` publisher — the "no monolithic per-tick *re-allocation* of
|
|
/// every row" warning is honored because unchanged rows are reused, not rebuilt.
|
|
/// This is the documented acceptable interim (the spec's tick-publish-with-
|
|
/// Arc-reuse fallback), consistent with R3.
|
|
///
|
|
/// The snapshot holds typed rows (Q1-d data, not rendered `Response`
|
|
/// envelopes). Time-relative fields (`idle_ms`) are derived at render time from
|
|
/// captured absolute timestamps, so the rendered age stays fresh relative to
|
|
/// the read, exactly as the on-loop queries computed it.
|
|
///
|
|
/// Forward-compat: step 10 later extracts the session table into a typed
|
|
/// `(transport_id, our_index)`-indexed type; these projections then become thin
|
|
/// views over it without changing the read-handle interface or this publisher
|
|
/// placement.
|
|
#[derive(Clone)]
|
|
pub(crate) struct EntitySnapshot {
|
|
/// `show_peers` rows.
|
|
pub peers: Vec<Arc<PeerRow>>,
|
|
/// `show_sessions` rows.
|
|
pub sessions: Vec<Arc<SessionRow>>,
|
|
/// `show_links` rows.
|
|
pub links: Vec<Arc<LinkRow>>,
|
|
/// `show_connections` rows.
|
|
pub connections: Vec<Arc<ConnectionRow>>,
|
|
/// `show_transports` rows.
|
|
pub transports: Vec<Arc<TransportRow>>,
|
|
/// `show_mmp` link-layer rows (peers with an MMP instance).
|
|
pub mmp_peers: Vec<Arc<MmpPeerRow>>,
|
|
/// `show_mmp` session-layer rows (sessions with an MMP instance).
|
|
pub mmp_sessions: Vec<Arc<MmpSessionRow>>,
|
|
}
|
|
|
|
impl EntitySnapshot {
|
|
/// Build an empty snapshot for seeding the `ArcSwap` cell at construction,
|
|
/// before the first tick has published real state.
|
|
pub(crate) fn empty() -> Self {
|
|
Self {
|
|
peers: Vec::new(),
|
|
sessions: Vec::new(),
|
|
links: Vec::new(),
|
|
connections: Vec::new(),
|
|
transports: Vec::new(),
|
|
mmp_peers: Vec::new(),
|
|
mmp_sessions: Vec::new(),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Per-peer link/transport/connectivity fields for `show_peers` derived from a
|
|
/// peer's resolved link (present only when the link is found).
|
|
#[derive(Clone, PartialEq)]
|
|
pub(crate) struct PeerLinkInfo {
|
|
pub direction: String,
|
|
/// Transport type name, present only when the transport handle is found.
|
|
pub transport_type: Option<String>,
|
|
}
|
|
|
|
/// Nostr-traversal failure-state for a peer's npub in `show_peers`. Always
|
|
/// emitted (the on-loop query emits a default object even when absent); the
|
|
/// `present` flag distinguishes "seen by Nostr discovery" from the default.
|
|
#[derive(Clone, PartialEq)]
|
|
pub(crate) struct PeerNostrState {
|
|
pub consecutive_failures: u32,
|
|
pub cooldown_until_ms: Option<u64>,
|
|
pub last_observed_skew_ms: Option<i64>,
|
|
}
|
|
|
|
/// Noise session counters surfaced in `show_peers` (present when the peer has a
|
|
/// Noise session).
|
|
#[derive(Clone, PartialEq)]
|
|
pub(crate) struct PeerNoiseCounters {
|
|
pub send_counter: u64,
|
|
pub highest_recv_counter: u64,
|
|
}
|
|
|
|
/// Link/session MMP metrics surfaced inline in `show_peers` (and the
|
|
/// per-session block in `show_sessions`). Fields mirror the on-loop projection;
|
|
/// `Option` fields are emitted only when present.
|
|
#[derive(Clone, PartialEq)]
|
|
pub(crate) struct EntityMmp {
|
|
pub mode: String,
|
|
pub srtt_ms: Option<f64>,
|
|
pub loss_rate: f64,
|
|
pub etx: f64,
|
|
pub goodput_bps: f64,
|
|
pub delivery_ratio_forward: f64,
|
|
pub delivery_ratio_reverse: f64,
|
|
pub smoothed_loss: Option<f64>,
|
|
pub smoothed_etx: Option<f64>,
|
|
/// `lqi` (peers) / `sqi` (sessions): present only when both `srtt_ms` and
|
|
/// `smoothed_etx` are present. Precomputed so the render is a plain emit.
|
|
pub quality_index: Option<f64>,
|
|
/// Session-only: path MTU (`show_sessions`). `None` for peer rows.
|
|
pub path_mtu: Option<u16>,
|
|
}
|
|
|
|
/// Link-layer stat counters for a peer in `show_peers`.
|
|
#[derive(Clone, PartialEq)]
|
|
pub(crate) struct PeerLinkStats {
|
|
pub packets_sent: u64,
|
|
pub packets_recv: u64,
|
|
pub bytes_sent: u64,
|
|
pub bytes_recv: u64,
|
|
}
|
|
|
|
/// One authenticated peer in `show_peers`. Holds every field the on-loop
|
|
/// `show_peers` emits; `Option` fields gate the conditionally-emitted keys.
|
|
#[derive(Clone, PartialEq)]
|
|
pub(crate) struct PeerRow {
|
|
pub node_addr: NodeAddr,
|
|
pub npub: String,
|
|
pub display_name: String,
|
|
pub ipv6_addr: String,
|
|
pub connectivity: String,
|
|
pub link_id: u64,
|
|
pub authenticated_at_ms: u64,
|
|
pub last_seen_ms: u64,
|
|
pub has_tree_position: bool,
|
|
pub has_bloom_filter: bool,
|
|
pub filter_sequence: u64,
|
|
pub is_parent: bool,
|
|
pub is_child: bool,
|
|
pub transport_addr: Option<String>,
|
|
pub link_info: Option<PeerLinkInfo>,
|
|
pub tree_depth: Option<usize>,
|
|
/// `effective_depth = tree_depth + link_cost` — the same quantity
|
|
/// `evaluate_parent` ranks parent candidates on. `None` when the peer is
|
|
/// unmeasured (no SRTT while other peers have it) or has no coords,
|
|
/// mirroring the candidacy/cold-start rules. Pre-computed daemon-side so
|
|
/// fipstop never recomputes it.
|
|
pub effective_depth: Option<f64>,
|
|
pub stats: PeerLinkStats,
|
|
pub replay_suppressed: u32,
|
|
pub consecutive_decrypt_failures: u32,
|
|
pub nostr_traversal: PeerNostrState,
|
|
pub noise: Option<PeerNoiseCounters>,
|
|
pub our_session_index: Option<u32>,
|
|
pub rekey_in_progress: bool,
|
|
pub rekey_draining: bool,
|
|
pub current_k_bit: bool,
|
|
pub mmp: Option<EntityMmp>,
|
|
}
|
|
|
|
/// Traffic counters for a session in `show_sessions`.
|
|
#[derive(Clone, PartialEq)]
|
|
pub(crate) struct SessionStats {
|
|
pub packets_sent: u64,
|
|
pub packets_recv: u64,
|
|
pub bytes_sent: u64,
|
|
pub bytes_recv: u64,
|
|
}
|
|
|
|
/// One end-to-end session in `show_sessions`.
|
|
#[derive(Clone, PartialEq)]
|
|
pub(crate) struct SessionRow {
|
|
pub remote_addr: NodeAddr,
|
|
pub display_name: String,
|
|
pub state: &'static str,
|
|
pub is_initiator: bool,
|
|
pub last_activity_ms: u64,
|
|
pub npub: String,
|
|
pub stats: SessionStats,
|
|
/// Handshake resend count, emitted only while not established.
|
|
pub resend_count: Option<u32>,
|
|
/// Established-only health block (session_start_ms, current_k_bit,
|
|
/// coords_warmup_remaining, is_draining). `None` while handshaking.
|
|
pub established: Option<SessionEstablished>,
|
|
pub mmp: Option<EntityMmp>,
|
|
}
|
|
|
|
/// Established-session health fields in `show_sessions` (emitted only when the
|
|
/// session is established).
|
|
#[derive(Clone, PartialEq)]
|
|
pub(crate) struct SessionEstablished {
|
|
pub session_start_ms: u64,
|
|
pub current_k_bit: bool,
|
|
pub coords_warmup_remaining: u8,
|
|
pub is_draining: bool,
|
|
}
|
|
|
|
/// Stat counters for a link in `show_links`.
|
|
#[derive(Clone, PartialEq)]
|
|
pub(crate) struct LinkStats {
|
|
pub packets_sent: u64,
|
|
pub packets_recv: u64,
|
|
pub bytes_sent: u64,
|
|
pub bytes_recv: u64,
|
|
pub last_recv_ms: u64,
|
|
}
|
|
|
|
/// One active link in `show_links`.
|
|
#[derive(Clone, PartialEq)]
|
|
pub(crate) struct LinkRow {
|
|
pub link_id: u64,
|
|
pub transport_id: u32,
|
|
pub remote_addr: String,
|
|
pub direction: String,
|
|
pub state: String,
|
|
pub created_at_ms: u64,
|
|
pub stats: LinkStats,
|
|
}
|
|
|
|
/// One pending handshake in `show_connections`. `idle_ms` is derived at render
|
|
/// time from the captured `last_activity_ms`.
|
|
#[derive(Clone, PartialEq)]
|
|
pub(crate) struct ConnectionRow {
|
|
pub link_id: u64,
|
|
pub direction: String,
|
|
pub handshake_state: String,
|
|
pub started_at_ms: u64,
|
|
/// Absolute last-activity time (Unix ms); `idle_ms` derived at render time.
|
|
pub last_activity_ms: u64,
|
|
pub resend_count: u32,
|
|
/// Expected peer npub, emitted only when the connection has an expected
|
|
/// identity.
|
|
pub expected_peer: Option<String>,
|
|
}
|
|
|
|
/// One transport instance in `show_transports`. The `stats` and
|
|
/// `tor_monitoring` fields are stored as already-projected `serde_json::Value`
|
|
/// (data, produced by the transport handle), not as rendered `Response`
|
|
/// envelopes.
|
|
#[derive(Clone, PartialEq)]
|
|
pub(crate) struct TransportRow {
|
|
pub transport_id: u32,
|
|
pub transport_type: String,
|
|
pub state: String,
|
|
pub mtu: u16,
|
|
pub name: Option<String>,
|
|
pub local_addr: Option<String>,
|
|
pub tor_mode: Option<String>,
|
|
pub onion_address: Option<String>,
|
|
pub tor_monitoring: Option<serde_json::Value>,
|
|
pub stats: serde_json::Value,
|
|
}
|
|
|
|
/// MMP trend labels for a peer's link-layer block in `show_mmp` (each present
|
|
/// only when the corresponding trend is initialized).
|
|
#[derive(Clone, PartialEq)]
|
|
pub(crate) struct MmpTrends {
|
|
pub rtt_trend: Option<&'static str>,
|
|
pub loss_trend: Option<&'static str>,
|
|
pub goodput_trend: Option<&'static str>,
|
|
pub jitter_trend: Option<&'static str>,
|
|
}
|
|
|
|
/// One peer's link-layer MMP block in `show_mmp`.
|
|
#[derive(Clone, PartialEq)]
|
|
pub(crate) struct MmpPeerRow {
|
|
pub peer: NodeAddr,
|
|
pub display_name: String,
|
|
pub mode: String,
|
|
pub loss_rate: f64,
|
|
pub etx: f64,
|
|
pub goodput_bps: f64,
|
|
pub smoothed_loss: Option<f64>,
|
|
pub smoothed_etx: Option<f64>,
|
|
pub srtt_ms: Option<f64>,
|
|
/// `lqi`: present only when both `srtt_ms` and `smoothed_etx` are present.
|
|
pub lqi: Option<f64>,
|
|
pub trends: MmpTrends,
|
|
pub delivery_ratio_forward: f64,
|
|
pub delivery_ratio_reverse: f64,
|
|
pub ecn_ce_count: u32,
|
|
}
|
|
|
|
/// MMP trend labels for a session's session-layer block in `show_mmp` (each
|
|
/// present only when the corresponding trend is initialized). Mirrors the
|
|
/// link-layer [`MmpTrends`] arrow semantics on the session columns: srtt
|
|
/// (`rtt_trend`), loss (`loss_trend`), etx (`etx_trend`).
|
|
#[derive(Clone, PartialEq)]
|
|
pub(crate) struct MmpSessionTrends {
|
|
pub rtt_trend: Option<&'static str>,
|
|
pub loss_trend: Option<&'static str>,
|
|
pub etx_trend: Option<&'static str>,
|
|
}
|
|
|
|
/// One session's session-layer MMP block in `show_mmp`.
|
|
#[derive(Clone, PartialEq)]
|
|
pub(crate) struct MmpSessionRow {
|
|
pub remote: NodeAddr,
|
|
pub display_name: String,
|
|
pub mode: String,
|
|
pub loss_rate: f64,
|
|
pub etx: f64,
|
|
pub path_mtu: u16,
|
|
pub smoothed_loss: Option<f64>,
|
|
pub smoothed_etx: Option<f64>,
|
|
pub srtt_ms: Option<f64>,
|
|
/// `sqi`: present only when both `srtt_ms` and `smoothed_etx` are present.
|
|
pub sqi: Option<f64>,
|
|
pub trends: MmpSessionTrends,
|
|
}
|
|
|
|
/// Reconcile a freshly-projected entity table against the previously published
|
|
/// one, preserving structural sharing: an `Arc<Row>` from `prev` is reused
|
|
/// (kept by pointer) whenever a new row matches an old row by identity `key`
|
|
/// **and** compares equal by value, so only changed/new rows allocate a fresh
|
|
/// `Arc`. This is the `Vec<Arc<Row>>` discipline the R4 umbrella mandates — a
|
|
/// single-row change re-allocates one row, not the whole table, keeping the
|
|
/// per-tick publish cost off the hot path at scale.
|
|
///
|
|
/// `key` extracts a stable, hashable identity (e.g. `node_addr`, `link_id`) so
|
|
/// matching is order-independent across the source table's iteration order.
|
|
pub(crate) fn reconcile_rows<R, K, F>(prev: &[Arc<R>], new_rows: Vec<R>, key: F) -> Vec<Arc<R>>
|
|
where
|
|
R: PartialEq,
|
|
K: std::hash::Hash + Eq,
|
|
F: Fn(&R) -> K,
|
|
{
|
|
let index: HashMap<K, &Arc<R>> = prev.iter().map(|arc| (key(arc), arc)).collect();
|
|
new_rows
|
|
.into_iter()
|
|
.map(|row| match index.get(&key(&row)) {
|
|
Some(old) if ***old == row => Arc::clone(old),
|
|
_ => Arc::new(row),
|
|
})
|
|
.collect()
|
|
}
|