use std::collections::{BTreeSet, HashMap, HashSet};
#[derive(Debug)]
pub(super) struct RegionRequirement {
receiver_region: String,
outside_nodes: HashSet<String>,
}
impl RegionRequirement {
pub(super) fn resolve(
receiver: &str,
eligible: &HashSet<String>,
regions: &HashMap<String, String>,
) -> Result<Self, String> {
if !eligible.contains(receiver) {
return Err("receiving node is not an eligible replica".to_string());
}
let receiver_region = regions
.get(receiver)
.ok_or_else(|| "receiving node has no configured region".to_string())?;
let mut outside_nodes = HashSet::new();
for peer in eligible {
let region = regions
.get(peer)
.ok_or_else(|| "eligible replica has no configured region".to_string())?;
if peer != receiver && region != receiver_region {
outside_nodes.insert(peer.clone());
}
}
if outside_nodes.is_empty() {
return Err("eligible replicas do not include another region".to_string());
}
Ok(Self {
receiver_region: receiver_region.clone(),
outside_nodes,
})
}
pub(super) fn receiver_region(&self) -> &str {
&self.receiver_region
}
pub(super) fn outside_nodes(&self) -> &HashSet<String> {
&self.outside_nodes
}
pub(super) fn satisfied_by(&self, acknowledged: &BTreeSet<String>) -> bool {
self.outside_nodes
.iter()
.any(|peer| acknowledged.contains(peer))
}
}
#[cfg(test)]
mod tests {
use super::*;
fn eligible() -> HashSet<String> {
["receiver", "same", "outside"]
.into_iter()
.map(str::to_string)
.collect()
}
fn regions() -> HashMap<String, String> {
[
("receiver", "west"),
("same", "west"),
("outside", "east"),
("ineligible", "east"),
]
.into_iter()
.map(|(node, region)| (node.to_string(), region.to_string()))
.collect()
}
#[test]
fn region_requirement_rejects_missing_and_impossible_configuration() {
for missing in ["receiver", "same", "outside"] {
let mut regions = regions();
regions.remove(missing);
assert!(RegionRequirement::resolve("receiver", &eligible(), ®ions).is_err());
}
let mut regions = regions();
regions.insert("outside".to_string(), "west".to_string());
assert!(RegionRequirement::resolve("receiver", &eligible(), ®ions).is_err());
assert!(RegionRequirement::resolve("ineligible", &eligible(), ®ions).is_err());
assert!(RegionRequirement::resolve(
"receiver",
&HashSet::from(["receiver".to_string()]),
®ions,
)
.is_err());
}
#[test]
fn only_eligible_outside_region_acknowledgements_satisfy_requirement() {
let requirement = RegionRequirement::resolve("receiver", &eligible(), ®ions()).unwrap();
assert_eq!(requirement.receiver_region(), "west");
assert_eq!(
requirement.outside_nodes(),
&HashSet::from(["outside".to_string()]),
);
let mut acknowledgements = BTreeSet::from([
"receiver".to_string(),
"same".to_string(),
"ineligible".to_string(),
]);
assert!(!requirement.satisfied_by(&acknowledgements));
acknowledgements.insert("same".to_string());
assert!(!requirement.satisfied_by(&acknowledgements));
acknowledgements.insert("outside".to_string());
assert!(requirement.satisfied_by(&acknowledgements));
}
#[test]
fn requirement_freezes_receiver_and_eligible_regions() {
let mut regions = regions();
let requirement = RegionRequirement::resolve("receiver", &eligible(), ®ions).unwrap();
regions.insert("receiver".to_string(), "east".to_string());
regions.insert("same".to_string(), "east".to_string());
regions.insert("outside".to_string(), "west".to_string());
assert_eq!(requirement.receiver_region(), "west");
assert!(!requirement.satisfied_by(&BTreeSet::from(["same".to_string()])));
assert!(requirement.satisfied_by(&BTreeSet::from(["outside".to_string()])));
}
#[test]
fn requirement_uses_the_receiving_nodes_region() {
let requirement = RegionRequirement::resolve("outside", &eligible(), ®ions()).unwrap();
assert_eq!(requirement.receiver_region(), "east");
assert_eq!(
requirement.outside_nodes(),
&HashSet::from(["receiver".to_string(), "same".to_string()]),
);
}
}