1use std::collections::HashMap;
7#[cfg(feature = "exochain")]
8use std::sync::Arc;
9
10use serde::{Deserialize, Serialize};
11
12use crate::ipc::GlobalPid;
13
14#[derive(Debug, Clone, Serialize, Deserialize)]
16pub struct ServiceAdvertisement {
17 pub name: String,
19 pub methods: Vec<String>,
21 pub node_id: String,
23 pub global_pid: GlobalPid,
25 pub version: String,
27 pub contract_hash: Option<String>,
29 pub metadata: HashMap<String, String>,
31 pub last_updated: u64,
33}
34
35pub struct ClusterServiceRegistry {
37 services: HashMap<String, Vec<ServiceAdvertisement>>,
39 #[cfg(feature = "exochain")]
41 chain_manager: Option<Arc<crate::chain::ChainManager>>,
42}
43
44impl ClusterServiceRegistry {
45 pub fn new() -> Self {
46 Self {
47 services: HashMap::new(),
48 #[cfg(feature = "exochain")]
49 chain_manager: None,
50 }
51 }
52
53 #[cfg(feature = "exochain")]
55 pub fn set_chain_manager(&mut self, cm: Arc<crate::chain::ChainManager>) {
56 self.chain_manager = Some(cm);
57 }
58
59 pub fn merge(&mut self, advert: ServiceAdvertisement) {
61 #[cfg(feature = "exochain")]
62 if let Some(ref cm) = self.chain_manager {
63 cm.append(
64 "mesh_service",
65 crate::chain::EVENT_KIND_MESH_SERVICE_REGISTER,
66 Some(serde_json::json!({
67 "service_name": &advert.name,
68 "node_id": &advert.node_id,
69 "version": &advert.version,
70 "action": "merge",
71 })),
72 );
73 }
74 let entries = self.services.entry(advert.name.clone()).or_default();
75 if let Some(existing) = entries.iter_mut().find(|e| e.node_id == advert.node_id) {
77 if advert.last_updated > existing.last_updated {
78 *existing = advert;
79 }
80 } else {
81 entries.push(advert);
82 }
83 }
84
85 pub fn resolve(&self, name: &str) -> Vec<&ServiceAdvertisement> {
87 self.services
88 .get(name)
89 .map(|entries| entries.iter().collect())
90 .unwrap_or_default()
91 }
92
93 pub fn resolve_preferred(
95 &self,
96 name: &str,
97 preferred_node: &str,
98 ) -> Option<&ServiceAdvertisement> {
99 let entries = self.services.get(name)?;
100 entries
101 .iter()
102 .find(|e| e.node_id == preferred_node)
103 .or_else(|| entries.first())
104 }
105
106 pub fn service_names(&self) -> Vec<String> {
108 self.services.keys().cloned().collect()
109 }
110
111 pub fn remove_node(&mut self, node_id: &str) {
113 #[cfg(feature = "exochain")]
114 if let Some(ref cm) = self.chain_manager {
115 cm.append(
116 "mesh_service",
117 crate::chain::EVENT_KIND_MESH_SERVICE_DEREGISTER,
118 Some(serde_json::json!({
119 "node_id": node_id,
120 "action": "remove_node",
121 })),
122 );
123 }
124 for entries in self.services.values_mut() {
125 entries.retain(|e| e.node_id != node_id);
126 }
127 self.services.retain(|_, entries| !entries.is_empty());
128 }
129
130 pub fn all_advertisements(&self) -> Vec<&ServiceAdvertisement> {
132 self.services.values().flat_map(|v| v.iter()).collect()
133 }
134
135 pub fn service_count(&self) -> usize {
137 self.services.len()
138 }
139
140 pub fn total_advertisements(&self) -> usize {
142 self.services.values().map(|v| v.len()).sum()
143 }
144
145 pub fn resolve_round_robin(
151 &self,
152 name: &str,
153 counter: &std::sync::atomic::AtomicU64,
154 ) -> Option<&ServiceAdvertisement> {
155 let entries = self.services.get(name)?;
156 if entries.is_empty() {
157 return None;
158 }
159 let idx =
160 counter.fetch_add(1, std::sync::atomic::Ordering::Relaxed) as usize % entries.len();
161 entries.get(idx)
162 }
163}
164
165impl Default for ClusterServiceRegistry {
166 fn default() -> Self {
167 Self::new()
168 }
169}
170
171#[cfg(test)]
172mod tests {
173 use super::*;
174
175 fn make_svc_advert(name: &str, node: &str, pid: u64, ts: u64) -> ServiceAdvertisement {
176 ServiceAdvertisement {
177 name: name.to_string(),
178 methods: vec!["get".to_string(), "set".to_string()],
179 node_id: node.to_string(),
180 global_pid: GlobalPid::local(pid, node),
181 version: "1.0.0".to_string(),
182 contract_hash: None,
183 metadata: HashMap::new(),
184 last_updated: ts,
185 }
186 }
187
188 #[test]
189 fn service_advertisement_serde_roundtrip() {
190 let advert = make_svc_advert("cache", "node-a", 1, 100);
191 let json = serde_json::to_string(&advert).unwrap();
192 let restored: ServiceAdvertisement = serde_json::from_str(&json).unwrap();
193 assert_eq!(restored.name, "cache");
194 assert_eq!(restored.node_id, "node-a");
195 assert_eq!(restored.version, "1.0.0");
196 }
197
198 #[test]
199 fn merge_and_resolve() {
200 let mut registry = ClusterServiceRegistry::new();
201 registry.merge(make_svc_advert("cache", "node-a", 1, 100));
202
203 let results = registry.resolve("cache");
204 assert_eq!(results.len(), 1);
205 assert_eq!(results[0].node_id, "node-a");
206 }
207
208 #[test]
209 fn resolve_preferred_finds_preferred() {
210 let mut registry = ClusterServiceRegistry::new();
211 registry.merge(make_svc_advert("cache", "node-a", 1, 100));
212 registry.merge(make_svc_advert("cache", "node-b", 2, 100));
213
214 let result = registry.resolve_preferred("cache", "node-b").unwrap();
215 assert_eq!(result.node_id, "node-b");
216 }
217
218 #[test]
219 fn resolve_preferred_falls_back() {
220 let mut registry = ClusterServiceRegistry::new();
221 registry.merge(make_svc_advert("cache", "node-a", 1, 100));
222
223 let result = registry.resolve_preferred("cache", "node-z").unwrap();
225 assert_eq!(result.node_id, "node-a");
226 }
227
228 #[test]
229 fn resolve_preferred_missing_service() {
230 let registry = ClusterServiceRegistry::new();
231 assert!(registry.resolve_preferred("missing", "node-a").is_none());
232 }
233
234 #[test]
235 fn remove_node_removes_all_services() {
236 let mut registry = ClusterServiceRegistry::new();
237 registry.merge(make_svc_advert("cache", "node-a", 1, 100));
238 registry.merge(make_svc_advert("auth", "node-a", 2, 100));
239 registry.merge(make_svc_advert("cache", "node-b", 3, 100));
240
241 registry.remove_node("node-a");
242
243 assert_eq!(registry.service_count(), 1); assert!(registry.resolve("auth").is_empty());
245 assert_eq!(registry.resolve("cache").len(), 1);
246 }
247
248 #[test]
249 fn multiple_replicas_same_service() {
250 let mut registry = ClusterServiceRegistry::new();
251 registry.merge(make_svc_advert("cache", "node-a", 1, 100));
252 registry.merge(make_svc_advert("cache", "node-b", 2, 100));
253 registry.merge(make_svc_advert("cache", "node-c", 3, 100));
254
255 assert_eq!(registry.service_count(), 1);
256 assert_eq!(registry.total_advertisements(), 3);
257 assert_eq!(registry.resolve("cache").len(), 3);
258 }
259
260 #[test]
261 fn service_names_lists_all() {
262 let mut registry = ClusterServiceRegistry::new();
263 registry.merge(make_svc_advert("cache", "node-a", 1, 100));
264 registry.merge(make_svc_advert("auth", "node-a", 2, 100));
265 registry.merge(make_svc_advert("search", "node-b", 3, 100));
266
267 let mut names = registry.service_names();
268 names.sort();
269 assert_eq!(names, vec!["auth", "cache", "search"]);
270 }
271
272 #[test]
273 fn merge_updates_existing_node_entry() {
274 let mut registry = ClusterServiceRegistry::new();
275 registry.merge(make_svc_advert("cache", "node-a", 1, 100));
276
277 let mut updated = make_svc_advert("cache", "node-a", 1, 200);
279 updated.version = "2.0.0".to_string();
280 registry.merge(updated);
281
282 assert_eq!(registry.total_advertisements(), 1);
284 let results = registry.resolve("cache");
285 assert_eq!(results[0].version, "2.0.0");
286 }
287
288 #[test]
289 fn resolve_round_robin_distributes() {
290 let mut registry = ClusterServiceRegistry::new();
291 registry.merge(make_svc_advert("cache", "node-a", 1, 100));
292 registry.merge(make_svc_advert("cache", "node-b", 2, 100));
293 registry.merge(make_svc_advert("cache", "node-c", 3, 100));
294
295 let counter = std::sync::atomic::AtomicU64::new(0);
296
297 let r0 = registry.resolve_round_robin("cache", &counter).unwrap();
299 let r1 = registry.resolve_round_robin("cache", &counter).unwrap();
300 let r2 = registry.resolve_round_robin("cache", &counter).unwrap();
301
302 let mut nodes: Vec<&str> = vec![&r0.node_id, &r1.node_id, &r2.node_id];
304 nodes.sort();
305 assert_eq!(nodes, vec!["node-a", "node-b", "node-c"]);
306
307 let r3 = registry.resolve_round_robin("cache", &counter).unwrap();
309 assert_eq!(r3.node_id, r0.node_id);
310 }
311
312 #[test]
313 fn resolve_round_robin_missing_service() {
314 let registry = ClusterServiceRegistry::new();
315 let counter = std::sync::atomic::AtomicU64::new(0);
316 assert!(registry.resolve_round_robin("missing", &counter).is_none());
317 }
318
319 #[test]
320 fn merge_ignores_older_update() {
321 let mut registry = ClusterServiceRegistry::new();
322 registry.merge(make_svc_advert("cache", "node-a", 1, 200));
323
324 let mut older = make_svc_advert("cache", "node-a", 1, 100);
325 older.version = "0.1.0".to_string();
326 registry.merge(older);
327
328 let results = registry.resolve("cache");
329 assert_eq!(results[0].version, "1.0.0"); }
331}