- NodeAddr abstraction replacing SocketAddr across distribution crate (~30 files) - Core wasm32 compatibility: web-time, cfg-gated threads, wire encoding extraction - WebSocket transport (browser) with GatewayControl protocol - WebSocket gateway (native) with session routing and control protocol - BrowserRuntime API: JS actor support, connect/spawn/send/tick - Dashboard: extracted HTML to static files, gossip protocol panel with membership event log, visual fixes, click-to-filter, worker-colored rows Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
252 lines
8.8 KiB
Rust
252 lines
8.8 KiB
Rust
//! Serializable snapshot of a `DistributedNode`'s state.
|
|
//!
|
|
//! Used by the runtime-dashboard to display distribution monitoring data
|
|
//! for a single node without reaching out to other nodes.
|
|
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
use crate::node::DistributedNode;
|
|
use crate::types::{MemberState, NodeAddr, NodeId};
|
|
|
|
/// Snapshot of a single member in the SWIM membership list.
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct MemberInfo {
|
|
pub node_id: String,
|
|
pub addr: String,
|
|
pub state: String,
|
|
pub incarnation: u64,
|
|
}
|
|
|
|
/// Snapshot of a node in the Kademlia routing table.
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct NeighborInfo {
|
|
pub node_id: String,
|
|
pub addr: String,
|
|
}
|
|
|
|
/// Snapshot of a single LRU cache entry.
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct CacheEntryInfo {
|
|
pub actor_addr: String,
|
|
pub node_id: String,
|
|
}
|
|
|
|
/// SWIM gossip protocol internal state.
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct GossipInfo {
|
|
/// Current tick counter.
|
|
pub tick: u64,
|
|
/// Monotonic ping sequence number (total pings sent).
|
|
pub sequence: u64,
|
|
/// This node's current incarnation number.
|
|
pub incarnation: u64,
|
|
/// Current probe phase: "idle", "waiting_direct_ack", "waiting_indirect_ack".
|
|
pub probe_phase: String,
|
|
/// Node being probed (if not idle).
|
|
pub probe_target: Option<String>,
|
|
/// Active suspicion timers.
|
|
pub suspicion_timers: Vec<SuspicionInfo>,
|
|
/// Pending gossip dissemination entries.
|
|
pub dissemination_queue: Vec<DisseminationInfo>,
|
|
/// Protocol configuration.
|
|
pub config: GossipConfig,
|
|
}
|
|
|
|
/// A single active suspicion timer.
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct SuspicionInfo {
|
|
pub node_id: String,
|
|
pub started_at: u64,
|
|
/// How many ticks this timer has been active.
|
|
pub age_ticks: u64,
|
|
}
|
|
|
|
/// A pending gossip dissemination entry.
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct DisseminationInfo {
|
|
pub node_id: String,
|
|
pub state: String,
|
|
pub incarnation: u64,
|
|
/// Remaining piggyback transmissions before eviction.
|
|
pub remaining: usize,
|
|
}
|
|
|
|
/// SWIM protocol configuration values.
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct GossipConfig {
|
|
pub probe_interval: u64,
|
|
pub probe_timeout: u64,
|
|
pub indirect_probes: usize,
|
|
pub suspicion_timeout: u64,
|
|
}
|
|
|
|
/// Complete snapshot of a `DistributedNode`'s observable state.
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct DistributionNodeSnapshot {
|
|
/// This node's ID (hex-encoded).
|
|
pub node_id: String,
|
|
/// This node's listen address.
|
|
pub listen_addr: String,
|
|
|
|
// ─── SWIM membership ─────────────────────────────────────────────
|
|
/// All known members with their state.
|
|
pub members: Vec<MemberInfo>,
|
|
/// Count of alive members.
|
|
pub alive_count: usize,
|
|
/// Count of suspected members.
|
|
pub suspect_count: usize,
|
|
/// Count of dead members.
|
|
pub dead_count: usize,
|
|
|
|
// ─── Kademlia routing table ──────────────────────────────────────
|
|
/// Total nodes in the routing table.
|
|
pub routing_table_size: usize,
|
|
/// Non-empty buckets as (bucket_index, entry_count).
|
|
pub routing_buckets: Vec<(usize, usize)>,
|
|
/// All nodes in the routing table.
|
|
pub routing_neighbors: Vec<NeighborInfo>,
|
|
|
|
// ─── Location cache ──────────────────────────────────────────────
|
|
/// Number of entries in the LRU cache.
|
|
pub cache_size: usize,
|
|
/// All cache entries (actor → node).
|
|
pub cache_entries: Vec<CacheEntryInfo>,
|
|
|
|
// ─── Directory & repair ──────────────────────────────────────────
|
|
/// Total directory entries in this node's shard.
|
|
pub directory_entry_count: usize,
|
|
/// Number of entries pending re-replication.
|
|
pub repair_queue_size: usize,
|
|
|
|
// ─── Gossip pairs ────────────────────────────────────────────────
|
|
/// Recent SWIM probe targets (most recent last).
|
|
pub recent_probe_targets: Vec<String>,
|
|
|
|
// ─── Gossip protocol state ───────────────────────────────────────
|
|
/// SWIM gossip protocol internals (None for backward compat with old traces).
|
|
#[serde(default)]
|
|
pub gossip: Option<GossipInfo>,
|
|
}
|
|
|
|
fn node_id_hex(id: &NodeId) -> String {
|
|
id.0.iter().map(|b| format!("{:02x}", b)).collect()
|
|
}
|
|
|
|
fn addr_str(addr: &NodeAddr) -> String {
|
|
addr.to_string()
|
|
}
|
|
|
|
fn state_str(state: MemberState) -> String {
|
|
match state {
|
|
MemberState::Alive => "alive".into(),
|
|
MemberState::Suspect => "suspect".into(),
|
|
MemberState::Dead => "dead".into(),
|
|
}
|
|
}
|
|
|
|
impl DistributedNode {
|
|
/// Capture a serializable snapshot of this node's current state.
|
|
pub fn snapshot(&self) -> DistributionNodeSnapshot {
|
|
let all_members = self.all_members();
|
|
let members: Vec<MemberInfo> = all_members
|
|
.iter()
|
|
.map(|m| MemberInfo {
|
|
node_id: node_id_hex(&m.node_id),
|
|
addr: addr_str(&m.addr),
|
|
state: state_str(m.state),
|
|
incarnation: m.incarnation,
|
|
})
|
|
.collect();
|
|
|
|
let alive_count = all_members.iter().filter(|m| m.state == MemberState::Alive).count();
|
|
let suspect_count = all_members.iter().filter(|m| m.state == MemberState::Suspect).count();
|
|
let dead_count = all_members.iter().filter(|m| m.state == MemberState::Dead).count();
|
|
|
|
let rt = self.routing_table();
|
|
let routing_neighbors: Vec<NeighborInfo> = rt
|
|
.all_nodes()
|
|
.iter()
|
|
.map(|n| NeighborInfo {
|
|
node_id: node_id_hex(&n.node_id),
|
|
addr: addr_str(&n.addr),
|
|
})
|
|
.collect();
|
|
|
|
let cache_entries: Vec<CacheEntryInfo> = self
|
|
.cache()
|
|
.entries()
|
|
.iter()
|
|
.map(|(actor, node)| CacheEntryInfo {
|
|
actor_addr: format!("{:?}", actor),
|
|
node_id: node_id_hex(node),
|
|
})
|
|
.collect();
|
|
|
|
let recent_targets: Vec<String> = self
|
|
.recent_probe_targets()
|
|
.iter()
|
|
.map(|id| node_id_hex(id))
|
|
.collect();
|
|
|
|
// Build gossip info from SWIM internals
|
|
let swim = self.swim_node();
|
|
let probe = swim.probe();
|
|
let current_tick = probe.tick();
|
|
let cfg = probe.config();
|
|
|
|
let suspicion_timers: Vec<SuspicionInfo> = probe
|
|
.suspicion_timers()
|
|
.iter()
|
|
.map(|t| SuspicionInfo {
|
|
node_id: node_id_hex(&t.node_id),
|
|
started_at: t.started_at,
|
|
age_ticks: current_tick.saturating_sub(t.started_at),
|
|
})
|
|
.collect();
|
|
|
|
let dissemination_queue: Vec<DisseminationInfo> = swim
|
|
.dissemination()
|
|
.pending_entries()
|
|
.map(|(update, remaining)| DisseminationInfo {
|
|
node_id: node_id_hex(&update.node_id),
|
|
state: state_str(update.state),
|
|
incarnation: update.incarnation,
|
|
remaining,
|
|
})
|
|
.collect();
|
|
|
|
let gossip = GossipInfo {
|
|
tick: current_tick,
|
|
sequence: probe.sequence(),
|
|
incarnation: swim.members().self_incarnation(),
|
|
probe_phase: probe.phase_name().to_string(),
|
|
probe_target: probe.probe_target().map(|id| node_id_hex(&id)),
|
|
suspicion_timers,
|
|
dissemination_queue,
|
|
config: GossipConfig {
|
|
probe_interval: cfg.probe_interval,
|
|
probe_timeout: cfg.probe_timeout,
|
|
indirect_probes: cfg.indirect_probes,
|
|
suspicion_timeout: cfg.suspicion_timeout,
|
|
},
|
|
};
|
|
|
|
DistributionNodeSnapshot {
|
|
node_id: node_id_hex(&self.node_id()),
|
|
listen_addr: addr_str(&self.listen_addr()),
|
|
members,
|
|
alive_count,
|
|
suspect_count,
|
|
dead_count,
|
|
routing_table_size: rt.len(),
|
|
routing_buckets: rt.bucket_sizes(),
|
|
routing_neighbors,
|
|
cache_size: self.cache().len(),
|
|
cache_entries,
|
|
directory_entry_count: self.directory().entry_count(),
|
|
repair_queue_size: self.repair_queue_len(),
|
|
recent_probe_targets: recent_targets,
|
|
gossip: Some(gossip),
|
|
}
|
|
}
|
|
}
|