1use dashmap::DashMap;
4use std::collections::HashMap;
5use std::sync::Arc;
6use std::time::{Duration, Instant};
7use uuid::Uuid;
8
9#[derive(Clone, Debug, PartialEq, Eq, Hash)]
11pub struct Endpoint {
12 pub uri: String,
13}
14
15#[derive(Clone, Debug, PartialEq, Eq)]
17pub enum EndpointKind {
18 Tcp(std::net::SocketAddr),
20 Uds(std::path::PathBuf),
22 Other(String),
24}
25
26impl Endpoint {
27 pub fn from_uri<S: Into<String>>(s: S) -> Self {
28 Self { uri: s.into() }
29 }
30
31 pub fn uds(path: impl AsRef<std::path::Path>) -> Self {
32 Self {
33 uri: format!("unix://{}", path.as_ref().display()),
34 }
35 }
36
37 #[must_use]
38 pub fn http(host: &str, port: u16) -> Self {
39 Self {
40 uri: format!("http://{host}:{port}"),
41 }
42 }
43
44 #[must_use]
45 pub fn https(host: &str, port: u16) -> Self {
46 Self {
47 uri: format!("https://{host}:{port}"),
48 }
49 }
50
51 #[must_use]
53 pub fn kind(&self) -> EndpointKind {
54 if let Some(rest) = self.uri.strip_prefix("unix://") {
55 return EndpointKind::Uds(std::path::PathBuf::from(rest));
56 }
57 if let Some(rest) = self.uri.strip_prefix("http://")
58 && let Ok(addr) = rest.parse::<std::net::SocketAddr>()
59 {
60 return EndpointKind::Tcp(addr);
61 }
62 if let Some(rest) = self.uri.strip_prefix("https://")
63 && let Ok(addr) = rest.parse::<std::net::SocketAddr>()
64 {
65 return EndpointKind::Tcp(addr);
66 }
67 EndpointKind::Other(self.uri.clone())
68 }
69}
70
71#[derive(Clone, Copy, Debug, PartialEq, Eq)]
72pub enum InstanceState {
73 Registered,
74 Ready,
75 Healthy,
76 Quarantined,
77 Draining,
78}
79
80#[derive(Clone, Debug)]
82pub struct InstanceRuntimeState {
83 pub last_heartbeat: Instant,
84 pub state: InstanceState,
85}
86
87#[derive(Debug)]
89#[must_use]
90pub struct GearInstance {
91 pub gear: String,
92 pub instance_id: Uuid,
93 pub control: Option<Endpoint>,
94 pub grpc_services: HashMap<String, Endpoint>,
95 pub version: Option<String>,
96 pub rest_endpoint: Option<Endpoint>,
97 pub openapi_spec: Option<String>,
98 inner: Arc<parking_lot::RwLock<InstanceRuntimeState>>,
99}
100
101impl Clone for GearInstance {
102 fn clone(&self) -> Self {
103 Self {
104 gear: self.gear.clone(),
105 instance_id: self.instance_id,
106 control: self.control.clone(),
107 grpc_services: self.grpc_services.clone(),
108 version: self.version.clone(),
109 rest_endpoint: self.rest_endpoint.clone(),
110 openapi_spec: self.openapi_spec.clone(),
111 inner: Arc::clone(&self.inner),
112 }
113 }
114}
115
116impl GearInstance {
117 fn with_metadata_of(&self, other: &GearInstance) -> GearInstance {
122 GearInstance {
123 gear: other.gear.clone(),
124 instance_id: other.instance_id,
125 control: other.control.clone(),
126 grpc_services: other.grpc_services.clone(),
127 version: other.version.clone(),
128 rest_endpoint: other.rest_endpoint.clone(),
129 openapi_spec: other.openapi_spec.clone(),
130 inner: Arc::clone(&self.inner),
131 }
132 }
133}
134
135impl GearInstance {
136 pub fn new(gear: impl Into<String>, instance_id: Uuid) -> Self {
137 Self {
138 gear: gear.into(),
139 instance_id,
140 control: None,
141 grpc_services: HashMap::new(),
142 version: None,
143 rest_endpoint: None,
144 openapi_spec: None,
145 inner: Arc::new(parking_lot::RwLock::new(InstanceRuntimeState {
146 last_heartbeat: Instant::now(),
147 state: InstanceState::Registered,
148 })),
149 }
150 }
151
152 pub fn with_control(mut self, ep: Endpoint) -> Self {
153 self.control = Some(ep);
154 self
155 }
156
157 pub fn with_version(mut self, v: impl Into<String>) -> Self {
158 self.version = Some(v.into());
159 self
160 }
161
162 pub fn with_grpc_service(mut self, name: impl Into<String>, ep: Endpoint) -> Self {
163 self.grpc_services.insert(name.into(), ep);
164 self
165 }
166
167 pub fn with_rest_endpoint(mut self, ep: Endpoint) -> Self {
168 self.rest_endpoint = Some(ep);
169 self
170 }
171
172 pub fn with_openapi_spec(mut self, spec: impl Into<String>) -> Self {
173 self.openapi_spec = Some(spec.into());
174 self
175 }
176
177 #[must_use]
179 pub fn state(&self) -> InstanceState {
180 self.inner.read().state
181 }
182
183 #[must_use]
185 pub fn last_heartbeat(&self) -> Instant {
186 self.inner.read().last_heartbeat
187 }
188}
189
190#[derive(Clone)]
193#[must_use]
194pub struct GearManager {
195 inner: DashMap<String, Vec<Arc<GearInstance>>>,
196 rr_counters: DashMap<String, usize>,
197 hb_ttl: Duration,
198 hb_grace: Duration,
199}
200
201impl std::fmt::Debug for GearManager {
202 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
203 let gears: Vec<String> = self.inner.iter().map(|e| e.key().clone()).collect();
204 f.debug_struct("GearManager")
205 .field("instances_count", &self.inner.len())
206 .field("gears", &gears)
207 .field("heartbeat_ttl", &self.hb_ttl)
208 .field("heartbeat_grace", &self.hb_grace)
209 .finish_non_exhaustive()
210 }
211}
212
213impl GearManager {
214 pub fn new() -> Self {
215 Self {
216 inner: DashMap::new(),
217 rr_counters: DashMap::new(),
218 hb_ttl: Duration::from_secs(15),
219 hb_grace: Duration::from_secs(30),
220 }
221 }
222
223 pub fn with_heartbeat_policy(mut self, ttl: Duration, grace: Duration) -> Self {
224 self.hb_ttl = ttl;
225 self.hb_grace = grace;
226 self
227 }
228
229 pub fn register_instance(&self, instance: Arc<GearInstance>) {
240 let gear = instance.gear.clone();
241 let mut vec = self.inner.entry(gear).or_default();
242 if let Some(pos) = vec
244 .iter()
245 .position(|i| i.instance_id == instance.instance_id)
246 {
247 vec[pos] = Arc::new(vec[pos].with_metadata_of(&instance));
248 } else {
249 vec.push(instance);
250 }
251 }
252
253 pub fn mark_ready(&self, gear: &str, instance_id: Uuid) {
255 if let Some(mut vec) = self.inner.get_mut(gear)
256 && let Some(inst) = vec.iter_mut().find(|i| i.instance_id == instance_id)
257 {
258 let mut state = inst.inner.write();
259 state.state = InstanceState::Ready;
260 }
261 }
262
263 pub fn update_heartbeat(&self, gear: &str, instance_id: Uuid, at: Instant) {
265 if let Some(mut vec) = self.inner.get_mut(gear)
266 && let Some(inst) = vec.iter_mut().find(|i| i.instance_id == instance_id)
267 {
268 let mut state = inst.inner.write();
269 state.last_heartbeat = at;
270 if state.state == InstanceState::Registered {
272 state.state = InstanceState::Healthy;
273 }
274 }
275 }
276
277 pub fn mark_quarantined(&self, gear: &str, instance_id: Uuid) {
279 if let Some(mut vec) = self.inner.get_mut(gear)
280 && let Some(inst) = vec.iter_mut().find(|i| i.instance_id == instance_id)
281 {
282 inst.inner.write().state = InstanceState::Quarantined;
283 }
284 }
285
286 pub fn mark_draining(&self, gear: &str, instance_id: Uuid) {
288 if let Some(mut vec) = self.inner.get_mut(gear)
289 && let Some(inst) = vec.iter_mut().find(|i| i.instance_id == instance_id)
290 {
291 inst.inner.write().state = InstanceState::Draining;
292 }
293 }
294
295 pub fn deregister(&self, gear: &str, instance_id: Uuid) {
297 let mut remove_gear = false;
298 {
299 if let Some(mut vec) = self.inner.get_mut(gear) {
300 let list = vec.value_mut();
301 list.retain(|inst| inst.instance_id != instance_id);
302 if list.is_empty() {
303 remove_gear = true;
304 }
305 }
306 }
307
308 if remove_gear {
309 self.inner.remove(gear);
310 self.rr_counters.remove(gear);
311 self.rr_counters.remove(&format!("rest:{gear}"));
312 }
313 }
314
315 #[must_use]
317 pub fn instances_of(&self, gear: &str) -> Vec<Arc<GearInstance>> {
318 self.inner.get(gear).map(|v| v.clone()).unwrap_or_default()
319 }
320
321 #[must_use]
323 pub fn all_instances(&self) -> Vec<Arc<GearInstance>> {
324 self.inner
325 .iter()
326 .flat_map(|entry| entry.value().clone())
327 .collect()
328 }
329
330 pub fn evict_stale(&self, now: Instant) {
332 use InstanceState::{Draining, Quarantined};
333 let mut empty_gears = Vec::new();
334
335 for mut entry in self.inner.iter_mut() {
336 let gear = entry.key().clone();
337 let vec = entry.value_mut();
338 vec.retain(|inst| {
339 let state = inst.inner.read();
340 let age = now.saturating_duration_since(state.last_heartbeat);
341
342 if age >= self.hb_ttl && !matches!(state.state, Quarantined | Draining) {
344 drop(state); inst.inner.write().state = Quarantined;
346 return true; }
348
349 if state.state == Quarantined && age >= self.hb_ttl + self.hb_grace {
351 return false; }
353
354 true
355 });
356
357 if vec.is_empty() {
358 empty_gears.push(gear);
359 }
360 }
361
362 for gear in empty_gears {
363 self.inner.remove(&gear);
364 self.rr_counters.remove(&gear);
365 self.rr_counters.remove(&format!("rest:{gear}"));
366 }
367 }
368
369 #[must_use]
371 pub fn pick_instance_round_robin(&self, gear: &str) -> Option<Arc<GearInstance>> {
372 let instances_entry = self.inner.get(gear)?;
373 let instances = instances_entry.value();
374
375 if instances.is_empty() {
376 return None;
377 }
378
379 let healthy: Vec<_> = instances
381 .iter()
382 .filter(|inst| matches!(inst.state(), InstanceState::Healthy | InstanceState::Ready))
383 .cloned()
384 .collect();
385
386 let candidates: Vec<_> = if healthy.is_empty() {
387 instances.clone()
388 } else {
389 healthy
390 };
391
392 if candidates.is_empty() {
393 return None;
394 }
395
396 let len = candidates.len();
397 let mut counter = self.rr_counters.entry(gear.to_owned()).or_insert(0);
398 let idx = *counter % len;
399 *counter = (*counter + 1) % len;
400
401 candidates.get(idx).cloned()
402 }
403
404 #[must_use]
407 pub fn pick_service_round_robin(
408 &self,
409 service_name: &str,
410 ) -> Option<(String, Arc<GearInstance>, Endpoint)> {
411 let mut candidates = Vec::new();
413 for entry in &self.inner {
414 let gear = entry.key().clone();
415 for inst in entry.value() {
416 if let Some(ep) = inst.grpc_services.get(service_name) {
417 let state = inst.state();
418 if matches!(state, InstanceState::Healthy | InstanceState::Ready) {
419 candidates.push((gear.clone(), inst.clone(), ep.clone()));
420 }
421 }
422 }
423 }
424
425 if candidates.is_empty() {
426 return None;
427 }
428
429 let len = candidates.len();
431 let service_key = service_name.to_owned();
432 let mut counter = self.rr_counters.entry(service_key).or_insert(0);
433 let idx = *counter % len;
434 *counter = (*counter + 1) % len;
435
436 candidates.get(idx).cloned()
437 }
438
439 #[must_use]
442 pub fn pick_rest_endpoint_round_robin(&self, gear: &str) -> Option<Endpoint> {
443 let instances_entry = self.inner.get(gear)?;
444 let instances = instances_entry.value();
445
446 let with_rest: Vec<_> = instances
448 .iter()
449 .filter(|inst| inst.rest_endpoint.is_some())
450 .cloned()
451 .collect();
452
453 if with_rest.is_empty() {
454 return None;
455 }
456
457 let healthy: Vec<_> = with_rest
459 .iter()
460 .filter(|inst| matches!(inst.state(), InstanceState::Healthy | InstanceState::Ready))
461 .cloned()
462 .collect();
463
464 let candidates = if healthy.is_empty() {
465 with_rest
466 } else {
467 healthy
468 };
469
470 let len = candidates.len();
471 let rr_key = format!("rest:{gear}");
472 let mut counter = self.rr_counters.entry(rr_key).or_insert(0);
473 let idx = *counter % len;
474 *counter = (*counter + 1) % len;
475
476 candidates
477 .get(idx)
478 .and_then(|inst| inst.rest_endpoint.clone())
479 }
480
481 #[must_use]
484 pub fn openapi_spec_of(&self, gear: &str) -> Option<String> {
485 let instances_entry = self.inner.get(gear)?;
486 instances_entry
487 .value()
488 .iter()
489 .find_map(|inst| inst.openapi_spec.clone())
490 }
491}
492
493impl Default for GearManager {
494 fn default() -> Self {
495 Self::new()
496 }
497}
498
499#[cfg(test)]
500#[cfg_attr(coverage_nightly, coverage(off))]
501mod tests {
502 use super::*;
503 use std::thread::sleep;
504 use std::time::Duration;
505
506 #[test]
507 fn test_register_and_retrieve_instances() {
508 let dir = GearManager::new();
509 let instance_id = Uuid::new_v4();
510 let instance = Arc::new(
511 GearInstance::new("test_gear", instance_id)
512 .with_control(Endpoint::http("localhost", 8080))
513 .with_version("1.0.0"),
514 );
515
516 dir.register_instance(instance);
517
518 let instances = dir.instances_of("test_gear");
519 assert_eq!(instances.len(), 1);
520 assert_eq!(instances[0].instance_id, instance_id);
521 assert_eq!(instances[0].gear, "test_gear");
522 assert_eq!(instances[0].version, Some("1.0.0".to_owned()));
523 }
524
525 #[test]
526 fn test_register_multiple_instances() {
527 let dir = GearManager::new();
528
529 let id1 = Uuid::new_v4();
530 let id2 = Uuid::new_v4();
531 let instance1 = Arc::new(GearInstance::new("test_gear", id1));
532 let instance2 = Arc::new(GearInstance::new("test_gear", id2));
533
534 dir.register_instance(instance1);
535 dir.register_instance(instance2);
536
537 let registered = dir.instances_of("test_gear");
538 assert_eq!(registered.len(), 2);
539
540 let ids: Vec<_> = registered.iter().map(|i| i.instance_id).collect();
541 assert!(ids.contains(&id1));
542 assert!(ids.contains(&id2));
543 }
544
545 #[test]
546 fn test_update_existing_instance() {
547 let dir = GearManager::new();
548 let instance_id = Uuid::new_v4();
549
550 let initial_instance =
551 Arc::new(GearInstance::new("test_gear", instance_id).with_version("1.0.0"));
552 dir.register_instance(initial_instance);
553
554 let updated_instance =
555 Arc::new(GearInstance::new("test_gear", instance_id).with_version("2.0.0"));
556 dir.register_instance(updated_instance);
557
558 let registered = dir.instances_of("test_gear");
559 assert_eq!(registered.len(), 1, "Should not duplicate instance");
560 assert_eq!(registered[0].version, Some("2.0.0".to_owned()));
561 }
562
563 #[test]
564 fn test_reregistration_preserves_liveness_state() {
565 let dir = GearManager::new();
566 let instance_id = Uuid::new_v4();
567
568 dir.register_instance(Arc::new(
570 GearInstance::new("test_gear", instance_id).with_version("1.0.0"),
571 ));
572 dir.update_heartbeat("test_gear", instance_id, Instant::now());
573 assert!(matches!(
574 dir.instances_of("test_gear")[0].state(),
575 InstanceState::Healthy
576 ));
577
578 dir.register_instance(Arc::new(
582 GearInstance::new("test_gear", instance_id).with_version("2.0.0"),
583 ));
584
585 let instances = dir.instances_of("test_gear");
586 assert_eq!(instances.len(), 1);
587 assert!(
588 matches!(instances[0].state(), InstanceState::Healthy),
589 "re-registration must preserve the Healthy state"
590 );
591 assert_eq!(
592 instances[0].version,
593 Some("2.0.0".to_owned()),
594 "re-registration must still refresh metadata/endpoints"
595 );
596 }
597
598 #[test]
599 fn test_concurrent_reregister_and_heartbeat_preserves_state() {
600 let dir = GearManager::new();
601 let instance_id = Uuid::new_v4();
602
603 let initial = Arc::new(GearInstance::new("test_gear", instance_id).with_version("1.0.0"));
604 dir.register_instance(initial);
605 dir.update_heartbeat("test_gear", instance_id, Instant::now());
606 assert!(matches!(
607 dir.instances_of("test_gear")[0].state(),
608 InstanceState::Healthy
609 ));
610
611 let start = Instant::now();
612
613 std::thread::scope(|s| {
614 s.spawn(|| {
615 for _ in 0..1000 {
616 dir.update_heartbeat("test_gear", instance_id, Instant::now());
617 }
618 });
619 s.spawn(|| {
620 for i in 0..1000 {
621 let version = if i % 2 == 0 { "2.0.0" } else { "3.0.0" };
622 let reinst = Arc::new(
623 GearInstance::new("test_gear", instance_id)
624 .with_version(version)
625 .with_rest_endpoint(Endpoint::http(
626 "127.0.0.1",
627 8000u16 + u16::try_from(i % 10).expect("i % 10 fits in u16"),
628 )),
629 );
630 dir.register_instance(reinst);
631 }
632 });
633 });
634
635 let instances = dir.instances_of("test_gear");
636 assert_eq!(instances.len(), 1);
637 assert!(
638 matches!(instances[0].state(), InstanceState::Healthy),
639 "concurrent re-registration must not reset Healthy state"
640 );
641 assert!(
642 instances[0].last_heartbeat() >= start,
643 "concurrent re-registration must not lose heartbeat updates"
644 );
645 }
646
647 #[test]
648 fn test_mark_ready() {
649 let dir = GearManager::new();
650 let instance_id = Uuid::new_v4();
651 let instance = Arc::new(GearInstance::new("test_gear", instance_id));
652
653 dir.register_instance(instance);
654
655 dir.mark_ready("test_gear", instance_id);
656
657 let instances = dir.instances_of("test_gear");
658 assert_eq!(instances.len(), 1);
659 assert!(matches!(instances[0].state(), InstanceState::Ready));
660 }
661
662 #[test]
663 fn test_update_heartbeat() {
664 let dir = GearManager::new();
665 let instance_id = Uuid::new_v4();
666 let instance = Arc::new(GearInstance::new("test_gear", instance_id));
667 let initial_heartbeat = instance.last_heartbeat();
668
669 dir.register_instance(instance);
670
671 sleep(Duration::from_millis(10));
673
674 let new_heartbeat = Instant::now();
675 dir.update_heartbeat("test_gear", instance_id, new_heartbeat);
676
677 let instances = dir.instances_of("test_gear");
678 assert!(instances[0].last_heartbeat() > initial_heartbeat);
679 assert!(matches!(instances[0].state(), InstanceState::Healthy));
680 }
681
682 #[test]
683 fn test_all_instances() {
684 let dir = GearManager::new();
685
686 let instance1 = Arc::new(GearInstance::new("gear_a", Uuid::new_v4()));
687 let instance2 = Arc::new(GearInstance::new("gear_b", Uuid::new_v4()));
688 let instance3 = Arc::new(GearInstance::new("gear_a", Uuid::new_v4()));
689
690 dir.register_instance(instance1);
691 dir.register_instance(instance2);
692 dir.register_instance(instance3);
693
694 let all = dir.all_instances();
695 assert_eq!(all.len(), 3);
696
697 let gears: Vec<_> = all.iter().map(|i| i.gear.as_str()).collect();
698 assert_eq!(gears.iter().filter(|&m| *m == "gear_a").count(), 2);
699 assert_eq!(gears.iter().filter(|&m| *m == "gear_b").count(), 1);
700 }
701
702 #[test]
703 fn test_pick_instance_round_robin() {
704 let dir = GearManager::new();
705
706 let id1 = Uuid::new_v4();
707 let id2 = Uuid::new_v4();
708 let instance1 = Arc::new(GearInstance::new("test_gear", id1));
709 let instance2 = Arc::new(GearInstance::new("test_gear", id2));
710
711 dir.register_instance(instance1);
712 dir.register_instance(instance2);
713
714 let picked1 = dir.pick_instance_round_robin("test_gear").unwrap();
716 let picked2 = dir.pick_instance_round_robin("test_gear").unwrap();
717 let picked3 = dir.pick_instance_round_robin("test_gear").unwrap();
718
719 let ids = [
720 picked1.instance_id,
721 picked2.instance_id,
722 picked3.instance_id,
723 ];
724
725 assert!(ids.contains(&id1));
728 assert!(ids.contains(&id2));
729 assert_eq!(picked1.instance_id, picked3.instance_id);
731 assert_ne!(picked1.instance_id, picked2.instance_id);
733 }
734
735 #[test]
736 fn test_pick_instance_none_available() {
737 let dir = GearManager::new();
738 let picked = dir.pick_instance_round_robin("nonexistent_gear");
739 assert!(picked.is_none());
740 }
741
742 #[test]
743 fn test_endpoint_creation() {
744 let plain_ep = Endpoint::http("localhost", 8080);
745 assert_eq!(plain_ep.uri, "http://localhost:8080");
746
747 let secure_ep = Endpoint::https("localhost", 8443);
748 assert_eq!(secure_ep.uri, "https://localhost:8443");
749
750 let uds_ep = Endpoint::uds("/tmp/socket.sock");
751 assert!(uds_ep.uri.starts_with("unix://"));
752 assert!(uds_ep.uri.contains("socket.sock"));
753
754 let custom_ep = Endpoint::from_uri("http://example.com");
755 assert_eq!(custom_ep.uri, "http://example.com");
756 }
757
758 #[test]
759 fn test_endpoint_kind() {
760 let plain_ep = Endpoint::http("127.0.0.1", 8080);
761 match plain_ep.kind() {
762 EndpointKind::Tcp(addr) => {
763 assert_eq!(addr.ip().to_string(), "127.0.0.1");
764 assert_eq!(addr.port(), 8080);
765 }
766 _ => panic!("Expected TCP endpoint for http"),
767 }
768
769 let secure_ep = Endpoint::https("127.0.0.1", 8443);
770 match secure_ep.kind() {
771 EndpointKind::Tcp(addr) => {
772 assert_eq!(addr.ip().to_string(), "127.0.0.1");
773 assert_eq!(addr.port(), 8443);
774 }
775 _ => panic!("Expected TCP endpoint for https"),
776 }
777
778 let uds_ep = Endpoint::uds("/tmp/test.sock");
779 match uds_ep.kind() {
780 EndpointKind::Uds(path) => {
781 assert!(path.to_string_lossy().contains("test.sock"));
782 }
783 _ => panic!("Expected UDS endpoint"),
784 }
785
786 let other_ep = Endpoint::from_uri("grpc://example.com");
787 match other_ep.kind() {
788 EndpointKind::Other(uri) => {
789 assert_eq!(uri, "grpc://example.com");
790 }
791 _ => panic!("Expected Other endpoint"),
792 }
793 }
794
795 #[test]
796 fn test_gear_instance_builder() {
797 let instance_id = Uuid::new_v4();
798 let instance = GearInstance::new("test_gear", instance_id)
799 .with_control(Endpoint::http("localhost", 8080))
800 .with_version("1.2.3")
801 .with_grpc_service("service1", Endpoint::http("localhost", 8082))
802 .with_grpc_service("service2", Endpoint::http("localhost", 8083));
803
804 assert_eq!(instance.gear, "test_gear");
805 assert_eq!(instance.instance_id, instance_id);
806 assert!(instance.control.is_some());
807 assert_eq!(instance.version, Some("1.2.3".to_owned()));
808 assert_eq!(instance.grpc_services.len(), 2);
809 assert!(instance.grpc_services.contains_key("service1"));
810 assert!(instance.grpc_services.contains_key("service2"));
811 assert!(matches!(instance.state(), InstanceState::Registered));
812 }
813
814 #[test]
815 fn test_quarantine_and_evict() {
816 let ttl = Duration::from_millis(50);
817 let grace = Duration::from_millis(50);
818 let dir = GearManager::new().with_heartbeat_policy(ttl, grace);
819
820 let now = Instant::now();
821 let instance = GearInstance::new("test_gear", Uuid::new_v4());
822 instance.inner.write().last_heartbeat = now
824 .checked_sub(ttl)
825 .and_then(|t| t.checked_sub(Duration::from_millis(10)))
826 .expect("test duration subtraction should not underflow");
827
828 dir.register_instance(Arc::new(instance));
829
830 dir.evict_stale(now);
831 let instances = dir.instances_of("test_gear");
832 assert_eq!(instances.len(), 1);
833 assert!(matches!(instances[0].state(), InstanceState::Quarantined));
834
835 let later = now + grace + Duration::from_millis(10);
836 dir.evict_stale(later);
837
838 let instances_after = dir.instances_of("test_gear");
839 assert!(instances_after.is_empty());
840 }
841
842 #[test]
843 fn test_instances_of_empty() {
844 let dir = GearManager::new();
845 let instances = dir.instances_of("nonexistent");
846 assert!(instances.is_empty());
847 }
848
849 #[test]
850 fn test_rr_prefers_healthy() {
851 let dir = GearManager::new();
852
853 let healthy_id = Uuid::new_v4();
855 let healthy = Arc::new(GearInstance::new("test_gear", healthy_id));
856 dir.register_instance(healthy);
857 dir.update_heartbeat("test_gear", healthy_id, Instant::now());
858
859 let quarantined_id = Uuid::new_v4();
860 let quarantined = Arc::new(GearInstance::new("test_gear", quarantined_id));
861 dir.register_instance(quarantined);
862 dir.mark_quarantined("test_gear", quarantined_id);
863
864 for _ in 0..5 {
866 let picked = dir.pick_instance_round_robin("test_gear").unwrap();
867 assert_eq!(picked.instance_id, healthy_id);
868 }
869 }
870
871 #[test]
872 fn test_pick_rest_endpoint_and_openapi() {
873 let dir = GearManager::new();
874 let id = Uuid::new_v4();
875 let instance = Arc::new(
876 GearInstance::new("billing", id)
877 .with_rest_endpoint(Endpoint::http("billing", 8080))
878 .with_openapi_spec("{\"openapi\":\"3.1.0\"}"),
879 );
880 dir.register_instance(instance);
881
882 let rest = dir.pick_rest_endpoint_round_robin("billing").unwrap();
883 assert_eq!(rest.uri, "http://billing:8080");
884
885 let spec = dir.openapi_spec_of("billing").unwrap();
886 assert!(spec.contains("openapi"));
887 }
888
889 #[test]
890 fn test_pick_rest_endpoint_none_when_absent() {
891 let dir = GearManager::new();
892 let id = Uuid::new_v4();
893 let instance = Arc::new(
895 GearInstance::new("grpc_only", id)
896 .with_grpc_service("some.Service", Endpoint::http("127.0.0.1", 9000)),
897 );
898 dir.register_instance(instance);
899
900 assert!(dir.pick_rest_endpoint_round_robin("grpc_only").is_none());
901 assert!(dir.openapi_spec_of("grpc_only").is_none());
902 assert!(dir.pick_rest_endpoint_round_robin("missing").is_none());
903 }
904
905 #[test]
906 fn test_pick_rest_endpoint_round_robin_rotates() {
907 let dir = GearManager::new();
908 let id1 = Uuid::new_v4();
909 let id2 = Uuid::new_v4();
910 let inst1 = Arc::new(
911 GearInstance::new("web", id1).with_rest_endpoint(Endpoint::http("127.0.0.1", 8001)),
912 );
913 let inst2 = Arc::new(
914 GearInstance::new("web", id2).with_rest_endpoint(Endpoint::http("127.0.0.1", 8002)),
915 );
916 dir.register_instance(inst1);
917 dir.register_instance(inst2);
918 dir.update_heartbeat("web", id1, Instant::now());
919 dir.update_heartbeat("web", id2, Instant::now());
920
921 let ep1 = dir.pick_rest_endpoint_round_robin("web").unwrap();
922 let ep2 = dir.pick_rest_endpoint_round_robin("web").unwrap();
923 let ep3 = dir.pick_rest_endpoint_round_robin("web").unwrap();
924
925 assert_ne!(ep1, ep2);
926 assert_eq!(ep1, ep3);
927 }
928
929 #[test]
930 fn test_pick_service_round_robin() {
931 let dir = GearManager::new();
932
933 let id1 = Uuid::new_v4();
934 let id2 = Uuid::new_v4();
935 let inst1 = Arc::new(
937 GearInstance::new("test_gear", id1)
938 .with_grpc_service("test.Service", Endpoint::http("127.0.0.1", 8001)),
939 );
940 let inst2 = Arc::new(
941 GearInstance::new("test_gear", id2)
942 .with_grpc_service("test.Service", Endpoint::http("127.0.0.1", 8002)),
943 );
944
945 dir.register_instance(inst1);
946 dir.register_instance(inst2);
947
948 dir.update_heartbeat("test_gear", id1, Instant::now());
950 dir.update_heartbeat("test_gear", id2, Instant::now());
951
952 let pick1 = dir.pick_service_round_robin("test.Service");
954 let pick2 = dir.pick_service_round_robin("test.Service");
955 let pick3 = dir.pick_service_round_robin("test.Service");
956
957 assert!(pick1.is_some());
958 assert!(pick2.is_some());
959 assert!(pick3.is_some());
960
961 let (_, inst1, ep1) = pick1.unwrap();
962 let (_, inst2, ep2) = pick2.unwrap();
963 let (_, inst3, _) = pick3.unwrap();
964
965 assert_eq!(inst1.instance_id, inst3.instance_id);
967 assert_ne!(inst1.instance_id, inst2.instance_id);
969 assert_ne!(ep1, ep2);
971 }
972
973 #[test]
974 fn test_deregister_clears_rr_counters() {
975 let dir = GearManager::new();
976 let id = Uuid::new_v4();
977 let instance = Arc::new(
978 GearInstance::new("web", id).with_rest_endpoint(Endpoint::http("127.0.0.1", 8001)),
979 );
980 dir.register_instance(instance);
981 dir.update_heartbeat("web", id, Instant::now());
982
983 assert!(dir.pick_instance_round_robin("web").is_some());
985 assert!(dir.pick_rest_endpoint_round_robin("web").is_some());
986
987 assert!(dir.rr_counters.contains_key("web"));
988 assert!(dir.rr_counters.contains_key("rest:web"));
989
990 dir.deregister("web", id);
991
992 assert!(!dir.rr_counters.contains_key("web"));
993 assert!(!dir.rr_counters.contains_key("rest:web"));
994 }
995
996 #[test]
997 fn test_evict_stale_clears_rr_counters() {
998 let ttl = Duration::from_millis(50);
999 let grace = Duration::from_millis(50);
1000 let dir = GearManager::new().with_heartbeat_policy(ttl, grace);
1001
1002 let now = Instant::now();
1003 let id = Uuid::new_v4();
1004 let instance = Arc::new(
1005 GearInstance::new("web", id).with_rest_endpoint(Endpoint::http("127.0.0.1", 8001)),
1006 );
1007 instance.inner.write().last_heartbeat = now
1009 .checked_sub(ttl)
1010 .and_then(|t| t.checked_sub(Duration::from_millis(10)))
1011 .expect("test duration subtraction should not underflow");
1012
1013 dir.register_instance(instance);
1014 assert!(dir.pick_rest_endpoint_round_robin("web").is_some());
1015
1016 assert!(dir.rr_counters.contains_key("rest:web"));
1017
1018 dir.evict_stale(now);
1020 let instances = dir.instances_of("web");
1021 assert_eq!(instances.len(), 1);
1022 assert!(matches!(instances[0].state(), InstanceState::Quarantined));
1023
1024 let later = now + grace + Duration::from_millis(10);
1026 dir.evict_stale(later);
1027
1028 assert!(dir.instances_of("web").is_empty());
1029 assert!(!dir.rr_counters.contains_key("web"));
1030 assert!(!dir.rr_counters.contains_key("rest:web"));
1031 }
1032}