1use std::collections::HashMap;
17use std::net::IpAddr;
18
19use super::builder::ProcessCount;
20
21#[derive(Clone, Copy, Debug, PartialEq, Eq)]
23pub enum DomainLevel {
24 Datacenter,
26 Zone,
28 Machine,
30}
31
32#[derive(Debug, Clone, PartialEq, Eq, Default)]
38pub struct LocalityInfo {
39 datacenter: String,
40 zone: String,
41 machine: String,
42}
43
44impl LocalityInfo {
45 #[must_use]
47 pub fn new(
48 datacenter: impl Into<String>,
49 zone: impl Into<String>,
50 machine: impl Into<String>,
51 ) -> Self {
52 Self {
53 datacenter: datacenter.into(),
54 zone: zone.into(),
55 machine: machine.into(),
56 }
57 }
58
59 #[must_use]
61 pub fn datacenter(&self) -> &str {
62 &self.datacenter
63 }
64
65 #[must_use]
67 pub fn zone(&self) -> &str {
68 &self.zone
69 }
70
71 #[must_use]
73 pub fn machine(&self) -> &str {
74 &self.machine
75 }
76
77 #[must_use]
79 pub fn id_for(&self, level: DomainLevel) -> &str {
80 match level {
81 DomainLevel::Datacenter => &self.datacenter,
82 DomainLevel::Zone => &self.zone,
83 DomainLevel::Machine => &self.machine,
84 }
85 }
86}
87
88#[derive(Debug, Clone)]
96pub struct LocalityConfig {
97 datacenters: ProcessCount,
98 zones_per_datacenter: ProcessCount,
99 machines_per_zone: ProcessCount,
100 processes_per_machine: ProcessCount,
101}
102
103impl LocalityConfig {
104 #[must_use]
107 pub fn new(
108 datacenters: impl Into<ProcessCount>,
109 zones_per_datacenter: impl Into<ProcessCount>,
110 machines_per_zone: impl Into<ProcessCount>,
111 processes_per_machine: impl Into<ProcessCount>,
112 ) -> Self {
113 Self {
114 datacenters: datacenters.into(),
115 zones_per_datacenter: zones_per_datacenter.into(),
116 machines_per_zone: machines_per_zone.into(),
117 processes_per_machine: processes_per_machine.into(),
118 }
119 }
120
121 pub(crate) fn resolve_topology(&self) -> Vec<LocalityInfo> {
128 let datacenters = self.datacenters.resolve().max(1);
129 let zones = self.zones_per_datacenter.resolve().max(1);
130 let machines = self.machines_per_zone.resolve().max(1);
131 let processes = self.processes_per_machine.resolve().max(1);
132
133 let mut out = Vec::with_capacity(datacenters * zones * machines * processes);
134 for d in 0..datacenters {
135 for z in 0..zones {
136 for m in 0..machines {
137 let datacenter = format!("dc{}", d + 1);
138 let zone = format!("dc{}-z{}", d + 1, z + 1);
139 let machine = format!("dc{}-z{}-m{}", d + 1, z + 1, m + 1);
140 for _ in 0..processes {
141 out.push(LocalityInfo::new(
142 datacenter.clone(),
143 zone.clone(),
144 machine.clone(),
145 ));
146 }
147 }
148 }
149 }
150 out
151 }
152}
153
154#[derive(Debug, Clone, Default, PartialEq)]
160pub struct MachineRegistry {
161 ip_locality: HashMap<IpAddr, LocalityInfo>,
162}
163
164impl MachineRegistry {
165 #[must_use]
167 pub fn new() -> Self {
168 Self {
169 ip_locality: HashMap::new(),
170 }
171 }
172
173 pub fn register(&mut self, ip: IpAddr, locality: LocalityInfo) {
175 self.ip_locality.insert(ip, locality);
176 }
177
178 #[must_use]
180 pub fn is_empty(&self) -> bool {
181 self.ip_locality.is_empty()
182 }
183
184 #[must_use]
186 pub fn locality_for(&self, ip: IpAddr) -> Option<&LocalityInfo> {
187 self.ip_locality.get(&ip)
188 }
189
190 #[must_use]
192 pub fn ips_in_domain(&self, level: DomainLevel, id: &str) -> Vec<IpAddr> {
193 self.ip_locality
194 .iter()
195 .filter(|(_, loc)| loc.id_for(level) == id)
196 .map(|(ip, _)| *ip)
197 .collect()
198 }
199
200 #[must_use]
202 pub fn ips_on_machine(&self, machine_id: &str) -> Vec<IpAddr> {
203 self.ips_in_domain(DomainLevel::Machine, machine_id)
204 }
205
206 #[must_use]
208 pub fn all_machines(&self) -> Vec<String> {
209 self.distinct_ids(DomainLevel::Machine)
210 }
211
212 #[must_use]
214 pub fn all_zones(&self) -> Vec<String> {
215 self.distinct_ids(DomainLevel::Zone)
216 }
217
218 #[must_use]
220 pub fn all_datacenters(&self) -> Vec<String> {
221 self.distinct_ids(DomainLevel::Datacenter)
222 }
223
224 fn distinct_ids(&self, level: DomainLevel) -> Vec<String> {
225 let mut ids: Vec<String> = self
226 .ip_locality
227 .values()
228 .map(|loc| loc.id_for(level).to_string())
229 .collect();
230 ids.sort();
231 ids.dedup();
232 ids
233 }
234}
235
236#[cfg(test)]
237mod tests {
238 use super::*;
239
240 fn ip(n: usize) -> IpAddr {
241 format!("10.0.1.{n}").parse().expect("valid IP")
242 }
243
244 #[test]
245 fn resolve_topology_slices_contiguously() {
246 let locs = LocalityConfig::new(2, 2, 2, 2).resolve_topology();
248 assert_eq!(locs.len(), 16);
249
250 assert_eq!(locs[0].machine(), "dc1-z1-m1");
252 assert_eq!(locs[1].machine(), "dc1-z1-m1");
253 assert_eq!(locs[2].machine(), "dc1-z1-m2");
254
255 assert_eq!(locs[0].zone(), "dc1-z1");
257 assert_eq!(locs[4].zone(), "dc1-z2");
258
259 assert_eq!(locs[7].datacenter(), "dc1");
261 assert_eq!(locs[8].datacenter(), "dc2");
262 }
263
264 #[test]
265 fn globally_unique_machine_ids_across_datacenters() {
266 let locs = LocalityConfig::new(2, 1, 1, 1).resolve_topology();
267 assert_eq!(locs.len(), 2);
268 assert_ne!(locs[0].machine(), locs[1].machine());
269 assert_eq!(locs[0].machine(), "dc1-z1-m1");
270 assert_eq!(locs[1].machine(), "dc2-z1-m1");
271 }
272
273 #[test]
274 fn registry_domain_queries() {
275 let locs = LocalityConfig::new(2, 1, 2, 1).resolve_topology();
277 let mut reg = MachineRegistry::new();
278 for (i, loc) in locs.iter().enumerate() {
279 reg.register(ip(i + 1), loc.clone());
280 }
281
282 assert_eq!(reg.ips_in_domain(DomainLevel::Datacenter, "dc1").len(), 2);
283 assert_eq!(reg.ips_on_machine("dc1-z1-m1").len(), 1);
284 assert_eq!(reg.all_machines().len(), 4);
285 assert_eq!(
286 reg.all_datacenters(),
287 vec!["dc1".to_string(), "dc2".to_string()]
288 );
289 assert!(!reg.is_empty());
290 }
291
292 #[test]
293 fn id_for_matches_accessors() {
294 let loc = LocalityInfo::new("dc1", "dc1-z2", "dc1-z2-m3");
295 assert_eq!(loc.id_for(DomainLevel::Datacenter), loc.datacenter());
296 assert_eq!(loc.id_for(DomainLevel::Zone), loc.zone());
297 assert_eq!(loc.id_for(DomainLevel::Machine), loc.machine());
298 }
299}