1use std::collections::{HashMap, VecDeque};
45
46#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
53pub enum StorageTier {
54 Hot,
56 Warm,
58 Cold,
60 Archive,
62}
63
64impl StorageTier {
65 pub fn priority(&self) -> u8 {
68 match self {
69 StorageTier::Hot => 3,
70 StorageTier::Warm => 2,
71 StorageTier::Cold => 1,
72 StorageTier::Archive => 0,
73 }
74 }
75
76 pub fn next_colder(&self) -> Option<StorageTier> {
78 match self {
79 StorageTier::Hot => Some(StorageTier::Warm),
80 StorageTier::Warm => Some(StorageTier::Cold),
81 StorageTier::Cold => Some(StorageTier::Archive),
82 StorageTier::Archive => None,
83 }
84 }
85
86 pub fn name(&self) -> &'static str {
88 match self {
89 StorageTier::Hot => "hot",
90 StorageTier::Warm => "warm",
91 StorageTier::Cold => "cold",
92 StorageTier::Archive => "archive",
93 }
94 }
95}
96
97impl std::fmt::Display for StorageTier {
98 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
99 f.write_str(self.name())
100 }
101}
102
103#[derive(Debug, Clone)]
112pub struct TierPolicy {
113 pub tier: StorageTier,
115 pub max_age_ms: Option<u64>,
118 pub min_access_count: Option<u64>,
121 pub max_block_size_bytes: Option<u64>,
123 pub min_block_size_bytes: Option<u64>,
125}
126
127#[derive(Debug, Clone)]
133pub struct BlockMeta {
134 pub cid: String,
136 pub size_bytes: u64,
138 pub access_count: u64,
140 pub last_accessed_ms: u64,
142 pub created_ms: u64,
144 pub current_tier: StorageTier,
146}
147
148#[derive(Debug, Clone, Copy, PartialEq, Eq)]
154pub enum MigrationReason {
155 TooOld,
157 UnderAccessed,
159 Oversized,
161 Undersized,
163 PolicyChange,
165 Manual,
167}
168
169impl std::fmt::Display for MigrationReason {
170 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
171 let s = match self {
172 MigrationReason::TooOld => "too_old",
173 MigrationReason::UnderAccessed => "under_accessed",
174 MigrationReason::Oversized => "oversized",
175 MigrationReason::Undersized => "undersized",
176 MigrationReason::PolicyChange => "policy_change",
177 MigrationReason::Manual => "manual",
178 };
179 f.write_str(s)
180 }
181}
182
183#[derive(Debug, Clone)]
189pub struct MigrationAction {
190 pub cid: String,
192 pub from_tier: StorageTier,
194 pub to_tier: StorageTier,
196 pub reason: MigrationReason,
198 pub scheduled_at: u64,
200}
201
202#[derive(Debug, Clone, Default)]
209pub struct MigrationResult {
210 pub actions_planned: usize,
212 pub actions_executed: usize,
214 pub bytes_migrated: u64,
216 pub failed_cids: Vec<String>,
218}
219
220#[derive(Debug, Clone)]
226pub struct MigratorConfig {
227 pub policies: Vec<TierPolicy>,
231 pub dry_run: bool,
234 pub batch_size: usize,
236 pub max_migrations_per_run: usize,
238}
239
240impl Default for MigratorConfig {
241 fn default() -> Self {
242 Self {
243 policies: Vec::new(),
244 dry_run: false,
245 batch_size: 100,
246 max_migrations_per_run: 1000,
247 }
248 }
249}
250
251#[derive(Debug, Clone)]
257pub struct MigratorStats {
258 pub total_blocks: usize,
260 pub hot_blocks: usize,
262 pub warm_blocks: usize,
264 pub cold_blocks: usize,
266 pub archive_blocks: usize,
268 pub total_migrations: u64,
270 pub total_bytes_migrated: u64,
272}
273
274pub struct StorageTierMigrator {
293 pub config: MigratorConfig,
295 pub blocks: HashMap<String, BlockMeta>,
297 pub migration_log: VecDeque<MigrationAction>,
299 pub total_migrations: u64,
301 pub total_bytes_migrated: u64,
303}
304
305const LOG_CAPACITY: usize = 10_000;
307
308impl StorageTierMigrator {
309 pub fn new(config: MigratorConfig) -> Self {
315 Self {
316 config,
317 blocks: HashMap::new(),
318 migration_log: VecDeque::with_capacity(LOG_CAPACITY),
319 total_migrations: 0,
320 total_bytes_migrated: 0,
321 }
322 }
323
324 pub fn register_block(&mut self, meta: BlockMeta) {
330 self.blocks.insert(meta.cid.clone(), meta);
331 }
332
333 pub fn unregister_block(&mut self, cid: &str) -> bool {
335 self.blocks.remove(cid).is_some()
336 }
337
338 pub fn record_access(&mut self, cid: &str, now_ms: u64) -> bool {
341 if let Some(meta) = self.blocks.get_mut(cid) {
342 meta.access_count = meta.access_count.saturating_add(1);
343 meta.last_accessed_ms = now_ms;
344 true
345 } else {
346 false
347 }
348 }
349
350 pub fn evaluate_block(&self, meta: &BlockMeta, now_ms: u64) -> Option<MigrationAction> {
362 let mut applicable: Vec<&TierPolicy> = self
364 .config
365 .policies
366 .iter()
367 .filter(|p| p.tier == meta.current_tier)
368 .collect();
369
370 applicable.sort_by_key(|b| std::cmp::Reverse(b.tier.priority()));
373
374 for policy in applicable {
375 if let Some(reason) = Self::check_demotion(policy, meta, now_ms) {
376 let to_tier = meta.current_tier.next_colder()?;
377 return Some(MigrationAction {
378 cid: meta.cid.clone(),
379 from_tier: meta.current_tier,
380 to_tier,
381 reason,
382 scheduled_at: now_ms,
383 });
384 }
385 }
386 None
387 }
388
389 fn check_demotion(
391 policy: &TierPolicy,
392 meta: &BlockMeta,
393 now_ms: u64,
394 ) -> Option<MigrationReason> {
395 if let Some(max_age) = policy.max_age_ms {
397 if meta.last_accessed_ms.saturating_add(max_age) < now_ms {
398 return Some(MigrationReason::TooOld);
399 }
400 }
401
402 if let Some(min_access) = policy.min_access_count {
404 if meta.access_count < min_access {
405 return Some(MigrationReason::UnderAccessed);
406 }
407 }
408
409 if let Some(max_size) = policy.max_block_size_bytes {
411 if meta.size_bytes > max_size {
412 return Some(MigrationReason::Oversized);
413 }
414 }
415
416 if let Some(min_size) = policy.min_block_size_bytes {
418 if meta.size_bytes < min_size {
419 return Some(MigrationReason::Undersized);
420 }
421 }
422
423 None
424 }
425
426 pub fn plan_migrations(&self, now_ms: u64) -> Vec<MigrationAction> {
436 let mut actions: Vec<MigrationAction> = self
437 .blocks
438 .values()
439 .filter_map(|meta| self.evaluate_block(meta, now_ms))
440 .collect();
441
442 actions.sort_by_key(|b| std::cmp::Reverse(b.from_tier.priority()));
444
445 actions.truncate(self.config.max_migrations_per_run);
447 actions
448 }
449
450 pub fn execute_migrations(
461 &mut self,
462 actions: Vec<MigrationAction>,
463 now_ms: u64,
464 ) -> MigrationResult {
465 let actions_planned = actions.len();
466 let mut result = MigrationResult {
467 actions_planned,
468 actions_executed: 0,
469 bytes_migrated: 0,
470 failed_cids: Vec::new(),
471 };
472
473 if self.config.dry_run {
474 for action in actions {
476 self.push_log(action);
477 }
478 return result;
479 }
480
481 let batch_size = self.config.batch_size.max(1);
482 for chunk in actions.chunks(batch_size) {
483 for action in chunk {
484 match self.blocks.get_mut(&action.cid) {
485 Some(meta) => {
486 let size = meta.size_bytes;
487 meta.current_tier = action.to_tier;
488 result.actions_executed += 1;
489 result.bytes_migrated = result.bytes_migrated.saturating_add(size);
490 self.total_migrations = self.total_migrations.saturating_add(1);
491 self.total_bytes_migrated = self.total_bytes_migrated.saturating_add(size);
492 let log_entry = MigrationAction {
494 cid: action.cid.clone(),
495 from_tier: action.from_tier,
496 to_tier: action.to_tier,
497 reason: action.reason,
498 scheduled_at: now_ms,
499 };
500 self.push_log(log_entry);
501 }
502 None => {
503 result.failed_cids.push(action.cid.clone());
504 }
505 }
506 }
507 }
508
509 result
510 }
511
512 pub fn run_migration_cycle(&mut self, now_ms: u64) -> MigrationResult {
515 let actions = self.plan_migrations(now_ms);
516 self.execute_migrations(actions, now_ms)
517 }
518
519 pub fn blocks_in_tier(&self, tier: &StorageTier) -> Vec<&BlockMeta> {
526 let mut result: Vec<&BlockMeta> = self
527 .blocks
528 .values()
529 .filter(|m| &m.current_tier == tier)
530 .collect();
531 result.sort_by_key(|b| std::cmp::Reverse(b.access_count));
532 result
533 }
534
535 pub fn migration_log(&self) -> &VecDeque<MigrationAction> {
537 &self.migration_log
538 }
539
540 pub fn migrator_stats(&self) -> MigratorStats {
542 let mut hot = 0usize;
543 let mut warm = 0usize;
544 let mut cold = 0usize;
545 let mut archive = 0usize;
546
547 for meta in self.blocks.values() {
548 match meta.current_tier {
549 StorageTier::Hot => hot += 1,
550 StorageTier::Warm => warm += 1,
551 StorageTier::Cold => cold += 1,
552 StorageTier::Archive => archive += 1,
553 }
554 }
555
556 MigratorStats {
557 total_blocks: self.blocks.len(),
558 hot_blocks: hot,
559 warm_blocks: warm,
560 cold_blocks: cold,
561 archive_blocks: archive,
562 total_migrations: self.total_migrations,
563 total_bytes_migrated: self.total_bytes_migrated,
564 }
565 }
566
567 fn push_log(&mut self, action: MigrationAction) {
573 if self.migration_log.len() >= LOG_CAPACITY {
574 self.migration_log.pop_front();
575 }
576 self.migration_log.push_back(action);
577 }
578}
579
580#[cfg(test)]
585mod tests {
586 use super::{
587 BlockMeta, MigrationReason, MigratorConfig, StorageTier, StorageTierMigrator, TierPolicy,
588 };
589
590 fn make_policy(
595 tier: StorageTier,
596 max_age_ms: Option<u64>,
597 min_access: Option<u64>,
598 max_size: Option<u64>,
599 min_size: Option<u64>,
600 ) -> TierPolicy {
601 TierPolicy {
602 tier,
603 max_age_ms,
604 min_access_count: min_access,
605 max_block_size_bytes: max_size,
606 min_block_size_bytes: min_size,
607 }
608 }
609
610 fn make_block(
611 cid: &str,
612 tier: StorageTier,
613 size: u64,
614 access_count: u64,
615 last_accessed_ms: u64,
616 created_ms: u64,
617 ) -> BlockMeta {
618 BlockMeta {
619 cid: cid.to_string(),
620 size_bytes: size,
621 access_count,
622 last_accessed_ms,
623 created_ms,
624 current_tier: tier,
625 }
626 }
627
628 fn simple_migrator() -> StorageTierMigrator {
629 let policy = make_policy(
630 StorageTier::Hot,
631 Some(3_600_000), Some(5),
633 None,
634 None,
635 );
636 StorageTierMigrator::new(MigratorConfig {
637 policies: vec![policy],
638 ..MigratorConfig::default()
639 })
640 }
641
642 const NOW: u64 = 1_000_000_000;
643
644 #[test]
649 fn test_tier_priority_ordering() {
650 assert!(StorageTier::Hot.priority() > StorageTier::Warm.priority());
651 assert!(StorageTier::Warm.priority() > StorageTier::Cold.priority());
652 assert!(StorageTier::Cold.priority() > StorageTier::Archive.priority());
653 }
654
655 #[test]
656 fn test_tier_priority_values() {
657 assert_eq!(StorageTier::Hot.priority(), 3);
658 assert_eq!(StorageTier::Warm.priority(), 2);
659 assert_eq!(StorageTier::Cold.priority(), 1);
660 assert_eq!(StorageTier::Archive.priority(), 0);
661 }
662
663 #[test]
664 fn test_tier_next_colder() {
665 assert_eq!(StorageTier::Hot.next_colder(), Some(StorageTier::Warm));
666 assert_eq!(StorageTier::Warm.next_colder(), Some(StorageTier::Cold));
667 assert_eq!(StorageTier::Cold.next_colder(), Some(StorageTier::Archive));
668 assert_eq!(StorageTier::Archive.next_colder(), None);
669 }
670
671 #[test]
672 fn test_tier_display() {
673 assert_eq!(StorageTier::Hot.to_string(), "hot");
674 assert_eq!(StorageTier::Warm.to_string(), "warm");
675 assert_eq!(StorageTier::Cold.to_string(), "cold");
676 assert_eq!(StorageTier::Archive.to_string(), "archive");
677 }
678
679 #[test]
680 fn test_tier_equality() {
681 assert_eq!(StorageTier::Hot, StorageTier::Hot);
682 assert_ne!(StorageTier::Hot, StorageTier::Cold);
683 }
684
685 #[test]
690 fn test_register_block() {
691 let mut m = simple_migrator();
692 let block = make_block("cid1", StorageTier::Hot, 4096, 10, NOW, NOW - 1000);
693 m.register_block(block);
694 assert_eq!(m.blocks.len(), 1);
695 assert!(m.blocks.contains_key("cid1"));
696 }
697
698 #[test]
699 fn test_register_block_overwrite() {
700 let mut m = simple_migrator();
701 m.register_block(make_block("cid1", StorageTier::Hot, 4096, 10, NOW, NOW));
702 m.register_block(make_block("cid1", StorageTier::Warm, 8192, 20, NOW, NOW));
703 assert_eq!(m.blocks.len(), 1);
704 let b = m.blocks.get("cid1").expect("block must exist");
705 assert_eq!(b.current_tier, StorageTier::Warm);
706 assert_eq!(b.size_bytes, 8192);
707 }
708
709 #[test]
710 fn test_unregister_existing_block() {
711 let mut m = simple_migrator();
712 m.register_block(make_block("cid1", StorageTier::Hot, 1024, 5, NOW, NOW));
713 assert!(m.unregister_block("cid1"));
714 assert!(m.blocks.is_empty());
715 }
716
717 #[test]
718 fn test_unregister_missing_block() {
719 let mut m = simple_migrator();
720 assert!(!m.unregister_block("nonexistent"));
721 }
722
723 #[test]
728 fn test_record_access_increments_count() {
729 let mut m = simple_migrator();
730 m.register_block(make_block("cid1", StorageTier::Hot, 512, 0, NOW, NOW));
731 assert!(m.record_access("cid1", NOW + 100));
732 let b = m.blocks.get("cid1").expect("block must exist");
733 assert_eq!(b.access_count, 1);
734 assert_eq!(b.last_accessed_ms, NOW + 100);
735 }
736
737 #[test]
738 fn test_record_access_multiple_times() {
739 let mut m = simple_migrator();
740 m.register_block(make_block("cid1", StorageTier::Hot, 512, 0, NOW, NOW));
741 for i in 1..=5 {
742 m.record_access("cid1", NOW + i * 100);
743 }
744 let b = m.blocks.get("cid1").expect("block must exist");
745 assert_eq!(b.access_count, 5);
746 }
747
748 #[test]
749 fn test_record_access_missing_returns_false() {
750 let mut m = simple_migrator();
751 assert!(!m.record_access("ghost", NOW));
752 }
753
754 #[test]
759 fn test_evaluate_block_no_policy_no_action() {
760 let m = StorageTierMigrator::new(MigratorConfig::default());
761 let block = make_block("c1", StorageTier::Hot, 512, 0, NOW, NOW);
762 assert!(m.evaluate_block(&block, NOW + 1_000_000).is_none());
763 }
764
765 #[test]
766 fn test_evaluate_block_too_old_triggers() {
767 let policy = make_policy(StorageTier::Hot, Some(1_000), None, None, None);
768 let m = StorageTierMigrator::new(MigratorConfig {
769 policies: vec![policy],
770 ..MigratorConfig::default()
771 });
772 let block = make_block("c1", StorageTier::Hot, 512, 10, NOW - 2_000, NOW - 5_000);
774 let action = m.evaluate_block(&block, NOW).expect("should trigger");
775 assert_eq!(action.reason, MigrationReason::TooOld);
776 assert_eq!(action.from_tier, StorageTier::Hot);
777 assert_eq!(action.to_tier, StorageTier::Warm);
778 }
779
780 #[test]
781 fn test_evaluate_block_under_accessed_triggers() {
782 let policy = make_policy(StorageTier::Hot, None, Some(10), None, None);
783 let m = StorageTierMigrator::new(MigratorConfig {
784 policies: vec![policy],
785 ..MigratorConfig::default()
786 });
787 let block = make_block("c1", StorageTier::Hot, 512, 3, NOW, NOW);
788 let action = m.evaluate_block(&block, NOW).expect("should trigger");
789 assert_eq!(action.reason, MigrationReason::UnderAccessed);
790 }
791
792 #[test]
793 fn test_evaluate_block_oversized_triggers() {
794 let policy = make_policy(StorageTier::Hot, None, None, Some(1024), None);
795 let m = StorageTierMigrator::new(MigratorConfig {
796 policies: vec![policy],
797 ..MigratorConfig::default()
798 });
799 let block = make_block("c1", StorageTier::Hot, 2048, 100, NOW, NOW);
800 let action = m.evaluate_block(&block, NOW).expect("should trigger");
801 assert_eq!(action.reason, MigrationReason::Oversized);
802 }
803
804 #[test]
805 fn test_evaluate_block_undersized_triggers() {
806 let policy = make_policy(StorageTier::Warm, None, None, None, Some(4096));
807 let m = StorageTierMigrator::new(MigratorConfig {
808 policies: vec![policy],
809 ..MigratorConfig::default()
810 });
811 let block = make_block("c1", StorageTier::Warm, 512, 100, NOW, NOW);
812 let action = m.evaluate_block(&block, NOW).expect("should trigger");
813 assert_eq!(action.reason, MigrationReason::Undersized);
814 assert_eq!(action.to_tier, StorageTier::Cold);
815 }
816
817 #[test]
818 fn test_evaluate_block_no_trigger_when_within_policy() {
819 let policy = make_policy(StorageTier::Hot, Some(3_600_000), Some(5), None, None);
820 let m = StorageTierMigrator::new(MigratorConfig {
821 policies: vec![policy],
822 ..MigratorConfig::default()
823 });
824 let block = make_block("c1", StorageTier::Hot, 512, 10, NOW - 100, NOW - 200);
826 assert!(m.evaluate_block(&block, NOW).is_none());
827 }
828
829 #[test]
830 fn test_evaluate_block_wrong_tier_policy_skipped() {
831 let policy = make_policy(StorageTier::Cold, Some(1_000), None, None, None);
833 let m = StorageTierMigrator::new(MigratorConfig {
834 policies: vec![policy],
835 ..MigratorConfig::default()
836 });
837 let block = make_block("c1", StorageTier::Hot, 512, 0, NOW - 5_000, NOW - 5_000);
838 assert!(m.evaluate_block(&block, NOW).is_none());
839 }
840
841 #[test]
842 fn test_evaluate_archive_block_cannot_demote() {
843 let policy = make_policy(StorageTier::Archive, Some(1_000), None, None, None);
846 let m = StorageTierMigrator::new(MigratorConfig {
847 policies: vec![policy],
848 ..MigratorConfig::default()
849 });
850 let block = make_block("c1", StorageTier::Archive, 512, 0, NOW - 5_000, NOW - 5_000);
851 assert!(m.evaluate_block(&block, NOW).is_none());
852 }
853
854 #[test]
859 fn test_plan_migrations_empty() {
860 let m = simple_migrator();
861 assert!(m.plan_migrations(NOW).is_empty());
862 }
863
864 #[test]
865 fn test_plan_migrations_detects_stale_blocks() {
866 let mut m = simple_migrator();
867 m.register_block(make_block(
869 "c1",
870 StorageTier::Hot,
871 512,
872 10,
873 NOW - 7_200_000,
874 NOW - 7_200_000,
875 ));
876 let actions = m.plan_migrations(NOW);
877 assert_eq!(actions.len(), 1);
878 assert_eq!(actions[0].cid, "c1");
879 }
880
881 #[test]
882 fn test_plan_migrations_sorted_by_priority() {
883 let p_hot = make_policy(StorageTier::Hot, Some(1_000), None, None, None);
884 let p_warm = make_policy(StorageTier::Warm, Some(1_000), None, None, None);
885 let mut m = StorageTierMigrator::new(MigratorConfig {
886 policies: vec![p_hot, p_warm],
887 ..MigratorConfig::default()
888 });
889 m.register_block(make_block(
890 "warm1",
891 StorageTier::Warm,
892 512,
893 0,
894 NOW - 5_000,
895 NOW - 5_000,
896 ));
897 m.register_block(make_block(
898 "hot1",
899 StorageTier::Hot,
900 512,
901 0,
902 NOW - 5_000,
903 NOW - 5_000,
904 ));
905 let actions = m.plan_migrations(NOW);
906 assert_eq!(actions.len(), 2);
907 assert_eq!(actions[0].from_tier, StorageTier::Hot);
909 assert_eq!(actions[1].from_tier, StorageTier::Warm);
910 }
911
912 #[test]
913 fn test_plan_migrations_respects_max_cap() {
914 let policy = make_policy(StorageTier::Hot, Some(1_000), None, None, None);
915 let mut m = StorageTierMigrator::new(MigratorConfig {
916 policies: vec![policy],
917 max_migrations_per_run: 3,
918 ..MigratorConfig::default()
919 });
920 for i in 0..10 {
921 m.register_block(make_block(
922 &format!("c{i}"),
923 StorageTier::Hot,
924 512,
925 0,
926 NOW - 5_000,
927 NOW - 5_000,
928 ));
929 }
930 let actions = m.plan_migrations(NOW);
931 assert_eq!(actions.len(), 3);
932 }
933
934 #[test]
939 fn test_execute_migrations_updates_tier() {
940 let mut m = simple_migrator();
941 m.register_block(make_block(
942 "c1",
943 StorageTier::Hot,
944 1024,
945 0,
946 NOW - 7_200_000,
947 NOW,
948 ));
949 let actions = m.plan_migrations(NOW);
950 let result = m.execute_migrations(actions, NOW);
951 assert_eq!(result.actions_executed, 1);
952 assert_eq!(result.bytes_migrated, 1024);
953 let b = m.blocks.get("c1").expect("block must exist");
954 assert_eq!(b.current_tier, StorageTier::Warm);
955 }
956
957 #[test]
958 fn test_execute_migrations_dry_run_does_not_mutate() {
959 let policy = make_policy(StorageTier::Hot, Some(1_000), None, None, None);
960 let mut m = StorageTierMigrator::new(MigratorConfig {
961 policies: vec![policy],
962 dry_run: true,
963 ..MigratorConfig::default()
964 });
965 m.register_block(make_block(
966 "c1",
967 StorageTier::Hot,
968 512,
969 0,
970 NOW - 5_000,
971 NOW - 5_000,
972 ));
973 let actions = m.plan_migrations(NOW);
974 let result = m.execute_migrations(actions, NOW);
975 assert!(result.actions_planned >= 1);
977 assert_eq!(result.actions_executed, 0);
978 let b = m.blocks.get("c1").expect("block must exist");
979 assert_eq!(b.current_tier, StorageTier::Hot);
980 }
981
982 #[test]
983 fn test_execute_migrations_missing_block_recorded_as_failed() {
984 use super::MigrationAction;
985 let mut m = simple_migrator();
986 let action = MigrationAction {
987 cid: "ghost".to_string(),
988 from_tier: StorageTier::Hot,
989 to_tier: StorageTier::Warm,
990 reason: MigrationReason::TooOld,
991 scheduled_at: NOW,
992 };
993 let result = m.execute_migrations(vec![action], NOW);
994 assert_eq!(result.failed_cids, vec!["ghost".to_string()]);
995 assert_eq!(result.actions_executed, 0);
996 }
997
998 #[test]
999 fn test_execute_migrations_batching() {
1000 let policy = make_policy(StorageTier::Hot, Some(1_000), None, None, None);
1001 let mut m = StorageTierMigrator::new(MigratorConfig {
1002 policies: vec![policy],
1003 batch_size: 2,
1004 max_migrations_per_run: 1000,
1005 ..MigratorConfig::default()
1006 });
1007 for i in 0..6 {
1008 m.register_block(make_block(
1009 &format!("c{i}"),
1010 StorageTier::Hot,
1011 256,
1012 0,
1013 NOW - 5_000,
1014 NOW - 5_000,
1015 ));
1016 }
1017 let actions = m.plan_migrations(NOW);
1018 let result = m.execute_migrations(actions, NOW);
1019 assert_eq!(result.actions_executed, 6);
1020 }
1021
1022 #[test]
1027 fn test_run_migration_cycle_full_flow() {
1028 let mut m = simple_migrator();
1029 m.register_block(make_block(
1030 "c1",
1031 StorageTier::Hot,
1032 2048,
1033 2,
1034 NOW - 7_200_000,
1035 NOW,
1036 ));
1037 let result = m.run_migration_cycle(NOW);
1038 assert!(result.actions_planned >= 1);
1039 assert!(result.actions_executed >= 1);
1040 }
1041
1042 #[test]
1043 fn test_run_migration_cycle_updates_counters() {
1044 let mut m = simple_migrator();
1045 m.register_block(make_block(
1046 "c1",
1047 StorageTier::Hot,
1048 2048,
1049 2,
1050 NOW - 7_200_000,
1051 NOW,
1052 ));
1053 m.run_migration_cycle(NOW);
1054 assert!(m.total_migrations >= 1);
1055 assert!(m.total_bytes_migrated >= 2048);
1056 }
1057
1058 #[test]
1059 fn test_run_migration_cycle_empty_produces_zero_result() {
1060 let mut m = simple_migrator();
1061 let result = m.run_migration_cycle(NOW);
1062 assert_eq!(result.actions_planned, 0);
1063 assert_eq!(result.actions_executed, 0);
1064 assert_eq!(result.bytes_migrated, 0);
1065 }
1066
1067 #[test]
1072 fn test_blocks_in_tier_empty() {
1073 let m = simple_migrator();
1074 assert!(m.blocks_in_tier(&StorageTier::Hot).is_empty());
1075 }
1076
1077 #[test]
1078 fn test_blocks_in_tier_correct_tier() {
1079 let mut m = simple_migrator();
1080 m.register_block(make_block("hot1", StorageTier::Hot, 512, 5, NOW, NOW));
1081 m.register_block(make_block("warm1", StorageTier::Warm, 512, 5, NOW, NOW));
1082 let hot = m.blocks_in_tier(&StorageTier::Hot);
1083 assert_eq!(hot.len(), 1);
1084 assert_eq!(hot[0].cid, "hot1");
1085 }
1086
1087 #[test]
1088 fn test_blocks_in_tier_sorted_by_access_count_desc() {
1089 let mut m = simple_migrator();
1090 m.register_block(make_block("c1", StorageTier::Warm, 512, 1, NOW, NOW));
1091 m.register_block(make_block("c2", StorageTier::Warm, 512, 50, NOW, NOW));
1092 m.register_block(make_block("c3", StorageTier::Warm, 512, 10, NOW, NOW));
1093 let warm = m.blocks_in_tier(&StorageTier::Warm);
1094 assert_eq!(warm.len(), 3);
1095 assert_eq!(warm[0].access_count, 50);
1096 assert_eq!(warm[1].access_count, 10);
1097 assert_eq!(warm[2].access_count, 1);
1098 }
1099
1100 #[test]
1105 fn test_migration_log_appended_on_execute() {
1106 let mut m = simple_migrator();
1107 m.register_block(make_block(
1108 "c1",
1109 StorageTier::Hot,
1110 512,
1111 0,
1112 NOW - 7_200_000,
1113 NOW,
1114 ));
1115 m.run_migration_cycle(NOW);
1116 assert!(!m.migration_log().is_empty());
1117 assert_eq!(m.migration_log()[0].cid, "c1");
1118 }
1119
1120 #[test]
1121 fn test_migration_log_bounded_at_capacity() {
1122 use super::LOG_CAPACITY;
1123 let policy = make_policy(StorageTier::Hot, Some(1), None, None, None);
1124 let mut m = StorageTierMigrator::new(MigratorConfig {
1125 policies: vec![policy],
1126 max_migrations_per_run: LOG_CAPACITY + 100,
1127 ..MigratorConfig::default()
1128 });
1129 for i in 0..(LOG_CAPACITY + 100) {
1131 m.register_block(make_block(
1132 &format!("c{i}"),
1133 StorageTier::Hot,
1134 64,
1135 0,
1136 NOW - 5_000,
1137 NOW - 5_000,
1138 ));
1139 }
1140 m.run_migration_cycle(NOW);
1141 assert!(m.migration_log().len() <= LOG_CAPACITY);
1142 }
1143
1144 #[test]
1145 fn test_migration_log_dry_run_appends_but_no_exec() {
1146 let policy = make_policy(StorageTier::Hot, Some(1_000), None, None, None);
1147 let mut m = StorageTierMigrator::new(MigratorConfig {
1148 policies: vec![policy],
1149 dry_run: true,
1150 ..MigratorConfig::default()
1151 });
1152 m.register_block(make_block("c1", StorageTier::Hot, 512, 0, NOW - 5_000, NOW));
1153 let actions = m.plan_migrations(NOW);
1154 m.execute_migrations(actions, NOW);
1155 assert!(!m.migration_log().is_empty());
1157 let b = m.blocks.get("c1").expect("block must exist");
1158 assert_eq!(b.current_tier, StorageTier::Hot);
1159 }
1160
1161 #[test]
1166 fn test_migrator_stats_initial() {
1167 let m = simple_migrator();
1168 let s = m.migrator_stats();
1169 assert_eq!(s.total_blocks, 0);
1170 assert_eq!(s.total_migrations, 0);
1171 assert_eq!(s.total_bytes_migrated, 0);
1172 }
1173
1174 #[test]
1175 fn test_migrator_stats_tier_counts() {
1176 let mut m = simple_migrator();
1177 m.register_block(make_block("h1", StorageTier::Hot, 256, 0, NOW, NOW));
1178 m.register_block(make_block("h2", StorageTier::Hot, 256, 0, NOW, NOW));
1179 m.register_block(make_block("w1", StorageTier::Warm, 256, 0, NOW, NOW));
1180 m.register_block(make_block("c1", StorageTier::Cold, 256, 0, NOW, NOW));
1181 m.register_block(make_block("a1", StorageTier::Archive, 256, 0, NOW, NOW));
1182 let s = m.migrator_stats();
1183 assert_eq!(s.total_blocks, 5);
1184 assert_eq!(s.hot_blocks, 2);
1185 assert_eq!(s.warm_blocks, 1);
1186 assert_eq!(s.cold_blocks, 1);
1187 assert_eq!(s.archive_blocks, 1);
1188 }
1189
1190 #[test]
1191 fn test_migrator_stats_after_migration() {
1192 let mut m = simple_migrator();
1193 m.register_block(make_block(
1194 "c1",
1195 StorageTier::Hot,
1196 1024,
1197 0,
1198 NOW - 7_200_000,
1199 NOW,
1200 ));
1201 m.run_migration_cycle(NOW);
1202 let s = m.migrator_stats();
1203 assert!(s.total_migrations >= 1);
1204 assert!(s.total_bytes_migrated >= 1024);
1205 assert_eq!(s.hot_blocks, 0);
1206 assert_eq!(s.warm_blocks, 1);
1207 }
1208
1209 #[test]
1214 fn test_multi_tier_cascade_two_cycles() {
1215 let p_hot = make_policy(StorageTier::Hot, Some(1_000), None, None, None);
1216 let p_warm = make_policy(StorageTier::Warm, Some(1_000), None, None, None);
1217 let mut m = StorageTierMigrator::new(MigratorConfig {
1218 policies: vec![p_hot, p_warm],
1219 ..MigratorConfig::default()
1220 });
1221 m.register_block(make_block(
1222 "c1",
1223 StorageTier::Hot,
1224 512,
1225 0,
1226 NOW - 5_000,
1227 NOW - 5_000,
1228 ));
1229 m.run_migration_cycle(NOW);
1231 assert_eq!(m.blocks["c1"].current_tier, StorageTier::Warm);
1232 m.run_migration_cycle(NOW + 10_000);
1234 assert_eq!(m.blocks["c1"].current_tier, StorageTier::Cold);
1235 }
1236
1237 #[test]
1238 fn test_age_check_boundary_exact() {
1239 let policy = make_policy(StorageTier::Hot, Some(1_000), None, None, None);
1241 let m = StorageTierMigrator::new(MigratorConfig {
1242 policies: vec![policy],
1243 ..MigratorConfig::default()
1244 });
1245 let block = make_block("c1", StorageTier::Hot, 512, 0, NOW - 1_000, NOW - 1_000);
1246 assert!(m.evaluate_block(&block, NOW).is_none());
1248 }
1249
1250 #[test]
1251 fn test_age_check_boundary_one_over() {
1252 let policy = make_policy(StorageTier::Hot, Some(1_000), None, None, None);
1254 let m = StorageTierMigrator::new(MigratorConfig {
1255 policies: vec![policy],
1256 ..MigratorConfig::default()
1257 });
1258 let block = make_block("c1", StorageTier::Hot, 512, 0, NOW - 1_001, NOW - 1_001);
1259 assert!(m.evaluate_block(&block, NOW).is_some());
1260 }
1261
1262 #[test]
1263 fn test_reason_display() {
1264 assert_eq!(MigrationReason::TooOld.to_string(), "too_old");
1265 assert_eq!(MigrationReason::UnderAccessed.to_string(), "under_accessed");
1266 assert_eq!(MigrationReason::Oversized.to_string(), "oversized");
1267 assert_eq!(MigrationReason::Undersized.to_string(), "undersized");
1268 assert_eq!(MigrationReason::PolicyChange.to_string(), "policy_change");
1269 assert_eq!(MigrationReason::Manual.to_string(), "manual");
1270 }
1271
1272 #[test]
1273 fn test_multiple_blocks_partial_migration() {
1274 let policy = make_policy(StorageTier::Hot, Some(1_000), None, None, None);
1275 let mut m = StorageTierMigrator::new(MigratorConfig {
1276 policies: vec![policy],
1277 ..MigratorConfig::default()
1278 });
1279 m.register_block(make_block(
1281 "stale",
1282 StorageTier::Hot,
1283 512,
1284 0,
1285 NOW - 5_000,
1286 NOW,
1287 ));
1288 m.register_block(make_block(
1289 "fresh",
1290 StorageTier::Hot,
1291 512,
1292 0,
1293 NOW - 100,
1294 NOW,
1295 ));
1296 let result = m.run_migration_cycle(NOW);
1297 assert_eq!(result.actions_planned, 1);
1298 assert_eq!(result.actions_executed, 1);
1299 assert_eq!(m.blocks["stale"].current_tier, StorageTier::Warm);
1300 assert_eq!(m.blocks["fresh"].current_tier, StorageTier::Hot);
1301 }
1302
1303 #[test]
1304 fn test_default_config_values() {
1305 let c = MigratorConfig::default();
1306 assert!(!c.dry_run);
1307 assert_eq!(c.batch_size, 100);
1308 assert_eq!(c.max_migrations_per_run, 1000);
1309 assert!(c.policies.is_empty());
1310 }
1311
1312 #[test]
1313 fn test_bytes_migrated_accumulates_across_cycles() {
1314 let policy = make_policy(StorageTier::Hot, Some(1_000), None, None, None);
1315 let mut m = StorageTierMigrator::new(MigratorConfig {
1316 policies: vec![policy.clone()],
1317 ..MigratorConfig::default()
1318 });
1319 m.register_block(make_block(
1320 "c1",
1321 StorageTier::Hot,
1322 1024,
1323 0,
1324 NOW - 5_000,
1325 NOW,
1326 ));
1327 m.run_migration_cycle(NOW);
1328 assert_eq!(m.total_bytes_migrated, 1024);
1329 }
1330
1331 #[test]
1332 fn test_zero_batch_size_handled_gracefully() {
1333 let policy = make_policy(StorageTier::Hot, Some(1_000), None, None, None);
1335 let mut m = StorageTierMigrator::new(MigratorConfig {
1336 policies: vec![policy],
1337 batch_size: 0,
1338 ..MigratorConfig::default()
1339 });
1340 m.register_block(make_block("c1", StorageTier::Hot, 512, 0, NOW - 5_000, NOW));
1341 let result = m.run_migration_cycle(NOW);
1342 assert_eq!(result.actions_executed, 1);
1343 }
1344}