use std::collections::HashMap;
use std::net::IpAddr;
use super::builder::ProcessCount;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum DomainLevel {
Datacenter,
Zone,
Machine,
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct LocalityInfo {
datacenter: String,
zone: String,
machine: String,
}
impl LocalityInfo {
#[must_use]
pub fn new(
datacenter: impl Into<String>,
zone: impl Into<String>,
machine: impl Into<String>,
) -> Self {
Self {
datacenter: datacenter.into(),
zone: zone.into(),
machine: machine.into(),
}
}
#[must_use]
pub fn datacenter(&self) -> &str {
&self.datacenter
}
#[must_use]
pub fn zone(&self) -> &str {
&self.zone
}
#[must_use]
pub fn machine(&self) -> &str {
&self.machine
}
#[must_use]
pub fn id_for(&self, level: DomainLevel) -> &str {
match level {
DomainLevel::Datacenter => &self.datacenter,
DomainLevel::Zone => &self.zone,
DomainLevel::Machine => &self.machine,
}
}
}
#[derive(Debug, Clone)]
pub struct LocalityConfig {
datacenters: ProcessCount,
zones_per_datacenter: ProcessCount,
machines_per_zone: ProcessCount,
processes_per_machine: ProcessCount,
}
impl LocalityConfig {
#[must_use]
pub fn new(
datacenters: impl Into<ProcessCount>,
zones_per_datacenter: impl Into<ProcessCount>,
machines_per_zone: impl Into<ProcessCount>,
processes_per_machine: impl Into<ProcessCount>,
) -> Self {
Self {
datacenters: datacenters.into(),
zones_per_datacenter: zones_per_datacenter.into(),
machines_per_zone: machines_per_zone.into(),
processes_per_machine: processes_per_machine.into(),
}
}
pub(crate) fn resolve_topology(&self) -> Vec<LocalityInfo> {
let datacenters = self.datacenters.resolve().max(1);
let zones = self.zones_per_datacenter.resolve().max(1);
let machines = self.machines_per_zone.resolve().max(1);
let processes = self.processes_per_machine.resolve().max(1);
let mut out = Vec::with_capacity(datacenters * zones * machines * processes);
for d in 0..datacenters {
for z in 0..zones {
for m in 0..machines {
let datacenter = format!("dc{}", d + 1);
let zone = format!("dc{}-z{}", d + 1, z + 1);
let machine = format!("dc{}-z{}-m{}", d + 1, z + 1, m + 1);
for _ in 0..processes {
out.push(LocalityInfo::new(
datacenter.clone(),
zone.clone(),
machine.clone(),
));
}
}
}
}
out
}
}
#[derive(Debug, Clone, Default, PartialEq)]
pub struct MachineRegistry {
ip_locality: HashMap<IpAddr, LocalityInfo>,
}
impl MachineRegistry {
#[must_use]
pub fn new() -> Self {
Self {
ip_locality: HashMap::new(),
}
}
pub fn register(&mut self, ip: IpAddr, locality: LocalityInfo) {
self.ip_locality.insert(ip, locality);
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.ip_locality.is_empty()
}
#[must_use]
pub fn locality_for(&self, ip: IpAddr) -> Option<&LocalityInfo> {
self.ip_locality.get(&ip)
}
#[must_use]
pub fn ips_in_domain(&self, level: DomainLevel, id: &str) -> Vec<IpAddr> {
self.ip_locality
.iter()
.filter(|(_, loc)| loc.id_for(level) == id)
.map(|(ip, _)| *ip)
.collect()
}
#[must_use]
pub fn ips_on_machine(&self, machine_id: &str) -> Vec<IpAddr> {
self.ips_in_domain(DomainLevel::Machine, machine_id)
}
#[must_use]
pub fn all_machines(&self) -> Vec<String> {
self.distinct_ids(DomainLevel::Machine)
}
#[must_use]
pub fn all_zones(&self) -> Vec<String> {
self.distinct_ids(DomainLevel::Zone)
}
#[must_use]
pub fn all_datacenters(&self) -> Vec<String> {
self.distinct_ids(DomainLevel::Datacenter)
}
fn distinct_ids(&self, level: DomainLevel) -> Vec<String> {
let mut ids: Vec<String> = self
.ip_locality
.values()
.map(|loc| loc.id_for(level).to_string())
.collect();
ids.sort();
ids.dedup();
ids
}
}
#[cfg(test)]
mod tests {
use super::*;
fn ip(n: usize) -> IpAddr {
format!("10.0.1.{n}").parse().expect("valid IP")
}
#[test]
fn resolve_topology_slices_contiguously() {
let locs = LocalityConfig::new(2, 2, 2, 2).resolve_topology();
assert_eq!(locs.len(), 16);
assert_eq!(locs[0].machine(), "dc1-z1-m1");
assert_eq!(locs[1].machine(), "dc1-z1-m1");
assert_eq!(locs[2].machine(), "dc1-z1-m2");
assert_eq!(locs[0].zone(), "dc1-z1");
assert_eq!(locs[4].zone(), "dc1-z2");
assert_eq!(locs[7].datacenter(), "dc1");
assert_eq!(locs[8].datacenter(), "dc2");
}
#[test]
fn globally_unique_machine_ids_across_datacenters() {
let locs = LocalityConfig::new(2, 1, 1, 1).resolve_topology();
assert_eq!(locs.len(), 2);
assert_ne!(locs[0].machine(), locs[1].machine());
assert_eq!(locs[0].machine(), "dc1-z1-m1");
assert_eq!(locs[1].machine(), "dc2-z1-m1");
}
#[test]
fn registry_domain_queries() {
let locs = LocalityConfig::new(2, 1, 2, 1).resolve_topology();
let mut reg = MachineRegistry::new();
for (i, loc) in locs.iter().enumerate() {
reg.register(ip(i + 1), loc.clone());
}
assert_eq!(reg.ips_in_domain(DomainLevel::Datacenter, "dc1").len(), 2);
assert_eq!(reg.ips_on_machine("dc1-z1-m1").len(), 1);
assert_eq!(reg.all_machines().len(), 4);
assert_eq!(
reg.all_datacenters(),
vec!["dc1".to_string(), "dc2".to_string()]
);
assert!(!reg.is_empty());
}
#[test]
fn id_for_matches_accessors() {
let loc = LocalityInfo::new("dc1", "dc1-z2", "dc1-z2-m3");
assert_eq!(loc.id_for(DomainLevel::Datacenter), loc.datacenter());
assert_eq!(loc.id_for(DomainLevel::Zone), loc.zone());
assert_eq!(loc.id_for(DomainLevel::Machine), loc.machine());
}
}