1use std::collections::HashMap;
8
9#[derive(Clone, Debug, PartialEq, Eq)]
13pub struct ReplicaLocation {
14 pub peer_id: String,
16 pub confirmed_at_tick: u64,
18 pub is_primary: bool,
20}
21
22#[derive(Clone, Copy, Debug, PartialEq, Eq)]
26pub enum ReplicationStatus {
27 Healthy,
29 UnderReplicated,
31 OverReplicated,
33 Critical,
35}
36
37#[derive(Clone, Debug)]
41pub struct BlockReplicationEntry {
42 pub block_id: u64,
44 pub cid: String,
46 pub desired_replicas: usize,
48 pub replicas: Vec<ReplicaLocation>,
50}
51
52impl BlockReplicationEntry {
53 pub fn actual_replicas(&self) -> usize {
55 self.replicas.len()
56 }
57
58 pub fn status(&self) -> ReplicationStatus {
60 let actual = self.replicas.len();
61 if actual == 0 {
62 ReplicationStatus::Critical
63 } else if actual < self.desired_replicas {
64 ReplicationStatus::UnderReplicated
65 } else if actual > self.desired_replicas {
66 ReplicationStatus::OverReplicated
67 } else {
68 ReplicationStatus::Healthy
69 }
70 }
71
72 pub fn deficit(&self) -> usize {
76 self.desired_replicas.saturating_sub(self.replicas.len())
77 }
78
79 pub fn surplus(&self) -> usize {
83 self.replicas.len().saturating_sub(self.desired_replicas)
84 }
85}
86
87#[derive(Clone, Debug, PartialEq, Eq)]
91pub struct ReplicationTask {
92 pub block_id: u64,
94 pub cid: String,
96 pub needed_copies: usize,
98 pub priority: u32,
100}
101
102#[derive(Clone, Debug, PartialEq, Eq)]
106pub struct ReplicationStats {
107 pub total_blocks: usize,
109 pub healthy: usize,
111 pub under_replicated: usize,
113 pub over_replicated: usize,
115 pub critical: usize,
117 pub total_replicas: usize,
119}
120
121pub struct StorageReplicationTracker {
125 pub entries: HashMap<u64, BlockReplicationEntry>,
127 pub next_block_id: u64,
129 pub default_desired_replicas: usize,
131}
132
133impl StorageReplicationTracker {
134 pub fn new(default_desired_replicas: usize) -> Self {
136 Self {
137 entries: HashMap::new(),
138 next_block_id: 0,
139 default_desired_replicas,
140 }
141 }
142
143 pub fn register_block(&mut self, cid: String, desired_replicas: Option<usize>) -> u64 {
147 let block_id = self.next_block_id;
148 self.next_block_id += 1;
149 let desired = desired_replicas.unwrap_or(self.default_desired_replicas);
150 self.entries.insert(
151 block_id,
152 BlockReplicationEntry {
153 block_id,
154 cid,
155 desired_replicas: desired,
156 replicas: Vec::new(),
157 },
158 );
159 block_id
160 }
161
162 pub fn add_replica(
169 &mut self,
170 block_id: u64,
171 peer_id: String,
172 current_tick: u64,
173 is_primary: bool,
174 ) -> bool {
175 let Some(entry) = self.entries.get_mut(&block_id) else {
176 return false;
177 };
178
179 if let Some(loc) = entry.replicas.iter_mut().find(|l| l.peer_id == peer_id) {
180 loc.confirmed_at_tick = current_tick;
181 loc.is_primary = is_primary;
182 } else {
183 entry.replicas.push(ReplicaLocation {
184 peer_id,
185 confirmed_at_tick: current_tick,
186 is_primary,
187 });
188 }
189 true
190 }
191
192 pub fn remove_replica(&mut self, block_id: u64, peer_id: &str) -> bool {
196 let Some(entry) = self.entries.get_mut(&block_id) else {
197 return false;
198 };
199 let before = entry.replicas.len();
200 entry.replicas.retain(|l| l.peer_id != peer_id);
201 entry.replicas.len() < before
202 }
203
204 pub fn under_replicated_blocks(&self) -> Vec<&BlockReplicationEntry> {
207 let mut blocks: Vec<&BlockReplicationEntry> = self
208 .entries
209 .values()
210 .filter(|e| {
211 matches!(
212 e.status(),
213 ReplicationStatus::UnderReplicated | ReplicationStatus::Critical
214 )
215 })
216 .collect();
217 blocks.sort_by_key(|e| e.block_id);
218 blocks
219 }
220
221 pub fn generate_tasks(&self) -> Vec<ReplicationTask> {
225 let mut tasks: Vec<ReplicationTask> = self
226 .entries
227 .values()
228 .filter(|e| e.deficit() > 0)
229 .map(|e| {
230 let priority = match e.status() {
231 ReplicationStatus::Critical => 100,
232 _ => 50,
233 };
234 ReplicationTask {
235 block_id: e.block_id,
236 cid: e.cid.clone(),
237 needed_copies: e.deficit(),
238 priority,
239 }
240 })
241 .collect();
242
243 tasks.sort_by(|a, b| {
244 b.priority
245 .cmp(&a.priority)
246 .then_with(|| a.block_id.cmp(&b.block_id))
247 });
248 tasks
249 }
250
251 pub fn get_entry(&self, block_id: u64) -> Option<&BlockReplicationEntry> {
253 self.entries.get(&block_id)
254 }
255
256 pub fn stats(&self) -> ReplicationStats {
258 let mut stats = ReplicationStats {
259 total_blocks: self.entries.len(),
260 healthy: 0,
261 under_replicated: 0,
262 over_replicated: 0,
263 critical: 0,
264 total_replicas: 0,
265 };
266 for entry in self.entries.values() {
267 stats.total_replicas += entry.replicas.len();
268 match entry.status() {
269 ReplicationStatus::Healthy => stats.healthy += 1,
270 ReplicationStatus::UnderReplicated => stats.under_replicated += 1,
271 ReplicationStatus::OverReplicated => stats.over_replicated += 1,
272 ReplicationStatus::Critical => stats.critical += 1,
273 }
274 }
275 stats
276 }
277}
278
279#[cfg(test)]
282mod tests {
283 use super::*;
284
285 fn tracker() -> StorageReplicationTracker {
286 StorageReplicationTracker::new(3)
287 }
288
289 #[test]
292 fn test_register_block_creates_entry() {
293 let mut t = tracker();
294 let id = t.register_block("cid-a".into(), None);
295 assert!(t.entries.contains_key(&id));
296 }
297
298 #[test]
299 fn test_register_block_returns_incrementing_ids() {
300 let mut t = tracker();
301 let id0 = t.register_block("cid-0".into(), None);
302 let id1 = t.register_block("cid-1".into(), None);
303 assert_eq!(id0, 0);
304 assert_eq!(id1, 1);
305 }
306
307 #[test]
308 fn test_register_block_uses_default_desired_replicas_when_none() {
309 let mut t = StorageReplicationTracker::new(5);
310 let id = t.register_block("cid-x".into(), None);
311 assert_eq!(t.entries[&id].desired_replicas, 5);
312 }
313
314 #[test]
315 fn test_register_block_uses_provided_desired_replicas() {
316 let mut t = tracker();
317 let id = t.register_block("cid-x".into(), Some(7));
318 assert_eq!(t.entries[&id].desired_replicas, 7);
319 }
320
321 #[test]
322 fn test_register_block_starts_with_no_replicas() {
323 let mut t = tracker();
324 let id = t.register_block("cid-x".into(), None);
325 assert!(t.entries[&id].replicas.is_empty());
326 }
327
328 #[test]
329 fn test_register_block_stores_cid() {
330 let mut t = tracker();
331 let id = t.register_block("my-cid".into(), None);
332 assert_eq!(t.entries[&id].cid, "my-cid");
333 }
334
335 #[test]
338 fn test_add_replica_appends_new_location() {
339 let mut t = tracker();
340 let id = t.register_block("c".into(), None);
341 assert!(t.add_replica(id, "peer-1".into(), 10, false));
342 assert_eq!(t.entries[&id].replicas.len(), 1);
343 assert_eq!(t.entries[&id].replicas[0].peer_id, "peer-1");
344 }
345
346 #[test]
347 fn test_add_replica_returns_false_for_unknown_block_id() {
348 let mut t = tracker();
349 assert!(!t.add_replica(999, "peer-1".into(), 1, false));
350 }
351
352 #[test]
353 fn test_add_replica_updates_existing_peer_tick() {
354 let mut t = tracker();
355 let id = t.register_block("c".into(), None);
356 t.add_replica(id, "peer-1".into(), 10, false);
357 t.add_replica(id, "peer-1".into(), 99, true);
358 assert_eq!(t.entries[&id].replicas.len(), 1);
360 assert_eq!(t.entries[&id].replicas[0].confirmed_at_tick, 99);
361 assert!(t.entries[&id].replicas[0].is_primary);
362 }
363
364 #[test]
365 fn test_add_replica_multiple_peers() {
366 let mut t = tracker();
367 let id = t.register_block("c".into(), None);
368 t.add_replica(id, "peer-1".into(), 1, true);
369 t.add_replica(id, "peer-2".into(), 2, false);
370 t.add_replica(id, "peer-3".into(), 3, false);
371 assert_eq!(t.entries[&id].replicas.len(), 3);
372 }
373
374 #[test]
377 fn test_remove_replica_removes_entry() {
378 let mut t = tracker();
379 let id = t.register_block("c".into(), None);
380 t.add_replica(id, "peer-1".into(), 1, false);
381 assert!(t.remove_replica(id, "peer-1"));
382 assert!(t.entries[&id].replicas.is_empty());
383 }
384
385 #[test]
386 fn test_remove_replica_returns_false_for_unknown_block() {
387 let mut t = tracker();
388 assert!(!t.remove_replica(42, "peer-1"));
389 }
390
391 #[test]
392 fn test_remove_replica_returns_false_for_unknown_peer() {
393 let mut t = tracker();
394 let id = t.register_block("c".into(), None);
395 assert!(!t.remove_replica(id, "ghost-peer"));
396 }
397
398 #[test]
401 fn test_status_critical_when_zero_replicas() {
402 let mut t = tracker();
403 let id = t.register_block("c".into(), Some(3));
404 assert_eq!(t.entries[&id].status(), ReplicationStatus::Critical);
405 }
406
407 #[test]
408 fn test_status_under_replicated_when_below_desired() {
409 let mut t = tracker();
410 let id = t.register_block("c".into(), Some(3));
411 t.add_replica(id, "p1".into(), 1, false);
412 t.add_replica(id, "p2".into(), 2, false);
413 assert_eq!(t.entries[&id].status(), ReplicationStatus::UnderReplicated);
414 }
415
416 #[test]
417 fn test_status_healthy_when_equal_desired() {
418 let mut t = tracker();
419 let id = t.register_block("c".into(), Some(2));
420 t.add_replica(id, "p1".into(), 1, true);
421 t.add_replica(id, "p2".into(), 2, false);
422 assert_eq!(t.entries[&id].status(), ReplicationStatus::Healthy);
423 }
424
425 #[test]
426 fn test_status_over_replicated_when_above_desired() {
427 let mut t = tracker();
428 let id = t.register_block("c".into(), Some(1));
429 t.add_replica(id, "p1".into(), 1, true);
430 t.add_replica(id, "p2".into(), 2, false);
431 assert_eq!(t.entries[&id].status(), ReplicationStatus::OverReplicated);
432 }
433
434 #[test]
437 fn test_deficit_correct_value() {
438 let mut t = tracker();
439 let id = t.register_block("c".into(), Some(5));
440 t.add_replica(id, "p1".into(), 1, false);
441 t.add_replica(id, "p2".into(), 2, false);
442 assert_eq!(t.entries[&id].deficit(), 3);
443 }
444
445 #[test]
446 fn test_deficit_zero_when_healthy() {
447 let mut t = tracker();
448 let id = t.register_block("c".into(), Some(2));
449 t.add_replica(id, "p1".into(), 1, false);
450 t.add_replica(id, "p2".into(), 2, false);
451 assert_eq!(t.entries[&id].deficit(), 0);
452 }
453
454 #[test]
455 fn test_deficit_zero_when_over_replicated() {
456 let mut t = tracker();
457 let id = t.register_block("c".into(), Some(1));
458 t.add_replica(id, "p1".into(), 1, false);
459 t.add_replica(id, "p2".into(), 2, false);
460 assert_eq!(t.entries[&id].deficit(), 0);
461 }
462
463 #[test]
464 fn test_surplus_correct_value() {
465 let mut t = tracker();
466 let id = t.register_block("c".into(), Some(1));
467 t.add_replica(id, "p1".into(), 1, false);
468 t.add_replica(id, "p2".into(), 2, false);
469 t.add_replica(id, "p3".into(), 3, false);
470 assert_eq!(t.entries[&id].surplus(), 2);
471 }
472
473 #[test]
474 fn test_surplus_zero_when_healthy() {
475 let mut t = tracker();
476 let id = t.register_block("c".into(), Some(2));
477 t.add_replica(id, "p1".into(), 1, false);
478 t.add_replica(id, "p2".into(), 2, false);
479 assert_eq!(t.entries[&id].surplus(), 0);
480 }
481
482 #[test]
485 fn test_under_replicated_blocks_sorted_by_block_id_asc() {
486 let mut t = tracker();
487 let id0 = t.register_block("c0".into(), Some(3)); let id1 = t.register_block("c1".into(), Some(3)); let id2 = t.register_block("c2".into(), Some(2)); let id3 = t.register_block("c3".into(), Some(3)); t.add_replica(id1, "p1".into(), 1, false);
494 t.add_replica(id2, "p1".into(), 1, false);
495 t.add_replica(id2, "p2".into(), 2, false);
496 t.add_replica(id3, "p1".into(), 1, false);
497 t.add_replica(id3, "p2".into(), 2, false);
498
499 let under = t.under_replicated_blocks();
500 assert_eq!(under.len(), 3); assert_eq!(under[0].block_id, id0);
502 assert_eq!(under[1].block_id, id1);
503 assert_eq!(under[2].block_id, id3);
504 }
505
506 #[test]
507 fn test_under_replicated_blocks_excludes_healthy_and_over_replicated() {
508 let mut t = tracker();
509 let id_healthy = t.register_block("h".into(), Some(1));
510 let id_over = t.register_block("o".into(), Some(1));
511 t.add_replica(id_healthy, "p1".into(), 1, false);
512 t.add_replica(id_over, "p1".into(), 1, false);
513 t.add_replica(id_over, "p2".into(), 2, false);
514 assert!(t.under_replicated_blocks().is_empty());
515 }
516
517 #[test]
520 fn test_generate_tasks_priority_critical_before_under_replicated() {
521 let mut t = tracker();
522 let id_under = t.register_block("under".into(), Some(3));
523 let id_crit = t.register_block("crit".into(), Some(2));
524 t.add_replica(id_under, "p1".into(), 1, false);
526 let tasks = t.generate_tasks();
529 assert_eq!(tasks.len(), 2);
530 assert_eq!(tasks[0].block_id, id_crit);
531 assert_eq!(tasks[0].priority, 100);
532 assert_eq!(tasks[1].block_id, id_under);
533 assert_eq!(tasks[1].priority, 50);
534 }
535
536 #[test]
537 fn test_generate_tasks_needed_copies_equals_deficit() {
538 let mut t = tracker();
539 let id = t.register_block("c".into(), Some(5));
540 t.add_replica(id, "p1".into(), 1, false);
541 t.add_replica(id, "p2".into(), 2, false);
542 let tasks = t.generate_tasks();
543 assert_eq!(tasks.len(), 1);
544 assert_eq!(tasks[0].needed_copies, 3);
545 }
546
547 #[test]
548 fn test_generate_tasks_sorted_by_block_id_within_same_priority() {
549 let mut t = tracker();
550 let id0 = t.register_block("c0".into(), Some(2));
552 let id1 = t.register_block("c1".into(), Some(3));
553 let tasks = t.generate_tasks();
554 assert_eq!(tasks.len(), 2);
555 assert!(tasks[0].block_id <= tasks[1].block_id);
556 let _ = id0;
557 let _ = id1;
558 }
559
560 #[test]
561 fn test_generate_tasks_excludes_healthy_blocks() {
562 let mut t = tracker();
563 let id = t.register_block("c".into(), Some(1));
564 t.add_replica(id, "p1".into(), 1, false);
565 assert!(t.generate_tasks().is_empty());
566 }
567
568 #[test]
569 fn test_generate_tasks_excludes_over_replicated_blocks() {
570 let mut t = tracker();
571 let id = t.register_block("c".into(), Some(1));
572 t.add_replica(id, "p1".into(), 1, false);
573 t.add_replica(id, "p2".into(), 2, false);
574 assert!(t.generate_tasks().is_empty());
575 }
576
577 #[test]
580 fn test_stats_empty_tracker() {
581 let t = tracker();
582 let s = t.stats();
583 assert_eq!(s.total_blocks, 0);
584 assert_eq!(s.healthy, 0);
585 assert_eq!(s.critical, 0);
586 assert_eq!(s.total_replicas, 0);
587 }
588
589 #[test]
590 fn test_stats_counts_by_status() {
591 let mut t = tracker();
592 let id_crit = t.register_block("crit".into(), Some(2));
594 let id_under = t.register_block("under".into(), Some(2));
596 t.add_replica(id_under, "p1".into(), 1, false);
597 let id_healthy = t.register_block("healthy".into(), Some(2));
599 t.add_replica(id_healthy, "p1".into(), 1, false);
600 t.add_replica(id_healthy, "p2".into(), 2, false);
601 let id_over = t.register_block("over".into(), Some(2));
603 t.add_replica(id_over, "p1".into(), 1, false);
604 t.add_replica(id_over, "p2".into(), 2, false);
605 t.add_replica(id_over, "p3".into(), 3, false);
606 let _ = id_crit;
607
608 let s = t.stats();
609 assert_eq!(s.total_blocks, 4);
610 assert_eq!(s.critical, 1);
611 assert_eq!(s.under_replicated, 1);
612 assert_eq!(s.healthy, 1);
613 assert_eq!(s.over_replicated, 1);
614 assert_eq!(s.total_replicas, 6); }
616
617 #[test]
620 fn test_get_entry_returns_some_for_known_block() {
621 let mut t = tracker();
622 let id = t.register_block("c".into(), None);
623 assert!(t.get_entry(id).is_some());
624 }
625
626 #[test]
627 fn test_get_entry_returns_none_for_unknown_block() {
628 let t = tracker();
629 assert!(t.get_entry(9999).is_none());
630 }
631}