use std::collections::HashMap;
use std::sync::{Arc, RwLock};
use crate::instance::{Instance, now};
pub const SELF_PRESERVATION_THRESHOLD: f64 = 0.15;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Mode {
Evicting,
SelfPreservation,
}
#[derive(Clone, Default)]
pub struct Registry {
inner: Arc<RwLock<HashMap<String, Vec<Instance>>>>,
}
impl Registry {
pub fn new() -> Registry {
Registry::default()
}
pub fn register(&self, instance: Instance) {
let mut held = self.inner.write().expect("the registry lock is poisoned");
let instances = held.entry(instance.service.clone()).or_default();
match instances.iter_mut().find(|held| held.id == instance.id) {
Some(existing) => *existing = instance,
None => instances.push(instance),
}
}
pub fn renew(&self, service: &str, id: &str) -> bool {
let service = service.to_ascii_uppercase();
let mut held = self.inner.write().expect("the registry lock is poisoned");
let Some(instances) = held.get_mut(&service) else { return false };
let Some(instance) = instances.iter_mut().find(|held| held.id == id) else { return false };
instance.last_seen = now();
true
}
pub fn cancel(&self, service: &str, id: &str) -> bool {
let service = service.to_ascii_uppercase();
let mut held = self.inner.write().expect("the registry lock is poisoned");
let Some(instances) = held.get_mut(&service) else { return false };
let before = instances.len();
instances.retain(|held| held.id != id);
let removed = instances.len() != before;
if instances.is_empty() {
held.remove(&service);
}
removed
}
pub fn set_status(&self, service: &str, id: &str, status: crate::Status) -> bool {
let service = service.to_ascii_uppercase();
let mut held = self.inner.write().expect("the registry lock is poisoned");
let Some(instances) = held.get_mut(&service) else { return false };
let Some(instance) = instances.iter_mut().find(|held| held.id == id) else { return false };
instance.status = status;
instance.last_seen = now();
true
}
pub fn all(&self) -> Vec<Instance> {
self.inner
.read()
.expect("the registry lock is poisoned")
.values()
.flatten()
.cloned()
.collect()
}
pub fn alive(&self, service: &str) -> (Vec<Instance>, Mode) {
let mode = self.mode();
let service = service.to_ascii_uppercase();
let at = now();
let held = self.inner.read().expect("the registry lock is poisoned");
let instances = held
.get(&service)
.map(|instances| {
instances
.iter()
.filter(|instance| instance.status.takes_traffic())
.filter(|instance| mode == Mode::SelfPreservation || instance.fresh_at(at))
.cloned()
.collect()
})
.unwrap_or_default();
(instances, mode)
}
pub fn mode(&self) -> Mode {
let at = now();
let held = self.inner.read().expect("the registry lock is poisoned");
let mut total = 0usize;
let mut stale = 0usize;
for instance in held.values().flatten() {
total += 1;
if !instance.fresh_at(at) {
stale += 1;
}
}
if total < 8 {
return Mode::Evicting;
}
match stale as f64 / total as f64 > SELF_PRESERVATION_THRESHOLD {
true => Mode::SelfPreservation,
false => Mode::Evicting,
}
}
pub fn services(&self) -> Vec<String> {
let mut names: Vec<String> =
self.inner.read().expect("the registry lock is poisoned").keys().cloned().collect();
names.sort();
names
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::instance::Status;
fn aged(service: &str, id: &str, seconds_ago: u64) -> Instance {
let mut instance = Instance::new(service, "10.0.0.1", 8080).id(id).renew_every(30);
instance.last_seen = now().saturating_sub(seconds_ago);
instance
}
#[test]
fn registering_twice_refreshes_rather_than_duplicating() {
let registry = Registry::new();
registry.register(Instance::new("orders", "10.0.0.1", 8080));
registry.register(Instance::new("orders", "10.0.0.1", 8080));
assert_eq!(registry.all().len(), 1, "a restarted instance became two");
}
#[test]
fn a_heartbeat_for_something_unregistered_is_refused() {
let registry = Registry::new();
assert!(!registry.renew("orders", "10.0.0.1:8080"));
registry.register(Instance::new("orders", "10.0.0.1", 8080));
assert!(registry.renew("orders", "10.0.0.1:8080"));
assert!(registry.renew("ORDERS", "10.0.0.1:8080"));
}
#[test]
fn a_stale_instance_is_not_offered() {
let registry = Registry::new();
registry.register(aged("orders", "fresh", 0));
registry.register(aged("orders", "gone", 200));
let (alive, mode) = registry.alive("orders");
assert_eq!(mode, Mode::Evicting);
assert_eq!(alive.len(), 1);
assert_eq!(alive[0].id, "fresh");
}
#[test]
fn an_instance_out_of_service_keeps_its_registration_and_loses_its_traffic() {
let registry = Registry::new();
registry.register(Instance::new("orders", "10.0.0.1", 8080));
assert!(registry.set_status("orders", "10.0.0.1:8080", Status::OutOfService));
assert_eq!(registry.alive("orders").0.len(), 0, "a draining instance was given traffic");
assert_eq!(registry.all().len(), 1, "a draining instance lost its registration");
}
#[test]
fn losing_most_of_the_registry_at_once_is_treated_as_a_partition() {
let registry = Registry::new();
for n in 0..10 {
registry.register(aged("orders", &format!("gone-{n}"), 200));
}
for n in 0..2 {
registry.register(aged("orders", &format!("fresh-{n}"), 0));
}
let (alive, mode) = registry.alive("orders");
assert_eq!(mode, Mode::SelfPreservation);
assert_eq!(alive.len(), 12, "the stale instances were dropped during a partition");
}
#[test]
fn one_instance_dying_among_many_is_still_evicted() {
let registry = Registry::new();
for n in 0..20 {
registry.register(aged("orders", &format!("fresh-{n}"), 0));
}
registry.register(aged("orders", "gone", 200));
let (alive, mode) = registry.alive("orders");
assert_eq!(mode, Mode::Evicting);
assert_eq!(alive.len(), 20);
}
#[test]
fn a_small_registry_does_not_protect_itself_on_a_single_death() {
let registry = Registry::new();
registry.register(aged("orders", "a", 0));
registry.register(aged("orders", "b", 0));
registry.register(aged("orders", "c", 200));
assert_eq!(registry.mode(), Mode::Evicting);
assert_eq!(registry.alive("orders").0.len(), 2);
}
#[test]
fn cancelling_removes_the_instance_and_then_the_empty_service() {
let registry = Registry::new();
registry.register(Instance::new("orders", "10.0.0.1", 8080));
registry.cancel("orders", "10.0.0.1:8080");
assert!(registry.all().is_empty());
assert!(registry.services().is_empty(), "an empty service was left behind");
}
}