use crate::server::config::TracerConfig;
use indexmap::IndexMap;
use prometheus::{GaugeVec, Registry};
use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use std::time::Instant;
use tokio::sync::RwLock;
pub type NodeId = String;
pub type NodeSlug = String;
pub struct NodeState {
pub id: NodeId,
pub name: String,
pub slug: NodeSlug,
pub registry: Arc<Registry>,
pub connected_at: Instant,
pub trace_gauge_cache: Mutex<HashMap<String, GaugeVec>>,
}
impl NodeState {
pub fn new(id: NodeId, name: String) -> Self {
let slug = slugify(&name);
let registry = Arc::new(Registry::new());
NodeState {
id,
name,
slug,
registry,
connected_at: Instant::now(),
trace_gauge_cache: Mutex::new(HashMap::new()),
}
}
}
pub struct TracerState {
pub nodes: RwLock<IndexMap<NodeId, Arc<NodeState>>>,
pub config: Arc<TracerConfig>,
}
impl TracerState {
pub fn new(config: Arc<TracerConfig>) -> Self {
TracerState {
nodes: RwLock::new(IndexMap::new()),
config,
}
}
pub async fn register(&self, id: NodeId, name: String) -> Arc<NodeState> {
let node = Arc::new(NodeState::new(id.clone(), name));
self.nodes.write().await.insert(id, node.clone());
node
}
pub async fn deregister(&self, id: &NodeId) {
self.nodes.write().await.shift_remove(id);
}
pub async fn node_list(&self) -> Vec<(String, NodeSlug)> {
self.nodes
.read()
.await
.values()
.map(|n| (n.name.clone(), n.slug.clone()))
.collect()
}
pub async fn find_by_slug(&self, slug: &str) -> Option<Arc<NodeState>> {
self.nodes
.read()
.await
.values()
.find(|n| n.slug == slug)
.cloned()
}
pub async fn all_nodes(&self) -> Vec<Arc<NodeState>> {
self.nodes.read().await.values().cloned().collect()
}
}
pub fn slugify(s: &str) -> String {
let raw: String = s
.to_lowercase()
.chars()
.map(|c| if c.is_alphanumeric() { c } else { '-' })
.collect();
let mut result = String::with_capacity(raw.len());
let mut last_was_dash = true; for c in raw.chars() {
if c == '-' {
if !last_was_dash {
result.push('-');
last_was_dash = true;
}
} else {
result.push(c);
last_was_dash = false;
}
}
if result.ends_with('-') {
result.pop();
}
if result.is_empty() {
result.push('x');
}
result
}
#[cfg(test)]
mod tests {
use super::*;
use crate::server::config::TracerConfig;
fn make_config() -> Arc<TracerConfig> {
Arc::new(
TracerConfig::from_yaml(
r#"
networkMagic: 42
network:
tag: AcceptAt
contents: "/tmp/hermod.sock"
logging:
- logRoot: "/tmp"
logMode: FileMode
logFormat: ForMachine
"#,
)
.unwrap(),
)
}
#[test]
fn test_slugify_unix_path() {
assert_eq!(slugify("/tmp/forwarder.sock"), "tmp-forwarder-sock");
}
#[test]
fn test_slugify_tcp() {
assert_eq!(slugify("192.168.1.1:3000"), "192-168-1-1-3000");
}
#[test]
fn test_slugify_already_clean() {
assert_eq!(slugify("mynode"), "mynode");
}
#[test]
fn test_slugify_empty_becomes_x() {
assert_eq!(slugify("!!!"), "x");
}
#[test]
fn slugify_collapses_consecutive_separators() {
assert_eq!(slugify("a---b"), "a-b");
assert_eq!(slugify("--leading"), "leading");
assert_eq!(slugify("trailing--"), "trailing");
}
#[test]
fn slugify_uppercased_is_lowercased() {
assert_eq!(slugify("MyNode"), "mynode");
}
#[test]
fn node_state_slug_derived_from_name() {
let node = NodeState::new("conn-id".to_string(), "My Node".to_string());
assert_eq!(node.slug, "my-node");
assert_eq!(node.name, "My Node");
assert_eq!(node.id, "conn-id");
}
#[tokio::test]
async fn register_and_deregister_node() {
let state = TracerState::new(make_config());
state
.register("node1".to_string(), "Node One".to_string())
.await;
assert_eq!(state.node_list().await.len(), 1);
state.deregister(&"node1".to_string()).await;
assert_eq!(state.node_list().await.len(), 0);
}
#[tokio::test]
async fn find_by_slug_returns_correct_node() {
let state = TracerState::new(make_config());
state
.register("node1".to_string(), "My Node".to_string())
.await;
let found = state.find_by_slug("my-node").await;
assert!(found.is_some());
assert_eq!(found.unwrap().name, "My Node");
}
#[tokio::test]
async fn find_by_slug_missing_returns_none() {
let state = TracerState::new(make_config());
assert!(state.find_by_slug("nonexistent").await.is_none());
}
#[tokio::test]
async fn node_list_returns_name_and_slug_pairs() {
let state = TracerState::new(make_config());
state.register("n1".to_string(), "Alpha".to_string()).await;
state.register("n2".to_string(), "Beta".to_string()).await;
let list = state.node_list().await;
assert_eq!(list.len(), 2);
assert!(
list.iter()
.any(|(name, slug)| name == "Alpha" && slug == "alpha")
);
assert!(
list.iter()
.any(|(name, slug)| name == "Beta" && slug == "beta")
);
}
#[tokio::test]
async fn all_nodes_returns_arc_node_states() {
let state = TracerState::new(make_config());
state.register("n1".to_string(), "One".to_string()).await;
let all = state.all_nodes().await;
assert_eq!(all.len(), 1);
assert_eq!(all[0].name, "One");
}
#[tokio::test]
async fn register_overwrites_existing_node_with_same_id() {
let state = TracerState::new(make_config());
state.register("n1".to_string(), "First".to_string()).await;
state.register("n1".to_string(), "Second".to_string()).await;
let list = state.node_list().await;
assert_eq!(list.len(), 1);
assert_eq!(list[0].0, "Second");
}
}