1use std::collections::{HashMap, VecDeque};
9
10pub type BmpBlockId = [u8; 32];
16
17pub type BmpPlanId = u64;
19
20pub type BmpBlockMigrationPlanner = BlockMigrationPlanner;
22
23#[inline]
29fn xorshift64(state: &mut u64) -> u64 {
30 let mut x = *state;
31 x ^= x << 13;
32 x ^= x >> 7;
33 x ^= x << 17;
34 *state = x;
35 x
36}
37
38#[inline]
40fn fnv1a_64(data: &[u8]) -> u64 {
41 let mut h: u64 = 14_695_981_039_346_656_037;
42 for &b in data {
43 h ^= b as u64;
44 h = h.wrapping_mul(1_099_511_628_211);
45 }
46 h
47}
48
49fn now_ts() -> u64 {
53 use std::time::{SystemTime, UNIX_EPOCH};
54 SystemTime::now()
55 .duration_since(UNIX_EPOCH)
56 .map(|d| d.as_nanos() as u64)
57 .unwrap_or(1)
58}
59
60#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
66pub enum BmpPlanStatus {
67 Pending,
69 InProgress,
71 Completed,
73 Failed,
75 Cancelled,
77}
78
79impl std::fmt::Display for BmpPlanStatus {
80 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
81 match self {
82 Self::Pending => write!(f, "Pending"),
83 Self::InProgress => write!(f, "InProgress"),
84 Self::Completed => write!(f, "Completed"),
85 Self::Failed => write!(f, "Failed"),
86 Self::Cancelled => write!(f, "Cancelled"),
87 }
88 }
89}
90
91#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
93pub enum BmpPriorityPolicy {
94 FifoQueue,
96 HighFrequencyFirst,
98 LargestFirst,
100 SmallestFirst,
102 CostOptimized,
104}
105
106impl std::fmt::Display for BmpPriorityPolicy {
107 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
108 match self {
109 Self::FifoQueue => write!(f, "FifoQueue"),
110 Self::HighFrequencyFirst => write!(f, "HighFrequencyFirst"),
111 Self::LargestFirst => write!(f, "LargestFirst"),
112 Self::SmallestFirst => write!(f, "SmallestFirst"),
113 Self::CostOptimized => write!(f, "CostOptimized"),
114 }
115 }
116}
117
118#[derive(Clone, Debug)]
124pub struct BmpBlockMeta {
125 pub id: BmpBlockId,
127 pub size_bytes: u64,
129 pub src_node: String,
131 pub dst_node: Option<String>,
133 pub tier: u8,
135 pub access_frequency: f64,
137 pub last_accessed: u64,
139 pub is_pinned: bool,
141}
142
143impl BmpBlockMeta {
144 pub fn new(
146 id: BmpBlockId,
147 size_bytes: u64,
148 src_node: impl Into<String>,
149 tier: u8,
150 access_frequency: f64,
151 ) -> Self {
152 Self {
153 id,
154 size_bytes,
155 src_node: src_node.into(),
156 dst_node: None,
157 tier,
158 access_frequency,
159 last_accessed: now_ts(),
160 is_pinned: false,
161 }
162 }
163}
164
165#[derive(Clone, Debug)]
167pub struct BmpMigrationPlan {
168 pub id: BmpPlanId,
170 pub block_ids: Vec<BmpBlockId>,
172 pub src_node: String,
174 pub dst_node: String,
176 pub priority: u32,
178 pub estimated_bytes: u64,
180 pub status: BmpPlanStatus,
182 pub created_at: u64,
184}
185
186#[derive(Clone, Debug)]
188pub struct BmpMigrationRecord {
189 pub ts: u64,
191 pub plan_id: BmpPlanId,
193 pub block_id: BmpBlockId,
195 pub bytes_moved: u64,
197 pub success: bool,
199 pub error: Option<String>,
201}
202
203#[derive(Clone, Debug)]
205pub struct BmpPlannerConfig {
206 pub max_concurrent_migrations: usize,
208 pub bandwidth_limit_kbps: u64,
210 pub priority_policy: BmpPriorityPolicy,
212 pub dry_run: bool,
214}
215
216impl Default for BmpPlannerConfig {
217 fn default() -> Self {
218 Self {
219 max_concurrent_migrations: 4,
220 bandwidth_limit_kbps: 0,
221 priority_policy: BmpPriorityPolicy::FifoQueue,
222 dry_run: false,
223 }
224 }
225}
226
227#[derive(Clone, Debug, Default)]
229pub struct BmpPlannerStats {
230 pub total_blocks: usize,
232 pub pinned_blocks: usize,
234 pub total_plans: usize,
236 pub pending_plans: usize,
238 pub in_progress_plans: usize,
240 pub completed_plans: usize,
242 pub failed_plans: usize,
244 pub cancelled_plans: usize,
246 pub total_records: usize,
248 pub total_bytes_moved: u64,
250 pub total_failures: usize,
252}
253
254#[derive(Clone, Debug)]
256pub struct BmpExecutionResult {
257 pub plan_id: BmpPlanId,
259 pub status: BmpPlanStatus,
261 pub succeeded_blocks: Vec<BmpBlockId>,
263 pub failed_blocks: Vec<(BmpBlockId, String)>,
265 pub bytes_moved: u64,
267}
268
269#[derive(Clone, Debug, Default)]
271pub struct BmpBatchResult {
272 pub plans_executed: usize,
274 pub plans_completed: usize,
276 pub plans_failed: usize,
278 pub total_bytes_moved: u64,
280 pub results: Vec<BmpExecutionResult>,
282}
283
284#[derive(Debug, thiserror::Error)]
290pub enum BmpError {
291 #[error("block {0} is not registered")]
292 BlockNotFound(String),
293 #[error("plan {0} does not exist")]
294 PlanNotFound(BmpPlanId),
295 #[error("plan {0} is not in Pending state (current: {1})")]
296 PlanNotPending(BmpPlanId, BmpPlanStatus),
297 #[error("plan has no blocks")]
298 EmptyPlan,
299 #[error("block {0} is pinned and cannot be migrated")]
300 BlockPinned(String),
301 #[error("destination node is empty")]
302 EmptyDstNode,
303 #[error("no eligible blocks found for scheduling")]
304 NoEligibleBlocks,
305}
306
307fn block_id_hex(id: &BmpBlockId) -> String {
308 let mut s = String::with_capacity(64);
309 for b in id {
310 s.push_str(&format!("{:02x}", b));
311 }
312 s
313}
314
315pub struct BlockMigrationPlanner {
322 blocks: HashMap<BmpBlockId, BmpBlockMeta>,
324 plans: HashMap<BmpPlanId, BmpMigrationPlan>,
326 log: VecDeque<BmpMigrationRecord>,
328 config: BmpPlannerConfig,
330 next_plan_id: BmpPlanId,
332 rng_state: u64,
334}
335
336impl BlockMigrationPlanner {
337 pub fn new() -> Self {
341 Self::with_config(BmpPlannerConfig::default())
342 }
343
344 pub fn with_config(config: BmpPlannerConfig) -> Self {
346 let seed_data = b"block_migration_planner_v1";
347 let ts = now_ts();
348 let base = fnv1a_64(seed_data);
349 let mut rng_state = base ^ ts;
350 if rng_state == 0 {
351 rng_state = 0xdeadbeef_cafebabe;
352 }
353 Self {
354 blocks: HashMap::new(),
355 plans: HashMap::new(),
356 log: VecDeque::new(),
357 config,
358 next_plan_id: 1,
359 rng_state,
360 }
361 }
362
363 pub fn config(&self) -> &BmpPlannerConfig {
365 &self.config
366 }
367
368 pub fn set_config(&mut self, config: BmpPlannerConfig) {
370 self.config = config;
371 }
372
373 pub fn register_block(
377 &mut self,
378 id: BmpBlockId,
379 size_bytes: u64,
380 src_node: impl Into<String>,
381 tier: u8,
382 access_frequency: f64,
383 ) {
384 let meta = BmpBlockMeta::new(id, size_bytes, src_node, tier, access_frequency);
385 self.blocks.insert(id, meta);
386 }
387
388 pub fn update_access(&mut self, id: &BmpBlockId) -> Result<(), BmpError> {
391 let meta = self
392 .blocks
393 .get_mut(id)
394 .ok_or_else(|| BmpError::BlockNotFound(block_id_hex(id)))?;
395 meta.last_accessed = now_ts();
396 meta.access_frequency = meta.access_frequency * 0.9 + 1.0 * 0.1;
398 Ok(())
399 }
400
401 pub fn pin(&mut self, id: &BmpBlockId) -> Result<(), BmpError> {
403 let meta = self
404 .blocks
405 .get_mut(id)
406 .ok_or_else(|| BmpError::BlockNotFound(block_id_hex(id)))?;
407 meta.is_pinned = true;
408 Ok(())
409 }
410
411 pub fn unpin(&mut self, id: &BmpBlockId) -> Result<(), BmpError> {
413 let meta = self
414 .blocks
415 .get_mut(id)
416 .ok_or_else(|| BmpError::BlockNotFound(block_id_hex(id)))?;
417 meta.is_pinned = false;
418 Ok(())
419 }
420
421 pub fn block_meta(&self, id: &BmpBlockId) -> Option<&BmpBlockMeta> {
423 self.blocks.get(id)
424 }
425
426 pub fn block_count(&self) -> usize {
428 self.blocks.len()
429 }
430
431 pub fn create_plan(
441 &mut self,
442 block_ids: Vec<BmpBlockId>,
443 dst_node: impl Into<String>,
444 priority: u32,
445 ) -> Result<BmpPlanId, BmpError> {
446 if block_ids.is_empty() {
447 return Err(BmpError::EmptyPlan);
448 }
449 let dst_node = dst_node.into();
450 if dst_node.is_empty() {
451 return Err(BmpError::EmptyDstNode);
452 }
453
454 for bid in &block_ids {
456 let meta = self
457 .blocks
458 .get(bid)
459 .ok_or_else(|| BmpError::BlockNotFound(block_id_hex(bid)))?;
460 if meta.is_pinned {
461 return Err(BmpError::BlockPinned(block_id_hex(bid)));
462 }
463 }
464
465 let src_node = self
467 .blocks
468 .get(&block_ids[0])
469 .map(|m| m.src_node.clone())
470 .unwrap_or_default();
471
472 let estimated_bytes: u64 = block_ids
473 .iter()
474 .filter_map(|bid| self.blocks.get(bid).map(|m| m.size_bytes))
475 .sum();
476
477 let plan_id = self.next_plan_id;
478 self.next_plan_id += 1;
479
480 for bid in &block_ids {
482 if let Some(meta) = self.blocks.get_mut(bid) {
483 meta.dst_node = Some(dst_node.clone());
484 }
485 }
486
487 let plan = BmpMigrationPlan {
488 id: plan_id,
489 block_ids,
490 src_node,
491 dst_node,
492 priority,
493 estimated_bytes,
494 status: BmpPlanStatus::Pending,
495 created_at: now_ts(),
496 };
497 self.plans.insert(plan_id, plan);
498 Ok(plan_id)
499 }
500
501 pub fn cancel_plan(&mut self, plan_id: BmpPlanId) -> Result<(), BmpError> {
504 let plan = self
505 .plans
506 .get_mut(&plan_id)
507 .ok_or(BmpError::PlanNotFound(plan_id))?;
508 if plan.status != BmpPlanStatus::Pending {
509 return Err(BmpError::PlanNotPending(plan_id, plan.status));
510 }
511 plan.status = BmpPlanStatus::Cancelled;
512 Ok(())
513 }
514
515 pub fn execute_plan(&mut self, plan_id: BmpPlanId) -> Result<BmpExecutionResult, BmpError> {
521 {
523 let plan = self
524 .plans
525 .get(&plan_id)
526 .ok_or(BmpError::PlanNotFound(plan_id))?;
527 if plan.status != BmpPlanStatus::Pending {
528 return Err(BmpError::PlanNotPending(plan_id, plan.status));
529 }
530 }
531
532 if let Some(plan) = self.plans.get_mut(&plan_id) {
534 plan.status = BmpPlanStatus::InProgress;
535 }
536
537 let block_ids: Vec<BmpBlockId> = self
538 .plans
539 .get(&plan_id)
540 .map(|p| p.block_ids.clone())
541 .unwrap_or_default();
542
543 let dst_node: String = self
544 .plans
545 .get(&plan_id)
546 .map(|p| p.dst_node.clone())
547 .unwrap_or_default();
548
549 let dry_run = self.config.dry_run;
550
551 let mut succeeded_blocks: Vec<BmpBlockId> = Vec::new();
552 let mut failed_blocks: Vec<(BmpBlockId, String)> = Vec::new();
553 let mut bytes_moved: u64 = 0;
554
555 for bid in &block_ids {
556 let size = self.blocks.get(bid).map(|m| m.size_bytes).unwrap_or(0);
557
558 let (success, error_msg): (bool, Option<String>) = if dry_run {
559 (true, None)
560 } else {
561 let roll = xorshift64(&mut self.rng_state);
563 if (roll >> 60) != 0xF {
564 (true, None)
565 } else {
566 (
567 false,
568 Some(format!("simulated I/O error (roll=0x{:016x})", roll)),
569 )
570 }
571 };
572
573 if success {
574 bytes_moved += size;
575 succeeded_blocks.push(*bid);
576 if let Some(meta) = self.blocks.get_mut(bid) {
578 meta.src_node = dst_node.clone();
579 meta.dst_node = None;
580 }
581 } else {
582 failed_blocks.push((*bid, error_msg.clone().unwrap_or_default()));
583 }
584
585 let record = BmpMigrationRecord {
586 ts: now_ts(),
587 plan_id,
588 block_id: *bid,
589 bytes_moved: if success { size } else { 0 },
590 success,
591 error: error_msg,
592 };
593 self.append_log(record);
594 }
595
596 let final_status = if failed_blocks.is_empty() {
597 BmpPlanStatus::Completed
598 } else {
599 BmpPlanStatus::Failed
600 };
601
602 if let Some(plan) = self.plans.get_mut(&plan_id) {
603 plan.status = final_status;
604 }
605
606 Ok(BmpExecutionResult {
607 plan_id,
608 status: final_status,
609 succeeded_blocks,
610 failed_blocks,
611 bytes_moved,
612 })
613 }
614
615 pub fn schedule_migrations(
624 &mut self,
625 policy: BmpPriorityPolicy,
626 max_plans: usize,
627 ) -> Result<Vec<BmpPlanId>, BmpError> {
628 if max_plans == 0 {
629 return Ok(Vec::new());
630 }
631
632 let mut candidates: Vec<BmpBlockId> = self
634 .blocks
635 .values()
636 .filter(|m| !m.is_pinned && m.dst_node.is_some())
637 .map(|m| m.id)
638 .collect();
639
640 if candidates.is_empty() {
641 candidates = self
643 .blocks
644 .values()
645 .filter(|m| !m.is_pinned && m.tier > 0)
646 .map(|m| m.id)
647 .collect();
648 }
649
650 if candidates.is_empty() {
651 return Err(BmpError::NoEligibleBlocks);
652 }
653
654 self.sort_candidates_by_policy(&mut candidates, policy);
656
657 candidates.truncate(max_plans);
658
659 let mut plan_ids = Vec::with_capacity(candidates.len());
660 for bid in candidates {
661 let dst = self
662 .blocks
663 .get(&bid)
664 .and_then(|m| m.dst_node.clone())
665 .unwrap_or_else(|| "node-0".to_string());
666
667 match self.create_plan(vec![bid], dst, 0) {
668 Ok(pid) => plan_ids.push(pid),
669 Err(_) => continue,
670 }
671 }
672 Ok(plan_ids)
673 }
674
675 fn sort_candidates_by_policy(&self, ids: &mut [BmpBlockId], policy: BmpPriorityPolicy) {
677 match policy {
678 BmpPriorityPolicy::FifoQueue => {
679 ids.sort_by(|a, b| {
681 let ta = self.blocks.get(a).map_or(0, |m| m.last_accessed);
682 let tb = self.blocks.get(b).map_or(0, |m| m.last_accessed);
683 ta.cmp(&tb)
684 });
685 }
686 BmpPriorityPolicy::HighFrequencyFirst => {
687 ids.sort_by(|a, b| {
688 let fa = self.blocks.get(a).map_or(0.0, |m| m.access_frequency);
689 let fb = self.blocks.get(b).map_or(0.0, |m| m.access_frequency);
690 fb.partial_cmp(&fa).unwrap_or(std::cmp::Ordering::Equal)
691 });
692 }
693 BmpPriorityPolicy::LargestFirst => {
694 ids.sort_by(|a, b| {
695 let sa = self.blocks.get(a).map_or(0, |m| m.size_bytes);
696 let sb = self.blocks.get(b).map_or(0, |m| m.size_bytes);
697 sb.cmp(&sa)
698 });
699 }
700 BmpPriorityPolicy::SmallestFirst => {
701 ids.sort_by(|a, b| {
702 let sa = self.blocks.get(a).map_or(0, |m| m.size_bytes);
703 let sb = self.blocks.get(b).map_or(0, |m| m.size_bytes);
704 sa.cmp(&sb)
705 });
706 }
707 BmpPriorityPolicy::CostOptimized => {
708 ids.sort_by(|a, b| {
710 let ca = self
711 .blocks
712 .get(a)
713 .map_or(0, |m| m.size_bytes.saturating_mul(m.tier as u64));
714 let cb = self
715 .blocks
716 .get(b)
717 .map_or(0, |m| m.size_bytes.saturating_mul(m.tier as u64));
718 ca.cmp(&cb)
719 });
720 }
721 }
722 }
723
724 pub fn run_batch_migration(
727 &mut self,
728 policy: BmpPriorityPolicy,
729 batch_size: usize,
730 ) -> BmpBatchResult {
731 let max = self.config.max_concurrent_migrations.min(batch_size);
732
733 let plan_ids = match self.schedule_migrations(policy, max) {
734 Ok(ids) => ids,
735 Err(_) => return BmpBatchResult::default(),
736 };
737
738 let mut batch = BmpBatchResult {
739 plans_executed: plan_ids.len(),
740 ..BmpBatchResult::default()
741 };
742
743 for pid in plan_ids {
744 match self.execute_plan(pid) {
745 Ok(result) => {
746 batch.total_bytes_moved += result.bytes_moved;
747 if result.status == BmpPlanStatus::Completed {
748 batch.plans_completed += 1;
749 } else {
750 batch.plans_failed += 1;
751 }
752 batch.results.push(result);
753 }
754 Err(_) => {
755 batch.plans_failed += 1;
756 }
757 }
758 }
759 batch
760 }
761
762 pub fn defragment_plan(&mut self, nodes: &[String]) -> Vec<BmpPlanId> {
770 if nodes.is_empty() {
771 return Vec::new();
772 }
773
774 let mut node_counts: HashMap<String, usize> =
776 nodes.iter().map(|n| (n.clone(), 0)).collect();
777 for meta in self.blocks.values() {
778 if let Some(cnt) = node_counts.get_mut(&meta.src_node) {
779 *cnt += 1;
780 }
781 }
782
783 let total: usize = node_counts.values().sum();
784 let target = total.div_ceil(nodes.len());
785
786 let mut over_nodes: Vec<String> = node_counts
788 .iter()
789 .filter(|(_, &cnt)| cnt > target)
790 .map(|(n, _)| n.clone())
791 .collect();
792 let mut under_nodes: Vec<String> = node_counts
793 .iter()
794 .filter(|(_, &cnt)| cnt < target)
795 .map(|(n, _)| n.clone())
796 .collect();
797
798 if over_nodes.is_empty() || under_nodes.is_empty() {
799 return Vec::new();
800 }
801
802 over_nodes.sort();
804 under_nodes.sort();
805
806 struct Move {
808 bid: BmpBlockId,
809 dst: String,
810 }
811 let mut moves: Vec<Move> = Vec::new();
812
813 let mut capacity: HashMap<String, usize> = under_nodes
815 .iter()
816 .map(|n| {
817 let cnt = node_counts.get(n).copied().unwrap_or(0);
818 (n.clone(), target.saturating_sub(cnt))
819 })
820 .collect();
821
822 for over in &over_nodes {
823 let excess = node_counts
824 .get(over)
825 .map_or(0, |&cnt| cnt.saturating_sub(target));
826 if excess == 0 {
827 continue;
828 }
829 let candidates: Vec<BmpBlockId> = self
831 .blocks
832 .values()
833 .filter(|m| &m.src_node == over && !m.is_pinned && m.dst_node.is_none())
834 .map(|m| m.id)
835 .collect();
836
837 let mut moved = 0usize;
838 'outer: for bid in candidates {
839 if moved >= excess {
840 break;
841 }
842 for under in &under_nodes {
844 let cap = capacity.get_mut(under);
845 if let Some(c) = cap {
846 if *c > 0 {
847 *c -= 1;
848 moves.push(Move {
849 bid,
850 dst: under.clone(),
851 });
852 moved += 1;
853 continue 'outer;
854 }
855 }
856 }
857 break; }
859 }
860
861 let mut plan_ids = Vec::with_capacity(moves.len());
863 for m in moves {
864 if let Some(meta) = self.blocks.get_mut(&m.bid) {
866 meta.dst_node = Some(m.dst.clone());
867 }
868 match self.create_plan(vec![m.bid], m.dst, 0) {
869 Ok(pid) => plan_ids.push(pid),
870 Err(_) => continue,
871 }
872 }
873 plan_ids
874 }
875
876 pub fn migration_stats(&self) -> BmpPlannerStats {
880 let pinned_blocks = self.blocks.values().filter(|m| m.is_pinned).count();
881
882 let mut stats = BmpPlannerStats {
883 total_blocks: self.blocks.len(),
884 pinned_blocks,
885 total_plans: self.plans.len(),
886 total_records: self.log.len(),
887 ..Default::default()
888 };
889
890 for plan in self.plans.values() {
891 match plan.status {
892 BmpPlanStatus::Pending => stats.pending_plans += 1,
893 BmpPlanStatus::InProgress => stats.in_progress_plans += 1,
894 BmpPlanStatus::Completed => stats.completed_plans += 1,
895 BmpPlanStatus::Failed => stats.failed_plans += 1,
896 BmpPlanStatus::Cancelled => stats.cancelled_plans += 1,
897 }
898 }
899
900 for rec in &self.log {
901 if rec.success {
902 stats.total_bytes_moved += rec.bytes_moved;
903 } else {
904 stats.total_failures += 1;
905 }
906 }
907
908 stats
909 }
910
911 pub fn plan_count(&self) -> usize {
913 self.plans.len()
914 }
915
916 pub fn plan(&self, plan_id: BmpPlanId) -> Option<&BmpMigrationPlan> {
918 self.plans.get(&plan_id)
919 }
920
921 pub fn plans_iter(&self) -> impl Iterator<Item = &BmpMigrationPlan> {
923 self.plans.values()
924 }
925
926 pub fn recent_records(&self, n: usize) -> Vec<&BmpMigrationRecord> {
928 self.log.iter().rev().take(n).collect()
929 }
930
931 pub fn log_len(&self) -> usize {
933 self.log.len()
934 }
935
936 fn append_log(&mut self, record: BmpMigrationRecord) {
940 const MAX_LOG: usize = 1000;
941 if self.log.len() >= MAX_LOG {
942 self.log.pop_front();
943 }
944 self.log.push_back(record);
945 }
946}
947
948impl Default for BlockMigrationPlanner {
949 fn default() -> Self {
950 Self::new()
951 }
952}
953
954pub fn bmp_id_from_bytes(data: &[u8]) -> BmpBlockId {
963 let mut id = [0u8; 32];
964 let h0 = fnv1a_64(data);
965 let h1 = fnv1a_64(&h0.to_le_bytes());
966 let h2 = fnv1a_64(&h1.to_le_bytes());
967 let h3 = fnv1a_64(&h2.to_le_bytes());
968 id[0..8].copy_from_slice(&h0.to_le_bytes());
969 id[8..16].copy_from_slice(&h1.to_le_bytes());
970 id[16..24].copy_from_slice(&h2.to_le_bytes());
971 id[24..32].copy_from_slice(&h3.to_le_bytes());
972 id
973}
974
975#[cfg(test)]
980mod tests {
981 use super::*;
982
983 fn make_id(seed: u8) -> BmpBlockId {
986 bmp_id_from_bytes(&[seed; 16])
987 }
988
989 fn make_planner() -> BlockMigrationPlanner {
990 BlockMigrationPlanner::new()
991 }
992
993 fn register_n(planner: &mut BlockMigrationPlanner, n: usize, node: &str) -> Vec<BmpBlockId> {
994 let mut ids = Vec::with_capacity(n);
995 for i in 0..n {
996 let id = bmp_id_from_bytes(format!("block-{}", i).as_bytes());
997 planner.register_block(id, (i as u64 + 1) * 512, node, 1, 1.0);
998 ids.push(id);
999 }
1000 ids
1001 }
1002
1003 #[test]
1006 fn test_xorshift64_nonzero() {
1007 let mut state: u64 = 12345678;
1008 for _ in 0..100 {
1009 let v = xorshift64(&mut state);
1010 assert_ne!(v, 0);
1011 }
1012 }
1013
1014 #[test]
1015 fn test_xorshift64_changes_state() {
1016 let mut state: u64 = 1;
1017 let a = xorshift64(&mut state);
1018 let b = xorshift64(&mut state);
1019 assert_ne!(a, b);
1020 }
1021
1022 #[test]
1023 fn test_fnv1a_empty() {
1024 let h = fnv1a_64(&[]);
1025 assert_eq!(h, 14_695_981_039_346_656_037_u64);
1027 }
1028
1029 #[test]
1030 fn test_fnv1a_deterministic() {
1031 assert_eq!(fnv1a_64(b"hello"), fnv1a_64(b"hello"));
1032 }
1033
1034 #[test]
1035 fn test_fnv1a_differs_on_different_input() {
1036 assert_ne!(fnv1a_64(b"foo"), fnv1a_64(b"bar"));
1037 }
1038
1039 #[test]
1042 fn test_bmp_id_from_bytes_length() {
1043 let id = bmp_id_from_bytes(b"test");
1044 assert_eq!(id.len(), 32);
1045 }
1046
1047 #[test]
1048 fn test_bmp_id_from_bytes_deterministic() {
1049 assert_eq!(bmp_id_from_bytes(b"abc"), bmp_id_from_bytes(b"abc"));
1050 }
1051
1052 #[test]
1053 fn test_bmp_id_from_bytes_differs() {
1054 assert_ne!(bmp_id_from_bytes(b"a"), bmp_id_from_bytes(b"b"));
1055 }
1056
1057 #[test]
1060 fn test_new_planner_empty() {
1061 let p = make_planner();
1062 assert_eq!(p.block_count(), 0);
1063 assert_eq!(p.plan_count(), 0);
1064 assert_eq!(p.log_len(), 0);
1065 }
1066
1067 #[test]
1068 fn test_with_config_dry_run() {
1069 let cfg = BmpPlannerConfig {
1070 dry_run: true,
1071 ..Default::default()
1072 };
1073 let p = BlockMigrationPlanner::with_config(cfg);
1074 assert!(p.config().dry_run);
1075 }
1076
1077 #[test]
1078 fn test_set_config() {
1079 let mut p = make_planner();
1080 let cfg = BmpPlannerConfig {
1081 bandwidth_limit_kbps: 1024,
1082 ..Default::default()
1083 };
1084 p.set_config(cfg);
1085 assert_eq!(p.config().bandwidth_limit_kbps, 1024);
1086 }
1087
1088 #[test]
1091 fn test_register_block_increases_count() {
1092 let mut p = make_planner();
1093 let id = make_id(1);
1094 p.register_block(id, 1024, "node-0", 0, 1.0);
1095 assert_eq!(p.block_count(), 1);
1096 }
1097
1098 #[test]
1099 fn test_register_block_metadata() {
1100 let mut p = make_planner();
1101 let id = make_id(2);
1102 p.register_block(id, 2048, "node-1", 2, 3.5);
1103 let meta = p.block_meta(&id).expect("block should exist");
1104 assert_eq!(meta.size_bytes, 2048);
1105 assert_eq!(meta.src_node, "node-1");
1106 assert_eq!(meta.tier, 2);
1107 assert!((meta.access_frequency - 3.5).abs() < 1e-9);
1108 assert!(!meta.is_pinned);
1109 }
1110
1111 #[test]
1112 fn test_register_block_overwrite() {
1113 let mut p = make_planner();
1114 let id = make_id(3);
1115 p.register_block(id, 100, "node-a", 0, 0.5);
1116 p.register_block(id, 200, "node-b", 1, 2.0);
1117 assert_eq!(p.block_count(), 1); let meta = p.block_meta(&id).expect("exists");
1119 assert_eq!(meta.size_bytes, 200);
1120 assert_eq!(meta.src_node, "node-b");
1121 }
1122
1123 #[test]
1124 fn test_register_multiple_blocks() {
1125 let mut p = make_planner();
1126 for i in 0..10u8 {
1127 p.register_block(make_id(i), 512, "node-0", 0, 1.0);
1128 }
1129 assert_eq!(p.block_count(), 10);
1130 }
1131
1132 #[test]
1135 fn test_update_access_ok() {
1136 let mut p = make_planner();
1137 let id = make_id(10);
1138 p.register_block(id, 512, "node-0", 0, 1.0);
1139 let old_freq = p.block_meta(&id).map_or(0.0, |m| m.access_frequency);
1140 p.update_access(&id).expect("ok");
1141 let new_freq = p.block_meta(&id).map_or(0.0, |m| m.access_frequency);
1142 assert!(new_freq > 0.0);
1144 let _ = old_freq; }
1146
1147 #[test]
1148 fn test_update_access_unknown_block_errors() {
1149 let mut p = make_planner();
1150 let id = make_id(11);
1151 assert!(p.update_access(&id).is_err());
1152 }
1153
1154 #[test]
1155 fn test_update_access_bumps_last_accessed() {
1156 let mut p = make_planner();
1157 let id = make_id(12);
1158 p.register_block(id, 512, "node-0", 0, 1.0);
1159 let t0 = p.block_meta(&id).map_or(0, |m| m.last_accessed);
1160 std::thread::sleep(std::time::Duration::from_millis(1));
1161 p.update_access(&id).expect("ok");
1162 let t1 = p.block_meta(&id).map_or(0, |m| m.last_accessed);
1163 assert!(t1 >= t0);
1164 }
1165
1166 #[test]
1169 fn test_pin_block() {
1170 let mut p = make_planner();
1171 let id = make_id(20);
1172 p.register_block(id, 512, "node-0", 0, 1.0);
1173 p.pin(&id).expect("ok");
1174 assert!(p.block_meta(&id).is_some_and(|m| m.is_pinned));
1175 }
1176
1177 #[test]
1178 fn test_unpin_block() {
1179 let mut p = make_planner();
1180 let id = make_id(21);
1181 p.register_block(id, 512, "node-0", 0, 1.0);
1182 p.pin(&id).expect("ok");
1183 p.unpin(&id).expect("ok");
1184 assert!(!p.block_meta(&id).is_none_or(|m| m.is_pinned));
1185 }
1186
1187 #[test]
1188 fn test_pin_unknown_block_errors() {
1189 let mut p = make_planner();
1190 assert!(p.pin(&make_id(22)).is_err());
1191 }
1192
1193 #[test]
1194 fn test_unpin_unknown_block_errors() {
1195 let mut p = make_planner();
1196 assert!(p.unpin(&make_id(23)).is_err());
1197 }
1198
1199 #[test]
1202 fn test_create_plan_basic() {
1203 let mut p = make_planner();
1204 let id = make_id(30);
1205 p.register_block(id, 1024, "node-0", 0, 1.0);
1206 let pid = p.create_plan(vec![id], "node-1", 5).expect("ok");
1207 assert_eq!(pid, 1);
1208 let plan = p.plan(pid).expect("plan exists");
1209 assert_eq!(plan.status, BmpPlanStatus::Pending);
1210 assert_eq!(plan.estimated_bytes, 1024);
1211 assert_eq!(plan.dst_node, "node-1");
1212 assert_eq!(plan.priority, 5);
1213 }
1214
1215 #[test]
1216 fn test_create_plan_empty_blocks_errors() {
1217 let mut p = make_planner();
1218 assert!(p.create_plan(vec![], "node-1", 0).is_err());
1219 }
1220
1221 #[test]
1222 fn test_create_plan_empty_dst_errors() {
1223 let mut p = make_planner();
1224 let id = make_id(31);
1225 p.register_block(id, 512, "node-0", 0, 1.0);
1226 assert!(p.create_plan(vec![id], "", 0).is_err());
1227 }
1228
1229 #[test]
1230 fn test_create_plan_unknown_block_errors() {
1231 let mut p = make_planner();
1232 let id = make_id(32);
1233 assert!(p.create_plan(vec![id], "node-1", 0).is_err());
1234 }
1235
1236 #[test]
1237 fn test_create_plan_pinned_block_errors() {
1238 let mut p = make_planner();
1239 let id = make_id(33);
1240 p.register_block(id, 512, "node-0", 0, 1.0);
1241 p.pin(&id).expect("ok");
1242 assert!(p.create_plan(vec![id], "node-1", 0).is_err());
1243 }
1244
1245 #[test]
1246 fn test_create_plan_increments_id() {
1247 let mut p = make_planner();
1248 for i in 0..5u8 {
1249 let id = make_id(40 + i);
1250 p.register_block(id, 512, "node-0", 0, 1.0);
1251 let pid = p.create_plan(vec![id], "node-1", 0).expect("ok");
1252 assert_eq!(pid, i as u64 + 1);
1253 }
1254 }
1255
1256 #[test]
1257 fn test_create_plan_multi_block_bytes() {
1258 let mut p = make_planner();
1259 let ids: Vec<_> = (0..3u8)
1260 .map(|i| {
1261 let id = make_id(50 + i);
1262 p.register_block(id, 1000, "node-0", 0, 1.0);
1263 id
1264 })
1265 .collect();
1266 let pid = p.create_plan(ids, "node-1", 0).expect("ok");
1267 let plan = p.plan(pid).expect("plan");
1268 assert_eq!(plan.estimated_bytes, 3000);
1269 }
1270
1271 #[test]
1274 fn test_cancel_pending_plan() {
1275 let mut p = make_planner();
1276 let id = make_id(60);
1277 p.register_block(id, 512, "node-0", 0, 1.0);
1278 let pid = p.create_plan(vec![id], "node-1", 0).expect("ok");
1279 p.cancel_plan(pid).expect("ok");
1280 assert_eq!(
1281 p.plan(pid).map(|pl| pl.status),
1282 Some(BmpPlanStatus::Cancelled)
1283 );
1284 }
1285
1286 #[test]
1287 fn test_cancel_nonexistent_plan_errors() {
1288 let mut p = make_planner();
1289 assert!(p.cancel_plan(999).is_err());
1290 }
1291
1292 #[test]
1293 fn test_cancel_already_cancelled_errors() {
1294 let mut p = make_planner();
1295 let id = make_id(61);
1296 p.register_block(id, 512, "node-0", 0, 1.0);
1297 let pid = p.create_plan(vec![id], "node-1", 0).expect("ok");
1298 p.cancel_plan(pid).expect("ok");
1299 assert!(p.cancel_plan(pid).is_err());
1300 }
1301
1302 #[test]
1305 fn test_execute_plan_dry_run_always_succeeds() {
1306 let cfg = BmpPlannerConfig {
1307 dry_run: true,
1308 ..Default::default()
1309 };
1310 let mut p = BlockMigrationPlanner::with_config(cfg);
1311 let id = make_id(70);
1312 p.register_block(id, 512, "node-0", 0, 1.0);
1313 let pid = p.create_plan(vec![id], "node-1", 0).expect("ok");
1314 let result = p.execute_plan(pid).expect("ok");
1315 assert_eq!(result.status, BmpPlanStatus::Completed);
1316 assert_eq!(result.succeeded_blocks.len(), 1);
1317 assert!(result.failed_blocks.is_empty());
1318 assert_eq!(result.bytes_moved, 512);
1319 }
1320
1321 #[test]
1322 fn test_execute_plan_updates_log() {
1323 let cfg = BmpPlannerConfig {
1324 dry_run: true,
1325 ..Default::default()
1326 };
1327 let mut p = BlockMigrationPlanner::with_config(cfg);
1328 let id = make_id(71);
1329 p.register_block(id, 256, "node-0", 0, 1.0);
1330 let pid = p.create_plan(vec![id], "node-1", 0).expect("ok");
1331 p.execute_plan(pid).expect("ok");
1332 assert_eq!(p.log_len(), 1);
1333 }
1334
1335 #[test]
1336 fn test_execute_plan_updates_block_src_node() {
1337 let cfg = BmpPlannerConfig {
1338 dry_run: true,
1339 ..Default::default()
1340 };
1341 let mut p = BlockMigrationPlanner::with_config(cfg);
1342 let id = make_id(72);
1343 p.register_block(id, 512, "node-0", 0, 1.0);
1344 let pid = p.create_plan(vec![id], "node-2", 0).expect("ok");
1345 p.execute_plan(pid).expect("ok");
1346 let meta = p.block_meta(&id).expect("exists");
1347 assert_eq!(meta.src_node, "node-2");
1348 }
1349
1350 #[test]
1351 fn test_execute_plan_not_found_errors() {
1352 let mut p = make_planner();
1353 assert!(p.execute_plan(42).is_err());
1354 }
1355
1356 #[test]
1357 fn test_execute_plan_not_pending_errors() {
1358 let cfg = BmpPlannerConfig {
1359 dry_run: true,
1360 ..Default::default()
1361 };
1362 let mut p = BlockMigrationPlanner::with_config(cfg);
1363 let id = make_id(73);
1364 p.register_block(id, 512, "node-0", 0, 1.0);
1365 let pid = p.create_plan(vec![id], "node-1", 0).expect("ok");
1366 p.execute_plan(pid).expect("ok");
1367 assert!(p.execute_plan(pid).is_err());
1369 }
1370
1371 #[test]
1372 fn test_execute_plan_multi_block_dry_run() {
1373 let cfg = BmpPlannerConfig {
1374 dry_run: true,
1375 ..Default::default()
1376 };
1377 let mut p = BlockMigrationPlanner::with_config(cfg);
1378 let ids: Vec<_> = (0..5u8)
1379 .map(|i| {
1380 let id = make_id(80 + i);
1381 p.register_block(id, 100, "node-0", 0, 1.0);
1382 id
1383 })
1384 .collect();
1385 let pid = p.create_plan(ids.clone(), "node-1", 0).expect("ok");
1386 let res = p.execute_plan(pid).expect("ok");
1387 assert_eq!(res.succeeded_blocks.len(), 5);
1388 assert_eq!(res.bytes_moved, 500);
1389 }
1390
1391 #[test]
1394 fn test_schedule_migrations_no_eligible_blocks_errors() {
1395 let mut p = make_planner();
1396 let id = make_id(90);
1398 p.register_block(id, 512, "node-0", 0, 1.0);
1399 let result = p.schedule_migrations(BmpPriorityPolicy::FifoQueue, 5);
1400 assert!(result.is_err());
1401 }
1402
1403 #[test]
1404 fn test_schedule_migrations_with_dst_node_set() {
1405 let mut p = make_planner();
1406 let id = make_id(91);
1407 p.register_block(id, 512, "node-0", 1, 1.0);
1408 if let Some(meta) = p.blocks.get_mut(&id) {
1410 meta.dst_node = Some("node-1".to_string());
1411 }
1412 let ids = p
1413 .schedule_migrations(BmpPriorityPolicy::FifoQueue, 5)
1414 .expect("ok");
1415 assert_eq!(ids.len(), 1);
1416 }
1417
1418 #[test]
1419 fn test_schedule_migrations_respects_max_plans() {
1420 let mut p = make_planner();
1421 for i in 0..10u8 {
1422 let id = make_id(100 + i);
1423 p.register_block(id, 512, "node-0", 1, 1.0);
1424 if let Some(meta) = p.blocks.get_mut(&id) {
1425 meta.dst_node = Some("node-1".to_string());
1426 }
1427 }
1428 let ids = p
1429 .schedule_migrations(BmpPriorityPolicy::FifoQueue, 3)
1430 .expect("ok");
1431 assert_eq!(ids.len(), 3);
1432 }
1433
1434 #[test]
1435 fn test_schedule_migrations_skips_pinned() {
1436 let mut p = make_planner();
1437 for i in 0..3u8 {
1438 let id = make_id(110 + i);
1439 p.register_block(id, 512, "node-0", 1, 1.0);
1440 if let Some(meta) = p.blocks.get_mut(&id) {
1441 meta.dst_node = Some("node-1".to_string());
1442 }
1443 if i == 0 {
1444 p.pin(&id).expect("ok");
1445 }
1446 }
1447 let ids = p
1448 .schedule_migrations(BmpPriorityPolicy::FifoQueue, 10)
1449 .expect("ok");
1450 assert_eq!(ids.len(), 2);
1452 }
1453
1454 #[test]
1455 fn test_schedule_migrations_zero_max_returns_empty() {
1456 let mut p = make_planner();
1457 let id = make_id(120);
1458 p.register_block(id, 512, "node-0", 1, 1.0);
1459 if let Some(meta) = p.blocks.get_mut(&id) {
1460 meta.dst_node = Some("node-1".to_string());
1461 }
1462 let ids = p
1463 .schedule_migrations(BmpPriorityPolicy::FifoQueue, 0)
1464 .expect("ok");
1465 assert!(ids.is_empty());
1466 }
1467
1468 #[test]
1469 fn test_schedule_largest_first_ordering() {
1470 let mut p = make_planner();
1471 let sizes = [100u64, 500, 200, 800, 50];
1472 let mut block_ids = Vec::new();
1473 for (i, &sz) in sizes.iter().enumerate() {
1474 let id = make_id(130 + i as u8);
1475 p.register_block(id, sz, "node-0", 1, 1.0);
1476 if let Some(meta) = p.blocks.get_mut(&id) {
1477 meta.dst_node = Some("node-1".to_string());
1478 }
1479 block_ids.push(id);
1480 }
1481 let pids = p
1482 .schedule_migrations(BmpPriorityPolicy::LargestFirst, 5)
1483 .expect("ok");
1484 let first_bytes = p.plan(pids[0]).map_or(0, |pl| pl.estimated_bytes);
1486 let last_bytes = p
1487 .plan(*pids.last().unwrap())
1488 .map_or(0, |pl| pl.estimated_bytes);
1489 assert!(
1490 first_bytes >= last_bytes,
1491 "largest first violated: {} < {}",
1492 first_bytes,
1493 last_bytes
1494 );
1495 }
1496
1497 #[test]
1498 fn test_schedule_smallest_first_ordering() {
1499 let mut p = make_planner();
1500 let sizes = [100u64, 500, 200, 800, 50];
1501 for (i, &sz) in sizes.iter().enumerate() {
1502 let id = make_id(140 + i as u8);
1503 p.register_block(id, sz, "node-0", 1, 1.0);
1504 if let Some(meta) = p.blocks.get_mut(&id) {
1505 meta.dst_node = Some("node-1".to_string());
1506 }
1507 }
1508 let pids = p
1509 .schedule_migrations(BmpPriorityPolicy::SmallestFirst, 5)
1510 .expect("ok");
1511 let first_bytes = p.plan(pids[0]).map_or(0, |pl| pl.estimated_bytes);
1512 let last_bytes = p
1513 .plan(*pids.last().unwrap())
1514 .map_or(0, |pl| pl.estimated_bytes);
1515 assert!(
1516 first_bytes <= last_bytes,
1517 "smallest first violated: {} > {}",
1518 first_bytes,
1519 last_bytes
1520 );
1521 }
1522
1523 #[test]
1524 fn test_schedule_high_frequency_first() {
1525 let mut p = make_planner();
1526 let freqs = [0.1f64, 5.0, 2.0];
1527 let mut bid_map: Vec<(BmpBlockId, f64)> = Vec::new();
1528 for (i, &f) in freqs.iter().enumerate() {
1529 let id = make_id(150 + i as u8);
1530 p.register_block(id, 512, "node-0", 1, f);
1531 if let Some(meta) = p.blocks.get_mut(&id) {
1532 meta.dst_node = Some("node-1".to_string());
1533 }
1534 bid_map.push((id, f));
1535 }
1536 let pids = p
1537 .schedule_migrations(BmpPriorityPolicy::HighFrequencyFirst, 3)
1538 .expect("ok");
1539 let first_plan_bid = p.plan(pids[0]).and_then(|pl| pl.block_ids.first().copied());
1541 if let Some(bid) = first_plan_bid {
1542 let freq = p.block_meta(&bid).map_or(0.0, |m| m.access_frequency);
1543 assert!(
1545 (freq - 5.0).abs() < 1e-6 || freq >= 2.0,
1546 "unexpected freq {}",
1547 freq
1548 );
1549 }
1550 }
1551
1552 #[test]
1555 fn test_run_batch_migration_dry_run() {
1556 let cfg = BmpPlannerConfig {
1557 dry_run: true,
1558 max_concurrent_migrations: 10,
1559 ..Default::default()
1560 };
1561 let mut p = BlockMigrationPlanner::with_config(cfg);
1562 let _ids = register_n(&mut p, 5, "node-0");
1563 for id in &_ids {
1564 if let Some(meta) = p.blocks.get_mut(id) {
1565 meta.dst_node = Some("node-1".to_string());
1566 meta.tier = 1;
1567 }
1568 }
1569 let batch = p.run_batch_migration(BmpPriorityPolicy::FifoQueue, 5);
1570 assert!(batch.plans_executed > 0);
1571 assert_eq!(batch.plans_failed, 0);
1572 }
1573
1574 #[test]
1575 fn test_run_batch_migration_empty_result_on_no_candidates() {
1576 let mut p = make_planner();
1577 let id = make_id(160);
1579 p.register_block(id, 512, "node-0", 0, 1.0);
1580 let batch = p.run_batch_migration(BmpPriorityPolicy::FifoQueue, 5);
1581 assert_eq!(batch.plans_executed, 0);
1582 }
1583
1584 #[test]
1585 fn test_run_batch_migration_respects_max_concurrent() {
1586 let cfg = BmpPlannerConfig {
1587 dry_run: true,
1588 max_concurrent_migrations: 2,
1589 ..Default::default()
1590 };
1591 let mut p = BlockMigrationPlanner::with_config(cfg);
1592 let _ids = register_n(&mut p, 10, "node-0");
1593 for id in &_ids {
1594 if let Some(meta) = p.blocks.get_mut(id) {
1595 meta.dst_node = Some("node-1".to_string());
1596 meta.tier = 1;
1597 }
1598 }
1599 let batch = p.run_batch_migration(BmpPriorityPolicy::FifoQueue, 10);
1600 assert!(batch.plans_executed <= 2);
1601 }
1602
1603 #[test]
1604 fn test_run_batch_total_bytes_moved() {
1605 let cfg = BmpPlannerConfig {
1606 dry_run: true,
1607 max_concurrent_migrations: 5,
1608 ..Default::default()
1609 };
1610 let mut p = BlockMigrationPlanner::with_config(cfg);
1611 for i in 0..5u8 {
1612 let id = make_id(170 + i);
1613 p.register_block(id, 1000, "node-0", 1, 1.0);
1614 if let Some(meta) = p.blocks.get_mut(&id) {
1615 meta.dst_node = Some("node-1".to_string());
1616 }
1617 }
1618 let batch = p.run_batch_migration(BmpPriorityPolicy::FifoQueue, 5);
1619 assert_eq!(
1620 batch.total_bytes_moved,
1621 (batch.plans_completed as u64) * 1000
1622 );
1623 }
1624
1625 #[test]
1628 fn test_defragment_plan_empty_nodes() {
1629 let mut p = make_planner();
1630 let result = p.defragment_plan(&[]);
1631 assert!(result.is_empty());
1632 }
1633
1634 #[test]
1635 fn test_defragment_plan_already_balanced() {
1636 let mut p = make_planner();
1637 let nodes = vec!["node-0".to_string(), "node-1".to_string()];
1638 for i in 0..4u8 {
1639 let node = if i < 2 { "node-0" } else { "node-1" };
1640 let id = make_id(180 + i);
1641 p.register_block(id, 512, node, 0, 1.0);
1642 }
1643 let plans = p.defragment_plan(&nodes);
1644 assert!(plans.is_empty());
1646 }
1647
1648 #[test]
1649 fn test_defragment_plan_imbalanced() {
1650 let mut p = make_planner();
1651 let nodes = vec!["node-0".to_string(), "node-1".to_string()];
1652 for i in 0..4u8 {
1654 let id = make_id(190 + i);
1655 p.register_block(id, 512, "node-0", 0, 1.0);
1656 }
1657 let plans = p.defragment_plan(&nodes);
1658 assert!(!plans.is_empty());
1659 }
1660
1661 #[test]
1662 fn test_defragment_plan_skips_pinned() {
1663 let mut p = make_planner();
1664 let nodes = vec!["node-0".to_string(), "node-1".to_string()];
1665 for i in 0..4u8 {
1666 let id = make_id(200 + i);
1667 p.register_block(id, 512, "node-0", 0, 1.0);
1668 if i < 2 {
1669 p.pin(&id).expect("ok");
1670 }
1671 }
1672 let plans = p.defragment_plan(&nodes);
1673 assert!(plans.len() <= 2);
1675 }
1676
1677 #[test]
1678 fn test_defragment_plan_single_node_no_plans() {
1679 let mut p = make_planner();
1680 let nodes = vec!["node-0".to_string()];
1681 for i in 0..3u8 {
1682 let id = make_id(210 + i);
1683 p.register_block(id, 512, "node-0", 0, 1.0);
1684 }
1685 let plans = p.defragment_plan(&nodes);
1687 assert!(plans.is_empty());
1688 }
1689
1690 #[test]
1693 fn test_migration_stats_empty() {
1694 let p = make_planner();
1695 let stats = p.migration_stats();
1696 assert_eq!(stats.total_blocks, 0);
1697 assert_eq!(stats.total_plans, 0);
1698 assert_eq!(stats.total_records, 0);
1699 assert_eq!(stats.total_bytes_moved, 0);
1700 }
1701
1702 #[test]
1703 fn test_migration_stats_after_register() {
1704 let mut p = make_planner();
1705 for i in 0..5u8 {
1706 p.register_block(make_id(220 + i), 512, "node-0", 0, 1.0);
1707 }
1708 let stats = p.migration_stats();
1709 assert_eq!(stats.total_blocks, 5);
1710 }
1711
1712 #[test]
1713 fn test_migration_stats_pinned_count() {
1714 let mut p = make_planner();
1715 for i in 0..4u8 {
1716 let id = make_id(230 + i);
1717 p.register_block(id, 512, "node-0", 0, 1.0);
1718 if i < 2 {
1719 p.pin(&id).expect("ok");
1720 }
1721 }
1722 let stats = p.migration_stats();
1723 assert_eq!(stats.pinned_blocks, 2);
1724 }
1725
1726 #[test]
1727 fn test_migration_stats_plan_counts() {
1728 let cfg = BmpPlannerConfig {
1729 dry_run: true,
1730 ..Default::default()
1731 };
1732 let mut p = BlockMigrationPlanner::with_config(cfg);
1733 for i in 0..3u8 {
1734 let id = make_id(240 + i);
1735 p.register_block(id, 512, "node-0", 0, 1.0);
1736 p.create_plan(vec![id], "node-1", 0).expect("ok");
1737 }
1738 let stats = p.migration_stats();
1739 assert_eq!(stats.total_plans, 3);
1740 assert_eq!(stats.pending_plans, 3);
1741 }
1742
1743 #[test]
1744 fn test_migration_stats_bytes_moved() {
1745 let cfg = BmpPlannerConfig {
1746 dry_run: true,
1747 ..Default::default()
1748 };
1749 let mut p = BlockMigrationPlanner::with_config(cfg);
1750 let id = make_id(250);
1751 p.register_block(id, 4096, "node-0", 0, 1.0);
1752 let pid = p.create_plan(vec![id], "node-1", 0).expect("ok");
1753 p.execute_plan(pid).expect("ok");
1754 let stats = p.migration_stats();
1755 assert_eq!(stats.total_bytes_moved, 4096);
1756 }
1757
1758 #[test]
1759 fn test_migration_stats_cancelled_count() {
1760 let mut p = make_planner();
1761 for i in 200..203u8 {
1762 let id = make_id(i);
1763 p.register_block(id, 512, "node-0", 0, 1.0);
1764 let pid = p.create_plan(vec![id], "node-1", 0).expect("ok");
1765 p.cancel_plan(pid).expect("ok");
1766 }
1767 let stats = p.migration_stats();
1768 assert_eq!(stats.cancelled_plans, 3);
1769 }
1770
1771 #[test]
1774 fn test_log_bounded_at_1000() {
1775 let cfg = BmpPlannerConfig {
1776 dry_run: true,
1777 ..Default::default()
1778 };
1779 let mut p = BlockMigrationPlanner::with_config(cfg);
1780 for i in 0..1100u16 {
1782 let id = bmp_id_from_bytes(&i.to_le_bytes());
1783 p.register_block(id, 1, "node-0", 0, 1.0);
1785 if let Ok(pid) = p.create_plan(vec![id], "node-1", 0) {
1786 let _ = p.execute_plan(pid);
1787 }
1788 }
1789 assert_eq!(p.log_len(), 1000);
1790 }
1791
1792 #[test]
1793 fn test_recent_records_returns_n() {
1794 let cfg = BmpPlannerConfig {
1795 dry_run: true,
1796 ..Default::default()
1797 };
1798 let mut p = BlockMigrationPlanner::with_config(cfg);
1799 for i in 0..10u8 {
1800 let id = make_id(i);
1801 p.register_block(id, 512, "node-0", 0, 1.0);
1802 if let Ok(pid) = p.create_plan(vec![id], "node-1", 0) {
1803 let _ = p.execute_plan(pid);
1804 }
1805 }
1806 assert_eq!(p.recent_records(5).len(), 5);
1807 }
1808
1809 #[test]
1810 fn test_plans_iter() {
1811 let mut p = make_planner();
1812 for i in 0..4u8 {
1813 let id = make_id(i);
1814 p.register_block(id, 512, "node-0", 0, 1.0);
1815 p.create_plan(vec![id], "node-1", 0).expect("ok");
1816 }
1817 assert_eq!(p.plans_iter().count(), 4);
1818 }
1819
1820 #[test]
1823 fn test_plan_status_display() {
1824 assert_eq!(BmpPlanStatus::Pending.to_string(), "Pending");
1825 assert_eq!(BmpPlanStatus::InProgress.to_string(), "InProgress");
1826 assert_eq!(BmpPlanStatus::Completed.to_string(), "Completed");
1827 assert_eq!(BmpPlanStatus::Failed.to_string(), "Failed");
1828 assert_eq!(BmpPlanStatus::Cancelled.to_string(), "Cancelled");
1829 }
1830
1831 #[test]
1834 fn test_priority_policy_display() {
1835 assert_eq!(BmpPriorityPolicy::FifoQueue.to_string(), "FifoQueue");
1836 assert_eq!(
1837 BmpPriorityPolicy::HighFrequencyFirst.to_string(),
1838 "HighFrequencyFirst"
1839 );
1840 assert_eq!(BmpPriorityPolicy::LargestFirst.to_string(), "LargestFirst");
1841 assert_eq!(
1842 BmpPriorityPolicy::SmallestFirst.to_string(),
1843 "SmallestFirst"
1844 );
1845 assert_eq!(
1846 BmpPriorityPolicy::CostOptimized.to_string(),
1847 "CostOptimized"
1848 );
1849 }
1850
1851 #[test]
1854 fn test_planner_default() {
1855 let p = BlockMigrationPlanner::default();
1856 assert_eq!(p.block_count(), 0);
1857 }
1858
1859 #[test]
1860 fn test_config_default() {
1861 let cfg = BmpPlannerConfig::default();
1862 assert_eq!(cfg.max_concurrent_migrations, 4);
1863 assert_eq!(cfg.bandwidth_limit_kbps, 0);
1864 assert!(!cfg.dry_run);
1865 }
1866
1867 #[test]
1868 fn test_batch_result_default() {
1869 let b = BmpBatchResult::default();
1870 assert_eq!(b.plans_executed, 0);
1871 assert_eq!(b.total_bytes_moved, 0);
1872 }
1873
1874 #[test]
1875 fn test_planner_stats_default() {
1876 let s = BmpPlannerStats::default();
1877 assert_eq!(s.total_blocks, 0);
1878 assert_eq!(s.total_failures, 0);
1879 }
1880
1881 #[test]
1884 fn test_schedule_cost_optimized() {
1885 let mut p = make_planner();
1886 let id_a = make_id(20);
1888 let id_b = make_id(21);
1889 p.register_block(id_a, 100, "node-0", 3, 1.0);
1890 p.register_block(id_b, 500, "node-0", 1, 1.0);
1891 for id in [id_a, id_b] {
1892 if let Some(m) = p.blocks.get_mut(&id) {
1893 m.dst_node = Some("node-1".to_string());
1894 }
1895 }
1896 let pids = p
1897 .schedule_migrations(BmpPriorityPolicy::CostOptimized, 2)
1898 .expect("ok");
1899 let first_plan = p.plan(pids[0]).expect("plan");
1901 let first_bid = first_plan.block_ids[0];
1902 let first_meta = p.block_meta(&first_bid).expect("meta");
1903 let first_cost = first_meta.size_bytes * first_meta.tier as u64;
1904 assert!(
1906 first_cost <= 500,
1907 "cost optimized ordering failed: {} > 500",
1908 first_cost
1909 );
1910 }
1911}