1use std::collections::HashMap;
8
9#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
15pub enum StorageTier {
16 Nvme = 0,
18 Ssd = 1,
20 Hdd = 2,
22 Archive = 3,
24}
25
26#[derive(Clone, Copy, Debug, PartialEq, Eq)]
28pub enum MigrationDirection {
29 Promote,
31 Demote,
33}
34
35#[derive(Clone, Debug)]
37pub struct MigrationTask {
38 pub task_id: u64,
40 pub block_id: u64,
42 pub cid: String,
44 pub from_tier: StorageTier,
46 pub to_tier: StorageTier,
48 pub direction: MigrationDirection,
50 pub size_bytes: u64,
52 pub estimated_cost_ms: u64,
54 pub depends_on: Vec<u64>,
57}
58
59impl MigrationTask {
60 pub fn is_promotion(&self) -> bool {
62 self.direction == MigrationDirection::Promote
63 }
64}
65
66#[derive(Clone, Copy, Debug, PartialEq, Eq)]
68pub enum MigrationStatus {
69 Pending,
71 InProgress,
73 Completed,
75 Failed,
77 RolledBack,
79}
80
81#[derive(Clone, Debug)]
83pub struct MigrationRecord {
84 pub task: MigrationTask,
86 pub status: MigrationStatus,
88 pub started_at_tick: Option<u64>,
91 pub completed_at_tick: Option<u64>,
93}
94
95#[derive(Clone, Debug, Default)]
97pub struct PlannerStats {
98 pub total_tasks: usize,
100 pub pending: usize,
102 pub in_progress: usize,
104 pub completed: usize,
106 pub failed: usize,
109 pub total_bytes_migrated: u64,
111 pub promotions: u64,
113 pub demotions: u64,
115}
116
117pub struct StorageMigrationPlanner {
134 pub records: HashMap<u64, MigrationRecord>,
136 pub next_task_id: u64,
138}
139
140impl StorageMigrationPlanner {
141 pub fn new() -> Self {
143 Self {
144 records: HashMap::new(),
145 next_task_id: 0,
146 }
147 }
148
149 pub fn plan_migration(
157 &mut self,
158 block_id: u64,
159 cid: String,
160 from: StorageTier,
161 to: StorageTier,
162 size_bytes: u64,
163 depends_on: Vec<u64>,
164 ) -> u64 {
165 let task_id = self.next_task_id;
166 self.next_task_id += 1;
167
168 let direction = if from > to {
169 MigrationDirection::Promote
170 } else {
171 MigrationDirection::Demote
172 };
173
174 let estimated_cost_ms = size_bytes / 1_000_000 + 10;
175
176 let task = MigrationTask {
177 task_id,
178 block_id,
179 cid,
180 from_tier: from,
181 to_tier: to,
182 direction,
183 size_bytes,
184 estimated_cost_ms,
185 depends_on,
186 };
187
188 let record = MigrationRecord {
189 task,
190 status: MigrationStatus::Pending,
191 started_at_tick: None,
192 completed_at_tick: None,
193 };
194
195 self.records.insert(task_id, record);
196 task_id
197 }
198
199 pub fn start_task(&mut self, task_id: u64, current_tick: u64) -> bool {
207 let depends_on: Vec<u64> = match self.records.get(&task_id) {
209 Some(r) if r.status == MigrationStatus::Pending => r.task.depends_on.clone(),
210 _ => return false,
211 };
212
213 for dep_id in &depends_on {
215 match self.records.get(dep_id) {
216 Some(dep) if dep.status == MigrationStatus::Completed => {}
217 _ => return false,
218 }
219 }
220
221 if let Some(record) = self.records.get_mut(&task_id) {
222 record.status = MigrationStatus::InProgress;
223 record.started_at_tick = Some(current_tick);
224 true
225 } else {
226 false
227 }
228 }
229
230 pub fn complete_task(&mut self, task_id: u64, current_tick: u64) -> bool {
235 match self.records.get_mut(&task_id) {
236 Some(r) if r.status == MigrationStatus::InProgress => {
237 r.status = MigrationStatus::Completed;
238 r.completed_at_tick = Some(current_tick);
239 true
240 }
241 _ => false,
242 }
243 }
244
245 pub fn fail_task(&mut self, task_id: u64) -> bool {
251 match self.records.get_mut(&task_id) {
252 Some(r)
253 if r.status == MigrationStatus::Pending
254 || r.status == MigrationStatus::InProgress =>
255 {
256 r.status = MigrationStatus::Failed;
257 true
258 }
259 _ => false,
260 }
261 }
262
263 pub fn rollback_task(&mut self, task_id: u64) -> bool {
268 match self.records.get_mut(&task_id) {
269 Some(r) if r.status == MigrationStatus::Failed => {
270 r.status = MigrationStatus::RolledBack;
271 true
272 }
273 _ => false,
274 }
275 }
276
277 pub fn ready_tasks(&self) -> Vec<u64> {
282 let mut ready: Vec<u64> = self
283 .records
284 .values()
285 .filter(|r| {
286 if r.status != MigrationStatus::Pending {
287 return false;
288 }
289 r.task.depends_on.iter().all(|dep_id| {
290 self.records
291 .get(dep_id)
292 .is_some_and(|d| d.status == MigrationStatus::Completed)
293 })
294 })
295 .map(|r| r.task.task_id)
296 .collect();
297
298 ready.sort_unstable();
299 ready
300 }
301
302 pub fn get_record(&self, task_id: u64) -> Option<&MigrationRecord> {
304 self.records.get(&task_id)
305 }
306
307 pub fn stats(&self) -> PlannerStats {
309 let mut stats = PlannerStats {
310 total_tasks: self.records.len(),
311 ..Default::default()
312 };
313
314 for record in self.records.values() {
315 match record.status {
316 MigrationStatus::Pending => stats.pending += 1,
317 MigrationStatus::InProgress => stats.in_progress += 1,
318 MigrationStatus::Completed => {
319 stats.completed += 1;
320 stats.total_bytes_migrated += record.task.size_bytes;
321 match record.task.direction {
322 MigrationDirection::Promote => stats.promotions += 1,
323 MigrationDirection::Demote => stats.demotions += 1,
324 }
325 }
326 MigrationStatus::Failed | MigrationStatus::RolledBack => stats.failed += 1,
327 }
328 }
329
330 stats
331 }
332}
333
334impl Default for StorageMigrationPlanner {
335 fn default() -> Self {
336 Self::new()
337 }
338}
339
340#[cfg(test)]
345mod tests {
346 use super::*;
347
348 fn make_planner() -> StorageMigrationPlanner {
351 StorageMigrationPlanner::new()
352 }
353
354 fn add_simple(
355 p: &mut StorageMigrationPlanner,
356 from: StorageTier,
357 to: StorageTier,
358 size: u64,
359 ) -> u64 {
360 p.plan_migration(1, "bafytest".into(), from, to, size, vec![])
361 }
362
363 #[test]
366 fn plan_migration_returns_sequential_ids() {
367 let mut p = make_planner();
368 let id0 = add_simple(&mut p, StorageTier::Nvme, StorageTier::Hdd, 0);
369 let id1 = add_simple(&mut p, StorageTier::Nvme, StorageTier::Hdd, 0);
370 assert_eq!(id0, 0);
371 assert_eq!(id1, 1);
372 }
373
374 #[test]
375 fn plan_migration_demote_direction() {
376 let mut p = make_planner();
377 let id = add_simple(&mut p, StorageTier::Nvme, StorageTier::Hdd, 0);
378 let record = p.get_record(id).expect("record must exist");
379 assert_eq!(record.task.direction, MigrationDirection::Demote);
380 assert!(!record.task.is_promotion());
381 }
382
383 #[test]
384 fn plan_migration_promote_direction() {
385 let mut p = make_planner();
386 let id = add_simple(&mut p, StorageTier::Archive, StorageTier::Ssd, 0);
387 let record = p.get_record(id).expect("record must exist");
388 assert_eq!(record.task.direction, MigrationDirection::Promote);
389 assert!(record.task.is_promotion());
390 }
391
392 #[test]
393 fn plan_migration_same_tier_is_demote() {
394 let mut p = make_planner();
395 let id = add_simple(&mut p, StorageTier::Ssd, StorageTier::Ssd, 0);
396 let record = p.get_record(id).expect("record must exist");
397 assert_eq!(record.task.direction, MigrationDirection::Demote);
398 }
399
400 #[test]
401 fn plan_migration_cost_estimate_small() {
402 let mut p = make_planner();
403 let id = add_simple(&mut p, StorageTier::Nvme, StorageTier::Archive, 500_000);
405 let record = p.get_record(id).unwrap();
406 assert_eq!(record.task.estimated_cost_ms, 10);
407 }
408
409 #[test]
410 fn plan_migration_cost_estimate_large() {
411 let mut p = make_planner();
412 let id = add_simple(&mut p, StorageTier::Ssd, StorageTier::Hdd, 5_000_000);
414 let record = p.get_record(id).unwrap();
415 assert_eq!(record.task.estimated_cost_ms, 15);
416 }
417
418 #[test]
419 fn plan_migration_status_is_pending() {
420 let mut p = make_planner();
421 let id = add_simple(&mut p, StorageTier::Nvme, StorageTier::Hdd, 0);
422 let record = p.get_record(id).unwrap();
423 assert_eq!(record.status, MigrationStatus::Pending);
424 assert!(record.started_at_tick.is_none());
425 assert!(record.completed_at_tick.is_none());
426 }
427
428 #[test]
429 fn plan_migration_stores_block_fields() {
430 let mut p = make_planner();
431 let id = p.plan_migration(
432 42,
433 "bafycid".into(),
434 StorageTier::Hdd,
435 StorageTier::Nvme,
436 1_000,
437 vec![],
438 );
439 let record = p.get_record(id).unwrap();
440 assert_eq!(record.task.block_id, 42);
441 assert_eq!(record.task.cid, "bafycid");
442 assert_eq!(record.task.from_tier, StorageTier::Hdd);
443 assert_eq!(record.task.to_tier, StorageTier::Nvme);
444 assert_eq!(record.task.size_bytes, 1_000);
445 }
446
447 #[test]
450 fn start_task_sets_in_progress() {
451 let mut p = make_planner();
452 let id = add_simple(&mut p, StorageTier::Ssd, StorageTier::Hdd, 0);
453 assert!(p.start_task(id, 5));
454 let record = p.get_record(id).unwrap();
455 assert_eq!(record.status, MigrationStatus::InProgress);
456 assert_eq!(record.started_at_tick, Some(5));
457 }
458
459 #[test]
460 fn start_task_unknown_id_returns_false() {
461 let mut p = make_planner();
462 assert!(!p.start_task(999, 0));
463 }
464
465 #[test]
466 fn start_task_already_in_progress_returns_false() {
467 let mut p = make_planner();
468 let id = add_simple(&mut p, StorageTier::Ssd, StorageTier::Hdd, 0);
469 assert!(p.start_task(id, 1));
470 assert!(!p.start_task(id, 2));
471 }
472
473 #[test]
474 fn start_task_blocked_by_pending_dependency() {
475 let mut p = make_planner();
476 let dep = add_simple(&mut p, StorageTier::Nvme, StorageTier::Ssd, 0);
477 let child = p.plan_migration(
478 2,
479 "baf".into(),
480 StorageTier::Ssd,
481 StorageTier::Hdd,
482 0,
483 vec![dep],
484 );
485 assert!(!p.start_task(child, 0));
486 }
487
488 #[test]
489 fn start_task_blocked_by_in_progress_dependency() {
490 let mut p = make_planner();
491 let dep = add_simple(&mut p, StorageTier::Nvme, StorageTier::Ssd, 0);
492 let child = p.plan_migration(
493 2,
494 "baf".into(),
495 StorageTier::Ssd,
496 StorageTier::Hdd,
497 0,
498 vec![dep],
499 );
500 p.start_task(dep, 0);
501 assert!(!p.start_task(child, 1));
502 }
503
504 #[test]
505 fn start_task_unblocked_when_dependency_completes() {
506 let mut p = make_planner();
507 let dep = add_simple(&mut p, StorageTier::Nvme, StorageTier::Ssd, 0);
508 let child = p.plan_migration(
509 2,
510 "baf".into(),
511 StorageTier::Ssd,
512 StorageTier::Hdd,
513 0,
514 vec![dep],
515 );
516 p.start_task(dep, 0);
517 p.complete_task(dep, 1);
518 assert!(p.start_task(child, 2));
519 }
520
521 #[test]
524 fn complete_task_sets_completed() {
525 let mut p = make_planner();
526 let id = add_simple(&mut p, StorageTier::Ssd, StorageTier::Archive, 0);
527 p.start_task(id, 0);
528 assert!(p.complete_task(id, 10));
529 let record = p.get_record(id).unwrap();
530 assert_eq!(record.status, MigrationStatus::Completed);
531 assert_eq!(record.completed_at_tick, Some(10));
532 }
533
534 #[test]
535 fn complete_task_not_in_progress_returns_false() {
536 let mut p = make_planner();
537 let id = add_simple(&mut p, StorageTier::Ssd, StorageTier::Archive, 0);
538 assert!(!p.complete_task(id, 1));
539 }
540
541 #[test]
542 fn complete_task_unknown_id_returns_false() {
543 let mut p = make_planner();
544 assert!(!p.complete_task(999, 0));
545 }
546
547 #[test]
550 fn fail_task_from_pending() {
551 let mut p = make_planner();
552 let id = add_simple(&mut p, StorageTier::Nvme, StorageTier::Hdd, 0);
553 assert!(p.fail_task(id));
554 assert_eq!(p.get_record(id).unwrap().status, MigrationStatus::Failed);
555 }
556
557 #[test]
558 fn fail_task_from_in_progress() {
559 let mut p = make_planner();
560 let id = add_simple(&mut p, StorageTier::Nvme, StorageTier::Hdd, 0);
561 p.start_task(id, 0);
562 assert!(p.fail_task(id));
563 assert_eq!(p.get_record(id).unwrap().status, MigrationStatus::Failed);
564 }
565
566 #[test]
567 fn fail_task_from_completed_returns_false() {
568 let mut p = make_planner();
569 let id = add_simple(&mut p, StorageTier::Nvme, StorageTier::Hdd, 0);
570 p.start_task(id, 0);
571 p.complete_task(id, 1);
572 assert!(!p.fail_task(id));
573 }
574
575 #[test]
576 fn fail_task_unknown_id_returns_false() {
577 let mut p = make_planner();
578 assert!(!p.fail_task(999));
579 }
580
581 #[test]
584 fn rollback_task_from_failed_succeeds() {
585 let mut p = make_planner();
586 let id = add_simple(&mut p, StorageTier::Nvme, StorageTier::Hdd, 0);
587 p.fail_task(id);
588 assert!(p.rollback_task(id));
589 assert_eq!(
590 p.get_record(id).unwrap().status,
591 MigrationStatus::RolledBack
592 );
593 }
594
595 #[test]
596 fn rollback_task_from_pending_returns_false() {
597 let mut p = make_planner();
598 let id = add_simple(&mut p, StorageTier::Nvme, StorageTier::Hdd, 0);
599 assert!(!p.rollback_task(id));
600 }
601
602 #[test]
603 fn rollback_task_from_completed_returns_false() {
604 let mut p = make_planner();
605 let id = add_simple(&mut p, StorageTier::Nvme, StorageTier::Hdd, 0);
606 p.start_task(id, 0);
607 p.complete_task(id, 1);
608 assert!(!p.rollback_task(id));
609 }
610
611 #[test]
612 fn rollback_task_idempotency_returns_false() {
613 let mut p = make_planner();
615 let id = add_simple(&mut p, StorageTier::Nvme, StorageTier::Hdd, 0);
616 p.fail_task(id);
617 p.rollback_task(id);
618 assert!(!p.rollback_task(id));
619 }
620
621 #[test]
624 fn ready_tasks_no_deps_all_pending() {
625 let mut p = make_planner();
626 let id0 = add_simple(&mut p, StorageTier::Nvme, StorageTier::Hdd, 0);
627 let id1 = add_simple(&mut p, StorageTier::Ssd, StorageTier::Archive, 0);
628 let ready = p.ready_tasks();
629 assert_eq!(ready, vec![id0, id1]);
630 }
631
632 #[test]
633 fn ready_tasks_sorted_ascending() {
634 let mut p = make_planner();
635 for _ in 0..5 {
636 add_simple(&mut p, StorageTier::Nvme, StorageTier::Hdd, 0);
637 }
638 let ready = p.ready_tasks();
639 let mut sorted = ready.clone();
640 sorted.sort_unstable();
641 assert_eq!(ready, sorted);
642 }
643
644 #[test]
645 fn ready_tasks_excludes_in_progress() {
646 let mut p = make_planner();
647 let id0 = add_simple(&mut p, StorageTier::Nvme, StorageTier::Hdd, 0);
648 let id1 = add_simple(&mut p, StorageTier::Ssd, StorageTier::Archive, 0);
649 p.start_task(id0, 0);
650 let ready = p.ready_tasks();
651 assert_eq!(ready, vec![id1]);
652 }
653
654 #[test]
655 fn ready_tasks_excludes_completed() {
656 let mut p = make_planner();
657 let id0 = add_simple(&mut p, StorageTier::Nvme, StorageTier::Hdd, 0);
658 p.start_task(id0, 0);
659 p.complete_task(id0, 1);
660 assert!(p.ready_tasks().is_empty());
661 }
662
663 #[test]
664 fn ready_tasks_with_dependency_chain() {
665 let mut p = make_planner();
666 let a = add_simple(&mut p, StorageTier::Nvme, StorageTier::Ssd, 0);
667 let b = p.plan_migration(
668 2,
669 "b".into(),
670 StorageTier::Ssd,
671 StorageTier::Hdd,
672 0,
673 vec![a],
674 );
675 let c = p.plan_migration(
676 3,
677 "c".into(),
678 StorageTier::Hdd,
679 StorageTier::Archive,
680 0,
681 vec![b],
682 );
683
684 assert_eq!(p.ready_tasks(), vec![a]);
686
687 p.start_task(a, 0);
688 p.complete_task(a, 1);
689 assert_eq!(p.ready_tasks(), vec![b]);
691
692 p.start_task(b, 2);
693 p.complete_task(b, 3);
694 assert_eq!(p.ready_tasks(), vec![c]);
696 }
697
698 #[test]
701 fn stats_counts_by_status() {
702 let mut p = make_planner();
703 let a = add_simple(&mut p, StorageTier::Nvme, StorageTier::Hdd, 1_000);
704 let b = add_simple(&mut p, StorageTier::Ssd, StorageTier::Archive, 2_000);
705 let c = add_simple(&mut p, StorageTier::Hdd, StorageTier::Nvme, 3_000);
706 let d = add_simple(&mut p, StorageTier::Archive, StorageTier::Ssd, 500);
707
708 p.start_task(a, 0);
709 p.complete_task(a, 1);
710 p.start_task(b, 2);
711 p.fail_task(b);
712 let s = p.stats();
715 assert_eq!(s.total_tasks, 4);
716 assert_eq!(s.completed, 1);
717 assert_eq!(s.failed, 1);
718 assert_eq!(s.pending, 2);
719 assert_eq!(s.in_progress, 0);
720 let _ = c;
721 let _ = d;
722 }
723
724 #[test]
725 fn stats_total_bytes_migrated_accumulates() {
726 let mut p = make_planner();
727 let a = add_simple(&mut p, StorageTier::Nvme, StorageTier::Hdd, 1_000_000);
728 let b = add_simple(&mut p, StorageTier::Ssd, StorageTier::Archive, 2_000_000);
729
730 p.start_task(a, 0);
731 p.complete_task(a, 1);
732 p.start_task(b, 2);
733 p.complete_task(b, 3);
734
735 assert_eq!(p.stats().total_bytes_migrated, 3_000_000);
736 }
737
738 #[test]
739 fn stats_bytes_not_counted_for_failed() {
740 let mut p = make_planner();
741 let a = add_simple(&mut p, StorageTier::Nvme, StorageTier::Hdd, 5_000_000);
742 p.start_task(a, 0);
743 p.fail_task(a);
744
745 assert_eq!(p.stats().total_bytes_migrated, 0);
746 }
747
748 #[test]
749 fn stats_promotions_and_demotions() {
750 let mut p = make_planner();
751 let d0 = add_simple(&mut p, StorageTier::Nvme, StorageTier::Hdd, 0);
753 let d1 = add_simple(&mut p, StorageTier::Ssd, StorageTier::Archive, 0);
754 let p0 = add_simple(&mut p, StorageTier::Hdd, StorageTier::Ssd, 0);
756
757 for id in [d0, d1, p0] {
758 p.start_task(id, 0);
759 p.complete_task(id, 1);
760 }
761
762 let s = p.stats();
763 assert_eq!(s.demotions, 2);
764 assert_eq!(s.promotions, 1);
765 }
766
767 #[test]
768 fn stats_rolledback_counted_as_failed() {
769 let mut p = make_planner();
770 let id = add_simple(&mut p, StorageTier::Nvme, StorageTier::Hdd, 0);
771 p.fail_task(id);
772 p.rollback_task(id);
773
774 let s = p.stats();
775 assert_eq!(s.failed, 1);
776 assert_eq!(s.completed, 0);
777 }
778
779 #[test]
780 fn stats_empty_planner() {
781 let p = make_planner();
782 let s = p.stats();
783 assert_eq!(s.total_tasks, 0);
784 assert_eq!(s.pending, 0);
785 assert_eq!(s.total_bytes_migrated, 0);
786 }
787
788 #[test]
791 fn get_record_missing_returns_none() {
792 let p = make_planner();
793 assert!(p.get_record(0).is_none());
794 }
795
796 #[test]
799 fn multiple_deps_all_must_complete() {
800 let mut p = make_planner();
801 let a = add_simple(&mut p, StorageTier::Nvme, StorageTier::Ssd, 0);
802 let b = add_simple(&mut p, StorageTier::Nvme, StorageTier::Hdd, 0);
803 let c = p.plan_migration(
804 10,
805 "c".into(),
806 StorageTier::Ssd,
807 StorageTier::Archive,
808 0,
809 vec![a, b],
810 );
811
812 assert!(!p.ready_tasks().contains(&c));
814
815 p.start_task(a, 0);
816 p.complete_task(a, 1);
817 assert!(!p.ready_tasks().contains(&c));
819
820 p.start_task(b, 2);
821 p.complete_task(b, 3);
822 assert!(p.ready_tasks().contains(&c));
824 }
825
826 #[test]
827 fn default_impl_is_empty() {
828 let p = StorageMigrationPlanner::default();
829 assert_eq!(p.stats().total_tasks, 0);
830 }
831}