1#![allow(dead_code)]
2use std::collections::{BTreeMap, HashMap};
12use std::fmt;
13
14#[derive(Debug, Clone, PartialEq, Eq, Hash)]
16pub struct NodeId {
17 pub id: String,
19 pub label: String,
21 pub weight: u32,
23}
24
25impl NodeId {
26 #[must_use]
28 pub fn new(id: &str, label: &str) -> Self {
29 Self {
30 id: id.to_string(),
31 label: label.to_string(),
32 weight: 100,
33 }
34 }
35
36 #[must_use]
38 pub fn with_weight(mut self, weight: u32) -> Self {
39 self.weight = weight;
40 self
41 }
42}
43
44impl fmt::Display for NodeId {
45 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
46 write!(f, "{}({})", self.label, self.id)
47 }
48}
49
50#[derive(Debug, Clone)]
52struct VNode {
53 hash: u64,
55 node_id: String,
57 vnode_index: u32,
59}
60
61#[derive(Debug, Clone)]
63pub struct ShardMapConfig {
64 pub vnodes_per_node: u32,
66 pub use_weights: bool,
68}
69
70impl Default for ShardMapConfig {
71 fn default() -> Self {
72 Self {
73 vnodes_per_node: 150,
74 use_weights: true,
75 }
76 }
77}
78
79#[derive(Debug, Clone)]
81pub struct ShardMap {
82 ring: BTreeMap<u64, String>,
84 nodes: HashMap<String, NodeId>,
86 config: ShardMapConfig,
88}
89
90impl ShardMap {
91 #[must_use]
93 pub fn new() -> Self {
94 Self {
95 ring: BTreeMap::new(),
96 nodes: HashMap::new(),
97 config: ShardMapConfig::default(),
98 }
99 }
100
101 #[must_use]
103 pub fn with_config(config: ShardMapConfig) -> Self {
104 Self {
105 ring: BTreeMap::new(),
106 nodes: HashMap::new(),
107 config,
108 }
109 }
110
111 pub fn add_node(&mut self, node: NodeId) {
113 let vnode_count = self.effective_vnodes(&node);
114 for i in 0..vnode_count {
115 let key = format!("{}:vnode:{}", node.id, i);
116 let hash = Self::hash_key(&key);
117 self.ring.insert(hash, node.id.clone());
118 }
119 self.nodes.insert(node.id.clone(), node);
120 }
121
122 pub fn remove_node(&mut self, node_id: &str) -> bool {
124 if let Some(node) = self.nodes.remove(node_id) {
125 let vnode_count = self.effective_vnodes(&node);
126 for i in 0..vnode_count {
127 let key = format!("{}:vnode:{}", node.id, i);
128 let hash = Self::hash_key(&key);
129 self.ring.remove(&hash);
130 }
131 true
132 } else {
133 false
134 }
135 }
136
137 #[must_use]
139 pub fn lookup(&self, key: &str) -> Option<&str> {
140 if self.ring.is_empty() {
141 return None;
142 }
143 let hash = Self::hash_key(key);
144 if let Some((_h, node_id)) = self.ring.range(hash..).next() {
146 return Some(node_id.as_str());
147 }
148 self.ring.values().next().map(std::string::String::as_str)
150 }
151
152 #[must_use]
154 pub fn node_count(&self) -> usize {
155 self.nodes.len()
156 }
157
158 #[must_use]
160 pub fn vnode_count(&self) -> usize {
161 self.ring.len()
162 }
163
164 #[must_use]
166 pub fn has_node(&self, node_id: &str) -> bool {
167 self.nodes.contains_key(node_id)
168 }
169
170 #[must_use]
172 pub fn node_ids(&self) -> Vec<&str> {
173 self.nodes.keys().map(std::string::String::as_str).collect()
174 }
175
176 #[must_use]
178 pub fn load_distribution(&self) -> HashMap<String, usize> {
179 let mut dist: HashMap<String, usize> = HashMap::new();
180 for node_id in self.ring.values() {
181 *dist.entry(node_id.clone()).or_insert(0) += 1;
182 }
183 dist
184 }
185
186 #[allow(clippy::cast_precision_loss)]
188 #[must_use]
189 pub fn load_factor(&self) -> f64 {
190 let dist = self.load_distribution();
191 if dist.is_empty() {
192 return 0.0;
193 }
194 let counts: Vec<f64> = dist.values().map(|&c| c as f64).collect();
195 let mean = counts.iter().sum::<f64>() / counts.len() as f64;
196 if mean == 0.0 {
197 return 0.0;
198 }
199 let variance = counts.iter().map(|c| (c - mean).powi(2)).sum::<f64>() / counts.len() as f64;
200 variance.sqrt() / mean
201 }
202
203 #[allow(
205 clippy::cast_precision_loss,
206 clippy::cast_possible_truncation,
207 clippy::cast_sign_loss
208 )]
209 fn effective_vnodes(&self, node: &NodeId) -> u32 {
210 if self.config.use_weights {
211 let scaled = f64::from(self.config.vnodes_per_node) * (f64::from(node.weight) / 100.0);
212 scaled.round() as u32
213 } else {
214 self.config.vnodes_per_node
215 }
216 }
217
218 fn hash_key(key: &str) -> u64 {
220 let mut hash: u64 = 0xcbf2_9ce4_8422_2325;
221 for byte in key.as_bytes() {
222 hash ^= u64::from(*byte);
223 hash = hash.wrapping_mul(0x0100_0000_01b3);
224 }
225 hash
226 }
227}
228
229impl Default for ShardMap {
230 fn default() -> Self {
231 Self::new()
232 }
233}
234
235#[derive(Debug, Clone)]
237pub struct BatchAssignment {
238 pub assignments: HashMap<String, String>,
240 pub unassigned: Vec<String>,
242}
243
244impl BatchAssignment {
245 #[must_use]
247 pub fn new() -> Self {
248 Self {
249 assignments: HashMap::new(),
250 unassigned: Vec::new(),
251 }
252 }
253
254 #[must_use]
256 pub fn assigned_count(&self) -> usize {
257 self.assignments.len()
258 }
259
260 #[must_use]
262 pub fn unassigned_count(&self) -> usize {
263 self.unassigned.len()
264 }
265}
266
267impl Default for BatchAssignment {
268 fn default() -> Self {
269 Self::new()
270 }
271}
272
273#[must_use]
275pub fn batch_assign(shard_map: &ShardMap, keys: &[&str]) -> BatchAssignment {
276 let mut result = BatchAssignment::new();
277 for &key in keys {
278 if let Some(node_id) = shard_map.lookup(key) {
279 result
280 .assignments
281 .insert(key.to_string(), node_id.to_string());
282 } else {
283 result.unassigned.push(key.to_string());
284 }
285 }
286 result
287}
288
289#[cfg(test)]
290mod tests {
291 use super::*;
292
293 #[test]
294 fn test_node_id_creation() {
295 let node = NodeId::new("node-1", "Worker 1");
296 assert_eq!(node.id, "node-1");
297 assert_eq!(node.label, "Worker 1");
298 assert_eq!(node.weight, 100);
299 }
300
301 #[test]
302 fn test_node_id_with_weight() {
303 let node = NodeId::new("n1", "N1").with_weight(200);
304 assert_eq!(node.weight, 200);
305 }
306
307 #[test]
308 fn test_node_id_display() {
309 let node = NodeId::new("n1", "Worker");
310 assert_eq!(node.to_string(), "Worker(n1)");
311 }
312
313 #[test]
314 fn test_shard_map_empty() {
315 let sm = ShardMap::new();
316 assert_eq!(sm.node_count(), 0);
317 assert_eq!(sm.vnode_count(), 0);
318 assert_eq!(sm.lookup("any-key"), None);
319 }
320
321 #[test]
322 fn test_shard_map_add_node() {
323 let mut sm = ShardMap::new();
324 sm.add_node(NodeId::new("n1", "Node 1"));
325 assert_eq!(sm.node_count(), 1);
326 assert!(sm.has_node("n1"));
327 assert!(sm.vnode_count() > 0);
328 }
329
330 #[test]
331 fn test_shard_map_remove_node() {
332 let mut sm = ShardMap::new();
333 sm.add_node(NodeId::new("n1", "Node 1"));
334 assert!(sm.remove_node("n1"));
335 assert_eq!(sm.node_count(), 0);
336 assert_eq!(sm.vnode_count(), 0);
337 assert!(!sm.has_node("n1"));
338 }
339
340 #[test]
341 fn test_shard_map_remove_nonexistent() {
342 let mut sm = ShardMap::new();
343 assert!(!sm.remove_node("nonexistent"));
344 }
345
346 #[test]
347 fn test_shard_map_lookup_single_node() {
348 let mut sm = ShardMap::new();
349 sm.add_node(NodeId::new("n1", "Node 1"));
350 assert_eq!(sm.lookup("key-a"), Some("n1"));
352 assert_eq!(sm.lookup("key-b"), Some("n1"));
353 assert_eq!(sm.lookup("key-c"), Some("n1"));
354 }
355
356 #[test]
357 fn test_shard_map_lookup_deterministic() {
358 let mut sm = ShardMap::new();
359 sm.add_node(NodeId::new("n1", "Node 1"));
360 sm.add_node(NodeId::new("n2", "Node 2"));
361 let result1 = sm
362 .lookup("my-key")
363 .expect("lookup should succeed")
364 .to_string();
365 let result2 = sm
366 .lookup("my-key")
367 .expect("lookup should succeed")
368 .to_string();
369 assert_eq!(result1, result2);
370 }
371
372 #[test]
373 fn test_shard_map_distribution() {
374 let mut sm = ShardMap::new();
375 sm.add_node(NodeId::new("n1", "N1"));
376 sm.add_node(NodeId::new("n2", "N2"));
377 sm.add_node(NodeId::new("n3", "N3"));
378
379 let dist = sm.load_distribution();
380 assert_eq!(dist.len(), 3);
381 for count in dist.values() {
383 assert!(*count > 0);
384 }
385 }
386
387 #[test]
388 fn test_shard_map_load_factor() {
389 let mut sm = ShardMap::new();
390 sm.add_node(NodeId::new("n1", "N1"));
391 sm.add_node(NodeId::new("n2", "N2"));
392 sm.add_node(NodeId::new("n3", "N3"));
393 let lf = sm.load_factor();
394 assert!(lf < 0.5, "load factor too high: {}", lf);
396 }
397
398 #[test]
399 fn test_shard_map_load_factor_empty() {
400 let sm = ShardMap::new();
401 assert_eq!(sm.load_factor(), 0.0);
402 }
403
404 #[test]
405 fn test_shard_map_node_ids() {
406 let mut sm = ShardMap::new();
407 sm.add_node(NodeId::new("a", "A"));
408 sm.add_node(NodeId::new("b", "B"));
409 let mut ids = sm.node_ids();
410 ids.sort();
411 assert_eq!(ids, vec!["a", "b"]);
412 }
413
414 #[test]
415 fn test_weighted_nodes() {
416 let mut sm = ShardMap::with_config(ShardMapConfig {
417 vnodes_per_node: 100,
418 use_weights: true,
419 });
420 sm.add_node(NodeId::new("n1", "N1").with_weight(100));
421 sm.add_node(NodeId::new("n2", "N2").with_weight(200));
422
423 let dist = sm.load_distribution();
424 let n1_count = dist.get("n1").copied().unwrap_or(0);
425 let n2_count = dist.get("n2").copied().unwrap_or(0);
426 assert!(
428 n2_count > n1_count,
429 "n2={} should be > n1={}",
430 n2_count,
431 n1_count
432 );
433 }
434
435 #[test]
436 fn test_batch_assign() {
437 let mut sm = ShardMap::new();
438 sm.add_node(NodeId::new("n1", "N1"));
439 sm.add_node(NodeId::new("n2", "N2"));
440
441 let keys = vec!["key1", "key2", "key3"];
442 let result = batch_assign(&sm, &keys);
443 assert_eq!(result.assigned_count(), 3);
444 assert_eq!(result.unassigned_count(), 0);
445 }
446
447 #[test]
448 fn test_batch_assign_empty_ring() {
449 let sm = ShardMap::new();
450 let keys = vec!["key1", "key2"];
451 let result = batch_assign(&sm, &keys);
452 assert_eq!(result.assigned_count(), 0);
453 assert_eq!(result.unassigned_count(), 2);
454 }
455
456 #[test]
457 fn test_default_config() {
458 let config = ShardMapConfig::default();
459 assert_eq!(config.vnodes_per_node, 150);
460 assert!(config.use_weights);
461 }
462
463 #[test]
464 fn test_shard_map_default_trait() {
465 let sm = ShardMap::default();
466 assert_eq!(sm.node_count(), 0);
467 }
468
469 #[test]
472 fn test_consistent_hash_lookup_binary_matches_linear() {
473 let mut sm = ShardMap::new();
474 for i in 0..5_u32 {
475 sm.add_node(NodeId::new(&format!("node-{i}"), &format!("Node {i}")));
476 }
477
478 let ring_vec: Vec<(u64, String)> = sm.ring.iter().map(|(&h, v)| (h, v.clone())).collect();
480
481 let linear_lookup = |key: &str| -> Option<&str> {
482 if ring_vec.is_empty() {
483 return None;
484 }
485 let hash = ShardMap::hash_key(key);
486 ring_vec
488 .iter()
489 .find(|(h, _)| *h >= hash)
490 .or_else(|| ring_vec.first())
491 .map(|(_, id)| id.as_str())
492 };
493
494 let queries: Vec<String> = (0..1000_u32).map(|i| format!("key-{i}")).collect();
496 let mut mismatches = 0_u32;
497 for q in &queries {
498 let btree_result = sm.lookup(q);
499 let linear_result = linear_lookup(q);
500 if btree_result != linear_result {
501 mismatches += 1;
502 }
503 }
504 assert_eq!(
505 mismatches, 0,
506 "BTreeMap lookup and linear scan disagree on {mismatches} of 1000 queries"
507 );
508 }
509}