use hashring::HashRing;
use std::fmt;
use std::hash::Hash;
#[derive(Debug, Clone, Hash, PartialEq)]
pub struct VNode {
id: usize,
name: String,
}
impl VNode {
fn new(id: usize, name: String) -> Self {
VNode { id, name }
}
}
impl fmt::Display for VNode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}|{}", self.name, self.id)
}
}
impl VNode {
pub fn name(&self) -> &str {
&self.name
}
}
pub struct VNodeHashRing {
replica_count: usize,
ring: HashRing<VNode>,
}
impl VNodeHashRing {
pub fn new(replica_count: usize) -> Self {
VNodeHashRing {
replica_count,
ring: HashRing::new(),
}
}
pub fn add(&mut self, name: String) {
for id in 0..self.replica_count {
let vnode = VNode::new(id, name.clone());
self.ring.add(vnode);
}
}
pub fn get<U: Hash>(&self, key: &U) -> Option<&VNode> {
self.ring.get(key)
}
pub fn get_with_replicas<U: Hash>(&self, key: &U, replicas: usize) -> Option<Vec<VNode>> {
self.ring.get_with_replicas(key, replicas)
}
pub fn len(&self) -> usize {
self.ring.len()
}
pub fn is_empty(&self) -> bool {
self.ring.len() == 0
}
}
#[cfg(test)]
mod tests {
#![allow(clippy::type_complexity)]
use super::*;
use uuid::Uuid;
#[test]
fn vnode_formats_as_name_and_id() {
let vnode = VNode::new(1, "default-pod-1".to_string());
assert_eq!(vnode.id, 1);
assert_eq!(vnode.name(), "default-pod-1");
assert_eq!(vnode.to_string(), "default-pod-1|1");
}
#[test]
fn add_inserts_replica_count_vnodes_per_node() {
let test_cases = vec![
(3, vec![], 0),
(2, vec!["default-pod-1"], 2),
(2, vec!["default-pod-1", "default-pod-2"], 4),
(0, vec!["default-pod-1"], 0),
];
for (replica_count, names, expected_len) in test_cases {
let mut ring = VNodeHashRing::new(replica_count);
for name in names {
ring.add(name.to_string());
}
assert_eq!(ring.replica_count, replica_count);
assert_eq!(ring.len(), expected_len);
assert_eq!(ring.is_empty(), expected_len == 0);
}
}
#[test]
fn get_returns_a_vnode_of_an_added_node() {
let test_cases: Vec<(Vec<&str>, fn(Option<&VNode>))> = vec![
(vec![], |vnode| assert!(vnode.is_none())),
(vec!["default-pod-1", "default-pod-2"], |vnode| {
let vnode = vnode.unwrap();
assert!(["default-pod-1", "default-pod-2"].contains(&vnode.name()) && vnode.id < 2);
}),
];
for (names, expect) in test_cases {
let mut ring = VNodeHashRing::new(2);
for name in names {
ring.add(name.to_string());
}
expect(ring.get(&"test_key"));
}
}
#[test]
fn get_with_replicas_spans_the_nodes() {
let test_cases: Vec<(Vec<&str>, usize, fn(Option<Vec<VNode>>))> = vec![
(vec![], 2, |vnodes| assert!(vnodes.is_none())),
(vec!["default-pod-1", "default-pod-2"], 2, |vnodes| {
let vnodes = vnodes.unwrap();
assert_eq!(vnodes.len(), 3);
assert!(vnodes.iter().all(|vnode| ["default-pod-1", "default-pod-2"]
.contains(&vnode.name())
&& vnode.id < 2));
}),
(vec!["default-pod-1", "default-pod-2"], 3, |vnodes| {
let vnodes = vnodes.unwrap();
assert_eq!(vnodes.len(), 4);
assert!(vnodes.iter().all(|vnode| ["default-pod-1", "default-pod-2"]
.contains(&vnode.name())
&& vnode.id < 2));
}),
(vec!["default-pod-1", "default-pod-2"], 4, |vnodes| {
let vnodes = vnodes.unwrap();
assert_eq!(vnodes.len(), 5);
assert!(vnodes.iter().all(|vnode| ["default-pod-1", "default-pod-2"]
.contains(&vnode.name())
&& vnode.id < 2));
}),
];
for (names, replicas, expect) in test_cases {
let mut ring = VNodeHashRing::new(2);
for name in names {
ring.add(name.to_string());
}
expect(ring.get_with_replicas(&"test_key", replicas));
}
}
#[test]
fn add_order_does_not_affect_get_result() {
let mut ring_a = VNodeHashRing::new(150);
for name in ["default-pod-1", "default-pod-2", "default-pod-3"] {
ring_a.add(name.to_string());
}
let mut ring_b = VNodeHashRing::new(150);
for name in ["default-pod-3", "default-pod-1", "default-pod-2"] {
ring_b.add(name.to_string());
}
for _ in 0..200 {
let key = Uuid::new_v4().to_string();
let vnode_a = ring_a.get(&key).unwrap();
let vnode_b = ring_b.get(&key).unwrap();
assert_eq!(vnode_a.to_string(), vnode_b.to_string());
}
}
}