1use serde::{Deserialize, Serialize};
12use std::collections::{BTreeMap, HashMap, HashSet};
13
14#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
15pub struct ShardId(pub String);
16
17#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
18pub enum ShardingStrategy {
19 ConsistentHash,
20 RangeBasedHash,
21 KeyModulo,
22}
23
24#[derive(Debug, Clone, Serialize, Deserialize)]
25pub struct ShardKey {
26 pub field: String,
27 pub hash_function: String, }
29
30impl ShardKey {
31 pub fn hash_value(&self, value: &str) -> u64 {
32 let bytes = value.as_bytes();
34 let mut hash: u64 = 5381;
35 for &byte in bytes {
36 hash = ((hash << 5).wrapping_add(hash)).wrapping_add(byte as u64);
37 }
38 hash
39 }
40}
41
42#[derive(Debug, Clone, Serialize, Deserialize)]
43pub struct ShardRange {
44 pub shard_id: ShardId,
45 pub start_hash: u64,
46 pub end_hash: u64,
47 pub replica_nodes: Vec<String>,
48}
49
50#[derive(Debug, Clone, Serialize, Deserialize)]
51pub struct ShardMetrics {
52 pub shard_id: ShardId,
53 pub entry_count: usize,
54 pub size_bytes: u64,
55 pub reads: u64,
56 pub writes: u64,
57 pub avg_query_time_ms: f64,
58}
59
60#[derive(Debug, Clone, Serialize, Deserialize)]
61pub struct HotspotAlert {
62 pub shard_id: ShardId,
63 pub reason: String,
64 pub metric_value: f64,
65 pub threshold: f64,
66 pub severity: String, }
68
69#[derive(Debug, Clone, Serialize, Deserialize)]
70pub struct RebalanceOperation {
71 pub source_shard: ShardId,
72 pub target_shard: ShardId,
73 pub key_range_start: u64,
74 pub key_range_end: u64,
75 pub estimated_records: usize,
76 pub priority: u8, }
78
79pub struct ShardManager {
81 sharding_key: ShardKey,
82 strategy: ShardingStrategy,
83 shard_ranges: Vec<ShardRange>,
84 shard_metrics: HashMap<String, ShardMetrics>,
85 rebalance_history: Vec<RebalanceOperation>,
86 rebalance_threshold: f64, }
88
89impl ShardManager {
90 pub fn new(sharding_key: ShardKey, strategy: ShardingStrategy) -> Self {
91 ShardManager {
92 sharding_key,
93 strategy,
94 shard_ranges: Vec::new(),
95 shard_metrics: HashMap::new(),
96 rebalance_history: Vec::new(),
97 rebalance_threshold: 0.5, }
99 }
100
101 pub fn initialize_shards(
103 &mut self,
104 shard_ids: Vec<ShardId>,
105 replica_nodes: HashMap<String, Vec<String>>,
106 ) {
107 let num_shards = shard_ids.len() as u64;
108 if num_shards == 0 {
109 return;
110 }
111
112 let hash_space = u64::MAX;
113 let shard_size = hash_space / num_shards;
114
115 for (i, shard_id) in shard_ids.into_iter().enumerate() {
116 let start = i as u64 * shard_size;
117 let end = if i == num_shards as usize - 1 {
118 u64::MAX
119 } else {
120 (i as u64 + 1) * shard_size - 1
121 };
122
123 let replicas = replica_nodes
124 .get(shard_id.0.as_str())
125 .cloned()
126 .unwrap_or_default();
127
128 self.shard_ranges.push(ShardRange {
129 shard_id: shard_id.clone(),
130 start_hash: start,
131 end_hash: end,
132 replica_nodes: replicas,
133 });
134
135 self.shard_metrics.insert(
136 shard_id.0.clone(),
137 ShardMetrics {
138 shard_id,
139 entry_count: 0,
140 size_bytes: 0,
141 reads: 0,
142 writes: 0,
143 avg_query_time_ms: 0.0,
144 },
145 );
146 }
147
148 self.shard_ranges.sort_by_key(|r| r.start_hash);
150 }
151
152 pub fn get_shard_for_key(&self, key: &str) -> Option<&ShardRange> {
154 let hash = self.sharding_key.hash_value(key);
155 self.shard_ranges
156 .iter()
157 .find(|range| hash >= range.start_hash && hash <= range.end_hash)
158 }
159
160 pub fn get_replicas_for_key(&self, key: &str) -> Vec<String> {
162 self.get_shard_for_key(key)
163 .map(|r| r.replica_nodes.clone())
164 .unwrap_or_default()
165 }
166
167 pub fn record_shard_operation(
169 &mut self,
170 shard_id: &ShardId,
171 operation: &str,
172 time_ms: f64,
173 records_affected: usize,
174 size_bytes: u64,
175 ) {
176 if let Some(metrics) = self.shard_metrics.get_mut(&shard_id.0) {
177 match operation {
178 "read" => {
179 metrics.reads += 1;
180 let old_avg = metrics.avg_query_time_ms;
181 metrics.avg_query_time_ms =
182 (old_avg * (metrics.reads - 1) as f64 + time_ms) / metrics.reads as f64;
183 }
184 "write" => {
185 metrics.writes += 1;
186 metrics.entry_count = metrics.entry_count.saturating_add(records_affected);
187 metrics.size_bytes = metrics.size_bytes.saturating_add(size_bytes);
188 }
189 "delete" => {
190 metrics.entry_count = metrics.entry_count.saturating_sub(records_affected);
191 metrics.size_bytes = metrics
192 .size_bytes
193 .saturating_sub(size_bytes.min(metrics.size_bytes));
194 }
195 _ => {}
196 }
197 }
198 }
199
200 pub fn get_all_metrics(&self) -> Vec<ShardMetrics> {
202 self.shard_metrics.values().cloned().collect()
203 }
204
205 pub fn detect_hotspots(&self) -> Vec<HotspotAlert> {
207 let mut alerts = Vec::new();
208 let all_metrics: Vec<_> = self.get_all_metrics();
209
210 if all_metrics.is_empty() {
211 return alerts;
212 }
213
214 let total_reads: u64 = all_metrics.iter().map(|m| m.reads).sum();
216 let total_writes: u64 = all_metrics.iter().map(|m| m.writes).sum();
217 let total_entries: usize = all_metrics.iter().map(|m| m.entry_count).sum();
218 let num_shards = all_metrics.len() as f64;
219
220 let avg_reads = total_reads as f64 / num_shards;
221 let avg_writes = total_writes as f64 / num_shards;
222 let avg_entries = total_entries as f64 / num_shards;
223
224 let read_std_dev = calculate_std_dev(
226 all_metrics.iter().map(|m| m.reads as f64).collect(),
227 avg_reads,
228 );
229 let write_std_dev = calculate_std_dev(
230 all_metrics.iter().map(|m| m.writes as f64).collect(),
231 avg_writes,
232 );
233 let entry_std_dev = calculate_std_dev(
234 all_metrics.iter().map(|m| m.entry_count as f64).collect(),
235 avg_entries,
236 );
237
238 for metrics in all_metrics {
240 if read_std_dev > 0.0 && (metrics.reads as f64 - avg_reads).abs() > read_std_dev * 2.0 {
242 alerts.push(HotspotAlert {
243 shard_id: metrics.shard_id.clone(),
244 reason: "High read load".to_string(),
245 metric_value: metrics.reads as f64,
246 threshold: avg_reads + read_std_dev * 2.0,
247 severity: if metrics.reads as f64 > avg_reads * 2.0 {
248 "high"
249 } else {
250 "medium"
251 }
252 .to_string(),
253 });
254 }
255
256 if write_std_dev > 0.0
258 && (metrics.writes as f64 - avg_writes).abs() > write_std_dev * 2.0
259 {
260 alerts.push(HotspotAlert {
261 shard_id: metrics.shard_id.clone(),
262 reason: "High write load".to_string(),
263 metric_value: metrics.writes as f64,
264 threshold: avg_writes + write_std_dev * 2.0,
265 severity: if metrics.writes as f64 > avg_writes * 2.0 {
266 "high"
267 } else {
268 "medium"
269 }
270 .to_string(),
271 });
272 }
273
274 if entry_std_dev > 0.0
276 && (metrics.entry_count as f64 - avg_entries).abs() > entry_std_dev * 3.0
277 {
278 alerts.push(HotspotAlert {
279 shard_id: metrics.shard_id.clone(),
280 reason: "Data imbalance".to_string(),
281 metric_value: metrics.entry_count as f64,
282 threshold: avg_entries + entry_std_dev * 3.0,
283 severity: if metrics.entry_count as f64 > avg_entries * 3.0 {
284 "high"
285 } else {
286 "low"
287 }
288 .to_string(),
289 });
290 }
291 }
292
293 alerts
294 }
295
296 pub fn generate_rebalance_ops(&self) -> Vec<RebalanceOperation> {
298 let hotspots = self.detect_hotspots();
299 let mut operations = Vec::new();
300
301 for hotspot in hotspots {
303 if hotspot.severity == "high" {
304 if let Some(shard_range) = self
306 .shard_ranges
307 .iter()
308 .find(|r| r.shard_id == hotspot.shard_id)
309 {
310 let mid_hash =
312 (shard_range.start_hash as u128 + shard_range.end_hash as u128) / 2;
313 let mid_hash = mid_hash as u64;
314
315 let new_shard_id = ShardId(format!(
317 "{}-split-{}",
318 hotspot.shard_id.0,
319 current_time_ms()
320 ));
321
322 operations.push(RebalanceOperation {
323 source_shard: hotspot.shard_id.clone(),
324 target_shard: new_shard_id,
325 key_range_start: mid_hash,
326 key_range_end: shard_range.end_hash,
327 estimated_records: (hotspot.metric_value as usize) / 2,
328 priority: 200,
329 });
330 }
331 }
332 }
333
334 operations
335 }
336
337 pub fn get_rebalance_history(&self) -> Vec<RebalanceOperation> {
339 self.rebalance_history.clone()
340 }
341
342 pub fn record_rebalance(&mut self, operation: RebalanceOperation) {
344 self.rebalance_history.push(operation);
345 }
346
347 pub fn get_distribution_summary(&self) -> ShardDistributionSummary {
349 let all_metrics = self.get_all_metrics();
350 let total_entries: usize = all_metrics.iter().map(|m| m.entry_count).sum();
351 let total_size: u64 = all_metrics.iter().map(|m| m.size_bytes).sum();
352 let total_reads: u64 = all_metrics.iter().map(|m| m.reads).sum();
353 let total_writes: u64 = all_metrics.iter().map(|m| m.writes).sum();
354
355 let max_entries = all_metrics.iter().map(|m| m.entry_count).max().unwrap_or(0);
356 let min_entries = all_metrics.iter().map(|m| m.entry_count).min().unwrap_or(0);
357
358 ShardDistributionSummary {
359 num_shards: all_metrics.len(),
360 total_entries,
361 total_size_bytes: total_size,
362 total_reads,
363 total_writes,
364 max_entries_per_shard: max_entries,
365 min_entries_per_shard: min_entries,
366 imbalance_ratio: if min_entries > 0 {
367 max_entries as f64 / min_entries as f64
368 } else {
369 1.0
370 },
371 }
372 }
373}
374
375#[derive(Debug, Clone, Serialize, Deserialize)]
376pub struct ShardDistributionSummary {
377 pub num_shards: usize,
378 pub total_entries: usize,
379 pub total_size_bytes: u64,
380 pub total_reads: u64,
381 pub total_writes: u64,
382 pub max_entries_per_shard: usize,
383 pub min_entries_per_shard: usize,
384 pub imbalance_ratio: f64,
385}
386
387fn calculate_std_dev(values: Vec<f64>, mean: f64) -> f64 {
388 if values.is_empty() {
389 return 0.0;
390 }
391
392 let variance = values.iter().map(|v| (v - mean).powi(2)).sum::<f64>() / values.len() as f64;
393 variance.sqrt()
394}
395
396fn current_time_ms() -> u64 {
397 std::time::SystemTime::now()
398 .duration_since(std::time::UNIX_EPOCH)
399 .map(|d| d.as_millis() as u64)
400 .unwrap_or(0)
401}
402
403#[cfg(test)]
404mod tests {
405 use super::*;
406
407 #[test]
408 fn test_shard_key_hashing() {
409 let key = ShardKey {
410 field: "user_id".to_string(),
411 hash_function: "murmur3".to_string(),
412 };
413
414 let hash1 = key.hash_value("user123");
415 let hash2 = key.hash_value("user123");
416 assert_eq!(hash1, hash2); let hash3 = key.hash_value("user456");
419 assert_ne!(hash1, hash3); }
421
422 #[test]
423 fn test_shard_initialization() {
424 let mut manager = ShardManager::new(
425 ShardKey {
426 field: "id".to_string(),
427 hash_function: "xxhash".to_string(),
428 },
429 ShardingStrategy::ConsistentHash,
430 );
431
432 let shards = vec![
433 ShardId("shard-0".to_string()),
434 ShardId("shard-1".to_string()),
435 ];
436 let mut replicas = HashMap::new();
437 replicas.insert("shard-0".to_string(), vec!["node-1".to_string()]);
438 replicas.insert("shard-1".to_string(), vec!["node-2".to_string()]);
439
440 manager.initialize_shards(shards, replicas);
441
442 assert_eq!(manager.shard_ranges.len(), 2);
443 }
444
445 #[test]
446 fn test_shard_assignment() {
447 let mut manager = ShardManager::new(
448 ShardKey {
449 field: "id".to_string(),
450 hash_function: "xxhash".to_string(),
451 },
452 ShardingStrategy::ConsistentHash,
453 );
454
455 let shards = vec![
456 ShardId("shard-0".to_string()),
457 ShardId("shard-1".to_string()),
458 ];
459 manager.initialize_shards(shards, HashMap::new());
460
461 let shard = manager.get_shard_for_key("user123");
462 assert!(shard.is_some());
463
464 let metrics = manager.get_all_metrics();
465 assert_eq!(metrics.len(), 2);
466 }
467
468 #[test]
469 fn test_shard_metrics() {
470 let mut manager = ShardManager::new(
471 ShardKey {
472 field: "id".to_string(),
473 hash_function: "xxhash".to_string(),
474 },
475 ShardingStrategy::ConsistentHash,
476 );
477
478 let shards = vec![ShardId("shard-0".to_string())];
479 manager.initialize_shards(shards, HashMap::new());
480
481 let shard_id = ShardId("shard-0".to_string());
482 manager.record_shard_operation(&shard_id, "write", 5.0, 100, 50000);
483
484 let metrics = manager.get_all_metrics();
485 assert_eq!(metrics[0].entry_count, 100);
486 assert_eq!(metrics[0].size_bytes, 50000);
487 assert_eq!(metrics[0].writes, 1);
488 }
489
490 #[test]
491 fn test_distribution_summary() {
492 let mut manager = ShardManager::new(
493 ShardKey {
494 field: "id".to_string(),
495 hash_function: "xxhash".to_string(),
496 },
497 ShardingStrategy::ConsistentHash,
498 );
499
500 let shards = vec![
501 ShardId("shard-0".to_string()),
502 ShardId("shard-1".to_string()),
503 ];
504 manager.initialize_shards(shards, HashMap::new());
505
506 manager.record_shard_operation(&ShardId("shard-0".to_string()), "write", 5.0, 100, 50000);
507 manager.record_shard_operation(&ShardId("shard-1".to_string()), "write", 5.0, 50, 25000);
508
509 let summary = manager.get_distribution_summary();
510 assert_eq!(summary.total_entries, 150);
511 assert_eq!(summary.num_shards, 2);
512 assert_eq!(summary.total_size_bytes, 75000);
513 }
514
515 #[test]
516 fn test_hotspot_detection() {
517 let mut manager = ShardManager::new(
518 ShardKey {
519 field: "id".to_string(),
520 hash_function: "xxhash".to_string(),
521 },
522 ShardingStrategy::ConsistentHash,
523 );
524
525 let shards = vec![
526 ShardId("shard-0".to_string()),
527 ShardId("shard-1".to_string()),
528 ShardId("shard-2".to_string()),
529 ];
530 manager.initialize_shards(shards, HashMap::new());
531
532 manager.record_shard_operation(
534 &ShardId("shard-0".to_string()),
535 "write",
536 5.0,
537 100000,
538 50000000,
539 );
540 manager.record_shard_operation(&ShardId("shard-1".to_string()), "write", 5.0, 1000, 500000);
541 manager.record_shard_operation(&ShardId("shard-2".to_string()), "write", 5.0, 100, 50000);
542
543 let hotspots = manager.detect_hotspots();
545 assert!(hotspots.len() >= 0);
548 }
549}