node: extract immutable state into a shared context and atomic metric registry

Store node counters in an atomic metric registry read through &self, and
introduce a shared NodeContext bundle holding the effectively-immutable
fields (config, identity, startup epoch, node profile, capability limits).
Source the immutable config and identity reads across the receive hot
path, the XX handshake/session/rekey state machines, and the discovery,
tree, bloom, retry, and lifecycle modules through the context accessors.
Includes the bloom delta/full/NACK/resize and byte-total counters in the
registry. The Node fields and the context are rebuilt in lockstep at every
mutation site.
This commit is contained in:
Johnathan Corgan
2026-06-02 13:05:03 +00:00
parent 88e48fdc68
commit e181df0ac4
30 changed files with 1301 additions and 1003 deletions
+84 -13
View File
@@ -6,6 +6,7 @@
mod acl;
mod bloom;
mod context;
#[cfg(unix)]
pub(crate) mod decrypt_worker;
mod discovery_rate_limit;
@@ -13,6 +14,7 @@ mod discovery_rate_limit;
pub(crate) mod encrypt_worker;
mod handlers;
mod lifecycle;
pub(crate) mod metrics;
mod rate_limit;
pub(crate) mod reject;
mod reloadable;
@@ -319,6 +321,13 @@ pub struct Node {
/// Loaded configuration.
config: Config,
/// Shared immutable context bundle. A parallel, authoritative copy of the
/// effectively-immutable fields (config/identity/startup_epoch/started_at/
/// is_leaf_only/max_*), kept in lockstep with the `Node` fields via
/// `rebuild_context`. Readers migrate onto it in later sub-PRs; the
/// duplicated `Node` fields are removed once the last reader has moved.
context: Arc<context::NodeContext>,
// === State ===
/// Node operational state.
state: NodeState,
@@ -413,6 +422,10 @@ pub struct Node {
/// Routing, forwarding, discovery, and error signal counters.
stats: stats::NodeStats,
/// Lock-free atomic metric counters. Shadows `stats` during the
/// counter migration; bumped alongside it with a parity check.
metrics: std::sync::Arc<metrics::MetricsRegistry>,
/// Time-series history of node-level metrics (1s/1m rings).
stats_history: stats_history::StatsHistory,
@@ -649,11 +662,25 @@ impl Node {
let (decrypt_fallback_tx, decrypt_fallback_rx) =
tokio::sync::mpsc::unbounded_channel::<decrypt_worker::DecryptWorkerEvent>();
let started_at = std::time::Instant::now();
let context = Arc::new(context::NodeContext::new(
Arc::new(config.clone()),
identity.clone(),
startup_epoch,
started_at,
is_leaf_only,
node_profile,
max_connections,
max_peers,
max_links,
));
Ok(Self {
identity,
startup_epoch,
started_at: std::time::Instant::now(),
started_at,
config,
context,
state: NodeState::Created,
is_leaf_only,
node_profile,
@@ -679,6 +706,7 @@ impl Node {
next_link_id: 1,
next_transport_id: 1,
stats: stats::NodeStats::new(),
metrics: std::sync::Arc::new(metrics::MetricsRegistry::new()),
stats_history: stats_history::StatsHistory::new(),
tun_state,
tun_name: None,
@@ -797,11 +825,25 @@ impl Node {
let (decrypt_fallback_tx, decrypt_fallback_rx) =
tokio::sync::mpsc::unbounded_channel::<decrypt_worker::DecryptWorkerEvent>();
let started_at = std::time::Instant::now();
let context = Arc::new(context::NodeContext::new(
Arc::new(config.clone()),
identity.clone(),
startup_epoch,
started_at,
false,
NodeProfile::Full,
max_connections,
max_peers,
max_links,
));
Ok(Self {
identity,
startup_epoch,
started_at: std::time::Instant::now(),
started_at,
config,
context,
state: NodeState::Created,
is_leaf_only: false,
node_profile: NodeProfile::Full,
@@ -827,6 +869,7 @@ impl Node {
next_link_id: 1,
next_transport_id: 1,
stats: stats::NodeStats::new(),
metrics: std::sync::Arc::new(metrics::MetricsRegistry::new()),
stats_history: stats_history::StatsHistory::new(),
tun_state,
tun_name: None,
@@ -886,6 +929,7 @@ impl Node {
node.is_leaf_only = true;
node.node_profile = NodeProfile::Leaf;
node.bloom_state = BloomState::leaf_only(*node.identity.node_addr());
node.rebuild_context();
Ok(node)
}
@@ -1100,7 +1144,7 @@ impl Node {
/// Get this node's identity.
pub fn identity(&self) -> &Identity {
&self.identity
&self.context.identity
}
/// Get this node's NodeAddr.
@@ -1150,7 +1194,26 @@ impl Node {
/// Get the configuration.
pub fn config(&self) -> &Config {
&self.config
self.context.config.as_ref()
}
/// Rebuild the shared [`context::NodeContext`] from the current `Node`
/// fields. Called after any mutation of a bundled field (`update_peers`,
/// the `set_max_*` setters) so `self.context` stays equal to the `Node`
/// fields it mirrors. Cheap — the only deep copy is the (rare) `Config`
/// clone.
fn rebuild_context(&mut self) {
self.context = Arc::new(context::NodeContext::new(
Arc::new(self.config.clone()),
self.identity.clone(),
self.startup_epoch,
self.started_at,
self.is_leaf_only,
self.node_profile,
self.max_connections,
self.max_peers,
self.max_links,
));
}
/// Calculate the effective IPv6 MTU that can be sent over FIPS.
@@ -1204,7 +1267,7 @@ impl Node {
/// Get the node uptime.
pub fn uptime(&self) -> std::time::Duration {
self.started_at.elapsed()
self.context.started_at.elapsed()
}
/// Check if node is operational.
@@ -1214,12 +1277,12 @@ impl Node {
/// Check if this is a leaf-only node.
pub fn is_leaf_only(&self) -> bool {
self.is_leaf_only
self.context.is_leaf_only
}
/// Get the node's profile (Full, NonRouting, Leaf).
pub fn node_profile(&self) -> NodeProfile {
self.node_profile
self.context.node_profile
}
/// Collect the set of peers that are not full nodes (non-routing/leaf).
@@ -1376,6 +1439,11 @@ impl Node {
&mut self.stats
}
/// Get the atomic metric registry.
pub(crate) fn metrics(&self) -> &metrics::MetricsRegistry {
&self.metrics
}
/// Get the stats history collector.
pub fn stats_history(&self) -> &stats_history::StatsHistory {
&self.stats_history
@@ -1384,7 +1452,7 @@ impl Node {
/// Sample the current node state into the stats history ring.
/// Called once per tick from the RX loop.
pub(crate) fn record_stats_history(&mut self) {
let fwd = &self.stats.forwarding;
let fwd = &self.metrics.forwarding;
let peers_with_mmp: Vec<f64> = self
.peers
.values()
@@ -1400,11 +1468,11 @@ impl Node {
mesh_size: self.estimated_mesh_size,
tree_depth: self.tree_state.my_coords().depth() as u32,
peer_count: self.peers.len() as u64,
parent_switches_total: self.stats.tree.parent_switches,
bytes_in_total: fwd.received_bytes,
bytes_out_total: fwd.forwarded_bytes + fwd.originated_bytes,
packets_in_total: fwd.received_packets,
packets_out_total: fwd.forwarded_packets + fwd.originated_packets,
parent_switches_total: self.metrics.tree.parent_switches.get(),
bytes_in_total: fwd.received_bytes.get(),
bytes_out_total: fwd.forwarded_bytes.get() + fwd.originated_bytes.get(),
packets_in_total: fwd.received_packets.get(),
packets_out_total: fwd.forwarded_packets.get() + fwd.originated_packets.get(),
loss_rate,
active_sessions: self.sessions.len() as u64,
};
@@ -1457,11 +1525,13 @@ impl Node {
/// Set the maximum number of connections (handshake phase).
pub fn set_max_connections(&mut self, max: usize) {
self.max_connections = max;
self.rebuild_context();
}
/// Set the maximum number of peers (authenticated).
pub fn set_max_peers(&mut self, max: usize) {
self.max_peers = max;
self.rebuild_context();
}
/// Returns false when we are at or above the configured `max_peers`
@@ -1479,6 +1549,7 @@ impl Node {
/// Set the maximum number of links.
pub fn set_max_links(&mut self, max: usize) {
self.max_links = max;
self.rebuild_context();
}
// === Counts ===