use dashmap::DashMap;
use std::collections::{BTreeMap, HashMap};
use std::sync::Arc;
use std::time::{Duration, Instant};
use uuid::Uuid;
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct Endpoint {
pub uri: String,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum EndpointKind {
Tcp(std::net::SocketAddr),
Uds(std::path::PathBuf),
Other(String),
}
impl Endpoint {
pub fn from_uri<S: Into<String>>(s: S) -> Self {
Self { uri: s.into() }
}
pub fn uds(path: impl AsRef<std::path::Path>) -> Self {
Self {
uri: format!("unix://{}", path.as_ref().display()),
}
}
#[must_use]
pub fn http(host: &str, port: u16) -> Self {
Self {
uri: format!("http://{host}:{port}"),
}
}
#[must_use]
pub fn https(host: &str, port: u16) -> Self {
Self {
uri: format!("https://{host}:{port}"),
}
}
#[must_use]
pub fn kind(&self) -> EndpointKind {
if let Some(rest) = self.uri.strip_prefix("unix://") {
return EndpointKind::Uds(std::path::PathBuf::from(rest));
}
if let Some(rest) = self.uri.strip_prefix("http://")
&& let Ok(addr) = rest.parse::<std::net::SocketAddr>()
{
return EndpointKind::Tcp(addr);
}
if let Some(rest) = self.uri.strip_prefix("https://")
&& let Ok(addr) = rest.parse::<std::net::SocketAddr>()
{
return EndpointKind::Tcp(addr);
}
EndpointKind::Other(self.uri.clone())
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum InstanceState {
Registered,
Ready,
Healthy,
Quarantined,
Draining,
}
#[derive(Clone, Debug)]
pub struct InstanceRuntimeState {
pub last_heartbeat: Instant,
pub state: InstanceState,
}
#[derive(Debug)]
#[must_use]
pub struct GearInstance {
pub gear: String,
pub instance_id: Uuid,
pub control: Option<Endpoint>,
pub grpc_services: HashMap<String, Endpoint>,
pub version: Option<String>,
pub rest_endpoint: Option<Endpoint>,
pub openapi_spec: Option<String>,
pub labels: BTreeMap<String, String>,
inner: Arc<parking_lot::RwLock<InstanceRuntimeState>>,
}
impl Clone for GearInstance {
fn clone(&self) -> Self {
Self {
gear: self.gear.clone(),
instance_id: self.instance_id,
control: self.control.clone(),
grpc_services: self.grpc_services.clone(),
version: self.version.clone(),
rest_endpoint: self.rest_endpoint.clone(),
openapi_spec: self.openapi_spec.clone(),
labels: self.labels.clone(),
inner: Arc::clone(&self.inner),
}
}
}
impl GearInstance {
fn with_metadata_of(&self, other: &GearInstance) -> GearInstance {
GearInstance {
gear: other.gear.clone(),
instance_id: other.instance_id,
control: other.control.clone(),
grpc_services: other.grpc_services.clone(),
version: other.version.clone(),
rest_endpoint: other.rest_endpoint.clone(),
openapi_spec: other.openapi_spec.clone(),
labels: if other.labels.is_empty() {
self.labels.clone()
} else {
other.labels.clone()
},
inner: Arc::clone(&self.inner),
}
}
}
impl GearInstance {
pub fn new(gear: impl Into<String>, instance_id: Uuid) -> Self {
Self {
gear: gear.into(),
instance_id,
control: None,
grpc_services: HashMap::new(),
version: None,
rest_endpoint: None,
openapi_spec: None,
labels: BTreeMap::new(),
inner: Arc::new(parking_lot::RwLock::new(InstanceRuntimeState {
last_heartbeat: Instant::now(),
state: InstanceState::Registered,
})),
}
}
pub fn with_control(mut self, ep: Endpoint) -> Self {
self.control = Some(ep);
self
}
pub fn with_version(mut self, v: impl Into<String>) -> Self {
self.version = Some(v.into());
self
}
pub fn with_grpc_service(mut self, name: impl Into<String>, ep: Endpoint) -> Self {
self.grpc_services.insert(name.into(), ep);
self
}
pub fn with_rest_endpoint(mut self, ep: Endpoint) -> Self {
self.rest_endpoint = Some(ep);
self
}
pub fn with_openapi_spec(mut self, spec: impl Into<String>) -> Self {
self.openapi_spec = Some(spec.into());
self
}
pub fn with_labels(mut self, labels: BTreeMap<String, String>) -> Self {
self.labels = labels;
self
}
#[must_use]
pub fn state(&self) -> InstanceState {
self.inner.read().state
}
#[must_use]
pub fn last_heartbeat(&self) -> Instant {
self.inner.read().last_heartbeat
}
}
#[must_use]
pub struct GearManager {
inner: DashMap<String, Vec<Arc<GearInstance>>>,
rr_counters: DashMap<String, usize>,
hb_ttl: Duration,
hb_grace: Duration,
reg_lock: parking_lot::Mutex<()>,
service_owners: parking_lot::RwLock<HashMap<String, String>>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct GrpcServiceNameConflict {
pub service_name: String,
pub owner: String,
pub recoverable: bool,
}
impl std::fmt::Display for GrpcServiceNameConflict {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"gRPC service name '{}' is already owned by gear '{}'",
self.service_name, self.owner
)
}
}
impl std::error::Error for GrpcServiceNameConflict {}
impl std::fmt::Debug for GearManager {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let gears: Vec<String> = self.inner.iter().map(|e| e.key().clone()).collect();
f.debug_struct("GearManager")
.field("instances_count", &self.inner.len())
.field("gears", &gears)
.field("heartbeat_ttl", &self.hb_ttl)
.field("heartbeat_grace", &self.hb_grace)
.finish_non_exhaustive()
}
}
impl GearManager {
pub fn new() -> Self {
Self {
inner: DashMap::new(),
rr_counters: DashMap::new(),
hb_ttl: Duration::from_secs(15),
hb_grace: Duration::from_secs(30),
reg_lock: parking_lot::Mutex::new(()),
service_owners: parking_lot::RwLock::new(HashMap::new()),
}
}
pub fn with_heartbeat_policy(mut self, ttl: Duration, grace: Duration) -> Self {
self.hb_ttl = ttl;
self.hb_grace = grace;
self
}
pub fn set_grpc_service_owners(&self, owners: HashMap<String, String>) {
let _gate = self.reg_lock.lock();
*self.service_owners.write() = owners;
}
pub fn merge_authoritative_grpc_service_owners(&self, authoritative: HashMap<String, String>) {
let _gate = self.reg_lock.lock();
let mut owners = self.service_owners.write();
for (service_name, gear) in authoritative {
if let Some(configured) = owners.get(&service_name)
&& configured != &gear
{
tracing::warn!(
service = %service_name,
configured_owner = %configured,
compiled_owner = %gear,
"grpc-service-owners: operator config assigned a compiled-in service name to a \
different gear; the compiled-in provider is authoritative and overrides it"
);
}
owners.insert(service_name, gear);
}
}
pub fn register_instance(
&self,
instance: Arc<GearInstance>,
) -> Result<(), GrpcServiceNameConflict> {
let _gate = self.reg_lock.lock();
self.check_grpc_service_ownership(&instance)?;
let gear = instance.gear.clone();
let mut vec = self.inner.entry(gear).or_default();
if let Some(pos) = vec
.iter()
.position(|i| i.instance_id == instance.instance_id)
{
vec[pos] = Arc::new(vec[pos].with_metadata_of(&instance));
} else {
vec.push(instance);
}
Ok(())
}
fn check_grpc_service_ownership(
&self,
instance: &GearInstance,
) -> Result<(), GrpcServiceNameConflict> {
let declared = self.service_owners.read();
let mut to_scan: Vec<&str> = Vec::new();
for service_name in instance.grpc_services.keys() {
match declared.get(service_name) {
Some(owner) if *owner != instance.gear => {
return Err(GrpcServiceNameConflict {
service_name: service_name.clone(),
owner: owner.clone(),
recoverable: false,
});
}
_ => to_scan.push(service_name),
}
}
drop(declared);
if to_scan.is_empty() {
return Ok(());
}
for entry in &self.inner {
if entry.key() == &instance.gear {
continue; }
for existing in entry.value() {
if let Some(&name) = to_scan
.iter()
.find(|&&n| existing.grpc_services.contains_key(n))
{
return Err(GrpcServiceNameConflict {
service_name: name.to_owned(),
owner: entry.key().clone(),
recoverable: true,
});
}
}
}
Ok(())
}
pub fn mark_ready(&self, gear: &str, instance_id: Uuid) {
if let Some(mut vec) = self.inner.get_mut(gear)
&& let Some(inst) = vec.iter_mut().find(|i| i.instance_id == instance_id)
{
let mut state = inst.inner.write();
state.state = InstanceState::Ready;
}
}
pub fn update_heartbeat(&self, gear: &str, instance_id: Uuid, at: Instant) {
if let Some(mut vec) = self.inner.get_mut(gear)
&& let Some(inst) = vec.iter_mut().find(|i| i.instance_id == instance_id)
{
let mut state = inst.inner.write();
state.last_heartbeat = at;
if state.state == InstanceState::Registered {
state.state = InstanceState::Healthy;
}
}
}
pub fn mark_quarantined(&self, gear: &str, instance_id: Uuid) {
if let Some(mut vec) = self.inner.get_mut(gear)
&& let Some(inst) = vec.iter_mut().find(|i| i.instance_id == instance_id)
{
inst.inner.write().state = InstanceState::Quarantined;
}
}
pub fn mark_draining(&self, gear: &str, instance_id: Uuid) {
if let Some(mut vec) = self.inner.get_mut(gear)
&& let Some(inst) = vec.iter_mut().find(|i| i.instance_id == instance_id)
{
inst.inner.write().state = InstanceState::Draining;
}
}
pub fn deregister(&self, gear: &str, instance_id: Uuid) {
let mut remove_gear = false;
{
if let Some(mut vec) = self.inner.get_mut(gear) {
let list = vec.value_mut();
list.retain(|inst| inst.instance_id != instance_id);
if list.is_empty() {
remove_gear = true;
}
}
}
if remove_gear {
self.inner.remove(gear);
self.rr_counters.remove(gear);
self.rr_counters.remove(&format!("rest:{gear}"));
}
}
#[must_use]
pub fn instances_of(&self, gear: &str) -> Vec<Arc<GearInstance>> {
self.inner.get(gear).map(|v| v.clone()).unwrap_or_default()
}
#[must_use]
pub fn all_instances(&self) -> Vec<Arc<GearInstance>> {
self.inner
.iter()
.flat_map(|entry| entry.value().clone())
.collect()
}
pub fn evict_stale(&self, now: Instant) {
use InstanceState::{Draining, Quarantined};
let mut empty_gears = Vec::new();
for mut entry in self.inner.iter_mut() {
let gear = entry.key().clone();
let vec = entry.value_mut();
vec.retain(|inst| {
let state = inst.inner.read();
let age = now.saturating_duration_since(state.last_heartbeat);
if age >= self.hb_ttl && !matches!(state.state, Quarantined | Draining) {
drop(state); inst.inner.write().state = Quarantined;
return true; }
if state.state == Quarantined && age >= self.hb_ttl + self.hb_grace {
return false; }
true
});
if vec.is_empty() {
empty_gears.push(gear);
}
}
for gear in empty_gears {
self.inner.remove(&gear);
self.rr_counters.remove(&gear);
self.rr_counters.remove(&format!("rest:{gear}"));
}
}
fn prefer_serving(candidates: Vec<Arc<GearInstance>>, context: &str) -> Vec<Arc<GearInstance>> {
let serving: Vec<Arc<GearInstance>> = candidates
.iter()
.filter(|inst| matches!(inst.state(), InstanceState::Healthy | InstanceState::Ready))
.cloned()
.collect();
if serving.is_empty() {
tracing::debug!(
context,
"no serving (Ready/Healthy) instance available; round-robining over the \
not-ready set instead of returning None"
);
candidates
} else {
serving
}
}
#[must_use]
pub fn pick_instance_round_robin(&self, gear: &str) -> Option<Arc<GearInstance>> {
let instances_entry = self.inner.get(gear)?;
let instances = instances_entry.value();
if instances.is_empty() {
return None;
}
let candidates = Self::prefer_serving(instances.clone(), gear);
let len = candidates.len();
let mut counter = self.rr_counters.entry(gear.to_owned()).or_insert(0);
let idx = *counter % len;
*counter = (*counter + 1) % len;
candidates.get(idx).cloned()
}
#[must_use]
pub fn pick_service_round_robin(
&self,
service_name: &str,
) -> Option<(String, Arc<GearInstance>, Endpoint)> {
let mut providing: Vec<Arc<GearInstance>> = Vec::new();
for entry in &self.inner {
for inst in entry.value() {
if inst.grpc_services.contains_key(service_name) {
providing.push(inst.clone());
}
}
}
if providing.is_empty() {
return None;
}
let mut candidates = Self::prefer_serving(providing, service_name);
let len = candidates.len();
let service_key = service_name.to_owned();
let mut counter = self.rr_counters.entry(service_key).or_insert(0);
let idx = *counter % len;
*counter = (*counter + 1) % len;
let inst = candidates.swap_remove(idx);
let endpoint = inst.grpc_services.get(service_name)?.clone();
let gear = inst.gear.clone();
Some((gear, inst, endpoint))
}
#[must_use]
pub fn pick_rest_endpoint_round_robin(&self, gear: &str) -> Option<Endpoint> {
let instances_entry = self.inner.get(gear)?;
let instances = instances_entry.value();
let with_rest: Vec<_> = instances
.iter()
.filter(|inst| inst.rest_endpoint.is_some())
.cloned()
.collect();
if with_rest.is_empty() {
return None;
}
let candidates = Self::prefer_serving(with_rest, gear);
let len = candidates.len();
let rr_key = format!("rest:{gear}");
let mut counter = self.rr_counters.entry(rr_key).or_insert(0);
let idx = *counter % len;
*counter = (*counter + 1) % len;
candidates
.get(idx)
.and_then(|inst| inst.rest_endpoint.clone())
}
#[cfg(test)]
#[must_use]
fn grpc_service_owner(&self, service_name: &str) -> Option<String> {
for entry in &self.inner {
if entry
.value()
.iter()
.any(|inst| inst.grpc_services.contains_key(service_name))
{
return Some(entry.key().clone());
}
}
None
}
#[must_use]
pub fn openapi_spec_of(&self, gear: &str) -> Option<String> {
let instances_entry = self.inner.get(gear)?;
instances_entry
.value()
.iter()
.find_map(|inst| inst.openapi_spec.clone())
}
}
impl Default for GearManager {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
#[cfg_attr(coverage_nightly, coverage(off))]
mod tests {
use super::*;
use std::thread::sleep;
use std::time::Duration;
#[test]
fn test_register_and_retrieve_instances() {
let dir = GearManager::new();
let instance_id = Uuid::new_v4();
let instance = Arc::new(
GearInstance::new("test_gear", instance_id)
.with_control(Endpoint::http("localhost", 8080))
.with_version("1.0.0"),
);
seed(&dir, instance);
let instances = dir.instances_of("test_gear");
assert_eq!(instances.len(), 1);
assert_eq!(instances[0].instance_id, instance_id);
assert_eq!(instances[0].gear, "test_gear");
assert_eq!(instances[0].version, Some("1.0.0".to_owned()));
}
#[track_caller]
fn seed(mgr: &GearManager, instance: Arc<GearInstance>) {
mgr.register_instance(instance)
.expect("test fixture must not create a gRPC service-name conflict");
}
fn labels(pairs: &[(&str, &str)]) -> BTreeMap<String, String> {
pairs
.iter()
.map(|(k, v)| ((*k).to_owned(), (*v).to_owned()))
.collect()
}
#[test]
fn reregister_without_labels_preserves_stored_labels() {
let dir = GearManager::new();
let instance_id = Uuid::new_v4();
seed(
&dir,
Arc::new(
GearInstance::new("shard-gear", instance_id).with_labels(labels(&[("shard", "7")])),
),
);
seed(
&dir,
Arc::new(GearInstance::new("shard-gear", instance_id).with_version("2.0.0")),
);
let registered = dir.instances_of("shard-gear");
assert_eq!(registered.len(), 1);
assert_eq!(
registered[0].labels.get("shard"),
Some(&"7".to_owned()),
"label-less re-registration must preserve stored labels"
);
assert_eq!(registered[0].version, Some("2.0.0".to_owned()));
}
#[test]
fn reregister_with_labels_replaces_stored_labels() {
let dir = GearManager::new();
let instance_id = Uuid::new_v4();
seed(
&dir,
Arc::new(
GearInstance::new("shard-gear", instance_id).with_labels(labels(&[("shard", "7")])),
),
);
seed(
&dir,
Arc::new(
GearInstance::new("shard-gear", instance_id).with_labels(labels(&[("shard", "8")])),
),
);
let registered = dir.instances_of("shard-gear");
assert_eq!(registered.len(), 1);
assert_eq!(registered[0].labels.get("shard"), Some(&"8".to_owned()));
}
#[test]
fn test_register_multiple_instances() {
let dir = GearManager::new();
let id1 = Uuid::new_v4();
let id2 = Uuid::new_v4();
let instance1 = Arc::new(GearInstance::new("test_gear", id1));
let instance2 = Arc::new(GearInstance::new("test_gear", id2));
seed(&dir, instance1);
seed(&dir, instance2);
let registered = dir.instances_of("test_gear");
assert_eq!(registered.len(), 2);
let ids: Vec<_> = registered.iter().map(|i| i.instance_id).collect();
assert!(ids.contains(&id1));
assert!(ids.contains(&id2));
}
#[test]
fn test_update_existing_instance() {
let dir = GearManager::new();
let instance_id = Uuid::new_v4();
let initial_instance =
Arc::new(GearInstance::new("test_gear", instance_id).with_version("1.0.0"));
seed(&dir, initial_instance);
let updated_instance =
Arc::new(GearInstance::new("test_gear", instance_id).with_version("2.0.0"));
seed(&dir, updated_instance);
let registered = dir.instances_of("test_gear");
assert_eq!(registered.len(), 1, "Should not duplicate instance");
assert_eq!(registered[0].version, Some("2.0.0".to_owned()));
}
#[test]
fn test_reregistration_preserves_liveness_state() {
let dir = GearManager::new();
let instance_id = Uuid::new_v4();
seed(
&dir,
Arc::new(GearInstance::new("test_gear", instance_id).with_version("1.0.0")),
);
dir.update_heartbeat("test_gear", instance_id, Instant::now());
assert!(matches!(
dir.instances_of("test_gear")[0].state(),
InstanceState::Healthy
));
seed(
&dir,
Arc::new(GearInstance::new("test_gear", instance_id).with_version("2.0.0")),
);
let instances = dir.instances_of("test_gear");
assert_eq!(instances.len(), 1);
assert!(
matches!(instances[0].state(), InstanceState::Healthy),
"re-registration must preserve the Healthy state"
);
assert_eq!(
instances[0].version,
Some("2.0.0".to_owned()),
"re-registration must still refresh metadata/endpoints"
);
}
#[test]
fn test_concurrent_reregister_and_heartbeat_preserves_state() {
let dir = GearManager::new();
let instance_id = Uuid::new_v4();
let initial = Arc::new(GearInstance::new("test_gear", instance_id).with_version("1.0.0"));
seed(&dir, initial);
dir.update_heartbeat("test_gear", instance_id, Instant::now());
assert!(matches!(
dir.instances_of("test_gear")[0].state(),
InstanceState::Healthy
));
let start = Instant::now();
std::thread::scope(|s| {
s.spawn(|| {
for _ in 0..1000 {
dir.update_heartbeat("test_gear", instance_id, Instant::now());
}
});
s.spawn(|| {
for i in 0..1000 {
let version = if i % 2 == 0 { "2.0.0" } else { "3.0.0" };
let reinst = Arc::new(
GearInstance::new("test_gear", instance_id)
.with_version(version)
.with_rest_endpoint(Endpoint::http(
"127.0.0.1",
8000u16 + u16::try_from(i % 10).expect("i % 10 fits in u16"),
)),
);
seed(&dir, reinst);
}
});
});
let instances = dir.instances_of("test_gear");
assert_eq!(instances.len(), 1);
assert!(
matches!(instances[0].state(), InstanceState::Healthy),
"concurrent re-registration must not reset Healthy state"
);
assert!(
instances[0].last_heartbeat() >= start,
"concurrent re-registration must not lose heartbeat updates"
);
}
#[test]
fn test_mark_ready() {
let dir = GearManager::new();
let instance_id = Uuid::new_v4();
let instance = Arc::new(GearInstance::new("test_gear", instance_id));
seed(&dir, instance);
dir.mark_ready("test_gear", instance_id);
let instances = dir.instances_of("test_gear");
assert_eq!(instances.len(), 1);
assert!(matches!(instances[0].state(), InstanceState::Ready));
}
#[test]
fn test_update_heartbeat() {
let dir = GearManager::new();
let instance_id = Uuid::new_v4();
let instance = Arc::new(GearInstance::new("test_gear", instance_id));
let initial_heartbeat = instance.last_heartbeat();
seed(&dir, instance);
sleep(Duration::from_millis(10));
let new_heartbeat = Instant::now();
dir.update_heartbeat("test_gear", instance_id, new_heartbeat);
let instances = dir.instances_of("test_gear");
assert!(instances[0].last_heartbeat() > initial_heartbeat);
assert!(matches!(instances[0].state(), InstanceState::Healthy));
}
#[test]
fn test_all_instances() {
let dir = GearManager::new();
let instance1 = Arc::new(GearInstance::new("gear_a", Uuid::new_v4()));
let instance2 = Arc::new(GearInstance::new("gear_b", Uuid::new_v4()));
let instance3 = Arc::new(GearInstance::new("gear_a", Uuid::new_v4()));
seed(&dir, instance1);
seed(&dir, instance2);
seed(&dir, instance3);
let all = dir.all_instances();
assert_eq!(all.len(), 3);
let gears: Vec<_> = all.iter().map(|i| i.gear.as_str()).collect();
assert_eq!(gears.iter().filter(|&m| *m == "gear_a").count(), 2);
assert_eq!(gears.iter().filter(|&m| *m == "gear_b").count(), 1);
}
#[test]
fn test_pick_instance_round_robin() {
let dir = GearManager::new();
let id1 = Uuid::new_v4();
let id2 = Uuid::new_v4();
let instance1 = Arc::new(GearInstance::new("test_gear", id1));
let instance2 = Arc::new(GearInstance::new("test_gear", id2));
seed(&dir, instance1);
seed(&dir, instance2);
let picked1 = dir.pick_instance_round_robin("test_gear").unwrap();
let picked2 = dir.pick_instance_round_robin("test_gear").unwrap();
let picked3 = dir.pick_instance_round_robin("test_gear").unwrap();
let ids = [
picked1.instance_id,
picked2.instance_id,
picked3.instance_id,
];
assert!(ids.contains(&id1));
assert!(ids.contains(&id2));
assert_eq!(picked1.instance_id, picked3.instance_id);
assert_ne!(picked1.instance_id, picked2.instance_id);
}
#[test]
fn test_pick_instance_none_available() {
let dir = GearManager::new();
let picked = dir.pick_instance_round_robin("nonexistent_gear");
assert!(picked.is_none());
}
#[test]
fn test_endpoint_creation() {
let plain_ep = Endpoint::http("localhost", 8080);
assert_eq!(plain_ep.uri, "http://localhost:8080");
let secure_ep = Endpoint::https("localhost", 8443);
assert_eq!(secure_ep.uri, "https://localhost:8443");
let uds_ep = Endpoint::uds("/tmp/socket.sock");
assert!(uds_ep.uri.starts_with("unix://"));
assert!(uds_ep.uri.contains("socket.sock"));
let custom_ep = Endpoint::from_uri("http://example.com");
assert_eq!(custom_ep.uri, "http://example.com");
}
#[test]
fn test_endpoint_kind() {
let plain_ep = Endpoint::http("127.0.0.1", 8080);
match plain_ep.kind() {
EndpointKind::Tcp(addr) => {
assert_eq!(addr.ip().to_string(), "127.0.0.1");
assert_eq!(addr.port(), 8080);
}
_ => panic!("Expected TCP endpoint for http"),
}
let secure_ep = Endpoint::https("127.0.0.1", 8443);
match secure_ep.kind() {
EndpointKind::Tcp(addr) => {
assert_eq!(addr.ip().to_string(), "127.0.0.1");
assert_eq!(addr.port(), 8443);
}
_ => panic!("Expected TCP endpoint for https"),
}
let uds_ep = Endpoint::uds("/tmp/test.sock");
match uds_ep.kind() {
EndpointKind::Uds(path) => {
assert!(path.to_string_lossy().contains("test.sock"));
}
_ => panic!("Expected UDS endpoint"),
}
let other_ep = Endpoint::from_uri("grpc://example.com");
match other_ep.kind() {
EndpointKind::Other(uri) => {
assert_eq!(uri, "grpc://example.com");
}
_ => panic!("Expected Other endpoint"),
}
}
#[test]
fn test_gear_instance_builder() {
let instance_id = Uuid::new_v4();
let instance = GearInstance::new("test_gear", instance_id)
.with_control(Endpoint::http("localhost", 8080))
.with_version("1.2.3")
.with_grpc_service("service1", Endpoint::http("localhost", 8082))
.with_grpc_service("service2", Endpoint::http("localhost", 8083));
assert_eq!(instance.gear, "test_gear");
assert_eq!(instance.instance_id, instance_id);
assert!(instance.control.is_some());
assert_eq!(instance.version, Some("1.2.3".to_owned()));
assert_eq!(instance.grpc_services.len(), 2);
assert!(instance.grpc_services.contains_key("service1"));
assert!(instance.grpc_services.contains_key("service2"));
assert!(matches!(instance.state(), InstanceState::Registered));
}
#[test]
fn test_quarantine_and_evict() {
let ttl = Duration::from_millis(50);
let grace = Duration::from_millis(50);
let dir = GearManager::new().with_heartbeat_policy(ttl, grace);
let now = Instant::now();
let instance = GearInstance::new("test_gear", Uuid::new_v4());
instance.inner.write().last_heartbeat = now
.checked_sub(ttl)
.and_then(|t| t.checked_sub(Duration::from_millis(10)))
.expect("test duration subtraction should not underflow");
seed(&dir, Arc::new(instance));
dir.evict_stale(now);
let instances = dir.instances_of("test_gear");
assert_eq!(instances.len(), 1);
assert!(matches!(instances[0].state(), InstanceState::Quarantined));
let later = now + grace + Duration::from_millis(10);
dir.evict_stale(later);
let instances_after = dir.instances_of("test_gear");
assert!(instances_after.is_empty());
}
#[test]
fn test_instances_of_empty() {
let dir = GearManager::new();
let instances = dir.instances_of("nonexistent");
assert!(instances.is_empty());
}
#[test]
fn test_rr_prefers_healthy() {
let dir = GearManager::new();
let healthy_id = Uuid::new_v4();
let healthy = Arc::new(GearInstance::new("test_gear", healthy_id));
seed(&dir, healthy);
dir.update_heartbeat("test_gear", healthy_id, Instant::now());
let quarantined_id = Uuid::new_v4();
let quarantined = Arc::new(GearInstance::new("test_gear", quarantined_id));
seed(&dir, quarantined);
dir.mark_quarantined("test_gear", quarantined_id);
for _ in 0..5 {
let picked = dir.pick_instance_round_robin("test_gear").unwrap();
assert_eq!(picked.instance_id, healthy_id);
}
}
#[test]
fn test_pick_rest_endpoint_and_openapi() {
let dir = GearManager::new();
let id = Uuid::new_v4();
let instance = Arc::new(
GearInstance::new("billing", id)
.with_rest_endpoint(Endpoint::http("billing", 8080))
.with_openapi_spec("{\"openapi\":\"3.1.0\"}"),
);
seed(&dir, instance);
let rest = dir.pick_rest_endpoint_round_robin("billing").unwrap();
assert_eq!(rest.uri, "http://billing:8080");
let spec = dir.openapi_spec_of("billing").unwrap();
assert!(spec.contains("openapi"));
}
#[test]
fn test_pick_rest_endpoint_none_when_absent() {
let dir = GearManager::new();
let id = Uuid::new_v4();
let instance = Arc::new(
GearInstance::new("grpc_only", id)
.with_grpc_service("some.Service", Endpoint::http("127.0.0.1", 9000)),
);
seed(&dir, instance);
assert!(dir.pick_rest_endpoint_round_robin("grpc_only").is_none());
assert!(dir.openapi_spec_of("grpc_only").is_none());
assert!(dir.pick_rest_endpoint_round_robin("missing").is_none());
}
#[test]
fn test_pick_rest_endpoint_round_robin_rotates() {
let dir = GearManager::new();
let id1 = Uuid::new_v4();
let id2 = Uuid::new_v4();
let inst1 = Arc::new(
GearInstance::new("web", id1).with_rest_endpoint(Endpoint::http("127.0.0.1", 8001)),
);
let inst2 = Arc::new(
GearInstance::new("web", id2).with_rest_endpoint(Endpoint::http("127.0.0.1", 8002)),
);
seed(&dir, inst1);
seed(&dir, inst2);
dir.update_heartbeat("web", id1, Instant::now());
dir.update_heartbeat("web", id2, Instant::now());
let ep1 = dir.pick_rest_endpoint_round_robin("web").unwrap();
let ep2 = dir.pick_rest_endpoint_round_robin("web").unwrap();
let ep3 = dir.pick_rest_endpoint_round_robin("web").unwrap();
assert_ne!(ep1, ep2);
assert_eq!(ep1, ep3);
}
#[test]
fn test_pick_service_round_robin() {
let dir = GearManager::new();
let id1 = Uuid::new_v4();
let id2 = Uuid::new_v4();
let inst1 = Arc::new(
GearInstance::new("test_gear", id1)
.with_grpc_service("test.Service", Endpoint::http("127.0.0.1", 8001)),
);
let inst2 = Arc::new(
GearInstance::new("test_gear", id2)
.with_grpc_service("test.Service", Endpoint::http("127.0.0.1", 8002)),
);
seed(&dir, inst1);
seed(&dir, inst2);
dir.update_heartbeat("test_gear", id1, Instant::now());
dir.update_heartbeat("test_gear", id2, Instant::now());
let pick1 = dir.pick_service_round_robin("test.Service");
let pick2 = dir.pick_service_round_robin("test.Service");
let pick3 = dir.pick_service_round_robin("test.Service");
assert!(pick1.is_some());
assert!(pick2.is_some());
assert!(pick3.is_some());
let (_, inst1, ep1) = pick1.unwrap();
let (_, inst2, ep2) = pick2.unwrap();
let (_, inst3, _) = pick3.unwrap();
assert_eq!(inst1.instance_id, inst3.instance_id);
assert_ne!(inst1.instance_id, inst2.instance_id);
assert_ne!(ep1, ep2);
}
#[test]
fn pick_service_falls_back_to_not_ready() {
let dir = GearManager::new();
let id = Uuid::new_v4();
seed(
&dir,
Arc::new(
GearInstance::new("worker", id)
.with_grpc_service("worker.Svc", Endpoint::http("127.0.0.1", 9000)),
),
);
assert!(matches!(
dir.instances_of("worker")[0].state(),
InstanceState::Registered
));
let picked = dir.pick_service_round_robin("worker.Svc");
assert!(
picked.is_some(),
"gRPC service resolution must fall back to the not-ready instance"
);
let (gear, _, ep) = picked.unwrap();
assert_eq!(gear, "worker");
assert_eq!(ep, Endpoint::http("127.0.0.1", 9000));
}
#[test]
fn grpc_service_owner_reports_owning_gear_regardless_of_health() {
let dir = GearManager::new();
seed(
&dir,
Arc::new(
GearInstance::new("authz-resolver", Uuid::new_v4()).with_grpc_service(
"cf.authz.v1.AuthzService",
Endpoint::http("127.0.0.1", 9000),
),
),
);
assert_eq!(
dir.grpc_service_owner("cf.authz.v1.AuthzService")
.as_deref(),
Some("authz-resolver"),
"the advertising gear owns the name even while only Registered"
);
assert!(
dir.grpc_service_owner("unowned.Service").is_none(),
"an unadvertised name has no owner"
);
}
#[test]
fn registered_but_unhealthy_owner_blocks_another_gears_claim() {
let dir = GearManager::new();
let owner_id = Uuid::new_v4();
dir.register_instance(Arc::new(
GearInstance::new("authz-resolver", owner_id).with_grpc_service(
"cf.authz.v1.AuthzService",
Endpoint::http("127.0.0.1", 9000),
),
))
.expect("first claim of an unowned name succeeds");
assert!(matches!(
dir.instances_of("authz-resolver")[0].state(),
InstanceState::Registered
));
let conflict = dir
.register_instance(Arc::new(
GearInstance::new("impostor", Uuid::new_v4()).with_grpc_service(
"cf.authz.v1.AuthzService",
Endpoint::http("127.0.0.1", 9001),
),
))
.unwrap_err();
assert_eq!(conflict.owner, "authz-resolver");
}
#[test]
fn deregister_releases_grpc_service_name_to_another_gear() {
let dir = GearManager::new();
let service = "cf.authz.v1.AuthzService";
let owner_id = Uuid::new_v4();
seed(
&dir,
Arc::new(
GearInstance::new("authz-resolver", owner_id)
.with_grpc_service(service, Endpoint::http("127.0.0.1", 9000)),
),
);
dir.register_instance(Arc::new(
GearInstance::new("successor", Uuid::new_v4())
.with_grpc_service(service, Endpoint::http("127.0.0.1", 9001)),
))
.unwrap_err();
dir.deregister("authz-resolver", owner_id);
assert!(
dir.grpc_service_owner(service).is_none(),
"the name is unowned once its owner deregisters"
);
dir.register_instance(Arc::new(
GearInstance::new("successor", Uuid::new_v4())
.with_grpc_service(service, Endpoint::http("127.0.0.1", 9001)),
))
.expect("a released gRPC service name must be claimable by another gear");
assert_eq!(
dir.grpc_service_owner(service).as_deref(),
Some("successor")
);
}
#[test]
fn eviction_releases_grpc_service_name_only_after_full_evict() {
let ttl = Duration::from_millis(50);
let grace = Duration::from_millis(50);
let dir = GearManager::new().with_heartbeat_policy(ttl, grace);
let service = "cf.authz.v1.AuthzService";
let now = Instant::now();
let owner = GearInstance::new("authz-resolver", Uuid::new_v4())
.with_grpc_service(service, Endpoint::http("127.0.0.1", 9000));
owner.inner.write().last_heartbeat = now
.checked_sub(ttl)
.and_then(|t| t.checked_sub(Duration::from_millis(10)))
.expect("test duration subtraction should not underflow");
seed(&dir, Arc::new(owner));
dir.evict_stale(now);
assert!(matches!(
dir.instances_of("authz-resolver")[0].state(),
InstanceState::Quarantined
));
let conflict = dir
.register_instance(Arc::new(
GearInstance::new("successor", Uuid::new_v4())
.with_grpc_service(service, Endpoint::http("127.0.0.1", 9001)),
))
.unwrap_err();
assert_eq!(
conflict.owner, "authz-resolver",
"a quarantined-but-not-evicted owner still holds the name"
);
dir.evict_stale(now + grace + Duration::from_millis(10));
assert!(
dir.grpc_service_owner(service).is_none(),
"eviction hands the name back once the grace period lapses"
);
dir.register_instance(Arc::new(
GearInstance::new("successor", Uuid::new_v4())
.with_grpc_service(service, Endpoint::http("127.0.0.1", 9002)),
))
.expect("an evicted owner's gRPC name must be claimable by another gear");
assert_eq!(
dir.grpc_service_owner(service).as_deref(),
Some("successor")
);
}
#[test]
fn test_deregister_clears_rr_counters() {
let dir = GearManager::new();
let id = Uuid::new_v4();
let instance = Arc::new(
GearInstance::new("web", id).with_rest_endpoint(Endpoint::http("127.0.0.1", 8001)),
);
seed(&dir, instance);
dir.update_heartbeat("web", id, Instant::now());
assert!(dir.pick_instance_round_robin("web").is_some());
assert!(dir.pick_rest_endpoint_round_robin("web").is_some());
assert!(dir.rr_counters.contains_key("web"));
assert!(dir.rr_counters.contains_key("rest:web"));
dir.deregister("web", id);
assert!(!dir.rr_counters.contains_key("web"));
assert!(!dir.rr_counters.contains_key("rest:web"));
}
#[test]
fn test_evict_stale_clears_rr_counters() {
let ttl = Duration::from_millis(50);
let grace = Duration::from_millis(50);
let dir = GearManager::new().with_heartbeat_policy(ttl, grace);
let now = Instant::now();
let id = Uuid::new_v4();
let instance = Arc::new(
GearInstance::new("web", id).with_rest_endpoint(Endpoint::http("127.0.0.1", 8001)),
);
instance.inner.write().last_heartbeat = now
.checked_sub(ttl)
.and_then(|t| t.checked_sub(Duration::from_millis(10)))
.expect("test duration subtraction should not underflow");
seed(&dir, instance);
assert!(dir.pick_rest_endpoint_round_robin("web").is_some());
assert!(dir.rr_counters.contains_key("rest:web"));
dir.evict_stale(now);
let instances = dir.instances_of("web");
assert_eq!(instances.len(), 1);
assert!(matches!(instances[0].state(), InstanceState::Quarantined));
let later = now + grace + Duration::from_millis(10);
dir.evict_stale(later);
assert!(dir.instances_of("web").is_empty());
assert!(!dir.rr_counters.contains_key("web"));
assert!(!dir.rr_counters.contains_key("rest:web"));
}
#[test]
fn register_instance_rejects_cross_gear_grpc_name() {
let mgr = GearManager::new();
let service = "cf.authz.v1.AuthzService";
mgr.register_instance(Arc::new(
GearInstance::new("authz-resolver", Uuid::new_v4())
.with_grpc_service(service, Endpoint::http("127.0.0.1", 9000)),
))
.expect("claiming an unowned service name must succeed");
let conflict = mgr
.register_instance(Arc::new(
GearInstance::new("evil", Uuid::new_v4())
.with_grpc_service(service, Endpoint::http("127.0.0.1", 9001)),
))
.unwrap_err();
assert_eq!(conflict.service_name, service);
assert_eq!(conflict.owner, "authz-resolver");
assert!(mgr.instances_of("evil").is_empty());
mgr.register_instance(Arc::new(
GearInstance::new("authz-resolver", Uuid::new_v4())
.with_grpc_service(service, Endpoint::http("127.0.0.1", 9002)),
))
.expect("the owning gear may add another instance for its own service");
}
#[test]
fn declared_ownership_beats_registration_order() {
let mgr = GearManager::new();
let service = "cf.authz.v1.AuthzService";
mgr.set_grpc_service_owners(HashMap::from([(service.to_owned(), "authz".to_owned())]));
let conflict = mgr
.register_instance(Arc::new(
GearInstance::new("evil", Uuid::new_v4())
.with_grpc_service(service, Endpoint::http("127.0.0.1", 9001)),
))
.unwrap_err();
assert_eq!(conflict.service_name, service);
assert_eq!(conflict.owner, "authz");
assert!(
!conflict.recoverable,
"a declared-owner conflict is pinned by config (permanent)"
);
assert!(mgr.instances_of("evil").is_empty());
mgr.register_instance(Arc::new(
GearInstance::new("authz", Uuid::new_v4())
.with_grpc_service(service, Endpoint::http("127.0.0.1", 9000)),
))
.expect("the declared owner must be admitted");
assert_eq!(mgr.grpc_service_owner(service).as_deref(), Some("authz"));
}
#[test]
fn declared_owner_refused_while_a_stale_advertiser_still_holds_the_name() {
let mgr = GearManager::new();
let service = "cf.authz.v1.AuthzService";
let holder = Uuid::new_v4();
mgr.register_instance(Arc::new(
GearInstance::new("first", holder)
.with_grpc_service(service, Endpoint::http("127.0.0.1", 9000)),
))
.expect("claiming an undeclared, unowned name must succeed");
mgr.set_grpc_service_owners(HashMap::from([(service.to_owned(), "authz".to_owned())]));
let conflict = mgr
.register_instance(Arc::new(
GearInstance::new("authz", Uuid::new_v4())
.with_grpc_service(service, Endpoint::http("127.0.0.1", 9001)),
))
.unwrap_err();
assert_eq!(conflict.service_name, service);
assert_eq!(
conflict.owner, "first",
"the current holder is the conflicting owner"
);
assert!(
conflict.recoverable,
"a stale-advertiser conflict clears once the holder deregisters"
);
assert!(mgr.instances_of("authz").is_empty());
mgr.deregister("first", holder);
mgr.register_instance(Arc::new(
GearInstance::new("authz", Uuid::new_v4())
.with_grpc_service(service, Endpoint::http("127.0.0.1", 9002)),
))
.expect("the declared owner must be admitted once the stale holder is gone");
assert_eq!(mgr.grpc_service_owner(service).as_deref(), Some("authz"));
}
#[test]
fn authoritative_owners_override_conflicting_config_and_keep_the_rest() {
let mgr = GearManager::new();
mgr.set_grpc_service_owners(HashMap::from([
("cf.authz.v1.AuthzService".to_owned(), "impostor".to_owned()),
("remote.only.Service".to_owned(), "remote-gear".to_owned()),
]));
mgr.merge_authoritative_grpc_service_owners(HashMap::from([(
"cf.authz.v1.AuthzService".to_owned(),
"authz-resolver".to_owned(),
)]));
let conflict = mgr
.register_instance(Arc::new(
GearInstance::new("impostor", Uuid::new_v4()).with_grpc_service(
"cf.authz.v1.AuthzService",
Endpoint::http("127.0.0.1", 9000),
),
))
.unwrap_err();
assert_eq!(conflict.owner, "authz-resolver");
assert!(
!conflict.recoverable,
"a pinned compiled-in owner is permanent"
);
mgr.register_instance(Arc::new(
GearInstance::new("authz-resolver", Uuid::new_v4()).with_grpc_service(
"cf.authz.v1.AuthzService",
Endpoint::http("127.0.0.1", 9001),
),
))
.expect("the compiled-in provider owns its service name");
let remote_conflict = mgr
.register_instance(Arc::new(
GearInstance::new("squatter", Uuid::new_v4())
.with_grpc_service("remote.only.Service", Endpoint::http("127.0.0.1", 9002)),
))
.unwrap_err();
assert_eq!(remote_conflict.owner, "remote-gear");
}
#[test]
fn names_absent_from_declared_map_keep_first_registration_ownership() {
let mgr = GearManager::new();
mgr.set_grpc_service_owners(HashMap::from([(
"cf.authz.v1.AuthzService".to_owned(),
"authz".to_owned(),
)]));
let service = "worker.Svc";
mgr.register_instance(Arc::new(
GearInstance::new("worker-a", Uuid::new_v4())
.with_grpc_service(service, Endpoint::http("127.0.0.1", 9000)),
))
.expect("claiming an undeclared, unowned name must succeed");
let conflict = mgr
.register_instance(Arc::new(
GearInstance::new("worker-b", Uuid::new_v4())
.with_grpc_service(service, Endpoint::http("127.0.0.1", 9001)),
))
.unwrap_err();
assert_eq!(conflict.owner, "worker-a");
assert!(
conflict.recoverable,
"a first-registration conflict clears if the holder leaves"
);
}
#[test]
fn concurrent_register_admits_exactly_one_owner() {
use std::sync::Barrier;
let mgr = Arc::new(GearManager::new());
let service = "cf.authz.v1.AuthzService";
let racers = 16;
let barrier = Arc::new(Barrier::new(racers));
#[expect(
clippy::needless_collect,
reason = "a lazy iterator would join before all racers spawn, deadlocking the Barrier"
)]
let handles: Vec<_> = (0..racers)
.map(|i| {
let mgr = Arc::clone(&mgr);
let barrier = Arc::clone(&barrier);
std::thread::spawn(move || {
let gear = format!("gear-{i}");
let instance = Arc::new(
GearInstance::new(gear.clone(), Uuid::new_v4()).with_grpc_service(
service,
Endpoint::http("127.0.0.1", 9000 + u16::try_from(i).unwrap()),
),
);
barrier.wait();
(gear, mgr.register_instance(instance))
})
})
.collect();
let results: Vec<(String, Result<(), GrpcServiceNameConflict>)> = handles
.into_iter()
.map(|h| h.join().expect("registration thread must not panic"))
.collect();
let winners: Vec<&str> = results
.iter()
.filter(|(_, r)| r.is_ok())
.map(|(gear, _)| gear.as_str())
.collect();
assert_eq!(
winners.len(),
1,
"exactly one competing registration must win"
);
let winner = winners[0];
for (gear, result) in &results {
if gear == winner {
continue;
}
let conflict = result
.as_ref()
.expect_err("a losing registration must be rejected, not silently dropped");
assert_eq!(conflict.service_name, service);
assert_eq!(
conflict.owner.as_str(),
winner,
"every loser's conflict must name the single winning gear as owner"
);
}
assert_eq!(
mgr.grpc_service_owner(service).as_deref(),
Some(winner),
"the contested name must resolve to the one gear that won"
);
}
}