1use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8use std::sync::Arc;
9use std::time::Duration;
10
11use crate::mesh_clock::{Clock, MonoTime, RealClock};
12
13#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct ServiceResolveRequest {
16 pub service_name: String,
18 pub requesting_node: String,
20}
21
22#[derive(Debug, Clone, Serialize, Deserialize)]
24pub struct ServiceResolveResponse {
25 pub found: bool,
27 pub endpoint: Option<RemoteServiceEndpoint>,
29 pub error: Option<String>,
31}
32
33#[derive(Debug, Clone, Serialize, Deserialize)]
35pub struct RemoteServiceEndpoint {
36 pub name: String,
38 pub node_id: String,
40 pub pid: u64,
42 pub methods: Vec<String>,
44 pub version: String,
46 pub contract_hash: Option<String>,
48 pub metadata: HashMap<String, String>,
50}
51
52pub type ServiceEndpoint = RemoteServiceEndpoint;
57
58#[derive(Debug, Clone)]
60pub struct ResolvedService {
61 pub endpoint: RemoteServiceEndpoint,
63 pub resolved_at: MonoTime,
65 pub ttl: Duration,
67}
68
69impl ResolvedService {
70 pub fn is_expired(&self, clock: &dyn Clock) -> bool {
72 clock.now().elapsed_since(self.resolved_at) > self.ttl
73 }
74}
75
76pub struct ServiceResolutionCache {
78 cache: HashMap<String, ResolvedService>,
80 negative_cache: HashMap<String, MonoTime>,
82 negative_ttl: Duration,
84 clock: Arc<dyn Clock>,
86 #[cfg(feature = "exochain")]
88 chain_manager: Option<Arc<crate::chain::ChainManager>>,
89}
90
91impl ServiceResolutionCache {
92 pub fn new() -> Self {
96 Self::with_clock(RealClock::shared())
97 }
98
99 pub fn with_clock(clock: Arc<dyn Clock>) -> Self {
101 Self {
102 cache: HashMap::new(),
103 negative_cache: HashMap::new(),
104 negative_ttl: Duration::from_secs(30),
105 clock,
106 #[cfg(feature = "exochain")]
107 chain_manager: None,
108 }
109 }
110
111 #[cfg(feature = "exochain")]
113 pub fn set_chain_manager(&mut self, cm: Arc<crate::chain::ChainManager>) {
114 self.chain_manager = Some(cm);
115 }
116
117 pub fn get(&self, service_name: &str) -> Option<&ResolvedService> {
119 self.cache
120 .get(service_name)
121 .filter(|r| !r.is_expired(self.clock.as_ref()))
122 }
123
124 pub fn is_known_missing(&self, service_name: &str) -> bool {
126 self.negative_cache.get(service_name).is_some_and(|t| {
127 self.clock.now().elapsed_since(*t) < self.negative_ttl
128 })
129 }
130
131 pub fn insert(&mut self, service_name: String, resolved: ResolvedService) {
133 #[cfg(feature = "exochain")]
134 if let Some(ref cm) = self.chain_manager {
135 cm.append(
136 "mesh_service",
137 crate::chain::EVENT_KIND_MESH_SERVICE_REGISTER,
138 Some(serde_json::json!({
139 "service_name": &service_name,
140 "node_id": &resolved.endpoint.node_id,
141 "version": &resolved.endpoint.version,
142 })),
143 );
144 }
145 self.negative_cache.remove(&service_name);
146 self.cache.insert(service_name, resolved);
147 }
148
149 pub fn insert_negative(&mut self, service_name: String) {
151 #[cfg(feature = "exochain")]
152 if let Some(ref cm) = self.chain_manager {
153 cm.append(
154 "mesh_service",
155 crate::chain::EVENT_KIND_MESH_SERVICE_DEREGISTER,
156 Some(serde_json::json!({
157 "service_name": &service_name,
158 "action": "negative_cache",
159 })),
160 );
161 }
162 self.negative_cache
163 .insert(service_name, self.clock.now());
164 }
165
166 pub fn evict_expired(&mut self) {
168 let clock = self.clock.as_ref();
169 self.cache.retain(|_, r| !r.is_expired(clock));
170 let now = clock.now();
171 let neg_ttl = self.negative_ttl;
172 self.negative_cache
173 .retain(|_, t| now.elapsed_since(*t) < neg_ttl);
174 }
175
176 pub fn len(&self) -> usize {
178 self.cache.len()
179 }
180
181 pub fn is_empty(&self) -> bool {
183 self.cache.is_empty()
184 }
185}
186
187impl Default for ServiceResolutionCache {
188 fn default() -> Self {
189 Self::new()
190 }
191}
192
193pub struct RegistryQueryService {
196 methods: Vec<String>,
198}
199
200impl RegistryQueryService {
201 pub fn new() -> Self {
203 Self {
204 methods: vec!["resolve".into(), "list".into(), "health".into()],
205 }
206 }
207
208 pub fn handle_query(&self, method: &str, params: &serde_json::Value) -> serde_json::Value {
210 match method {
211 "resolve" => {
212 let name = params.get("name").and_then(|v| v.as_str()).unwrap_or("");
213 serde_json::json!({
214 "method": "resolve",
215 "name": name,
216 "status": "query_dispatched"
217 })
218 }
219 "list" => {
220 serde_json::json!({
221 "method": "list",
222 "status": "query_dispatched"
223 })
224 }
225 "health" => {
226 let name = params.get("name").and_then(|v| v.as_str()).unwrap_or("");
227 serde_json::json!({
228 "method": "health",
229 "name": name,
230 "status": "query_dispatched"
231 })
232 }
233 _ => serde_json::json!({"error": format!("unknown method: {method}")}),
234 }
235 }
236
237 pub fn methods(&self) -> &[String] {
239 &self.methods
240 }
241}
242
243impl Default for RegistryQueryService {
244 fn default() -> Self {
245 Self::new()
246 }
247}
248
249#[non_exhaustive]
254#[derive(Debug, Clone)]
255pub enum CircuitState {
256 Closed { error_count: u32, threshold: u32 },
258 Open {
260 opened_at: MonoTime,
261 cooldown: Duration,
262 threshold: u32,
263 },
264 HalfOpen { threshold: u32 },
266}
267
268impl CircuitState {
269 pub fn new(threshold: u32) -> Self {
271 Self::Closed {
272 error_count: 0,
273 threshold,
274 }
275 }
276
277 pub fn record_success(&mut self) {
279 let t = self.threshold();
280 *self = Self::Closed {
281 error_count: 0,
282 threshold: t,
283 };
284 }
285
286 pub fn record_failure(&mut self) -> bool {
291 self.record_failure_at(&RealClock)
292 }
293
294 pub fn record_failure_at(&mut self, clock: &dyn Clock) -> bool {
296 match self {
297 Self::Closed {
298 error_count,
299 threshold,
300 } => {
301 *error_count += 1;
302 if *error_count >= *threshold {
303 let t = *threshold;
304 *self = Self::Open {
305 opened_at: clock.now(),
306 cooldown: Duration::from_secs(30),
307 threshold: t,
308 };
309 return true;
310 }
311 false
312 }
313 Self::HalfOpen { threshold } => {
314 let t = *threshold;
315 *self = Self::Open {
316 opened_at: clock.now(),
317 cooldown: Duration::from_secs(30),
318 threshold: t,
319 };
320 true
321 }
322 Self::Open { .. } => false,
323 }
324 }
325
326 pub fn is_allowed(&mut self) -> bool {
331 self.is_allowed_at(&RealClock)
332 }
333
334 pub fn is_allowed_at(&mut self, clock: &dyn Clock) -> bool {
340 match self {
341 Self::Closed { .. } => true,
342 Self::Open {
343 opened_at,
344 cooldown,
345 threshold,
346 } => {
347 if clock.now().elapsed_since(*opened_at) > *cooldown {
348 let t = *threshold;
349 *self = Self::HalfOpen { threshold: t };
350 true
351 } else {
352 false
353 }
354 }
355 Self::HalfOpen { .. } => true,
356 }
357 }
358
359 fn threshold(&self) -> u32 {
361 match self {
362 Self::Closed { threshold, .. } => *threshold,
363 Self::Open { threshold, .. } => *threshold,
364 Self::HalfOpen { threshold } => *threshold,
365 }
366 }
367}
368
369#[cfg(test)]
370mod tests {
371 use super::*;
372 use crate::mesh_clock::MockClock;
373
374 fn make_endpoint(name: &str) -> RemoteServiceEndpoint {
375 RemoteServiceEndpoint {
376 name: name.to_string(),
377 node_id: "node-1".to_string(),
378 pid: 42,
379 methods: vec!["ping".to_string()],
380 version: "1.0.0".to_string(),
381 contract_hash: None,
382 metadata: HashMap::new(),
383 }
384 }
385
386 fn make_resolved(name: &str, ttl_secs: u64, clock: &dyn Clock) -> ResolvedService {
387 ResolvedService {
388 endpoint: make_endpoint(name),
389 resolved_at: clock.now(),
390 ttl: Duration::from_secs(ttl_secs),
391 }
392 }
393
394 #[test]
395 fn cache_insert_and_get() {
396 let mut cache = ServiceResolutionCache::new();
397 cache.insert("auth".to_string(), make_resolved("auth", 60, &RealClock));
398 assert!(cache.get("auth").is_some());
399 assert_eq!(cache.get("auth").unwrap().endpoint.name, "auth");
400 }
401
402 #[test]
403 fn cache_expired_returns_none() {
404 let clock = MockClock::new();
405 let mut cache = ServiceResolutionCache::with_clock(Arc::new(clock.clone()));
406 cache.insert("old".to_string(), make_resolved("old", 1, &clock));
407 clock.advance(Duration::from_secs(2));
408 assert!(cache.get("old").is_none());
409 }
410
411 #[test]
412 fn cache_missing_key_returns_none() {
413 let cache = ServiceResolutionCache::new();
414 assert!(cache.get("nope").is_none());
415 }
416
417 #[test]
418 fn negative_cache() {
419 let mut cache = ServiceResolutionCache::new();
420 assert!(!cache.is_known_missing("gone"));
421 cache.insert_negative("gone".to_string());
422 assert!(cache.is_known_missing("gone"));
423 }
424
425 #[test]
426 fn positive_insert_clears_negative() {
427 let mut cache = ServiceResolutionCache::new();
428 cache.insert_negative("svc".to_string());
429 assert!(cache.is_known_missing("svc"));
430 cache.insert("svc".to_string(), make_resolved("svc", 60, &RealClock));
431 assert!(!cache.is_known_missing("svc"));
432 assert!(cache.get("svc").is_some());
433 }
434
435 #[test]
436 fn evict_expired_removes_stale() {
437 let clock = MockClock::new();
438 let mut cache = ServiceResolutionCache::with_clock(Arc::new(clock.clone()));
439 cache.insert("stale".to_string(), make_resolved("stale", 1, &clock));
440 clock.advance(Duration::from_secs(2));
441 cache.insert("fresh".to_string(), make_resolved("fresh", 300, &clock));
442 cache.evict_expired();
443 assert_eq!(cache.len(), 1);
444 assert!(cache.get("fresh").is_some());
445 }
446
447 #[test]
448 fn resolve_request_serde_roundtrip() {
449 let req = ServiceResolveRequest {
450 service_name: "auth".to_string(),
451 requesting_node: "node-5".to_string(),
452 };
453 let json = serde_json::to_string(&req).unwrap();
454 let restored: ServiceResolveRequest = serde_json::from_str(&json).unwrap();
455 assert_eq!(restored.service_name, "auth");
456 assert_eq!(restored.requesting_node, "node-5");
457 }
458
459 #[test]
460 fn resolve_response_found_serde_roundtrip() {
461 let resp = ServiceResolveResponse {
462 found: true,
463 endpoint: Some(make_endpoint("auth")),
464 error: None,
465 };
466 let json = serde_json::to_string(&resp).unwrap();
467 let restored: ServiceResolveResponse = serde_json::from_str(&json).unwrap();
468 assert!(restored.found);
469 assert_eq!(restored.endpoint.unwrap().name, "auth");
470 }
471
472 #[test]
473 fn resolve_response_not_found_serde_roundtrip() {
474 let resp = ServiceResolveResponse {
475 found: false,
476 endpoint: None,
477 error: Some("service not registered".to_string()),
478 };
479 let json = serde_json::to_string(&resp).unwrap();
480 let restored: ServiceResolveResponse = serde_json::from_str(&json).unwrap();
481 assert!(!restored.found);
482 assert!(restored.endpoint.is_none());
483 assert_eq!(restored.error.unwrap(), "service not registered");
484 }
485
486 #[test]
487 fn resolved_service_expiry() {
488 let clock = MockClock::new();
489 let fresh = make_resolved("x", 60, &clock);
490 assert!(!fresh.is_expired(&clock));
491
492 let stale = make_resolved("y", 1, &clock);
493 clock.advance(Duration::from_secs(2));
494 assert!(stale.is_expired(&clock));
495 }
496
497 #[test]
498 fn empty_cache() {
499 let cache = ServiceResolutionCache::new();
500 assert!(cache.is_empty());
501 assert_eq!(cache.len(), 0);
502 }
503
504 #[test]
507 fn registry_query_service_methods() {
508 let svc = RegistryQueryService::new();
509 let methods = svc.methods();
510 assert_eq!(methods.len(), 3);
511 assert!(methods.contains(&"resolve".to_string()));
512 assert!(methods.contains(&"list".to_string()));
513 assert!(methods.contains(&"health".to_string()));
514 }
515
516 #[test]
517 fn registry_query_resolve() {
518 let svc = RegistryQueryService::new();
519 let params = serde_json::json!({"name": "auth"});
520 let result = svc.handle_query("resolve", ¶ms);
521 assert_eq!(result["method"], "resolve");
522 assert_eq!(result["name"], "auth");
523 assert_eq!(result["status"], "query_dispatched");
524 }
525
526 #[test]
527 fn registry_query_list() {
528 let svc = RegistryQueryService::new();
529 let result = svc.handle_query("list", &serde_json::json!({}));
530 assert_eq!(result["method"], "list");
531 assert_eq!(result["status"], "query_dispatched");
532 }
533
534 #[test]
535 fn registry_query_health() {
536 let svc = RegistryQueryService::new();
537 let params = serde_json::json!({"name": "db"});
538 let result = svc.handle_query("health", ¶ms);
539 assert_eq!(result["method"], "health");
540 assert_eq!(result["name"], "db");
541 }
542
543 #[test]
544 fn registry_query_unknown_method() {
545 let svc = RegistryQueryService::new();
546 let result = svc.handle_query("unknown", &serde_json::json!({}));
547 assert!(result.get("error").is_some());
548 }
549
550 #[test]
551 fn registry_query_default() {
552 let svc = RegistryQueryService::default();
553 assert_eq!(svc.methods().len(), 3);
554 }
555
556 #[test]
559 fn circuit_starts_closed() {
560 let cb = CircuitState::new(3);
561 assert!(matches!(
562 cb,
563 CircuitState::Closed {
564 error_count: 0,
565 threshold: 3
566 }
567 ));
568 }
569
570 #[test]
571 fn circuit_stays_closed_below_threshold() {
572 let mut cb = CircuitState::new(3);
573 assert!(!cb.record_failure()); assert!(!cb.record_failure()); assert!(cb.is_allowed());
576 }
577
578 #[test]
579 fn circuit_opens_at_threshold() {
580 let mut cb = CircuitState::new(3);
581 assert!(!cb.record_failure()); assert!(!cb.record_failure()); assert!(cb.record_failure()); assert!(matches!(cb, CircuitState::Open { .. }));
585 assert!(!cb.is_allowed()); }
587
588 #[test]
589 fn circuit_success_resets_to_closed() {
590 let mut cb = CircuitState::new(3);
591 cb.record_failure();
592 cb.record_failure();
593 cb.record_success();
594 assert!(matches!(cb, CircuitState::Closed { error_count: 0, .. }));
595 assert!(cb.is_allowed());
596 }
597
598 #[test]
599 fn circuit_open_transitions_to_half_open_after_cooldown() {
600 let clock = MockClock::new();
601 let mut cb = CircuitState::Open {
602 opened_at: clock.now(),
603 cooldown: Duration::from_secs(30),
604 threshold: 3,
605 };
606 assert!(!cb.is_allowed_at(&clock));
608 clock.advance(Duration::from_secs(31));
609 assert!(cb.is_allowed_at(&clock));
611 assert!(matches!(cb, CircuitState::HalfOpen { .. }));
612 }
613
614 #[test]
615 fn circuit_half_open_success_closes() {
616 let mut cb = CircuitState::HalfOpen { threshold: 3 };
617 assert!(cb.is_allowed());
618 cb.record_success();
619 assert!(matches!(cb, CircuitState::Closed { error_count: 0, .. }));
620 }
621
622 #[test]
623 fn circuit_half_open_failure_reopens() {
624 let mut cb = CircuitState::HalfOpen { threshold: 3 };
625 assert!(cb.record_failure()); assert!(matches!(cb, CircuitState::Open { .. }));
627 assert!(!cb.is_allowed()); }
629}