1use parking_lot::RwLock;
8use std::collections::HashMap;
9use std::sync::atomic::{AtomicU64, Ordering};
10use std::time::{Duration, Instant};
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
19pub enum StorageTier {
20 Hot,
22 Warm,
24 Cold,
26 Archive,
28}
29
30impl StorageTier {
31 pub fn tier_latency_ms_estimate(&self) -> u64 {
33 match self {
34 StorageTier::Hot => 1,
35 StorageTier::Warm => 10,
36 StorageTier::Cold => 100,
37 StorageTier::Archive => 10_000,
38 }
39 }
40
41 pub fn name(&self) -> &'static str {
43 match self {
44 StorageTier::Hot => "hot",
45 StorageTier::Warm => "warm",
46 StorageTier::Cold => "cold",
47 StorageTier::Archive => "archive",
48 }
49 }
50}
51
52#[derive(Debug, Clone)]
64pub struct TierPolicy {
65 pub hot_threshold_access_rate: f64,
67 pub warm_threshold_access_rate: f64,
69 pub cold_threshold_access_rate: f64,
71}
72
73impl Default for TierPolicy {
74 fn default() -> Self {
75 Self {
76 hot_threshold_access_rate: 1.0,
77 warm_threshold_access_rate: 0.1,
78 cold_threshold_access_rate: 0.01,
79 }
80 }
81}
82
83impl TierPolicy {
84 pub fn classify(&self, access_rate: f64) -> StorageTier {
86 if access_rate >= self.hot_threshold_access_rate {
87 StorageTier::Hot
88 } else if access_rate >= self.warm_threshold_access_rate {
89 StorageTier::Warm
90 } else if access_rate >= self.cold_threshold_access_rate {
91 StorageTier::Cold
92 } else {
93 StorageTier::Archive
94 }
95 }
96}
97
98#[derive(Debug, Clone)]
104pub struct BlockTierRecord {
105 pub cid: String,
107 pub current_tier: StorageTier,
109 pub access_count: u64,
111 pub last_accessed: Instant,
113 pub size_bytes: u64,
115 pub created_at: Instant,
117}
118
119impl BlockTierRecord {
120 pub fn access_rate_per_sec(&self, window: Duration) -> f64 {
125 let secs = window.as_secs_f64();
126 if secs <= 0.0 {
127 return 0.0;
128 }
129 self.access_count as f64 / secs
130 }
131}
132
133#[derive(Debug, Clone, PartialEq, Eq)]
139pub struct TierTransition {
140 pub cid: String,
142 pub from: StorageTier,
144 pub to: StorageTier,
146}
147
148#[derive(Debug, Default)]
154pub struct TierStats {
155 pub total_accesses_recorded: AtomicU64,
157 pub total_reclassifications: AtomicU64,
159 pub total_transitions: AtomicU64,
161}
162
163#[derive(Debug, Clone, PartialEq, Eq)]
165pub struct TierStatsSnapshot {
166 pub total_accesses_recorded: u64,
167 pub total_reclassifications: u64,
168 pub total_transitions: u64,
169}
170
171impl TierStats {
172 pub fn snapshot(&self) -> TierStatsSnapshot {
174 TierStatsSnapshot {
175 total_accesses_recorded: self.total_accesses_recorded.load(Ordering::Relaxed),
176 total_reclassifications: self.total_reclassifications.load(Ordering::Relaxed),
177 total_transitions: self.total_transitions.load(Ordering::Relaxed),
178 }
179 }
180}
181
182pub struct StorageTierManager {
194 pub records: RwLock<HashMap<String, BlockTierRecord>>,
196 pub policy: TierPolicy,
198 pub stats: TierStats,
200}
201
202impl StorageTierManager {
203 pub fn new(policy: TierPolicy) -> Self {
205 Self {
206 records: RwLock::new(HashMap::new()),
207 policy,
208 stats: TierStats::default(),
209 }
210 }
211
212 pub fn with_default_policy() -> Self {
214 Self::new(TierPolicy::default())
215 }
216
217 pub fn record_access(&self, cid: &str, size_bytes: u64) {
228 let now = Instant::now();
229 let mut guard = self.records.write();
230 match guard.get_mut(cid) {
231 Some(rec) => {
232 rec.access_count += 1;
233 rec.last_accessed = now;
234 }
235 None => {
236 guard.insert(
237 cid.to_owned(),
238 BlockTierRecord {
239 cid: cid.to_owned(),
240 current_tier: StorageTier::Archive,
241 access_count: 1,
242 last_accessed: now,
243 size_bytes,
244 created_at: now,
245 },
246 );
247 }
248 }
249 self.stats
250 .total_accesses_recorded
251 .fetch_add(1, Ordering::Relaxed);
252 }
253
254 pub fn classify(&self, cid: &str, window: Duration) -> StorageTier {
263 let guard = self.records.read();
264 match guard.get(cid) {
265 Some(rec) => self.policy.classify(rec.access_rate_per_sec(window)),
266 None => StorageTier::Archive,
267 }
268 }
269
270 pub fn reclassify_all(&self, window: Duration) -> Vec<TierTransition> {
276 let mut transitions = Vec::new();
277 {
278 let mut guard = self.records.write();
279 for rec in guard.values_mut() {
280 let new_tier = self.policy.classify(rec.access_rate_per_sec(window));
281 if new_tier != rec.current_tier {
282 transitions.push(TierTransition {
283 cid: rec.cid.clone(),
284 from: rec.current_tier,
285 to: new_tier,
286 });
287 rec.current_tier = new_tier;
288 }
289 }
290 }
291 self.stats
292 .total_reclassifications
293 .fetch_add(1, Ordering::Relaxed);
294 self.stats
295 .total_transitions
296 .fetch_add(transitions.len() as u64, Ordering::Relaxed);
297 transitions
298 }
299
300 pub fn blocks_in_tier(&self, tier: StorageTier) -> Vec<String> {
306 let guard = self.records.read();
307 guard
308 .values()
309 .filter(|rec| rec.current_tier == tier)
310 .map(|rec| rec.cid.clone())
311 .collect()
312 }
313
314 pub fn tier_summary(&self) -> HashMap<String, usize> {
317 let guard = self.records.read();
318 let mut summary: HashMap<String, usize> = HashMap::new();
319 for tier in &[
320 StorageTier::Hot,
321 StorageTier::Warm,
322 StorageTier::Cold,
323 StorageTier::Archive,
324 ] {
325 summary.insert(tier.name().to_owned(), 0);
326 }
327 for rec in guard.values() {
328 *summary
329 .entry(rec.current_tier.name().to_owned())
330 .or_insert(0) += 1;
331 }
332 summary
333 }
334
335 pub fn eviction_candidates(&self, tier: StorageTier, max_count: usize) -> Vec<String> {
339 let guard = self.records.read();
340 let mut candidates: Vec<&BlockTierRecord> = guard
341 .values()
342 .filter(|rec| rec.current_tier == tier)
343 .collect();
344 candidates.sort_by_key(|rec| rec.last_accessed);
346 candidates
347 .into_iter()
348 .take(max_count)
349 .map(|rec| rec.cid.clone())
350 .collect()
351 }
352}
353
354#[cfg(test)]
359mod tests {
360 use super::*;
361 use std::thread;
362 use std::time::Duration;
363
364 #[test]
367 fn test_classify_hot() {
368 let policy = TierPolicy::default();
369 assert_eq!(policy.classify(5.0), StorageTier::Hot);
370 assert_eq!(policy.classify(1.0), StorageTier::Hot);
371 }
372
373 #[test]
374 fn test_classify_warm() {
375 let policy = TierPolicy::default();
376 assert_eq!(policy.classify(0.5), StorageTier::Warm);
377 assert_eq!(policy.classify(0.1), StorageTier::Warm);
378 }
379
380 #[test]
381 fn test_classify_cold() {
382 let policy = TierPolicy::default();
383 assert_eq!(policy.classify(0.05), StorageTier::Cold);
384 assert_eq!(policy.classify(0.01), StorageTier::Cold);
385 }
386
387 #[test]
388 fn test_classify_archive() {
389 let policy = TierPolicy::default();
390 assert_eq!(policy.classify(0.005), StorageTier::Archive);
391 assert_eq!(policy.classify(0.0), StorageTier::Archive);
392 }
393
394 #[test]
397 fn test_tier_latency_estimates() {
398 assert_eq!(StorageTier::Hot.tier_latency_ms_estimate(), 1);
399 assert_eq!(StorageTier::Warm.tier_latency_ms_estimate(), 10);
400 assert_eq!(StorageTier::Cold.tier_latency_ms_estimate(), 100);
401 assert_eq!(StorageTier::Archive.tier_latency_ms_estimate(), 10_000);
402 }
403
404 #[test]
407 fn test_record_access_creates_record() {
408 let mgr = StorageTierManager::with_default_policy();
409 mgr.record_access("cid-alpha", 1024);
410
411 let guard = mgr.records.read();
412 let rec = guard.get("cid-alpha").expect("record should exist");
413 assert_eq!(rec.cid, "cid-alpha");
414 assert_eq!(rec.access_count, 1);
415 assert_eq!(rec.size_bytes, 1024);
416 }
417
418 #[test]
419 fn test_record_access_increments_existing() {
420 let mgr = StorageTierManager::with_default_policy();
421 mgr.record_access("cid-beta", 512);
422 mgr.record_access("cid-beta", 512);
423 mgr.record_access("cid-beta", 512);
424
425 let guard = mgr.records.read();
426 let rec = guard.get("cid-beta").expect("record should exist");
427 assert_eq!(rec.access_count, 3);
428 }
429
430 #[test]
433 fn test_access_rate_per_sec_formula() {
434 let rec = BlockTierRecord {
435 cid: "test".to_owned(),
436 current_tier: StorageTier::Archive,
437 access_count: 100,
438 last_accessed: Instant::now(),
439 size_bytes: 256,
440 created_at: Instant::now(),
441 };
442 let rate = rec.access_rate_per_sec(Duration::from_secs(10));
443 assert!((rate - 10.0).abs() < 1e-9, "expected 10.0, got {rate}");
445 }
446
447 #[test]
448 fn test_access_rate_zero_window() {
449 let rec = BlockTierRecord {
450 cid: "test".to_owned(),
451 current_tier: StorageTier::Archive,
452 access_count: 50,
453 last_accessed: Instant::now(),
454 size_bytes: 128,
455 created_at: Instant::now(),
456 };
457 let rate = rec.access_rate_per_sec(Duration::from_secs(0));
459 assert_eq!(rate, 0.0);
460 }
461
462 #[test]
465 fn test_classify_returns_correct_tier_after_accesses() {
466 let mgr = StorageTierManager::with_default_policy();
467 for _ in 0..50 {
469 mgr.record_access("hot-cid", 128);
470 }
471 let tier = mgr.classify("hot-cid", Duration::from_secs(10));
472 assert_eq!(tier, StorageTier::Hot);
473 }
474
475 #[test]
476 fn test_classify_unknown_cid_returns_archive() {
477 let mgr = StorageTierManager::with_default_policy();
478 let tier = mgr.classify("no-such-cid", Duration::from_secs(60));
479 assert_eq!(tier, StorageTier::Archive);
480 }
481
482 #[test]
485 fn test_reclassify_all_detects_transitions() {
486 let mgr = StorageTierManager::with_default_policy();
487
488 mgr.record_access("migrate-cid", 64);
491 {
493 let mut guard = mgr.records.write();
494 if let Some(rec) = guard.get_mut("migrate-cid") {
495 rec.current_tier = StorageTier::Archive;
496 rec.access_count = 200;
497 }
498 }
499
500 let transitions = mgr.reclassify_all(Duration::from_secs(10));
502 assert_eq!(transitions.len(), 1);
503 assert_eq!(transitions[0].cid, "migrate-cid");
504 assert_eq!(transitions[0].from, StorageTier::Archive);
505 assert_eq!(transitions[0].to, StorageTier::Hot);
506 }
507
508 #[test]
509 fn test_reclassify_all_no_transitions_when_unchanged() {
510 let mgr = StorageTierManager::with_default_policy();
511 mgr.record_access("stable-cid", 256);
513 let t1 = mgr.reclassify_all(Duration::from_secs(60));
515 assert_eq!(t1.len(), 1);
516 let t2 = mgr.reclassify_all(Duration::from_secs(60));
518 assert_eq!(t2.len(), 0);
519 }
520
521 #[test]
524 fn test_blocks_in_tier_returns_correct_cids() {
525 let mgr = StorageTierManager::with_default_policy();
526 {
528 let mut guard = mgr.records.write();
529 for name in &["hot-1", "hot-2"] {
530 guard.insert(
531 name.to_string(),
532 BlockTierRecord {
533 cid: name.to_string(),
534 current_tier: StorageTier::Hot,
535 access_count: 100,
536 last_accessed: Instant::now(),
537 size_bytes: 64,
538 created_at: Instant::now(),
539 },
540 );
541 }
542 guard.insert(
543 "warm-1".to_owned(),
544 BlockTierRecord {
545 cid: "warm-1".to_owned(),
546 current_tier: StorageTier::Warm,
547 access_count: 5,
548 last_accessed: Instant::now(),
549 size_bytes: 64,
550 created_at: Instant::now(),
551 },
552 );
553 }
554 let mut hot_blocks = mgr.blocks_in_tier(StorageTier::Hot);
555 hot_blocks.sort();
556 assert_eq!(hot_blocks, vec!["hot-1", "hot-2"]);
557
558 let warm_blocks = mgr.blocks_in_tier(StorageTier::Warm);
559 assert_eq!(warm_blocks, vec!["warm-1"]);
560
561 let cold_blocks = mgr.blocks_in_tier(StorageTier::Cold);
562 assert!(cold_blocks.is_empty());
563 }
564
565 #[test]
568 fn test_eviction_candidates_sorted_by_lru() {
569 let mgr = StorageTierManager::with_default_policy();
570
571 let cids = ["cold-a", "cold-b", "cold-c"];
574 for cid in &cids {
575 mgr.record_access(cid, 128);
576 thread::sleep(Duration::from_millis(2));
578 }
579 {
581 let mut guard = mgr.records.write();
582 for cid in &cids {
583 if let Some(rec) = guard.get_mut(*cid) {
584 rec.current_tier = StorageTier::Cold;
585 }
586 }
587 }
588
589 let candidates = mgr.eviction_candidates(StorageTier::Cold, 2);
591 assert_eq!(candidates.len(), 2);
592 assert_eq!(candidates[0], "cold-a");
594 assert_eq!(candidates[1], "cold-b");
595 }
596
597 #[test]
600 fn test_tier_summary_counts_correct() {
601 let mgr = StorageTierManager::with_default_policy();
602 {
603 let mut guard = mgr.records.write();
604 let tiers = [
605 ("s-hot-1", StorageTier::Hot),
606 ("s-hot-2", StorageTier::Hot),
607 ("s-warm-1", StorageTier::Warm),
608 ("s-cold-1", StorageTier::Cold),
609 ];
610 for (name, tier) in &tiers {
611 guard.insert(
612 name.to_string(),
613 BlockTierRecord {
614 cid: name.to_string(),
615 current_tier: *tier,
616 access_count: 1,
617 last_accessed: Instant::now(),
618 size_bytes: 64,
619 created_at: Instant::now(),
620 },
621 );
622 }
623 }
624 let summary = mgr.tier_summary();
625 assert_eq!(*summary.get("hot").unwrap_or(&0), 2);
626 assert_eq!(*summary.get("warm").unwrap_or(&0), 1);
627 assert_eq!(*summary.get("cold").unwrap_or(&0), 1);
628 assert_eq!(*summary.get("archive").unwrap_or(&0), 0);
629 }
630
631 #[test]
634 fn test_stats_accumulation() {
635 let mgr = StorageTierManager::with_default_policy();
636
637 mgr.record_access("stat-cid-1", 64);
638 mgr.record_access("stat-cid-2", 64);
639 mgr.record_access("stat-cid-1", 64); let snap_before = mgr.stats.snapshot();
642 assert_eq!(snap_before.total_accesses_recorded, 3);
643
644 mgr.reclassify_all(Duration::from_secs(1));
647 let snap_after = mgr.stats.snapshot();
648 assert_eq!(snap_after.total_reclassifications, 1);
649 assert!(snap_after.total_transitions >= snap_before.total_transitions);
651 }
652}