1use crate::server::config::TracerConfig;
16use indexmap::IndexMap;
17use prometheus::{GaugeVec, Registry};
18use std::collections::HashMap;
19use std::sync::{Arc, Mutex};
20use std::time::Instant;
21use tokio::sync::RwLock;
22
23pub type NodeId = String;
25
26pub type NodeSlug = String;
28
29pub struct NodeState {
31 pub id: NodeId,
33 pub name: String,
37 pub slug: NodeSlug,
40 pub registry: Arc<Registry>,
42 pub connected_at: Instant,
44 pub trace_gauge_cache: Mutex<HashMap<String, GaugeVec>>,
46}
47
48impl NodeState {
49 pub fn new(id: NodeId, name: String) -> Self {
54 let slug = slugify(&name);
55 let registry = Arc::new(Registry::new());
56 NodeState {
57 id,
58 name,
59 slug,
60 registry,
61 connected_at: Instant::now(),
62 trace_gauge_cache: Mutex::new(HashMap::new()),
63 }
64 }
65}
66
67pub struct TracerState {
69 pub nodes: RwLock<IndexMap<NodeId, Arc<NodeState>>>,
71 pub config: Arc<TracerConfig>,
73}
74
75impl TracerState {
76 pub fn new(config: Arc<TracerConfig>) -> Self {
78 TracerState {
79 nodes: RwLock::new(IndexMap::new()),
80 config,
81 }
82 }
83
84 pub async fn register(&self, id: NodeId, name: String) -> Arc<NodeState> {
89 let node = Arc::new(NodeState::new(id.clone(), name));
90 self.nodes.write().await.insert(id, node.clone());
91 node
92 }
93
94 pub async fn deregister(&self, id: &NodeId) {
96 self.nodes.write().await.shift_remove(id);
97 }
98
99 pub async fn node_list(&self) -> Vec<(String, NodeSlug)> {
104 self.nodes
105 .read()
106 .await
107 .values()
108 .map(|n| (n.name.clone(), n.slug.clone()))
109 .collect()
110 }
111
112 pub async fn find_by_slug(&self, slug: &str) -> Option<Arc<NodeState>> {
114 self.nodes
115 .read()
116 .await
117 .values()
118 .find(|n| n.slug == slug)
119 .cloned()
120 }
121
122 pub async fn all_nodes(&self) -> Vec<Arc<NodeState>> {
124 self.nodes.read().await.values().cloned().collect()
125 }
126}
127
128pub fn slugify(s: &str) -> String {
131 let raw: String = s
132 .to_lowercase()
133 .chars()
134 .map(|c| if c.is_alphanumeric() { c } else { '-' })
135 .collect();
136
137 let mut result = String::with_capacity(raw.len());
139 let mut last_was_dash = true; for c in raw.chars() {
141 if c == '-' {
142 if !last_was_dash {
143 result.push('-');
144 last_was_dash = true;
145 }
146 } else {
147 result.push(c);
148 last_was_dash = false;
149 }
150 }
151 if result.ends_with('-') {
153 result.pop();
154 }
155 if result.is_empty() {
156 result.push('x');
157 }
158 result
159}
160
161#[cfg(test)]
162mod tests {
163 use super::*;
164 use crate::server::config::TracerConfig;
165
166 fn make_config() -> Arc<TracerConfig> {
167 Arc::new(
168 TracerConfig::from_yaml(
169 r#"
170networkMagic: 42
171network:
172 tag: AcceptAt
173 contents: "/tmp/hermod.sock"
174logging:
175- logRoot: "/tmp"
176 logMode: FileMode
177 logFormat: ForMachine
178"#,
179 )
180 .unwrap(),
181 )
182 }
183
184 #[test]
187 fn test_slugify_unix_path() {
188 assert_eq!(slugify("/tmp/forwarder.sock"), "tmp-forwarder-sock");
189 }
190
191 #[test]
192 fn test_slugify_tcp() {
193 assert_eq!(slugify("192.168.1.1:3000"), "192-168-1-1-3000");
194 }
195
196 #[test]
197 fn test_slugify_already_clean() {
198 assert_eq!(slugify("mynode"), "mynode");
199 }
200
201 #[test]
202 fn test_slugify_empty_becomes_x() {
203 assert_eq!(slugify("!!!"), "x");
204 }
205
206 #[test]
207 fn slugify_collapses_consecutive_separators() {
208 assert_eq!(slugify("a---b"), "a-b");
209 assert_eq!(slugify("--leading"), "leading");
210 assert_eq!(slugify("trailing--"), "trailing");
211 }
212
213 #[test]
214 fn slugify_uppercased_is_lowercased() {
215 assert_eq!(slugify("MyNode"), "mynode");
216 }
217
218 #[test]
221 fn node_state_slug_derived_from_name() {
222 let node = NodeState::new("conn-id".to_string(), "My Node".to_string());
223 assert_eq!(node.slug, "my-node");
224 assert_eq!(node.name, "My Node");
225 assert_eq!(node.id, "conn-id");
226 }
227
228 #[tokio::test]
231 async fn register_and_deregister_node() {
232 let state = TracerState::new(make_config());
233 state
234 .register("node1".to_string(), "Node One".to_string())
235 .await;
236 assert_eq!(state.node_list().await.len(), 1);
237 state.deregister(&"node1".to_string()).await;
238 assert_eq!(state.node_list().await.len(), 0);
239 }
240
241 #[tokio::test]
242 async fn find_by_slug_returns_correct_node() {
243 let state = TracerState::new(make_config());
244 state
245 .register("node1".to_string(), "My Node".to_string())
246 .await;
247 let found = state.find_by_slug("my-node").await;
248 assert!(found.is_some());
249 assert_eq!(found.unwrap().name, "My Node");
250 }
251
252 #[tokio::test]
253 async fn find_by_slug_missing_returns_none() {
254 let state = TracerState::new(make_config());
255 assert!(state.find_by_slug("nonexistent").await.is_none());
256 }
257
258 #[tokio::test]
259 async fn node_list_returns_name_and_slug_pairs() {
260 let state = TracerState::new(make_config());
261 state.register("n1".to_string(), "Alpha".to_string()).await;
262 state.register("n2".to_string(), "Beta".to_string()).await;
263 let list = state.node_list().await;
264 assert_eq!(list.len(), 2);
265 assert!(
266 list.iter()
267 .any(|(name, slug)| name == "Alpha" && slug == "alpha")
268 );
269 assert!(
270 list.iter()
271 .any(|(name, slug)| name == "Beta" && slug == "beta")
272 );
273 }
274
275 #[tokio::test]
276 async fn all_nodes_returns_arc_node_states() {
277 let state = TracerState::new(make_config());
278 state.register("n1".to_string(), "One".to_string()).await;
279 let all = state.all_nodes().await;
280 assert_eq!(all.len(), 1);
281 assert_eq!(all[0].name, "One");
282 }
283
284 #[tokio::test]
285 async fn register_overwrites_existing_node_with_same_id() {
286 let state = TracerState::new(make_config());
287 state.register("n1".to_string(), "First".to_string()).await;
288 state.register("n1".to_string(), "Second".to_string()).await;
289 let list = state.node_list().await;
290 assert_eq!(list.len(), 1);
291 assert_eq!(list[0].0, "Second");
292 }
293}