use std::collections::HashMap;
use std::time::{SystemTime, UNIX_EPOCH};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum StorageTier {
Hot,
Warm,
Cold,
Archive,
}
impl StorageTier {
pub fn parse_tier(s: &str) -> Option<Self> {
match s.to_lowercase().as_str() {
"hot" => Some(Self::Hot),
"warm" => Some(Self::Warm),
"cold" => Some(Self::Cold),
"archive" => Some(Self::Archive),
_ => None,
}
}
pub fn as_str(&self) -> &'static str {
match self {
Self::Hot => "hot",
Self::Warm => "warm",
Self::Cold => "cold",
Self::Archive => "archive",
}
}
}
#[derive(Debug, Clone)]
pub struct LifecyclePolicy {
pub name: String,
pub tier: StorageTier,
pub created_at: u64,
pub last_accessed_at: u64,
pub access_count: u64,
pub expires_at: Option<u64>,
}
impl LifecyclePolicy {
pub fn new(name: String, tier: StorageTier) -> Self {
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
Self {
name,
tier,
created_at: now,
last_accessed_at: now,
access_count: 0,
expires_at: None,
}
}
pub fn with_expiry(mut self, expires_at: u64) -> Self {
self.expires_at = Some(expires_at);
self
}
pub fn record_access(&mut self) {
self.last_accessed_at = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
self.access_count += 1;
}
pub fn is_expired(&self) -> bool {
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
self.expires_at.is_some_and(|exp| now >= exp)
}
pub fn seconds_since_access(&self) -> u64 {
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
now.saturating_sub(self.last_accessed_at)
}
}
#[derive(Debug, Clone)]
pub struct TieringRule {
pub from_tier: StorageTier,
pub to_tier: StorageTier,
pub idle_seconds: u64,
pub max_access_count: u64,
}
impl TieringRule {
pub fn should_tier(&self, policy: &LifecyclePolicy) -> bool {
policy.tier == self.from_tier
&& policy.seconds_since_access() >= self.idle_seconds
&& policy.access_count <= self.max_access_count
}
}
pub struct ExpirationCleaner {
pub interval: u64,
}
impl ExpirationCleaner {
pub fn new(interval: u64) -> Self {
Self { interval }
}
pub fn find_expired<'a>(&self, policies: &'a [LifecyclePolicy]) -> Vec<&'a LifecyclePolicy> {
policies.iter().filter(|p| p.is_expired()).collect()
}
}
pub struct StorageLifecycleManager {
policies: HashMap<String, LifecyclePolicy>,
tiering_rules: Vec<TieringRule>,
cleaner: ExpirationCleaner,
}
impl StorageLifecycleManager {
pub fn new(cleaner: ExpirationCleaner) -> Self {
Self {
policies: HashMap::new(),
tiering_rules: Vec::new(),
cleaner,
}
}
pub fn add(&mut self, policy: LifecyclePolicy) {
self.policies.insert(policy.name.clone(), policy);
}
pub fn add_tiering_rule(&mut self, rule: TieringRule) {
self.tiering_rules.push(rule);
}
pub fn access(&mut self, name: &str) -> bool {
if let Some(policy) = self.policies.get_mut(name) {
policy.record_access();
true
} else {
false
}
}
pub fn plan_tiering(&self) -> Vec<(String, StorageTier, StorageTier)> {
let mut plan = Vec::new();
for policy in self.policies.values() {
for rule in &self.tiering_rules {
if rule.should_tier(policy) {
plan.push((policy.name.clone(), rule.from_tier, rule.to_tier));
break;
}
}
}
plan
}
pub fn clean_expired(&mut self) -> Vec<String> {
let policies: Vec<LifecyclePolicy> = self.policies.values().cloned().collect();
let expired = self.cleaner.find_expired(&policies);
let names: Vec<String> = expired.into_iter().map(|p| p.name.clone()).collect();
for name in &names {
self.policies.remove(name);
}
names
}
pub fn count(&self) -> usize {
self.policies.len()
}
pub fn get(&self, name: &str) -> Option<&LifecyclePolicy> {
self.policies.get(name)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_storage_tier_parsing() {
assert_eq!(StorageTier::parse_tier("hot"), Some(StorageTier::Hot));
assert_eq!(StorageTier::parse_tier("WARM"), Some(StorageTier::Warm));
assert_eq!(StorageTier::parse_tier("invalid"), None);
}
#[test]
fn test_lifecycle_policy_creation() {
let policy = LifecyclePolicy::new("obj1".to_string(), StorageTier::Hot);
assert_eq!(policy.tier, StorageTier::Hot);
assert_eq!(policy.access_count, 0);
assert!(!policy.is_expired());
}
#[test]
fn test_lifecycle_policy_expiry() {
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
let policy =
LifecyclePolicy::new("obj1".to_string(), StorageTier::Hot).with_expiry(now - 1);
assert!(policy.is_expired());
}
#[test]
fn test_lifecycle_policy_access() {
let mut policy = LifecyclePolicy::new("obj1".to_string(), StorageTier::Hot);
assert_eq!(policy.access_count, 0);
policy.record_access();
assert_eq!(policy.access_count, 1);
assert!(policy.last_accessed_at > 0);
}
#[test]
fn test_tiering_rule() {
let mut policy = LifecyclePolicy::new("obj1".to_string(), StorageTier::Hot);
policy.access_count = 2;
policy.last_accessed_at = 0;
let rule = TieringRule {
from_tier: StorageTier::Hot,
to_tier: StorageTier::Warm,
idle_seconds: 3600,
max_access_count: 5,
};
assert!(rule.should_tier(&policy));
}
#[test]
fn test_tiering_rule_no_match() {
let policy = LifecyclePolicy::new("obj1".to_string(), StorageTier::Cold);
let rule = TieringRule {
from_tier: StorageTier::Hot,
to_tier: StorageTier::Warm,
idle_seconds: 3600,
max_access_count: 5,
};
assert!(!rule.should_tier(&policy));
}
#[test]
fn test_expiration_cleaner() {
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
let policies = vec![
LifecyclePolicy::new("active".to_string(), StorageTier::Hot),
LifecyclePolicy::new("expired".to_string(), StorageTier::Hot).with_expiry(now - 1),
];
let cleaner = ExpirationCleaner::new(3600);
let expired = cleaner.find_expired(&policies);
assert_eq!(expired.len(), 1);
assert_eq!(expired[0].name, "expired");
}
#[test]
fn test_lifecycle_manager_add_and_access() {
let mut manager = StorageLifecycleManager::new(ExpirationCleaner::new(3600));
manager.add(LifecyclePolicy::new("obj1".to_string(), StorageTier::Hot));
assert_eq!(manager.count(), 1);
assert!(manager.access("obj1"));
assert!(!manager.access("nonexistent"));
assert_eq!(manager.get("obj1").unwrap().access_count, 1);
}
#[test]
fn test_lifecycle_manager_tiering() {
let mut manager = StorageLifecycleManager::new(ExpirationCleaner::new(3600));
let mut policy = LifecyclePolicy::new("obj1".to_string(), StorageTier::Hot);
policy.last_accessed_at = 0;
policy.access_count = 1;
manager.add(policy);
manager.add_tiering_rule(TieringRule {
from_tier: StorageTier::Hot,
to_tier: StorageTier::Warm,
idle_seconds: 3600,
max_access_count: 5,
});
let plan = manager.plan_tiering();
assert_eq!(plan.len(), 1);
assert_eq!(plan[0].0, "obj1");
assert_eq!(plan[0].1, StorageTier::Hot);
assert_eq!(plan[0].2, StorageTier::Warm);
}
#[test]
fn test_lifecycle_manager_clean_expired() {
let mut manager = StorageLifecycleManager::new(ExpirationCleaner::new(3600));
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
manager.add(LifecyclePolicy::new("active".to_string(), StorageTier::Hot));
manager.add(
LifecyclePolicy::new("expired".to_string(), StorageTier::Hot).with_expiry(now - 1),
);
let cleaned = manager.clean_expired();
assert_eq!(cleaned.len(), 1);
assert_eq!(cleaned[0], "expired");
assert_eq!(manager.count(), 1);
}
}