use std::collections::{HashMap, HashSet};
use std::fmt::Debug;
use std::sync::RwLock;
use crate::shard::FragmentId;
use crate::wire::MAX_WIRE_NODE_ID_BYTES;
pub trait PlacementController: Debug + Send + Sync {
fn desired_placement(&self) -> HashMap<FragmentId, Vec<String>>;
fn on_membership_change(&self, _members: &[String]) {}
fn on_fragment_ready(&self, _fragment: FragmentId) {}
}
#[derive(Debug, Clone)]
pub struct StaticPlacement {
map: HashMap<FragmentId, Vec<String>>,
}
impl StaticPlacement {
pub fn new(map: HashMap<FragmentId, Vec<String>>) -> Self {
Self { map }
}
}
impl PlacementController for StaticPlacement {
fn desired_placement(&self) -> HashMap<FragmentId, Vec<String>> {
self.map.clone()
}
}
#[derive(Debug, Default)]
pub struct ManualPlacement {
map: RwLock<HashMap<FragmentId, Vec<String>>>,
}
impl ManualPlacement {
pub fn new(map: HashMap<FragmentId, Vec<String>>) -> Self {
Self {
map: RwLock::new(map),
}
}
pub fn set_placement(&self, map: HashMap<FragmentId, Vec<String>>) {
*self.map.write().unwrap() = map;
}
pub fn placement(&self) -> HashMap<FragmentId, Vec<String>> {
self.map.read().unwrap().clone()
}
}
impl PlacementController for ManualPlacement {
fn desired_placement(&self) -> HashMap<FragmentId, Vec<String>> {
self.placement()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FragmentPlacementState {
Bootstrapping,
Serving,
Draining,
Stopped,
}
#[derive(Debug, Clone)]
pub struct PlacementReport {
pub placement: HashMap<FragmentId, Vec<String>>,
pub states: HashMap<FragmentId, FragmentPlacementState>,
pub dynamic: bool,
}
pub fn validate_placement(
map: &HashMap<FragmentId, Vec<String>>,
fragment_count: usize,
) -> Result<(), String> {
if let Some(fragment) = map
.keys()
.find(|fragment| fragment.0 as usize >= fragment_count)
{
return Err(format!("placement contains out-of-range {fragment}"));
}
for i in 0..fragment_count {
let frag = FragmentId(i as u32);
let Some(replicas) = map.get(&frag).filter(|replicas| !replicas.is_empty()) else {
return Err(format!("placement leaves {frag} with no replicas"));
};
let mut unique = HashSet::with_capacity(replicas.len());
for replica in replicas {
if replica.is_empty() || replica.len() > MAX_WIRE_NODE_ID_BYTES {
return Err(format!(
"placement {frag} has a replica id outside 1 to {MAX_WIRE_NODE_ID_BYTES} bytes"
));
}
if !unique.insert(replica) {
return Err(format!(
"placement {frag} contains duplicate replica {replica:?}"
));
}
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn static_placement_returns_map() {
let mut map = HashMap::new();
map.insert(FragmentId(0), vec!["a".to_string()]);
let c = StaticPlacement::new(map.clone());
assert_eq!(c.desired_placement(), map);
}
#[test]
fn manual_placement_is_mutable() {
let mut map = HashMap::new();
map.insert(FragmentId(0), vec!["a".to_string()]);
let c = ManualPlacement::new(map);
assert_eq!(c.desired_placement()[&FragmentId(0)], vec!["a".to_string()]);
let mut map2 = HashMap::new();
map2.insert(FragmentId(0), vec!["a".to_string(), "b".to_string()]);
c.set_placement(map2.clone());
assert_eq!(c.desired_placement(), map2);
}
#[test]
fn validate_rejects_missing_fragment() {
let map = HashMap::new();
assert!(validate_placement(&map, 2).is_err());
}
#[test]
fn validate_rejects_empty_replicas() {
let mut map = HashMap::new();
map.insert(FragmentId(0), vec!["a".to_string()]);
map.insert(FragmentId(1), vec![]);
assert!(validate_placement(&map, 2).is_err());
}
#[test]
fn validate_rejects_unrepresentable_duplicate_and_out_of_range_replicas() {
for map in [
HashMap::from([(FragmentId(0), vec![String::new()])]),
HashMap::from([(FragmentId(0), vec!["x".repeat(256)])]),
HashMap::from([(FragmentId(0), vec!["a".to_string(), "a".to_string()])]),
HashMap::from([
(FragmentId(0), vec!["a".to_string()]),
(FragmentId(1), vec!["b".to_string()]),
]),
] {
assert!(validate_placement(&map, 1).is_err());
}
}
#[test]
fn validate_accepts_full_map() {
let mut map = HashMap::new();
map.insert(FragmentId(0), vec!["a".to_string()]);
map.insert(FragmentId(1), vec!["b".to_string(), "a".to_string()]);
assert!(validate_placement(&map, 2).is_ok());
}
}