use crate::raft::NodeId;
pub const DEFAULT_PARTITIONS: u32 = 256;
const FNV_OFFSET: u64 = 14695981039346656037;
const FNV_PRIME: u64 = 1099511628211;
#[inline]
fn fnv1a(bytes: &[u8]) -> u64 {
let mut h = FNV_OFFSET;
for &b in bytes {
h ^= b as u64;
h = h.wrapping_mul(FNV_PRIME);
}
h
}
pub fn partition_of(key: &str, partitions: u32) -> u32 {
let n = partitions.max(1);
(fnv1a(key.as_bytes()) % n as u64) as u32
}
#[inline]
fn hrw_score(partition: u32, node: NodeId) -> u64 {
let mut buf = [0u8; 12];
buf[..8].copy_from_slice(&node.to_le_bytes());
buf[8..].copy_from_slice(&partition.to_le_bytes());
fnv1a(&buf)
}
pub fn owner_of_partition(partition: u32, members: &[NodeId]) -> Option<NodeId> {
members
.iter()
.copied()
.map(|node| (hrw_score(partition, node), node))
.max()
.map(|(_, node)| node)
}
pub fn partitions_owned_by(me: NodeId, members: &[NodeId], partitions: u32) -> Vec<u32> {
let n = partitions.max(1);
(0..n)
.filter(|&p| owner_of_partition(p, members) == Some(me))
.collect()
}
pub fn owns_key(me: NodeId, key: &str, members: &[NodeId], partitions: u32) -> bool {
owner_of_partition(partition_of(key, partitions), members) == Some(me)
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::BTreeSet;
#[test]
fn assignment_is_deterministic_and_process_independent() {
let members = vec![1u64, 2, 3, 4, 5];
for p in 0..DEFAULT_PARTITIONS {
let a = owner_of_partition(p, &members);
let b = owner_of_partition(p, &members);
assert_eq!(a, b);
assert!(a.is_some());
}
assert_eq!(
partition_of("account-42", DEFAULT_PARTITIONS),
partition_of("account-42", DEFAULT_PARTITIONS)
);
}
#[test]
fn every_partition_owned_by_exactly_one_member() {
let members = vec![10u64, 20, 30];
let mut union: BTreeSet<u32> = BTreeSet::new();
for &m in &members {
for p in partitions_owned_by(m, &members, DEFAULT_PARTITIONS) {
assert!(
union.insert(p),
"partition {p} owned by more than one member"
);
}
}
assert_eq!(union.len(), DEFAULT_PARTITIONS as usize);
}
#[test]
fn load_is_roughly_balanced() {
let members = vec![1u64, 2, 3, 4];
let per = DEFAULT_PARTITIONS as usize / members.len();
for &m in &members {
let owned = partitions_owned_by(m, &members, DEFAULT_PARTITIONS).len();
assert!(
owned >= per / 2 && owned <= per * 3 / 2,
"member {m} owns {owned}, expected ~{per}"
);
}
}
#[test]
fn membership_change_moves_minimal_partitions() {
let before = vec![1u64, 2, 3];
let after = vec![1u64, 2, 3, 4];
let mut moved = 0;
for p in 0..DEFAULT_PARTITIONS {
let o1 = owner_of_partition(p, &before).unwrap();
let o2 = owner_of_partition(p, &after).unwrap();
if o1 != o2 {
moved += 1;
assert_eq!(o2, 4, "partition {p} moved to a non-new node");
}
}
let expected = DEFAULT_PARTITIONS as usize / after.len();
assert!(
moved > 0 && moved < DEFAULT_PARTITIONS as usize / 2,
"moved {moved}, expected ~{expected} (minimal reshuffle)"
);
}
#[test]
fn single_member_owns_everything() {
let members = vec![7u64];
assert_eq!(
partitions_owned_by(7, &members, DEFAULT_PARTITIONS).len(),
DEFAULT_PARTITIONS as usize
);
assert!(owns_key(7, "any-account", &members, DEFAULT_PARTITIONS));
}
#[test]
fn empty_members_owns_nothing() {
assert_eq!(owner_of_partition(0, &[]), None);
assert!(!owns_key(1, "acc", &[], DEFAULT_PARTITIONS));
}
}