1use std::collections::{HashMap, HashSet};
14use std::sync::atomic::{AtomicUsize, Ordering};
15use std::sync::{Arc, RwLock};
16
17#[derive(Debug, Clone)]
23pub struct ShardConfig {
24 pub num_shards: usize,
26 pub replication_factor: usize,
28 pub max_vectors_per_shard: usize,
30}
31
32impl Default for ShardConfig {
33 fn default() -> Self {
34 Self {
35 num_shards: 8,
36 replication_factor: 3,
37 max_vectors_per_shard: 200_000,
38 }
39 }
40}
41
42#[derive(Debug, Clone)]
48pub struct ShardAssignment {
49 pub shard_id: usize,
51 pub primary_peer: String,
53 pub replica_peers: Vec<String>,
55}
56
57pub struct ShardBalancer {
66 config: ShardConfig,
67 shard_loads: Arc<Vec<AtomicUsize>>,
68}
69
70impl ShardBalancer {
71 pub fn new(config: ShardConfig) -> Self {
77 assert!(config.num_shards > 0, "num_shards must be > 0");
78 let mut loads = Vec::with_capacity(config.num_shards);
79 for _ in 0..config.num_shards {
80 loads.push(AtomicUsize::new(0));
81 }
82 Self {
83 config,
84 shard_loads: Arc::new(loads),
85 }
86 }
87
88 pub fn assign_vector(&self, vector_id: u64) -> usize {
92 let hash = vector_id.wrapping_mul(2_654_435_761_u64);
94 (hash as usize) % self.config.num_shards
95 }
96
97 pub fn least_loaded_shard(&self) -> usize {
102 let mut min_load = usize::MAX;
103 let mut min_shard = 0usize;
104 for (idx, counter) in self.shard_loads.iter().enumerate() {
105 let load = counter.load(Ordering::Relaxed);
106 if load < min_load {
107 min_load = load;
108 min_shard = idx;
109 }
110 }
111 min_shard
112 }
113
114 pub fn increment_shard_load(&self, shard_id: usize) {
116 if let Some(counter) = self.shard_loads.get(shard_id) {
117 counter.fetch_add(1, Ordering::Relaxed);
118 }
119 }
120
121 pub fn decrement_shard_load(&self, shard_id: usize) {
125 if let Some(counter) = self.shard_loads.get(shard_id) {
126 let mut current = counter.load(Ordering::Relaxed);
128 loop {
129 if current == 0 {
130 break;
131 }
132 match counter.compare_exchange_weak(
133 current,
134 current - 1,
135 Ordering::Relaxed,
136 Ordering::Relaxed,
137 ) {
138 Ok(_) => break,
139 Err(actual) => current = actual,
140 }
141 }
142 }
143 }
144
145 pub fn shard_loads_snapshot(&self) -> Vec<usize> {
147 self.shard_loads
148 .iter()
149 .map(|c| c.load(Ordering::Relaxed))
150 .collect()
151 }
152
153 pub fn rebalance_needed(&self) -> bool {
156 let snapshot = self.shard_loads_snapshot();
157 let non_zero: Vec<usize> = snapshot.into_iter().filter(|&v| v > 0).collect();
159 if non_zero.len() < 2 {
160 return false;
161 }
162 let max = non_zero.iter().copied().max().unwrap_or(0);
163 let min = non_zero.iter().copied().min().unwrap_or(0);
164 if min == 0 {
165 return false;
166 }
167 (max as f64 / min as f64) > 2.0
168 }
169
170 pub fn hotspot_shards(&self) -> Vec<usize> {
172 let snapshot = self.shard_loads_snapshot();
173 if snapshot.is_empty() {
174 return vec![];
175 }
176 let total: usize = snapshot.iter().sum();
177 let n = snapshot.len();
178 snapshot
180 .iter()
181 .enumerate()
182 .filter(|&(_, &load)| load * n > total)
183 .map(|(idx, _)| idx)
184 .collect()
185 }
186
187 pub fn config(&self) -> &ShardConfig {
189 &self.config
190 }
191}
192
193pub struct DhtShardRouter {
200 pub balancer: Arc<ShardBalancer>,
202 peer_shard_map: Arc<RwLock<HashMap<String, HashSet<usize>>>>,
204}
205
206impl DhtShardRouter {
207 pub fn new(balancer: Arc<ShardBalancer>) -> Self {
209 Self {
210 balancer,
211 peer_shard_map: Arc::new(RwLock::new(HashMap::new())),
212 }
213 }
214
215 pub fn register_peer_shards(&self, peer_id: &str, shards: Vec<usize>) {
217 let mut map = self
218 .peer_shard_map
219 .write()
220 .expect("peer_shard_map write lock poisoned");
221 map.insert(peer_id.to_string(), shards.into_iter().collect());
222 }
223
224 pub fn unregister_peer(&self, peer_id: &str) {
226 let mut map = self
227 .peer_shard_map
228 .write()
229 .expect("peer_shard_map write lock poisoned");
230 map.remove(peer_id);
231 }
232
233 pub fn peers_for_shard(&self, shard_id: usize) -> Vec<String> {
235 let map = self
236 .peer_shard_map
237 .read()
238 .expect("peer_shard_map read lock poisoned");
239 map.iter()
240 .filter(|(_, shards)| shards.contains(&shard_id))
241 .map(|(peer_id, _)| peer_id.clone())
242 .collect()
243 }
244
245 pub fn all_peer_assignments(&self) -> HashMap<String, Vec<usize>> {
247 let map = self
248 .peer_shard_map
249 .read()
250 .expect("peer_shard_map read lock poisoned");
251 map.iter()
252 .map(|(peer_id, shards)| {
253 let mut shard_vec: Vec<usize> = shards.iter().copied().collect();
254 shard_vec.sort_unstable();
255 (peer_id.clone(), shard_vec)
256 })
257 .collect()
258 }
259
260 pub fn shard_coverage(&self) -> HashMap<usize, usize> {
262 let map = self
263 .peer_shard_map
264 .read()
265 .expect("peer_shard_map read lock poisoned");
266 let num_shards = self.balancer.config().num_shards;
267 let mut coverage: HashMap<usize, usize> = (0..num_shards).map(|s| (s, 0)).collect();
268 for shards in map.values() {
269 for &shard_id in shards {
270 *coverage.entry(shard_id).or_insert(0) += 1;
271 }
272 }
273 coverage
274 }
275}
276
277#[cfg(test)]
282mod tests {
283 use super::*;
284
285 fn make_balancer(num_shards: usize) -> Arc<ShardBalancer> {
286 Arc::new(ShardBalancer::new(ShardConfig {
287 num_shards,
288 replication_factor: 3,
289 max_vectors_per_shard: 200_000,
290 }))
291 }
292
293 fn make_router(num_shards: usize) -> DhtShardRouter {
294 DhtShardRouter::new(make_balancer(num_shards))
295 }
296
297 #[test]
301 fn test_shard_assignment_consistency() {
302 let balancer = make_balancer(8);
303 for vector_id in [0u64, 1, 42, 999, u64::MAX / 2, u64::MAX] {
304 let first = balancer.assign_vector(vector_id);
305 for _ in 0..100 {
307 assert_eq!(
308 balancer.assign_vector(vector_id),
309 first,
310 "assign_vector({vector_id}) is not consistent"
311 );
312 }
313 }
314 }
315
316 #[test]
320 fn test_shard_load_tracking() {
321 let balancer = make_balancer(4);
322
323 assert_eq!(balancer.shard_loads_snapshot(), vec![0, 0, 0, 0]);
325
326 balancer.increment_shard_load(0);
327 balancer.increment_shard_load(0);
328 balancer.increment_shard_load(1);
329
330 let snap = balancer.shard_loads_snapshot();
331 assert_eq!(snap[0], 2);
332 assert_eq!(snap[1], 1);
333 assert_eq!(snap[2], 0);
334 assert_eq!(snap[3], 0);
335
336 balancer.decrement_shard_load(0);
337 assert_eq!(balancer.shard_loads_snapshot()[0], 1);
338
339 balancer.decrement_shard_load(2);
341 assert_eq!(balancer.shard_loads_snapshot()[2], 0);
342 }
343
344 #[test]
348 fn test_rebalance_detection() {
349 let balancer = make_balancer(4);
350
351 for shard in 0..4 {
353 for _ in 0..10 {
354 balancer.increment_shard_load(shard);
355 }
356 }
357 assert!(
358 !balancer.rebalance_needed(),
359 "balanced shards should not need rebalance"
360 );
361
362 for _ in 0..30 {
364 balancer.increment_shard_load(0);
365 }
366 assert!(
367 balancer.rebalance_needed(),
368 "highly skewed shards should need rebalance"
369 );
370 }
371
372 #[test]
376 fn test_hotspot_shards() {
377 let balancer = make_balancer(4);
378
379 for _ in 0..100 {
381 balancer.increment_shard_load(0);
382 }
383 for shard in 1..4 {
384 for _ in 0..10 {
385 balancer.increment_shard_load(shard);
386 }
387 }
388
389 let hotspots = balancer.hotspot_shards();
390 assert!(hotspots.contains(&0), "shard 0 should be a hotspot");
391 assert!(!hotspots.contains(&1), "shard 1 should not be a hotspot");
392 }
393
394 #[test]
398 fn test_dht_shard_router_registration() {
399 let router = make_router(8);
400
401 router.register_peer_shards("peer-A", vec![0, 1, 2]);
402 router.register_peer_shards("peer-B", vec![3, 4, 5]);
403
404 let assignments = router.all_peer_assignments();
405 assert_eq!(assignments["peer-A"], vec![0, 1, 2]);
406 assert_eq!(assignments["peer-B"], vec![3, 4, 5]);
407
408 router.register_peer_shards("peer-A", vec![0, 7]);
410 let assignments2 = router.all_peer_assignments();
411 assert_eq!(assignments2["peer-A"], vec![0, 7]);
412 }
413
414 #[test]
418 fn test_peers_for_shard() {
419 let router = make_router(8);
420
421 router.register_peer_shards("peer-X", vec![0, 1, 2]);
422 router.register_peer_shards("peer-Y", vec![1, 2, 3]);
423 router.register_peer_shards("peer-Z", vec![4, 5, 6]);
424
425 let mut peers_for_1 = router.peers_for_shard(1);
426 peers_for_1.sort();
427 assert_eq!(peers_for_1, vec!["peer-X", "peer-Y"]);
428
429 let peers_for_4 = router.peers_for_shard(4);
430 assert_eq!(peers_for_4, vec!["peer-Z"]);
431
432 let peers_for_7 = router.peers_for_shard(7);
433 assert!(peers_for_7.is_empty(), "no peer hosts shard 7");
434 }
435
436 #[test]
440 fn test_shard_coverage() {
441 let router = make_router(4);
442
443 router.register_peer_shards("peer-1", vec![0, 1, 2, 3]);
444 router.register_peer_shards("peer-2", vec![0, 1, 2, 3]);
445 router.register_peer_shards("peer-3", vec![0, 2]);
446
447 let coverage = router.shard_coverage();
448 assert_eq!(coverage[&0], 3);
449 assert_eq!(coverage[&1], 2);
450 assert_eq!(coverage[&2], 3);
451 assert_eq!(coverage[&3], 2);
452 }
453
454 #[test]
458 fn test_least_loaded_shard() {
459 let balancer = make_balancer(4);
460
461 assert_eq!(balancer.least_loaded_shard(), 0);
463
464 balancer.increment_shard_load(0);
465 balancer.increment_shard_load(0);
466 balancer.increment_shard_load(1);
467
468 assert_eq!(balancer.least_loaded_shard(), 2);
470
471 balancer.increment_shard_load(2);
472 balancer.increment_shard_load(2);
473 balancer.increment_shard_load(2);
474
475 assert_eq!(balancer.least_loaded_shard(), 3);
477 }
478
479 #[test]
483 fn test_consistent_hash_distribution() {
484 let balancer = make_balancer(8);
485 let mut counts = [0usize; 8];
486
487 for id in 0u64..1000 {
488 let shard = balancer.assign_vector(id);
489 counts[shard] += 1;
490 }
491
492 let max = *counts.iter().max().expect("non-empty");
493 let min = *counts.iter().min().expect("non-empty");
494 assert!(
496 min > 0,
497 "every shard should receive at least one vector from 1000 IDs"
498 );
499 assert!(
501 (max as f64 / min as f64) < 3.0,
502 "hash distribution too skewed: max={max}, min={min}"
503 );
504 }
505
506 #[test]
510 fn test_unregister_removes_peer() {
511 let router = make_router(8);
512
513 router.register_peer_shards("peer-alpha", vec![0, 1, 2, 3]);
514 router.register_peer_shards("peer-beta", vec![0, 1]);
515
516 assert!(router
518 .peers_for_shard(0)
519 .contains(&"peer-alpha".to_string()));
520
521 router.unregister_peer("peer-alpha");
523
524 for shard in 0..8 {
526 let peers = router.peers_for_shard(shard);
527 assert!(
528 !peers.contains(&"peer-alpha".to_string()),
529 "peer-alpha still appears for shard {shard} after unregistration"
530 );
531 }
532
533 assert!(router.peers_for_shard(0).contains(&"peer-beta".to_string()));
535 }
536}