use std::collections::BTreeMap;
use std::collections::HashMap;
#[allow(dead_code)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ClusterNode {
pub node_id: String,
pub address: String,
pub weight: u32,
}
impl ClusterNode {
#[allow(dead_code)]
pub fn new(node_id: impl Into<String>, address: impl Into<String>, weight: u32) -> Self {
Self {
node_id: node_id.into(),
address: address.into(),
weight: weight.clamp(1, 100),
}
}
}
#[allow(dead_code)]
#[derive(Debug, Clone)]
pub struct Partition {
pub index: u32,
pub owner: String,
pub replicas: Vec<String>,
}
#[allow(dead_code)]
pub struct ConsistentHashRing {
ring: BTreeMap<u64, String>,
nodes: HashMap<String, ClusterNode>,
vnodes_per_weight: u32,
}
impl ConsistentHashRing {
#[allow(dead_code)]
#[must_use]
pub fn new(vnodes_per_weight: u32) -> Self {
Self {
ring: BTreeMap::new(),
nodes: HashMap::new(),
vnodes_per_weight,
}
}
#[allow(dead_code)]
pub fn add_node(&mut self, node: ClusterNode) {
let vnodes = node.weight * self.vnodes_per_weight;
for i in 0..vnodes {
let key = format!("{}-{}", node.node_id, i);
let hash = fnv1a_hash(key.as_bytes());
self.ring.insert(hash, node.node_id.clone());
}
self.nodes.insert(node.node_id.clone(), node);
}
#[allow(dead_code)]
pub fn remove_node(&mut self, node_id: &str) {
if let Some(node) = self.nodes.remove(node_id) {
let vnodes = node.weight * self.vnodes_per_weight;
for i in 0..vnodes {
let key = format!("{node_id}-{i}");
let hash = fnv1a_hash(key.as_bytes());
self.ring.remove(&hash);
}
}
}
#[allow(dead_code)]
#[must_use]
pub fn get_node(&self, key: &[u8]) -> Option<&ClusterNode> {
if self.ring.is_empty() {
return None;
}
let hash = fnv1a_hash(key);
let node_id = self
.ring
.range(hash..)
.next()
.or_else(|| self.ring.iter().next())
.map(|(_, v)| v.as_str())?;
self.nodes.get(node_id)
}
#[allow(dead_code)]
#[must_use]
pub fn node_count(&self) -> usize {
self.nodes.len()
}
#[allow(dead_code)]
#[must_use]
pub fn vnode_count(&self) -> usize {
self.ring.len()
}
}
#[allow(dead_code)]
fn fnv1a_hash(data: &[u8]) -> u64 {
const OFFSET_BASIS: u64 = 14695981039346656037;
const PRIME: u64 = 1099511628211;
let mut hash = OFFSET_BASIS;
for &byte in data {
hash ^= u64::from(byte);
hash = hash.wrapping_mul(PRIME);
}
hash
}
#[allow(dead_code)]
pub struct PartitionTable {
pub partition_count: u32,
assignments: Vec<Partition>,
}
impl PartitionTable {
#[allow(dead_code)]
#[must_use]
pub fn new(partition_count: u32, nodes: &[ClusterNode]) -> Self {
let mut ring = ConsistentHashRing::new(10);
for node in nodes {
ring.add_node(node.clone());
}
let mut assignments = Vec::with_capacity(partition_count as usize);
for i in 0..partition_count {
let key = i.to_le_bytes();
let owner = ring
.get_node(&key)
.map(|n| n.node_id.clone())
.unwrap_or_default();
assignments.push(Partition {
index: i,
owner,
replicas: Vec::new(),
});
}
Self {
partition_count,
assignments,
}
}
#[allow(dead_code)]
#[must_use]
pub fn owner_of(&self, partition_index: u32) -> Option<&str> {
self.assignments
.get(partition_index as usize)
.map(|p| p.owner.as_str())
}
#[allow(dead_code)]
#[must_use]
pub fn partitions_for_node<'a>(&'a self, node_id: &str) -> Vec<&'a Partition> {
self.assignments
.iter()
.filter(|p| p.owner == node_id)
.collect()
}
#[allow(dead_code)]
pub fn rebalance(&mut self, nodes: &[ClusterNode]) {
let new = PartitionTable::new(self.partition_count, nodes);
self.assignments = new.assignments;
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_nodes(n: usize) -> Vec<ClusterNode> {
(0..n)
.map(|i| ClusterNode::new(format!("node-{i}"), format!("127.0.0.1:{}", 7000 + i), 10))
.collect()
}
#[test]
fn test_cluster_node_weight_clamped() {
let node = ClusterNode::new("n0", "127.0.0.1:7000", 200);
assert_eq!(node.weight, 100);
let node2 = ClusterNode::new("n1", "127.0.0.1:7001", 0);
assert_eq!(node2.weight, 1);
}
#[test]
fn test_add_node_increases_vnode_count() {
let mut ring = ConsistentHashRing::new(10);
assert_eq!(ring.vnode_count(), 0);
ring.add_node(ClusterNode::new("n0", "127.0.0.1:7000", 1));
assert_eq!(ring.vnode_count(), 10);
}
#[test]
fn test_remove_node_decreases_vnode_count() {
let mut ring = ConsistentHashRing::new(10);
ring.add_node(ClusterNode::new("n0", "127.0.0.1:7000", 1));
ring.add_node(ClusterNode::new("n1", "127.0.0.1:7001", 1));
ring.remove_node("n0");
assert_eq!(ring.node_count(), 1);
assert_eq!(ring.vnode_count(), 10);
}
#[test]
fn test_get_node_empty_ring_returns_none() {
let ring = ConsistentHashRing::new(10);
assert!(ring.get_node(b"some_key").is_none());
}
#[test]
fn test_get_node_returns_some() {
let mut ring = ConsistentHashRing::new(10);
ring.add_node(ClusterNode::new("n0", "127.0.0.1:7000", 1));
assert!(ring.get_node(b"video/segment/0001").is_some());
}
#[test]
fn test_consistent_hashing_same_key_same_node() {
let mut ring = ConsistentHashRing::new(20);
for node in make_nodes(4) {
ring.add_node(node);
}
let node1 = ring.get_node(b"job-abc").map(|n| n.node_id.clone());
let node2 = ring.get_node(b"job-abc").map(|n| n.node_id.clone());
assert_eq!(node1, node2);
}
#[test]
fn test_fnv1a_hash_deterministic() {
let h1 = fnv1a_hash(b"hello");
let h2 = fnv1a_hash(b"hello");
assert_eq!(h1, h2);
}
#[test]
fn test_fnv1a_hash_different_inputs() {
assert_ne!(fnv1a_hash(b"foo"), fnv1a_hash(b"bar"));
}
#[test]
fn test_partition_table_all_partitions_assigned() {
let nodes = make_nodes(3);
let table = PartitionTable::new(64, &nodes);
for i in 0..64 {
assert!(!table.owner_of(i).unwrap_or("").is_empty());
}
}
#[test]
fn test_partition_table_owner_of_out_of_range() {
let nodes = make_nodes(2);
let table = PartitionTable::new(16, &nodes);
assert!(table.owner_of(100).is_none());
}
#[test]
fn test_partitions_for_node_coverage() {
let nodes = make_nodes(4);
let table = PartitionTable::new(64, &nodes);
let total: usize = nodes
.iter()
.map(|n| table.partitions_for_node(&n.node_id).len())
.sum();
assert_eq!(total, 64);
}
#[test]
fn test_rebalance_after_node_removal() {
let mut nodes = make_nodes(4);
let mut table = PartitionTable::new(32, &nodes);
nodes.remove(3);
table.rebalance(&nodes);
for i in 0..32 {
let owner = table.owner_of(i).unwrap_or("");
assert!(owner.starts_with("node-"));
assert_ne!(owner, "node-3");
}
}
#[test]
fn test_partition_table_empty_nodes() {
let table = PartitionTable::new(8, &[]);
for i in 0..8 {
assert_eq!(table.owner_of(i), Some(""));
}
}
}