1#![allow(dead_code)]
7
8use std::collections::BTreeMap;
9
10#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
16pub struct ShardKey(pub String);
17
18impl ShardKey {
19 #[must_use]
21 pub fn new(key: impl Into<String>) -> Self {
22 Self(key.into())
23 }
24
25 #[must_use]
30 pub fn hash64(&self) -> u64 {
31 const FNV_OFFSET: u64 = 14_695_981_039_346_656_037;
33 const FNV_PRIME: u64 = 1_099_511_628_211;
34 let mut h = FNV_OFFSET;
35 for &b in self.0.as_bytes() {
36 h ^= u64::from(b);
37 h = h.wrapping_mul(FNV_PRIME);
38 }
39 h ^= h >> 30;
41 h = h.wrapping_mul(0xbf58476d1ce4e5b9);
42 h ^= h >> 27;
43 h = h.wrapping_mul(0x94d049bb133111eb);
44 h ^= h >> 31;
45 h
46 }
47}
48
49#[derive(Debug, Clone)]
55pub struct VirtualNode {
56 pub position: u64,
58 pub node_id: String,
60 pub replica: u32,
62}
63
64#[derive(Debug, Default)]
74pub struct ConsistentHashRing {
75 ring: BTreeMap<u64, VirtualNode>,
76 replicas_per_node: u32,
77}
78
79impl ConsistentHashRing {
80 #[must_use]
82 pub fn new(replicas_per_node: u32) -> Self {
83 Self {
84 ring: BTreeMap::new(),
85 replicas_per_node: replicas_per_node.max(1),
86 }
87 }
88
89 pub fn add_node(&mut self, node_id: impl Into<String>) {
91 let node_id = node_id.into();
92 for replica in 0..self.replicas_per_node {
93 let key = ShardKey::new(format!("{node_id}:{replica}"));
94 let position = key.hash64();
95 self.ring.insert(
96 position,
97 VirtualNode {
98 position,
99 node_id: node_id.clone(),
100 replica,
101 },
102 );
103 }
104 }
105
106 pub fn remove_node(&mut self, node_id: &str) {
108 for replica in 0..self.replicas_per_node {
109 let key = ShardKey::new(format!("{node_id}:{replica}"));
110 let position = key.hash64();
111 self.ring.remove(&position);
112 }
113 }
114
115 #[must_use]
119 pub fn get_node(&self, key: &ShardKey) -> Option<&str> {
120 if self.ring.is_empty() {
121 return None;
122 }
123 let hash = key.hash64();
124 let node = self
126 .ring
127 .range(hash..)
128 .next()
129 .or_else(|| self.ring.iter().next())
130 .map(|(_, v)| v.node_id.as_str());
131 node
132 }
133
134 #[must_use]
136 pub fn virtual_node_count(&self) -> usize {
137 self.ring.len()
138 }
139
140 #[must_use]
142 pub fn physical_node_count(&self) -> usize {
143 let mut nodes: Vec<&str> = self.ring.values().map(|v| v.node_id.as_str()).collect();
144 nodes.sort_unstable();
145 nodes.dedup();
146 nodes.len()
147 }
148
149 #[must_use]
151 pub fn nodes(&self) -> Vec<String> {
152 let mut ids: Vec<String> = self.ring.values().map(|v| v.node_id.clone()).collect();
153 ids.sort();
154 ids.dedup();
155 ids
156 }
157}
158
159#[derive(Debug, Clone, Copy, PartialEq, Eq)]
165pub enum ShardState {
166 Active,
168 Migrating,
170 Archived,
172}
173
174#[derive(Debug, Clone)]
176pub struct ShardMetadata {
177 pub key: ShardKey,
179 pub owner_node: String,
181 pub size_bytes: u64,
183 pub state: ShardState,
185 pub updated_at_ms: u64,
187}
188
189impl ShardMetadata {
190 #[must_use]
192 pub fn new(key: ShardKey, owner_node: impl Into<String>, size_bytes: u64, now_ms: u64) -> Self {
193 Self {
194 key,
195 owner_node: owner_node.into(),
196 size_bytes,
197 state: ShardState::Active,
198 updated_at_ms: now_ms,
199 }
200 }
201
202 pub fn begin_migration(&mut self, target_node: impl Into<String>, now_ms: u64) {
204 self.owner_node = target_node.into();
205 self.state = ShardState::Migrating;
206 self.updated_at_ms = now_ms;
207 }
208
209 pub fn complete_migration(&mut self, now_ms: u64) {
211 self.state = ShardState::Active;
212 self.updated_at_ms = now_ms;
213 }
214
215 #[must_use]
217 pub fn is_active(&self) -> bool {
218 self.state == ShardState::Active
219 }
220}
221
222#[derive(Debug, Default)]
228pub struct ShardCatalog {
229 shards: Vec<ShardMetadata>,
230}
231
232impl ShardCatalog {
233 #[must_use]
235 pub fn new() -> Self {
236 Self::default()
237 }
238
239 pub fn upsert(&mut self, shard: ShardMetadata) {
241 if let Some(existing) = self.shards.iter_mut().find(|s| s.key == shard.key) {
242 *existing = shard;
243 } else {
244 self.shards.push(shard);
245 }
246 }
247
248 #[must_use]
250 pub fn get(&self, key: &ShardKey) -> Option<&ShardMetadata> {
251 self.shards.iter().find(|s| &s.key == key)
252 }
253
254 #[must_use]
256 pub fn shards_for_node(&self, node_id: &str) -> Vec<&ShardMetadata> {
257 self.shards
258 .iter()
259 .filter(|s| s.owner_node == node_id)
260 .collect()
261 }
262
263 #[must_use]
265 pub fn total_size_bytes(&self) -> u64 {
266 self.shards.iter().map(|s| s.size_bytes).sum()
267 }
268
269 #[must_use]
271 pub fn len(&self) -> usize {
272 self.shards.len()
273 }
274
275 #[must_use]
277 pub fn is_empty(&self) -> bool {
278 self.shards.is_empty()
279 }
280}
281
282#[cfg(test)]
287mod tests {
288 use super::*;
289
290 #[test]
293 fn test_shard_key_hash64_deterministic() {
294 let k = ShardKey::new("video-chunk-0042");
295 assert_eq!(k.hash64(), k.hash64());
296 }
297
298 #[test]
299 fn test_shard_key_different_keys_different_hashes() {
300 let k1 = ShardKey::new("a");
301 let k2 = ShardKey::new("b");
302 assert_ne!(k1.hash64(), k2.hash64());
303 }
304
305 #[test]
308 fn test_ring_empty_returns_none() {
309 let ring = ConsistentHashRing::new(3);
310 assert!(ring.get_node(&ShardKey::new("key")).is_none());
311 }
312
313 #[test]
314 fn test_ring_single_node_always_assigned() {
315 let mut ring = ConsistentHashRing::new(10);
316 ring.add_node("node-0");
317 for i in 0..20_u32 {
318 let key = ShardKey::new(format!("key-{i}"));
319 assert_eq!(ring.get_node(&key), Some("node-0"));
320 }
321 }
322
323 #[test]
324 fn test_ring_virtual_node_count() {
325 let mut ring = ConsistentHashRing::new(5);
326 ring.add_node("n0");
327 ring.add_node("n1");
328 assert_eq!(ring.virtual_node_count(), 10);
329 }
330
331 #[test]
332 fn test_ring_physical_node_count() {
333 let mut ring = ConsistentHashRing::new(5);
334 ring.add_node("n0");
335 ring.add_node("n1");
336 ring.add_node("n2");
337 assert_eq!(ring.physical_node_count(), 3);
338 }
339
340 #[test]
341 fn test_ring_remove_node() {
342 let mut ring = ConsistentHashRing::new(5);
343 ring.add_node("n0");
344 ring.add_node("n1");
345 ring.remove_node("n0");
346 assert_eq!(ring.physical_node_count(), 1);
347 }
348
349 #[test]
350 fn test_ring_nodes_list() {
351 let mut ring = ConsistentHashRing::new(3);
352 ring.add_node("alpha");
353 ring.add_node("beta");
354 let nodes = ring.nodes();
355 assert!(nodes.contains(&"alpha".to_string()));
356 assert!(nodes.contains(&"beta".to_string()));
357 assert_eq!(nodes.len(), 2);
358 }
359
360 #[test]
361 fn test_ring_distribution_two_nodes() {
362 let mut ring = ConsistentHashRing::new(50);
363 ring.add_node("node-A");
364 ring.add_node("node-B");
365 let mut a_count = 0_u32;
366 let mut b_count = 0_u32;
367 for i in 0..100_u32 {
368 let k = ShardKey::new(format!("shard-{i}"));
369 match ring.get_node(&k) {
370 Some("node-A") => a_count += 1,
371 Some("node-B") => b_count += 1,
372 _ => {}
373 }
374 }
375 assert!(a_count > 20 && b_count > 20, "a={a_count}, b={b_count}");
377 }
378
379 #[test]
382 fn test_shard_metadata_initial_state() {
383 let meta = ShardMetadata::new(ShardKey::new("k"), "node-0", 1024, 1000);
384 assert!(meta.is_active());
385 assert_eq!(meta.state, ShardState::Active);
386 }
387
388 #[test]
389 fn test_shard_metadata_begin_migration() {
390 let mut meta = ShardMetadata::new(ShardKey::new("k"), "node-0", 1024, 1000);
391 meta.begin_migration("node-1", 2000);
392 assert_eq!(meta.state, ShardState::Migrating);
393 assert_eq!(meta.owner_node, "node-1");
394 }
395
396 #[test]
397 fn test_shard_metadata_complete_migration() {
398 let mut meta = ShardMetadata::new(ShardKey::new("k"), "node-0", 1024, 1000);
399 meta.begin_migration("node-1", 2000);
400 meta.complete_migration(3000);
401 assert!(meta.is_active());
402 }
403
404 #[test]
407 fn test_catalog_empty() {
408 let catalog = ShardCatalog::new();
409 assert!(catalog.is_empty());
410 assert_eq!(catalog.len(), 0);
411 }
412
413 #[test]
414 fn test_catalog_upsert_and_get() {
415 let mut catalog = ShardCatalog::new();
416 let key = ShardKey::new("shard-0");
417 catalog.upsert(ShardMetadata::new(key.clone(), "n0", 512, 100));
418 let meta = catalog.get(&key).expect("get should return a value");
419 assert_eq!(meta.size_bytes, 512);
420 }
421
422 #[test]
423 fn test_catalog_upsert_updates_existing() {
424 let mut catalog = ShardCatalog::new();
425 let key = ShardKey::new("shard-0");
426 catalog.upsert(ShardMetadata::new(key.clone(), "n0", 512, 100));
427 catalog.upsert(ShardMetadata::new(key.clone(), "n1", 1024, 200));
428 assert_eq!(catalog.len(), 1);
429 assert_eq!(
430 catalog
431 .get(&key)
432 .expect("get should return a value")
433 .size_bytes,
434 1024
435 );
436 }
437
438 #[test]
439 fn test_catalog_shards_for_node() {
440 let mut catalog = ShardCatalog::new();
441 catalog.upsert(ShardMetadata::new(ShardKey::new("s0"), "n0", 100, 1));
442 catalog.upsert(ShardMetadata::new(ShardKey::new("s1"), "n0", 200, 1));
443 catalog.upsert(ShardMetadata::new(ShardKey::new("s2"), "n1", 300, 1));
444 let shards = catalog.shards_for_node("n0");
445 assert_eq!(shards.len(), 2);
446 }
447
448 #[test]
449 fn test_catalog_total_size_bytes() {
450 let mut catalog = ShardCatalog::new();
451 catalog.upsert(ShardMetadata::new(ShardKey::new("s0"), "n0", 100, 1));
452 catalog.upsert(ShardMetadata::new(ShardKey::new("s1"), "n0", 200, 1));
453 assert_eq!(catalog.total_size_bytes(), 300);
454 }
455}