1use std::collections::HashMap;
25
26#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
32pub enum StorageTier {
33 Hot = 0,
35 Warm = 1,
37 Cold = 2,
39 Archive = 3,
41}
42
43impl StorageTier {
44 pub fn cost_per_gb(&self) -> f64 {
46 match self {
47 StorageTier::Hot => 0.10,
48 StorageTier::Warm => 0.04,
49 StorageTier::Cold => 0.01,
50 StorageTier::Archive => 0.002,
51 }
52 }
53
54 pub fn access_latency_ms(&self) -> u64 {
56 match self {
57 StorageTier::Hot => 1,
58 StorageTier::Warm => 10,
59 StorageTier::Cold => 100,
60 StorageTier::Archive => 1000,
61 }
62 }
63}
64
65#[derive(Clone, Debug)]
71pub struct MigrationPolicy {
72 pub hot_to_warm_idle_secs: u64,
74 pub warm_to_cold_idle_secs: u64,
76 pub cold_to_archive_idle_secs: u64,
78 pub min_size_for_cold: u64,
80}
81
82impl Default for MigrationPolicy {
83 fn default() -> Self {
84 Self {
85 hot_to_warm_idle_secs: 86_400, warm_to_cold_idle_secs: 604_800, cold_to_archive_idle_secs: 2_592_000, min_size_for_cold: 4096,
89 }
90 }
91}
92
93#[derive(Clone, Debug)]
99pub struct BlockTierRecord {
100 pub cid: String,
102 pub tier: StorageTier,
104 pub size_bytes: u64,
106 pub last_accessed_secs: u64,
108 pub access_count: u64,
110}
111
112impl BlockTierRecord {
113 pub fn idle_secs(&self, now: u64) -> u64 {
115 now.saturating_sub(self.last_accessed_secs)
116 }
117
118 pub fn monthly_cost(&self) -> f64 {
120 let gib = self.size_bytes as f64 / (1024_u64.pow(3) as f64);
121 gib * self.tier.cost_per_gb()
122 }
123}
124
125#[derive(Clone, Debug, PartialEq, Eq)]
131pub struct MigrationTask {
132 pub cid: String,
134 pub from_tier: StorageTier,
136 pub to_tier: StorageTier,
138 pub size_bytes: u64,
140}
141
142#[derive(Clone, Debug, Default)]
148pub struct MigratorStats {
149 pub total_blocks: usize,
151 pub blocks_per_tier: [usize; 4],
153 pub total_bytes_per_tier: [u64; 4],
155}
156
157impl MigratorStats {
158 pub fn total_monthly_cost(&self, records: &[BlockTierRecord]) -> f64 {
160 records.iter().map(|r| r.monthly_cost()).sum()
161 }
162}
163
164pub struct BlockTierMigrator {
171 pub records: HashMap<String, BlockTierRecord>,
173 pub policy: MigrationPolicy,
175}
176
177impl BlockTierMigrator {
178 pub fn new(policy: MigrationPolicy) -> Self {
180 Self {
181 records: HashMap::new(),
182 policy,
183 }
184 }
185
186 pub fn register(&mut self, cid: String, tier: StorageTier, size_bytes: u64, now_secs: u64) {
190 self.records.insert(
191 cid.clone(),
192 BlockTierRecord {
193 cid,
194 tier,
195 size_bytes,
196 last_accessed_secs: now_secs,
197 access_count: 0,
198 },
199 );
200 }
201
202 pub fn record_access(&mut self, cid: &str, now_secs: u64) {
207 if let Some(record) = self.records.get_mut(cid) {
208 record.last_accessed_secs = now_secs;
209 record.access_count = record.access_count.saturating_add(1);
210 if record.tier != StorageTier::Hot {
211 record.tier = StorageTier::Hot;
212 }
213 }
214 }
215
216 pub fn plan_migrations(&self, now_secs: u64) -> Vec<MigrationTask> {
223 let mut tasks = Vec::new();
224
225 for record in self.records.values() {
226 let target = self.target_tier(record, now_secs);
227 if target != record.tier {
228 tasks.push(MigrationTask {
229 cid: record.cid.clone(),
230 from_tier: record.tier,
231 to_tier: target,
232 size_bytes: record.size_bytes,
233 });
234 }
235 }
236
237 tasks
238 }
239
240 pub fn apply_migration(&mut self, task: &MigrationTask) {
244 if let Some(record) = self.records.get_mut(&task.cid) {
245 record.tier = task.to_tier;
246 }
247 }
248
249 pub fn stats(&self) -> MigratorStats {
251 let mut blocks_per_tier = [0usize; 4];
252 let mut total_bytes_per_tier = [0u64; 4];
253
254 for record in self.records.values() {
255 let idx = record.tier as usize;
256 blocks_per_tier[idx] += 1;
257 total_bytes_per_tier[idx] = total_bytes_per_tier[idx].saturating_add(record.size_bytes);
258 }
259
260 MigratorStats {
261 total_blocks: self.records.len(),
262 blocks_per_tier,
263 total_bytes_per_tier,
264 }
265 }
266
267 fn target_tier(&self, record: &BlockTierRecord, now_secs: u64) -> StorageTier {
273 let idle = record.idle_secs(now_secs);
274 let small = record.size_bytes < self.policy.min_size_for_cold;
275
276 match record.tier {
277 StorageTier::Hot => {
278 if idle >= self.policy.hot_to_warm_idle_secs {
279 StorageTier::Warm
280 } else {
281 StorageTier::Hot
282 }
283 }
284 StorageTier::Warm => {
285 if idle >= self.policy.warm_to_cold_idle_secs && !small {
286 StorageTier::Cold
287 } else {
288 StorageTier::Warm
289 }
290 }
291 StorageTier::Cold => {
292 if idle >= self.policy.cold_to_archive_idle_secs && !small {
293 StorageTier::Archive
294 } else {
295 StorageTier::Cold
296 }
297 }
298 StorageTier::Archive => StorageTier::Archive,
299 }
300 }
301}
302
303#[cfg(test)]
308mod tests {
309 use super::*;
310
311 fn default_migrator() -> BlockTierMigrator {
312 BlockTierMigrator::new(MigrationPolicy::default())
313 }
314
315 #[test]
317 fn test_register_creates_record() {
318 let mut m = default_migrator();
319 m.register("cid1".to_string(), StorageTier::Hot, 1024, 1000);
320 assert!(m.records.contains_key("cid1"));
321 }
322
323 #[test]
325 fn test_register_sets_tier() {
326 let mut m = default_migrator();
327 m.register("cid2".to_string(), StorageTier::Warm, 2048, 2000);
328 assert_eq!(m.records["cid2"].tier, StorageTier::Warm);
329 }
330
331 #[test]
333 fn test_register_access_count_zero() {
334 let mut m = default_migrator();
335 m.register("cid3".to_string(), StorageTier::Cold, 512, 3000);
336 assert_eq!(m.records["cid3"].access_count, 0);
337 }
338
339 #[test]
341 fn test_record_access_promotes_cold_to_hot() {
342 let mut m = default_migrator();
343 m.register("cid4".to_string(), StorageTier::Cold, 8192, 1000);
344 m.record_access("cid4", 2000);
345 assert_eq!(m.records["cid4"].tier, StorageTier::Hot);
346 }
347
348 #[test]
350 fn test_record_access_promotes_warm_to_hot() {
351 let mut m = default_migrator();
352 m.register("cid5".to_string(), StorageTier::Warm, 8192, 1000);
353 m.record_access("cid5", 2000);
354 assert_eq!(m.records["cid5"].tier, StorageTier::Hot);
355 }
356
357 #[test]
359 fn test_record_access_increments_count() {
360 let mut m = default_migrator();
361 m.register("cid6".to_string(), StorageTier::Hot, 1024, 1000);
362 m.record_access("cid6", 1001);
363 m.record_access("cid6", 1002);
364 assert_eq!(m.records["cid6"].access_count, 2);
365 }
366
367 #[test]
369 fn test_record_access_updates_timestamp() {
370 let mut m = default_migrator();
371 m.register("cid7".to_string(), StorageTier::Hot, 1024, 1000);
372 m.record_access("cid7", 9999);
373 assert_eq!(m.records["cid7"].last_accessed_secs, 9999);
374 }
375
376 #[test]
378 fn test_record_access_unknown_cid_noop() {
379 let mut m = default_migrator();
380 m.record_access("nonexistent", 5000); }
382
383 #[test]
385 fn test_plan_migrations_hot_to_warm() {
386 let mut m = default_migrator();
387 let start = 0u64;
388 m.register("cid9".to_string(), StorageTier::Hot, 8192, start);
389 let tasks = m.plan_migrations(start + 86_401);
391 assert_eq!(tasks.len(), 1);
392 assert_eq!(tasks[0].from_tier, StorageTier::Hot);
393 assert_eq!(tasks[0].to_tier, StorageTier::Warm);
394 }
395
396 #[test]
398 fn test_plan_migrations_hot_not_yet_idle() {
399 let mut m = default_migrator();
400 let start = 0u64;
401 m.register("cid10".to_string(), StorageTier::Hot, 8192, start);
402 let tasks = m.plan_migrations(start + 86_399);
403 assert!(tasks.is_empty());
404 }
405
406 #[test]
408 fn test_plan_migrations_warm_to_cold_large_block() {
409 let mut m = default_migrator();
410 let start = 0u64;
411 m.register("cid11".to_string(), StorageTier::Warm, 8192, start);
412 let tasks = m.plan_migrations(start + 604_801);
413 assert_eq!(tasks.len(), 1);
414 assert_eq!(tasks[0].from_tier, StorageTier::Warm);
415 assert_eq!(tasks[0].to_tier, StorageTier::Cold);
416 }
417
418 #[test]
420 fn test_plan_migrations_small_block_stays_warm() {
421 let mut m = default_migrator();
422 let start = 0u64;
423 m.register("cid12".to_string(), StorageTier::Warm, 1024, start);
425 let tasks = m.plan_migrations(start + 604_801);
426 assert!(tasks.is_empty());
427 }
428
429 #[test]
431 fn test_plan_migrations_cold_to_archive() {
432 let mut m = default_migrator();
433 let start = 0u64;
434 m.register("cid13".to_string(), StorageTier::Cold, 8192, start);
435 let tasks = m.plan_migrations(start + 2_592_001);
436 assert_eq!(tasks.len(), 1);
437 assert_eq!(tasks[0].from_tier, StorageTier::Cold);
438 assert_eq!(tasks[0].to_tier, StorageTier::Archive);
439 }
440
441 #[test]
443 fn test_plan_migrations_archive_stays() {
444 let mut m = default_migrator();
445 m.register("cid14".to_string(), StorageTier::Archive, 8192, 0);
446 let tasks = m.plan_migrations(u64::MAX);
447 assert!(tasks.is_empty());
448 }
449
450 #[test]
452 fn test_apply_migration_updates_tier() {
453 let mut m = default_migrator();
454 m.register("cid15".to_string(), StorageTier::Hot, 8192, 0);
455 let task = MigrationTask {
456 cid: "cid15".to_string(),
457 from_tier: StorageTier::Hot,
458 to_tier: StorageTier::Warm,
459 size_bytes: 8192,
460 };
461 m.apply_migration(&task);
462 assert_eq!(m.records["cid15"].tier, StorageTier::Warm);
463 }
464
465 #[test]
467 fn test_apply_migration_unknown_cid_noop() {
468 let mut m = default_migrator();
469 let task = MigrationTask {
470 cid: "ghost".to_string(),
471 from_tier: StorageTier::Hot,
472 to_tier: StorageTier::Warm,
473 size_bytes: 512,
474 };
475 m.apply_migration(&task); }
477
478 #[test]
480 fn test_stats_counts_per_tier() {
481 let mut m = default_migrator();
482 m.register("h1".to_string(), StorageTier::Hot, 1024, 0);
483 m.register("h2".to_string(), StorageTier::Hot, 1024, 0);
484 m.register("w1".to_string(), StorageTier::Warm, 2048, 0);
485 m.register("c1".to_string(), StorageTier::Cold, 4096, 0);
486 m.register("a1".to_string(), StorageTier::Archive, 8192, 0);
487
488 let s = m.stats();
489 assert_eq!(s.total_blocks, 5);
490 assert_eq!(s.blocks_per_tier[StorageTier::Hot as usize], 2);
491 assert_eq!(s.blocks_per_tier[StorageTier::Warm as usize], 1);
492 assert_eq!(s.blocks_per_tier[StorageTier::Cold as usize], 1);
493 assert_eq!(s.blocks_per_tier[StorageTier::Archive as usize], 1);
494 }
495
496 #[test]
498 fn test_stats_bytes_per_tier() {
499 let mut m = default_migrator();
500 m.register("h1".to_string(), StorageTier::Hot, 500, 0);
501 m.register("h2".to_string(), StorageTier::Hot, 300, 0);
502 m.register("w1".to_string(), StorageTier::Warm, 1000, 0);
503
504 let s = m.stats();
505 assert_eq!(s.total_bytes_per_tier[StorageTier::Hot as usize], 800);
506 assert_eq!(s.total_bytes_per_tier[StorageTier::Warm as usize], 1000);
507 assert_eq!(s.total_bytes_per_tier[StorageTier::Cold as usize], 0);
508 }
509
510 #[test]
512 fn test_idle_secs_saturates() {
513 let record = BlockTierRecord {
514 cid: "x".to_string(),
515 tier: StorageTier::Hot,
516 size_bytes: 1024,
517 last_accessed_secs: 9999,
518 access_count: 0,
519 };
520 assert_eq!(record.idle_secs(1000), 0);
521 }
522
523 #[test]
525 fn test_monthly_cost_hot() {
526 let record = BlockTierRecord {
527 cid: "y".to_string(),
528 tier: StorageTier::Hot,
529 size_bytes: 1024 * 1024 * 1024, last_accessed_secs: 0,
531 access_count: 0,
532 };
533 let cost = record.monthly_cost();
534 assert!((cost - 0.10).abs() < 1e-9);
536 }
537
538 #[test]
540 fn test_total_monthly_cost() {
541 let records = vec![
542 BlockTierRecord {
543 cid: "a".to_string(),
544 tier: StorageTier::Hot,
545 size_bytes: 1024 * 1024 * 1024,
546 last_accessed_secs: 0,
547 access_count: 0,
548 },
549 BlockTierRecord {
550 cid: "b".to_string(),
551 tier: StorageTier::Archive,
552 size_bytes: 1024 * 1024 * 1024,
553 last_accessed_secs: 0,
554 access_count: 0,
555 },
556 ];
557 let stats = MigratorStats::default();
558 let total = stats.total_monthly_cost(&records);
559 assert!((total - 0.102).abs() < 1e-9);
561 }
562
563 #[test]
565 fn test_tier_ordering() {
566 assert!(StorageTier::Hot < StorageTier::Warm);
567 assert!(StorageTier::Warm < StorageTier::Cold);
568 assert!(StorageTier::Cold < StorageTier::Archive);
569 }
570
571 #[test]
573 fn test_cost_per_gb() {
574 assert!((StorageTier::Hot.cost_per_gb() - 0.10).abs() < 1e-12);
575 assert!((StorageTier::Warm.cost_per_gb() - 0.04).abs() < 1e-12);
576 assert!((StorageTier::Cold.cost_per_gb() - 0.01).abs() < 1e-12);
577 assert!((StorageTier::Archive.cost_per_gb() - 0.002).abs() < 1e-12);
578 }
579
580 #[test]
582 fn test_access_latency_ms() {
583 assert_eq!(StorageTier::Hot.access_latency_ms(), 1);
584 assert_eq!(StorageTier::Warm.access_latency_ms(), 10);
585 assert_eq!(StorageTier::Cold.access_latency_ms(), 100);
586 assert_eq!(StorageTier::Archive.access_latency_ms(), 1000);
587 }
588
589 #[test]
591 fn test_full_demotion_chain() {
592 let mut m = default_migrator();
593 let start = 0u64;
594 m.register("chain".to_string(), StorageTier::Hot, 8192, start);
595
596 let now1 = start + 86_401;
598 let tasks = m.plan_migrations(now1);
599 assert_eq!(tasks.len(), 1);
600 m.apply_migration(&tasks[0]);
601 assert_eq!(m.records["chain"].tier, StorageTier::Warm);
602
603 let now2 = start + 604_801;
605 let tasks = m.plan_migrations(now2);
606 assert_eq!(tasks.len(), 1);
607 m.apply_migration(&tasks[0]);
608 assert_eq!(m.records["chain"].tier, StorageTier::Cold);
609
610 let now3 = start + 2_592_001;
612 let tasks = m.plan_migrations(now3);
613 assert_eq!(tasks.len(), 1);
614 m.apply_migration(&tasks[0]);
615 assert_eq!(m.records["chain"].tier, StorageTier::Archive);
616
617 let tasks = m.plan_migrations(u64::MAX);
619 assert!(tasks.is_empty());
620 }
621}