use std::collections::BTreeMap;
use std::str::FromStr;
use mongreldb_types::ids::NodeId;
use serde::{Deserialize, Serialize};
use crate::node::{Locality, NodeDescriptor, NodeState};
use crate::placement::{PlacementError, PlacementPolicy};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum VoterDistribution {
SingleRegion {
region: String,
voters: u32,
},
MultiRegion {
voters_per_region: BTreeMap<String, u32>,
},
}
impl VoterDistribution {
pub fn total_voters(&self) -> u32 {
match self {
Self::SingleRegion { voters, .. } => *voters,
Self::MultiRegion { voters_per_region } => voters_per_region.values().sum(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct MultiRegionPolicy {
pub voters: VoterDistribution,
pub tenant_home_region: Option<String>,
pub regional_followers: bool,
pub async_dr_regions: Vec<String>,
pub prefer_home_leader: bool,
}
impl Default for MultiRegionPolicy {
fn default() -> Self {
Self {
voters: VoterDistribution::SingleRegion {
region: "default".into(),
voters: 3,
},
tenant_home_region: None,
regional_followers: false,
async_dr_regions: Vec::new(),
prefer_home_leader: true,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum MultiRegionReadPolicy {
LinearizableGlobal,
RegionalReadYourWrites,
BoundedStalenessLocal {
max_lag_ms: u64,
},
AsyncDr,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct RegionHealthSample {
pub region: String,
pub clock_skew_micros: u64,
pub rtt_ms: u64,
pub skew_exceeded: bool,
}
pub fn validate_multi_region(
policy: &MultiRegionPolicy,
nodes: &[NodeDescriptor],
) -> Result<(), PlacementError> {
let up: Vec<&NodeDescriptor> = nodes.iter().filter(|n| n.state == NodeState::Up).collect();
match &policy.voters {
VoterDistribution::SingleRegion { region, voters } => {
let count = up
.iter()
.filter(|n| n.locality.get("region") == Some(region.as_str()))
.count() as u32;
if count < *voters {
return Err(PlacementError::Infeasible(format!(
"region {region} has {count} Up nodes, need {voters} voters"
)));
}
}
VoterDistribution::MultiRegion { voters_per_region } => {
for (region, need) in voters_per_region {
let count = up
.iter()
.filter(|n| n.locality.get("region") == Some(region.as_str()))
.count() as u32;
if count < *need {
return Err(PlacementError::Infeasible(format!(
"region {region} has {count} Up nodes, need {need} voters"
)));
}
}
}
}
let _ = policy.prefer_home_leader;
Ok(())
}
pub fn prefer_leader(
policy: &MultiRegionPolicy,
candidates: &[(NodeId, String)],
) -> Option<NodeId> {
if !policy.prefer_home_leader {
return candidates.first().map(|(id, _)| *id);
}
if let Some(home) = &policy.tenant_home_region {
if let Some((id, _)) = candidates.iter().find(|(_, region)| region == home) {
return Some(*id);
}
}
candidates.first().map(|(id, _)| *id)
}
pub fn placement_from_multi_region(policy: &MultiRegionPolicy) -> PlacementPolicy {
PlacementPolicy {
replicas: u8::try_from(policy.voters.total_voters().max(1)).unwrap_or(u8::MAX),
..Default::default()
}
}
pub fn locality_of(region: &str, zone: Option<&str>) -> Locality {
let text = match zone {
Some(z) => format!("region={region},zone={z}"),
None => format!("region={region}"),
};
Locality::from_str(&text).expect("well-formed locality")
}
#[cfg(test)]
mod tests {
use super::*;
use crate::node::{BuildVersion, NodeCapacity, NodeDescriptor, NodeState, VersionInfo};
use mongreldb_types::ids::NodeId;
fn node(byte: u8, region: &str, state: NodeState) -> NodeDescriptor {
NodeDescriptor {
node_id: NodeId::from_bytes({
let mut b = [0u8; 16];
b[15] = byte;
b
}),
rpc_address: format!("n{byte}:8453"),
locality: locality_of(region, None),
capacity: NodeCapacity::default(),
state,
version: BuildVersion::current(),
version_info: VersionInfo::current(),
}
}
#[test]
fn single_region_requires_enough_up_nodes() {
let policy = MultiRegionPolicy {
voters: VoterDistribution::SingleRegion {
region: "us".into(),
voters: 3,
},
..MultiRegionPolicy::default()
};
let nodes = vec![
node(1, "us", NodeState::Up),
node(2, "us", NodeState::Up),
node(3, "eu", NodeState::Up),
];
assert!(validate_multi_region(&policy, &nodes).is_err());
let nodes = vec![
node(1, "us", NodeState::Up),
node(2, "us", NodeState::Up),
node(3, "us", NodeState::Up),
];
assert!(validate_multi_region(&policy, &nodes).is_ok());
}
#[test]
fn prefer_home_leader() {
let policy = MultiRegionPolicy {
tenant_home_region: Some("eu".into()),
prefer_home_leader: true,
..MultiRegionPolicy::default()
};
let a = NodeId::from_bytes([1; 16]);
let b = NodeId::from_bytes([2; 16]);
let candidates = vec![(a, "us".into()), (b, "eu".into())];
assert_eq!(prefer_leader(&policy, &candidates), Some(b));
}
#[test]
fn no_multi_leader_default() {
let p = MultiRegionPolicy::default();
assert!(matches!(p.voters, VoterDistribution::SingleRegion { .. }));
assert_eq!(placement_from_multi_region(&p).replicas, 3);
}
}