mirror of
https://github.com/jmcorgan/fips.git
synced 2026-08-09 08:14:42 +00:00
control: serve the full show_* read surface off the rx_loop via a read-snapshot plane
Complete the control-plane read-isolation work: every pure-read show_* query now renders in the control accept task from published read snapshots, so none round-trips the data-plane receive loop. Only the mutating connect/disconnect commands still reach that loop. Three subsystem snapshots are published via ArcSwap and served through the read handle's snapshot_dispatch: - A routing read view (spanning tree, bloom filters, coordinate cache, identity cache, and the discovery F-queue summary scalars), published from the tick, serving show_tree/show_bloom/show_cache/show_routing/ show_identity_cache. - A per-entity read view (peers, sessions, links, connections, transports, and the MMP link/session views) as Vec<Arc<Row>> tables reconciled against the prior snapshot so a republish reuses unchanged rows by pointer and re-allocates only changed or new rows, keeping the per-tick publish cost bounded as the peer/session count grows. Serves show_peers/show_sessions/show_links/show_connections/show_transports/ show_mmp. - The stats snapshot is extended with the peer-ACL status and a per-peer metadata map (is_active, npub, display name), resolved at publish time, serving show_acl and the two per-peer stats queries. Display names and other cross-subsystem fields are resolved at publish time; time-relative fields are derived at render time from captured absolute timestamps, so rendered output is byte-identical to the prior on-loop handlers, which are retained as the equality oracle. With every read query served off-loop, the show_* branch is removed from the rx_loop control handler and the now-dead on-loop dispatcher deleted. The snapshot projections are forward-compatible with the later structural extraction of the derived-state and session tables: they become thin views over the extracted types without changing the read-handle interface.
This commit is contained in:
+1242
-83
File diff suppressed because it is too large
Load Diff
@@ -32,7 +32,7 @@ use crate::node::context::NodeContext;
|
||||
use crate::node::metrics::MetricsRegistry;
|
||||
|
||||
use super::protocol::{Request, Response};
|
||||
use super::snapshot::StatsSnapshot;
|
||||
use super::snapshot::{EntitySnapshot, RoutingSnapshot, StatsSnapshot};
|
||||
|
||||
/// Cloneable read-only view of node state for off-loop control serving.
|
||||
///
|
||||
@@ -49,6 +49,13 @@ pub(crate) struct ControlReadHandle {
|
||||
/// stats_history dual-ring read copy + the scalar gauges/counts
|
||||
/// `show_status` needs, published from the tick (R2, Q1-b).
|
||||
stats: Arc<ArcSwap<StatsSnapshot>>,
|
||||
/// Category-D derived/routing/cache read view (tree / bloom / coord /
|
||||
/// identity + F-queue scalars), published from the tick (R3).
|
||||
routing: Arc<ArcSwap<RoutingSnapshot>>,
|
||||
/// Category-E per-entity table read view (peers / sessions / links /
|
||||
/// connections / transports + mmp), published from the tick with
|
||||
/// `Vec<Arc<Row>>` structural sharing (R4).
|
||||
entities: Arc<ArcSwap<EntitySnapshot>>,
|
||||
}
|
||||
|
||||
impl ControlReadHandle {
|
||||
@@ -60,11 +67,15 @@ impl ControlReadHandle {
|
||||
context: Arc<NodeContext>,
|
||||
metrics: Arc<MetricsRegistry>,
|
||||
stats: Arc<ArcSwap<StatsSnapshot>>,
|
||||
routing: Arc<ArcSwap<RoutingSnapshot>>,
|
||||
entities: Arc<ArcSwap<EntitySnapshot>>,
|
||||
) -> Self {
|
||||
Self {
|
||||
context,
|
||||
metrics,
|
||||
stats,
|
||||
routing,
|
||||
entities,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,6 +94,18 @@ impl ControlReadHandle {
|
||||
pub(crate) fn stats(&self) -> arc_swap::Guard<Arc<StatsSnapshot>> {
|
||||
self.stats.load()
|
||||
}
|
||||
|
||||
/// Load the latest published Category-D routing snapshot (freshest
|
||||
/// available by construction; no staleness gate, per Q1-e).
|
||||
pub(crate) fn routing(&self) -> arc_swap::Guard<Arc<RoutingSnapshot>> {
|
||||
self.routing.load()
|
||||
}
|
||||
|
||||
/// Load the latest published Category-E entity snapshot (freshest available
|
||||
/// by construction; no staleness gate, per Q1-e).
|
||||
pub(crate) fn entities(&self) -> arc_swap::Guard<Arc<EntitySnapshot>> {
|
||||
self.entities.load()
|
||||
}
|
||||
}
|
||||
|
||||
/// Attempt to serve a request entirely from the read handle, off the rx_loop.
|
||||
@@ -104,15 +127,22 @@ pub(crate) fn snapshot_dispatch(request: &Request, handle: &ControlReadHandle) -
|
||||
)),
|
||||
"show_stats_list" => Some(Response::ok(queries::show_stats_list())),
|
||||
"show_metrics" => Some(Response::ok(queries::show_metrics_from_handle(handle))),
|
||||
// R5: peer-ACL status, served from the tick-published `StatsSnapshot`.
|
||||
// The ACL is an `arc_swap::ArcSwap<PeerAcl>` reloaded only on the tick;
|
||||
// its status projection is captured at the same tick.
|
||||
"show_acl" => Some(Response::ok(queries::show_acl_from_handle(handle))),
|
||||
// R2: served from the tick-published `StatsSnapshot` (rings + scalar
|
||||
// gauges/counts). `show_status` and the two node-level/per-peer series
|
||||
// queries carry enough data in the snapshot to render faithfully
|
||||
// off-loop, including the parameterized series selectors (the snapshot
|
||||
// holds the full rings, so any metric / window / granularity is
|
||||
// satisfiable). `show_stats_peers` and `show_stats_history_all_peers`
|
||||
// stay on the rx_loop path: they need live peer membership
|
||||
// (`is_active`) and per-peer npub, which are Category-E state not yet
|
||||
// in the snapshot.
|
||||
// satisfiable).
|
||||
//
|
||||
// R5 closes out the per-peer stats queries: `show_stats_peers` and
|
||||
// `show_stats_history_all_peers` now read the snapshot's per-peer
|
||||
// `peer_meta` (live `is_active`, resolved npub / display name, captured
|
||||
// at publish time) joined against the `history` rings, so they no longer
|
||||
// need live `&Node` and render off-loop too.
|
||||
"show_status" => Some(Response::ok(queries::show_status_from_handle(handle))),
|
||||
"show_stats_history" => Some(queries::show_stats_history_from_handle(
|
||||
handle,
|
||||
@@ -122,6 +152,34 @@ pub(crate) fn snapshot_dispatch(request: &Request, handle: &ControlReadHandle) -
|
||||
handle,
|
||||
request.params.as_ref(),
|
||||
)),
|
||||
"show_stats_peers" => Some(Response::ok(queries::show_stats_peers_from_handle(handle))),
|
||||
"show_stats_history_all_peers" => Some(queries::show_stats_history_all_peers_from_handle(
|
||||
handle,
|
||||
request.params.as_ref(),
|
||||
)),
|
||||
// R3: served from the tick-published `RoutingSnapshot` (tree / bloom /
|
||||
// coord cache / identity cache + F-queue scalars). Display names are
|
||||
// resolved at publish time, so these render entirely off-loop. The
|
||||
// counter-family `stats` blocks come from the `MetricsRegistry` (also
|
||||
// in the handle). All five are parameterless.
|
||||
"show_tree" => Some(Response::ok(queries::show_tree_from_handle(handle))),
|
||||
"show_bloom" => Some(Response::ok(queries::show_bloom_from_handle(handle))),
|
||||
"show_cache" => Some(Response::ok(queries::show_cache_from_handle(handle))),
|
||||
"show_routing" => Some(Response::ok(queries::show_routing_from_handle(handle))),
|
||||
"show_identity_cache" => Some(Response::ok(queries::show_identity_cache_from_handle(
|
||||
handle,
|
||||
))),
|
||||
// R4: served from the tick-published `EntitySnapshot` (per-entity
|
||||
// `Vec<Arc<Row>>` tables with structural sharing). Display names,
|
||||
// tree-relationship flags, and Nostr-traversal state are resolved at
|
||||
// publish time, so these render entirely off-loop. All six are
|
||||
// parameterless.
|
||||
"show_peers" => Some(Response::ok(queries::show_peers_from_handle(handle))),
|
||||
"show_sessions" => Some(Response::ok(queries::show_sessions_from_handle(handle))),
|
||||
"show_links" => Some(Response::ok(queries::show_links_from_handle(handle))),
|
||||
"show_connections" => Some(Response::ok(queries::show_connections_from_handle(handle))),
|
||||
"show_transports" => Some(Response::ok(queries::show_transports_from_handle(handle))),
|
||||
"show_mmp" => Some(Response::ok(queries::show_mmp_from_handle(handle))),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ 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;
|
||||
|
||||
@@ -55,6 +56,32 @@ pub(crate) struct StatsSnapshot {
|
||||
/// 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 {
|
||||
@@ -74,6 +101,615 @@ impl StatsSnapshot {
|
||||
transport_count: 0,
|
||||
session_count: 0,
|
||||
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,
|
||||
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(),
|
||||
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>,
|
||||
}
|
||||
|
||||
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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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>,
|
||||
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 spin_bit_initiator: bool,
|
||||
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,
|
||||
}
|
||||
|
||||
/// 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>,
|
||||
}
|
||||
|
||||
/// 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()
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
//! RX event loop and packet dispatch.
|
||||
|
||||
use crate::control::queries;
|
||||
use crate::control::{ControlSocket, commands};
|
||||
use crate::node::wire::{
|
||||
COMMON_PREFIX_SIZE, CommonPrefix, FMP_VERSION, PHASE_ESTABLISHED, PHASE_MSG1, PHASE_MSG2,
|
||||
@@ -236,15 +235,18 @@ impl Node {
|
||||
self.register_identity(identity.node_addr, identity.pubkey);
|
||||
}
|
||||
Some((request, response_tx)) = control_rx.recv() => {
|
||||
let response = if request.command.starts_with("show_") {
|
||||
queries::dispatch(self, &request.command, request.params.as_ref())
|
||||
} else {
|
||||
commands::dispatch(
|
||||
self,
|
||||
&request.command,
|
||||
request.params.as_ref(),
|
||||
).await
|
||||
};
|
||||
// Only mutating COMMAND requests (`connect` / `disconnect`)
|
||||
// reach the rx_loop now. Every pure-read `show_*` query is
|
||||
// served off-loop from the read handle in the control accept
|
||||
// task (`snapshot_dispatch`), so it never round-trips here —
|
||||
// the data-plane dispatch path carries no `show_*` arm. A
|
||||
// `show_*` that somehow arrives (none does) falls through to
|
||||
// `commands::dispatch`, which returns "unknown command".
|
||||
let response = commands::dispatch(
|
||||
self,
|
||||
&request.command,
|
||||
request.params.as_ref(),
|
||||
).await;
|
||||
let _ = response_tx.send(response);
|
||||
}
|
||||
_ = tick.tick() => {
|
||||
|
||||
+590
-1
@@ -4,7 +4,7 @@
|
||||
//! holds all state required for mesh routing: identity, tree state,
|
||||
//! Bloom filters, coordinate caches, transports, links, and peers.
|
||||
|
||||
mod acl;
|
||||
pub(crate) mod acl;
|
||||
mod bloom;
|
||||
pub(crate) mod context;
|
||||
#[cfg(unix)]
|
||||
@@ -396,6 +396,21 @@ pub struct Node {
|
||||
/// live mutable `stats_history` above stays on the tick.
|
||||
stats_snapshot: std::sync::Arc<arc_swap::ArcSwap<crate::control::snapshot::StatsSnapshot>>,
|
||||
|
||||
/// Read-side snapshot of the Category-D derived/routing/cache subsystems
|
||||
/// (tree / bloom / coord cache / identity cache + F-queue scalars) that the
|
||||
/// `show_tree` / `show_bloom` / `show_cache` / `show_routing` /
|
||||
/// `show_identity_cache` queries render off the rx_loop. Published from the
|
||||
/// tick (see [`Self::publish_routing_snapshot`] for the Q1 rationale).
|
||||
routing_snapshot: std::sync::Arc<arc_swap::ArcSwap<crate::control::snapshot::RoutingSnapshot>>,
|
||||
|
||||
/// Read-side snapshot of the Category-E per-entity tables (peers / sessions
|
||||
/// / links / connections / transports + mmp) that the `show_peers` /
|
||||
/// `show_sessions` / `show_links` / `show_connections` / `show_transports`
|
||||
/// / `show_mmp` queries render off the rx_loop. Published from the tick with
|
||||
/// `Vec<Arc<Row>>` structural sharing (unchanged rows reused by pointer);
|
||||
/// see [`Self::publish_entities_snapshot`] for the Q1 rationale.
|
||||
entities_snapshot: std::sync::Arc<arc_swap::ArcSwap<crate::control::snapshot::EntitySnapshot>>,
|
||||
|
||||
// === TUN Interface ===
|
||||
/// TUN device state.
|
||||
tun_state: TunState,
|
||||
@@ -663,6 +678,12 @@ impl Node {
|
||||
stats_snapshot: std::sync::Arc::new(arc_swap::ArcSwap::from_pointee(
|
||||
crate::control::snapshot::StatsSnapshot::empty(),
|
||||
)),
|
||||
routing_snapshot: std::sync::Arc::new(arc_swap::ArcSwap::from_pointee(
|
||||
crate::control::snapshot::RoutingSnapshot::empty(),
|
||||
)),
|
||||
entities_snapshot: std::sync::Arc::new(arc_swap::ArcSwap::from_pointee(
|
||||
crate::control::snapshot::EntitySnapshot::empty(),
|
||||
)),
|
||||
tun_state,
|
||||
tun_name: None,
|
||||
tun_tx: None,
|
||||
@@ -818,6 +839,12 @@ impl Node {
|
||||
stats_snapshot: std::sync::Arc::new(arc_swap::ArcSwap::from_pointee(
|
||||
crate::control::snapshot::StatsSnapshot::empty(),
|
||||
)),
|
||||
routing_snapshot: std::sync::Arc::new(arc_swap::ArcSwap::from_pointee(
|
||||
crate::control::snapshot::RoutingSnapshot::empty(),
|
||||
)),
|
||||
entities_snapshot: std::sync::Arc::new(arc_swap::ArcSwap::from_pointee(
|
||||
crate::control::snapshot::EntitySnapshot::empty(),
|
||||
)),
|
||||
tun_state,
|
||||
tun_name: None,
|
||||
tun_tx: None,
|
||||
@@ -1395,6 +1422,8 @@ impl Node {
|
||||
self.context.clone(),
|
||||
self.metrics.clone(),
|
||||
self.stats_snapshot.clone(),
|
||||
self.routing_snapshot.clone(),
|
||||
self.entities_snapshot.clone(),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1468,6 +1497,30 @@ impl Node {
|
||||
// it is published only here, not in a monolithic per-tick rebuild of
|
||||
// every query (Q1-c). It also is not gated behind any slow I/O on the
|
||||
// tick the way the abandoned 2edc8a1 republish was.
|
||||
// Per-stats-history-peer metadata (R5). `show_stats_peers` /
|
||||
// `show_stats_history_all_peers` need each tracked peer's live
|
||||
// membership (`is_active`), resolved npub, and display name — all
|
||||
// cross-subsystem reads against the live peer table and host map,
|
||||
// available only here with `&self`. The lifecycle timestamps and
|
||||
// metric rings the renderers also read live in `history` (the dual-ring
|
||||
// read copy above), so this map carries only the resolved fields.
|
||||
let peer_meta: HashMap<NodeAddr, crate::control::snapshot::StatsPeerMeta> = self
|
||||
.stats_history
|
||||
.peer_addrs()
|
||||
.copied()
|
||||
.map(|addr| {
|
||||
let live = self.peers.get(&addr);
|
||||
let meta = crate::control::snapshot::StatsPeerMeta {
|
||||
is_active: live.is_some(),
|
||||
npub: live
|
||||
.map(|p| p.npub())
|
||||
.unwrap_or_else(|| hex::encode(addr.as_bytes())),
|
||||
display_name: self.peer_display_name(&addr),
|
||||
};
|
||||
(addr, meta)
|
||||
})
|
||||
.collect();
|
||||
|
||||
let snapshot = crate::control::snapshot::StatsSnapshot {
|
||||
history: std::sync::Arc::new(self.stats_history.clone()),
|
||||
estimated_mesh_size: self.estimated_mesh_size,
|
||||
@@ -1481,8 +1534,512 @@ impl Node {
|
||||
transport_count: self.transports.len(),
|
||||
session_count: self.sessions.len(),
|
||||
peer_aliases: std::sync::Arc::new(self.peer_aliases.clone()),
|
||||
acl_status: self.peer_acl_status(),
|
||||
peer_meta: std::sync::Arc::new(peer_meta),
|
||||
};
|
||||
self.stats_snapshot.store(std::sync::Arc::new(snapshot));
|
||||
|
||||
// Publish the Category-D routing read view alongside the stats
|
||||
// snapshot, from the same tick.
|
||||
self.publish_routing_snapshot();
|
||||
|
||||
// Publish the Category-E per-entity read view from the same tick, with
|
||||
// `Vec<Arc<Row>>` structural sharing against the previous snapshot.
|
||||
self.publish_entities_snapshot();
|
||||
}
|
||||
|
||||
/// Project the Category-D derived/routing/cache state into a
|
||||
/// [`RoutingSnapshot`](crate::control::snapshot::RoutingSnapshot) and
|
||||
/// publish it via `ArcSwap`, so `show_tree` / `show_bloom` / `show_cache`
|
||||
/// / `show_routing` / `show_identity_cache` render off the rx_loop.
|
||||
///
|
||||
/// **Q1 publisher placement.** The four projected subsystems (tree / bloom
|
||||
/// / coord cache / identity cache) mutate at dozens of scattered handler
|
||||
/// sites, and every projected row carries a *display name* resolved against
|
||||
/// the live peer/session tables and host map — state reachable only with
|
||||
/// `&Node`. Per-mutator on-change publication (Q1-a) would therefore be
|
||||
/// large, error-prone surgery, and each call would still need `&Node` to
|
||||
/// resolve names across subsystem boundaries. So this projection is
|
||||
/// published from the tick — the documented acceptable interim (the spec's
|
||||
/// "publish from the tick" allowance, mirroring R2's stats publish). The
|
||||
/// tick is the one site with coherent `&Node` access to resolve every
|
||||
/// display name together. A single combined cell is the natural shape
|
||||
/// because there is exactly one publisher, so the multi-mutator
|
||||
/// whole-snapshot-rebuild hazard Q1-c warns against does not arise.
|
||||
///
|
||||
/// The snapshot holds typed rows + scalars (Q1-d data, not rendered
|
||||
/// responses); the counter-family `stats` blocks the queries also emit are
|
||||
/// served from the `MetricsRegistry` (already `Arc`-shared) at render time.
|
||||
fn publish_routing_snapshot(&self) {
|
||||
use crate::control::snapshot as snap;
|
||||
|
||||
let now = Self::now_ms();
|
||||
|
||||
// --- tree (show_tree) ---
|
||||
let tree = self.tree_state();
|
||||
let my_coords = tree.my_coords();
|
||||
let tree_peers: Vec<snap::TreePeerRow> = tree
|
||||
.peer_ids()
|
||||
.map(|peer_id| {
|
||||
let coords = tree
|
||||
.peer_coords(peer_id)
|
||||
.map(|coords| snap::TreePeerCoords {
|
||||
depth: coords.depth(),
|
||||
root: *coords.root_id(),
|
||||
coord_path: coords.entries().iter().map(|e| e.node_addr).collect(),
|
||||
distance_to_us: my_coords.distance_to(coords),
|
||||
});
|
||||
snap::TreePeerRow {
|
||||
node_addr: *peer_id,
|
||||
display_name: self.peer_display_name(peer_id),
|
||||
coords,
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
let parent_addr = my_coords.parent_id();
|
||||
let tree_view = snap::TreeView {
|
||||
my_node_addr: *tree.my_node_addr(),
|
||||
root: *tree.root(),
|
||||
is_root: tree.is_root(),
|
||||
depth: my_coords.depth(),
|
||||
my_coords: my_coords.entries().iter().map(|e| e.node_addr).collect(),
|
||||
parent: *parent_addr,
|
||||
parent_display_name: self.peer_display_name(parent_addr),
|
||||
declaration_sequence: tree.my_declaration().sequence(),
|
||||
declaration_signed: tree.my_declaration().is_signed(),
|
||||
peer_tree_count: tree.peer_count(),
|
||||
peers: tree_peers,
|
||||
};
|
||||
|
||||
// --- bloom (show_bloom) ---
|
||||
let bloom = self.bloom_state();
|
||||
let max_inbound_fpr = self.config().node.bloom.max_inbound_fpr;
|
||||
let bloom_peers: Vec<snap::BloomPeerRow> = self
|
||||
.peers()
|
||||
.map(|peer| {
|
||||
let addr = *peer.node_addr();
|
||||
let filter = peer.inbound_filter().map(|f| snap::BloomPeerFilter {
|
||||
estimated_count: f.estimated_count(max_inbound_fpr),
|
||||
set_bits: f.count_ones(),
|
||||
fill_ratio: f.fill_ratio(),
|
||||
});
|
||||
snap::BloomPeerRow {
|
||||
peer: addr,
|
||||
display_name: self.peer_display_name(&addr),
|
||||
has_filter: peer.filter_sequence() > 0,
|
||||
filter_sequence: peer.filter_sequence(),
|
||||
filter,
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
let bloom_view = snap::BloomView {
|
||||
own_node_addr: *self.node_addr(),
|
||||
is_leaf_only: self.is_leaf_only(),
|
||||
sequence: bloom.sequence(),
|
||||
leaf_dependents: bloom.leaf_dependents().iter().copied().collect(),
|
||||
peer_filters: bloom_peers,
|
||||
};
|
||||
|
||||
// --- coord cache (show_cache, show_routing) ---
|
||||
let cache = self.coord_cache();
|
||||
let cache_stats = cache.stats(now);
|
||||
let cache_entries: Vec<snap::CacheEntryRow> = cache
|
||||
.iter(now)
|
||||
.map(|(addr, entry)| snap::CacheEntryRow {
|
||||
node_addr: *addr,
|
||||
display_name: self.peer_display_name(addr),
|
||||
depth: entry.coords().depth(),
|
||||
coord_path: entry
|
||||
.coords()
|
||||
.entries()
|
||||
.iter()
|
||||
.map(|e| e.node_addr)
|
||||
.collect(),
|
||||
created_at: entry.created_at(),
|
||||
last_used_ms: entry.last_used(),
|
||||
path_mtu: entry.path_mtu(),
|
||||
})
|
||||
.collect();
|
||||
let cache_view = snap::CacheView {
|
||||
count: cache_stats.entries,
|
||||
max_entries: cache_stats.max_entries,
|
||||
fill_ratio: cache_stats.fill_ratio(),
|
||||
default_ttl_ms: cache.default_ttl_ms(),
|
||||
expired: cache_stats.expired,
|
||||
avg_age_ms: cache_stats.avg_age_ms,
|
||||
entries: cache_entries,
|
||||
};
|
||||
|
||||
// --- F-queue / discovery routing scalars (show_routing) ---
|
||||
let pending_lookups: Vec<snap::PendingLookupRow> = self
|
||||
.pending_lookups_iter()
|
||||
.map(|(addr, lookup)| snap::PendingLookupRow {
|
||||
target: *addr,
|
||||
display_name: self.peer_display_name(addr),
|
||||
initiated_ms: lookup.initiated_ms,
|
||||
last_sent_ms: lookup.last_sent_ms,
|
||||
attempt: lookup.attempt,
|
||||
})
|
||||
.collect();
|
||||
let retries: Vec<snap::RetryRow> = self
|
||||
.retry_state_iter()
|
||||
.map(|(addr, state)| snap::RetryRow {
|
||||
node_addr: *addr,
|
||||
display_name: self.peer_display_name(addr),
|
||||
retry_count: state.retry_count,
|
||||
retry_after_ms: state.retry_after_ms,
|
||||
auto_reconnect: state.reconnect,
|
||||
})
|
||||
.collect();
|
||||
let routing_view = snap::RoutingView {
|
||||
pending_lookups,
|
||||
pending_tun_destinations: self.pending_tun_destinations(),
|
||||
pending_tun_packets: self.pending_tun_total_packets(),
|
||||
recent_requests: self.recent_request_count(),
|
||||
retries,
|
||||
};
|
||||
|
||||
// --- identity cache (show_identity_cache, show_routing) ---
|
||||
let identity_entries: Vec<snap::IdentityRow> = self
|
||||
.identity_cache_iter()
|
||||
.map(|(node_addr, pubkey, last_seen_ms)| {
|
||||
let (xonly, _parity) = pubkey.x_only_public_key();
|
||||
let fips_addr = crate::identity::FipsAddress::from_node_addr(node_addr);
|
||||
snap::IdentityRow {
|
||||
node_addr: *node_addr,
|
||||
npub: crate::identity::encode_npub(&xonly),
|
||||
display_name: self.peer_display_name(node_addr),
|
||||
ipv6_addr: format!("{}", fips_addr),
|
||||
last_seen_ms,
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
let identity_view = snap::IdentityView {
|
||||
entries: identity_entries,
|
||||
max_entries: self.identity_cache_max(),
|
||||
};
|
||||
|
||||
let snapshot = snap::RoutingSnapshot {
|
||||
tree: tree_view,
|
||||
bloom: bloom_view,
|
||||
cache: cache_view,
|
||||
routing: routing_view,
|
||||
identity: identity_view,
|
||||
};
|
||||
self.routing_snapshot.store(std::sync::Arc::new(snapshot));
|
||||
}
|
||||
|
||||
/// Project the Category-E per-entity tables (peers / sessions / links /
|
||||
/// connections / transports + mmp) into an
|
||||
/// [`EntitySnapshot`](crate::control::snapshot::EntitySnapshot) and publish
|
||||
/// it via `ArcSwap`, so `show_peers` / `show_sessions` / `show_links` /
|
||||
/// `show_connections` / `show_transports` / `show_mmp` render off the
|
||||
/// rx_loop.
|
||||
///
|
||||
/// **Q1 publisher placement (tick, like R3).** Every projected row needs a
|
||||
/// display name resolved against the live peer/session tables and host map
|
||||
/// (`&Node`); `show_peers` additionally needs the live tree state to derive
|
||||
/// `is_parent` / `is_child` plus the Nostr-discovery failure-state map —
|
||||
/// cross-subsystem reads available only with `&Node`. And most fields
|
||||
/// (link/session traffic counters, MMP metrics, `last_seen`, noise counters)
|
||||
/// mutate continuously on the data plane, not at the discrete entity
|
||||
/// lifecycle mutators, so per-lifecycle-mutator publication (Q1-a) would not
|
||||
/// capture their freshness anyway. The tick is the natural cadence with
|
||||
/// coherent `&Node` access.
|
||||
///
|
||||
/// **Structural sharing (the R4 umbrella mandate).** Each table is a
|
||||
/// `Vec<Arc<Row>>`. The freshly-projected rows are reconciled against the
|
||||
/// previously published snapshot via
|
||||
/// [`reconcile_rows`](crate::control::snapshot::reconcile_rows): a row's
|
||||
/// `Arc` is reused (kept by pointer) whenever it matches the prior row by
|
||||
/// identity and compares equal by value, so a tick in which only one
|
||||
/// peer/session changed re-allocates only that one row, not the whole table.
|
||||
/// This is what keeps the publish cost off the hot path at scale (the exact
|
||||
/// thing the umbrella warns a naive per-tick rebuild would violate).
|
||||
fn publish_entities_snapshot(&self) {
|
||||
use crate::control::snapshot as snap;
|
||||
|
||||
let prev = self.entities_snapshot.load();
|
||||
|
||||
// --- peers (show_peers) ---
|
||||
let tree = self.tree_state();
|
||||
let my_addr = *tree.my_node_addr();
|
||||
let parent_id = *tree.my_declaration().parent_id();
|
||||
let is_root = tree.is_root();
|
||||
|
||||
// Per-npub Nostr-traversal failure-state, indexed by npub for O(1)
|
||||
// per-peer lookup (empty when Nostr discovery is disabled).
|
||||
let nostr_state: std::collections::HashMap<String, _> = self
|
||||
.nostr_discovery_handle()
|
||||
.map(|d| {
|
||||
d.failure_state_snapshot()
|
||||
.into_iter()
|
||||
.map(|view| (view.npub.clone(), view))
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
let peer_rows: Vec<snap::PeerRow> = self
|
||||
.peers()
|
||||
.map(|peer| {
|
||||
let node_addr = *peer.node_addr();
|
||||
let is_parent = !is_root && node_addr == parent_id;
|
||||
let is_child = tree
|
||||
.peer_declaration(&node_addr)
|
||||
.is_some_and(|decl| *decl.parent_id() == my_addr);
|
||||
|
||||
let link_info = self.get_link(&peer.link_id()).map(|link| {
|
||||
let transport_type = self
|
||||
.get_transport(&link.transport_id())
|
||||
.map(|h| h.transport_type().name.to_string());
|
||||
snap::PeerLinkInfo {
|
||||
direction: format!("{}", link.direction()),
|
||||
transport_type,
|
||||
}
|
||||
});
|
||||
|
||||
let stats = peer.link_stats();
|
||||
let nostr = nostr_state.get(&peer.npub());
|
||||
let nostr_traversal = snap::PeerNostrState {
|
||||
consecutive_failures: nostr.map(|s| s.consecutive_failures).unwrap_or(0),
|
||||
cooldown_until_ms: nostr.and_then(|s| s.cooldown_until_ms),
|
||||
last_observed_skew_ms: nostr.and_then(|s| s.last_observed_skew_ms),
|
||||
};
|
||||
|
||||
let noise = peer.noise_session().map(|session| snap::PeerNoiseCounters {
|
||||
send_counter: session.current_send_counter(),
|
||||
highest_recv_counter: session.highest_received_counter(),
|
||||
});
|
||||
|
||||
let mmp = peer
|
||||
.mmp()
|
||||
.map(|mmp| project_entity_mmp(&mmp.metrics, format!("{}", mmp.mode()), None));
|
||||
|
||||
snap::PeerRow {
|
||||
node_addr,
|
||||
npub: peer.npub(),
|
||||
display_name: self.peer_display_name(&node_addr),
|
||||
ipv6_addr: format!("{}", peer.address()),
|
||||
connectivity: format!("{}", peer.connectivity()),
|
||||
link_id: peer.link_id().as_u64(),
|
||||
authenticated_at_ms: peer.authenticated_at(),
|
||||
last_seen_ms: peer.last_seen(),
|
||||
has_tree_position: peer.has_tree_position(),
|
||||
has_bloom_filter: peer.filter_sequence() > 0,
|
||||
filter_sequence: peer.filter_sequence(),
|
||||
is_parent,
|
||||
is_child,
|
||||
transport_addr: peer.current_addr().map(|a| format!("{}", a)),
|
||||
link_info,
|
||||
tree_depth: peer.coords().map(|c| c.depth()),
|
||||
stats: snap::PeerLinkStats {
|
||||
packets_sent: stats.packets_sent,
|
||||
packets_recv: stats.packets_recv,
|
||||
bytes_sent: stats.bytes_sent,
|
||||
bytes_recv: stats.bytes_recv,
|
||||
},
|
||||
replay_suppressed: peer.replay_suppressed_count(),
|
||||
consecutive_decrypt_failures: peer.consecutive_decrypt_failures(),
|
||||
nostr_traversal,
|
||||
noise,
|
||||
our_session_index: peer.our_index().map(|idx| idx.as_u32()),
|
||||
rekey_in_progress: peer.rekey_in_progress(),
|
||||
rekey_draining: peer.is_draining(),
|
||||
current_k_bit: peer.current_k_bit(),
|
||||
mmp,
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
// --- sessions (show_sessions) ---
|
||||
let session_rows: Vec<snap::SessionRow> = self
|
||||
.session_entries()
|
||||
.map(|(addr, entry)| {
|
||||
let state = if entry.is_established() {
|
||||
"established"
|
||||
} else if entry.is_initiating() {
|
||||
"initiating"
|
||||
} else if entry.is_awaiting_msg3() {
|
||||
"awaiting_msg3"
|
||||
} else {
|
||||
"unknown"
|
||||
};
|
||||
let (xonly, _parity) = entry.remote_pubkey().x_only_public_key();
|
||||
let (pkts_tx, pkts_rx, bytes_tx, bytes_rx) = entry.traffic_counters();
|
||||
|
||||
let resend_count = (!entry.is_established()).then(|| entry.resend_count());
|
||||
let established = entry.is_established().then(|| snap::SessionEstablished {
|
||||
session_start_ms: entry.session_start_ms(),
|
||||
current_k_bit: entry.current_k_bit(),
|
||||
coords_warmup_remaining: entry.coords_warmup_remaining(),
|
||||
is_draining: entry.is_draining(),
|
||||
});
|
||||
let mmp = entry.mmp().map(|mmp| {
|
||||
project_entity_mmp(
|
||||
&mmp.metrics,
|
||||
format!("{}", mmp.mode()),
|
||||
Some(mmp.path_mtu.current_mtu()),
|
||||
)
|
||||
});
|
||||
|
||||
snap::SessionRow {
|
||||
remote_addr: *addr,
|
||||
display_name: self.peer_display_name(addr),
|
||||
state,
|
||||
is_initiator: entry.is_initiator(),
|
||||
last_activity_ms: entry.last_activity(),
|
||||
npub: crate::identity::encode_npub(&xonly),
|
||||
stats: snap::SessionStats {
|
||||
packets_sent: pkts_tx,
|
||||
packets_recv: pkts_rx,
|
||||
bytes_sent: bytes_tx,
|
||||
bytes_recv: bytes_rx,
|
||||
},
|
||||
resend_count,
|
||||
established,
|
||||
mmp,
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
// --- links (show_links) ---
|
||||
let link_rows: Vec<snap::LinkRow> = self
|
||||
.links()
|
||||
.map(|link| {
|
||||
let stats = link.stats();
|
||||
snap::LinkRow {
|
||||
link_id: link.link_id().as_u64(),
|
||||
transport_id: link.transport_id().as_u32(),
|
||||
remote_addr: format!("{}", link.remote_addr()),
|
||||
direction: format!("{}", link.direction()),
|
||||
state: format!("{}", link.state()),
|
||||
created_at_ms: link.created_at(),
|
||||
stats: snap::LinkStats {
|
||||
packets_sent: stats.packets_sent,
|
||||
packets_recv: stats.packets_recv,
|
||||
bytes_sent: stats.bytes_sent,
|
||||
bytes_recv: stats.bytes_recv,
|
||||
last_recv_ms: stats.last_recv_ms,
|
||||
},
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
// --- connections (show_connections) ---
|
||||
let connection_rows: Vec<snap::ConnectionRow> = self
|
||||
.connections()
|
||||
.map(|conn| snap::ConnectionRow {
|
||||
link_id: conn.link_id().as_u64(),
|
||||
direction: format!("{}", conn.direction()),
|
||||
handshake_state: format!("{}", conn.handshake_state()),
|
||||
started_at_ms: conn.started_at(),
|
||||
last_activity_ms: conn.last_activity(),
|
||||
resend_count: conn.resend_count(),
|
||||
expected_peer: conn.expected_identity().map(|id| id.npub()),
|
||||
})
|
||||
.collect();
|
||||
|
||||
// --- transports (show_transports) ---
|
||||
let transport_rows: Vec<snap::TransportRow> = self
|
||||
.transport_ids()
|
||||
.map(|id| {
|
||||
let handle = self.get_transport(id).unwrap();
|
||||
snap::TransportRow {
|
||||
transport_id: id.as_u32(),
|
||||
transport_type: handle.transport_type().name.to_string(),
|
||||
state: format!("{}", handle.state()),
|
||||
mtu: handle.mtu(),
|
||||
name: handle.name().map(|s| s.to_string()),
|
||||
local_addr: handle.local_addr().map(|a| format!("{}", a)),
|
||||
tor_mode: handle.tor_mode().map(|s| s.to_string()),
|
||||
onion_address: handle.onion_address().map(|s| s.to_string()),
|
||||
tor_monitoring: handle
|
||||
.tor_monitoring()
|
||||
.map(|m| serde_json::to_value(&m).unwrap_or_default()),
|
||||
stats: handle.transport_stats(),
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
// --- mmp peers (show_mmp link-layer) ---
|
||||
let mmp_peer_rows: Vec<snap::MmpPeerRow> = self
|
||||
.peers()
|
||||
.filter_map(|peer| {
|
||||
let mmp = peer.mmp()?;
|
||||
let addr = *peer.node_addr();
|
||||
let metrics = &mmp.metrics;
|
||||
let srtt_ms = metrics.srtt_ms();
|
||||
let smoothed_etx = metrics.smoothed_etx();
|
||||
let lqi = match (srtt_ms, smoothed_etx) {
|
||||
(Some(srtt), Some(setx)) => Some(setx * (1.0 + srtt / 100.0)),
|
||||
_ => None,
|
||||
};
|
||||
let trend = |dual: &crate::mmp::algorithms::DualEwma| {
|
||||
dual.initialized()
|
||||
.then(|| crate::control::queries::trend_label(dual.short(), dual.long()))
|
||||
};
|
||||
Some(snap::MmpPeerRow {
|
||||
peer: addr,
|
||||
display_name: self.peer_display_name(&addr),
|
||||
mode: format!("{}", mmp.mode()),
|
||||
loss_rate: metrics.loss_rate(),
|
||||
etx: metrics.etx,
|
||||
goodput_bps: metrics.goodput_bps,
|
||||
spin_bit_initiator: mmp.spin_bit.is_initiator(),
|
||||
smoothed_loss: metrics.smoothed_loss(),
|
||||
smoothed_etx,
|
||||
srtt_ms,
|
||||
lqi,
|
||||
trends: snap::MmpTrends {
|
||||
rtt_trend: trend(&metrics.rtt_trend),
|
||||
loss_trend: trend(&metrics.loss_trend),
|
||||
goodput_trend: trend(&metrics.goodput_trend),
|
||||
jitter_trend: trend(&metrics.jitter_trend),
|
||||
},
|
||||
delivery_ratio_forward: metrics.delivery_ratio_forward,
|
||||
delivery_ratio_reverse: metrics.delivery_ratio_reverse,
|
||||
ecn_ce_count: metrics.last_ecn_ce_count(),
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
// --- mmp sessions (show_mmp session-layer) ---
|
||||
let mmp_session_rows: Vec<snap::MmpSessionRow> = self
|
||||
.session_entries()
|
||||
.filter_map(|(addr, entry)| {
|
||||
let mmp = entry.mmp()?;
|
||||
let metrics = &mmp.metrics;
|
||||
let srtt_ms = metrics.srtt_ms();
|
||||
let smoothed_etx = metrics.smoothed_etx();
|
||||
let sqi = match (srtt_ms, smoothed_etx) {
|
||||
(Some(srtt), Some(setx)) => Some(setx * (1.0 + srtt / 100.0)),
|
||||
_ => None,
|
||||
};
|
||||
Some(snap::MmpSessionRow {
|
||||
remote: *addr,
|
||||
display_name: self.peer_display_name(addr),
|
||||
mode: format!("{}", mmp.mode()),
|
||||
loss_rate: metrics.loss_rate(),
|
||||
etx: metrics.etx,
|
||||
path_mtu: mmp.path_mtu.current_mtu(),
|
||||
smoothed_loss: metrics.smoothed_loss(),
|
||||
smoothed_etx,
|
||||
srtt_ms,
|
||||
sqi,
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
let snapshot = snap::EntitySnapshot {
|
||||
peers: snap::reconcile_rows(&prev.peers, peer_rows, |r| r.node_addr),
|
||||
sessions: snap::reconcile_rows(&prev.sessions, session_rows, |r| r.remote_addr),
|
||||
links: snap::reconcile_rows(&prev.links, link_rows, |r| r.link_id),
|
||||
connections: snap::reconcile_rows(&prev.connections, connection_rows, |r| r.link_id),
|
||||
transports: snap::reconcile_rows(&prev.transports, transport_rows, |r| r.transport_id),
|
||||
mmp_peers: snap::reconcile_rows(&prev.mmp_peers, mmp_peer_rows, |r| r.peer),
|
||||
mmp_sessions: snap::reconcile_rows(&prev.mmp_sessions, mmp_session_rows, |r| r.remote),
|
||||
};
|
||||
self.entities_snapshot.store(std::sync::Arc::new(snapshot));
|
||||
}
|
||||
|
||||
// === TUN Interface ===
|
||||
@@ -2297,6 +2854,38 @@ impl Node {
|
||||
}
|
||||
}
|
||||
|
||||
/// Project an MMP metrics block into the snapshot
|
||||
/// [`EntityMmp`](crate::control::snapshot::EntityMmp) shared by `show_peers`
|
||||
/// (link-layer, `path_mtu = None`) and `show_sessions` (session-layer,
|
||||
/// `path_mtu = Some`). `quality_index` (`lqi` for peers / `sqi` for sessions)
|
||||
/// is precomputed here exactly as the on-loop queries do, so the render is a
|
||||
/// plain field emit.
|
||||
fn project_entity_mmp(
|
||||
metrics: &crate::mmp::metrics::MmpMetrics,
|
||||
mode: String,
|
||||
path_mtu: Option<u16>,
|
||||
) -> crate::control::snapshot::EntityMmp {
|
||||
let srtt_ms = metrics.srtt_ms();
|
||||
let smoothed_etx = metrics.smoothed_etx();
|
||||
let quality_index = match (srtt_ms, smoothed_etx) {
|
||||
(Some(srtt), Some(setx)) => Some(setx * (1.0 + srtt / 100.0)),
|
||||
_ => None,
|
||||
};
|
||||
crate::control::snapshot::EntityMmp {
|
||||
mode,
|
||||
srtt_ms,
|
||||
loss_rate: metrics.loss_rate(),
|
||||
etx: metrics.etx,
|
||||
goodput_bps: metrics.goodput_bps,
|
||||
delivery_ratio_forward: metrics.delivery_ratio_forward,
|
||||
delivery_ratio_reverse: metrics.delivery_ratio_reverse,
|
||||
smoothed_loss: metrics.smoothed_loss(),
|
||||
smoothed_etx,
|
||||
quality_index,
|
||||
path_mtu,
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for Node {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("Node")
|
||||
|
||||
Reference in New Issue
Block a user