use super::{retention::RetentionPolicy, QoSClass, QoSPolicy};
use crate::storage::ttl::TtlConfig;
use serde::{Deserialize, Serialize};
use std::time::Duration;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LifecyclePolicy {
pub qos_policy: QoSPolicy,
pub ttl_seconds: Option<u64>,
pub soft_delete_after_seconds: Option<u64>,
pub use_soft_delete: bool,
}
impl LifecyclePolicy {
pub fn for_qos_class(class: QoSClass) -> Self {
let qos_policy = match class {
QoSClass::Critical => QoSPolicy::critical(),
QoSClass::High => QoSPolicy::high(),
QoSClass::Normal => QoSPolicy::default(),
QoSClass::Low => QoSPolicy::low(),
QoSClass::Bulk => QoSPolicy::bulk(),
};
let (ttl, soft_delete_after, use_soft_delete) = match class {
QoSClass::Critical => (None, None, false), QoSClass::High => (Some(7 * 24 * 3600), Some(24 * 3600), false), QoSClass::Normal => (Some(24 * 3600), Some(3600), false), QoSClass::Low => (Some(3600), Some(300), true), QoSClass::Bulk => (Some(300), Some(60), true), };
Self {
qos_policy,
ttl_seconds: ttl,
soft_delete_after_seconds: soft_delete_after,
use_soft_delete,
}
}
pub fn from_qos_and_ttl(qos_policy: QoSPolicy, ttl_config: &TtlConfig) -> Self {
let ttl_seconds = qos_policy.ttl_seconds;
let use_soft_delete = matches!(qos_policy.base_class, QoSClass::Low | QoSClass::Bulk);
let soft_delete_after_seconds = if use_soft_delete {
Some(ttl_config.position_ttl.as_secs())
} else {
None
};
Self {
qos_policy,
ttl_seconds,
soft_delete_after_seconds,
use_soft_delete,
}
}
pub fn custom(
qos_policy: QoSPolicy,
ttl: Option<Duration>,
soft_delete_after: Option<Duration>,
use_soft_delete: bool,
) -> Self {
Self {
qos_policy,
ttl_seconds: ttl.map(|d| d.as_secs()),
soft_delete_after_seconds: soft_delete_after.map(|d| d.as_secs()),
use_soft_delete,
}
}
pub fn should_evict(&self, age_seconds: u64, storage_pressure: f32) -> bool {
if self.qos_policy.base_class == QoSClass::Critical {
return false;
}
if let Some(ttl) = self.ttl_seconds {
if age_seconds > ttl {
return true;
}
}
let eviction_threshold = match self.qos_policy.base_class {
QoSClass::Critical => f32::MAX, QoSClass::High => 0.95,
QoSClass::Normal => 0.85,
QoSClass::Low => 0.75,
QoSClass::Bulk => 0.65,
};
if storage_pressure > eviction_threshold {
let retention = RetentionPolicy::for_qos_class(self.qos_policy.base_class);
if age_seconds >= retention.min_retain_seconds {
return true;
}
}
false
}
pub fn should_soft_delete(&self, age_seconds: u64) -> bool {
if !self.use_soft_delete {
return false;
}
if let Some(threshold) = self.soft_delete_after_seconds {
return age_seconds > threshold;
}
false
}
pub fn should_retain(&self, age_seconds: u64, storage_pressure: f32) -> bool {
!self.should_evict(age_seconds, storage_pressure)
}
pub fn has_infinite_retention(&self) -> bool {
self.qos_policy.base_class == QoSClass::Critical || self.ttl_seconds.is_none()
}
pub fn ttl_duration(&self) -> Option<Duration> {
self.ttl_seconds.map(Duration::from_secs)
}
pub fn soft_delete_duration(&self) -> Option<Duration> {
self.soft_delete_after_seconds.map(Duration::from_secs)
}
pub fn qos_class(&self) -> QoSClass {
self.qos_policy.base_class
}
}
#[derive(Debug, Clone)]
pub struct LifecyclePolicies {
pub contact_report: LifecyclePolicy,
pub media: LifecyclePolicy,
pub health_status: LifecyclePolicy,
pub position: LifecyclePolicy,
pub bulk: LifecyclePolicy,
}
impl LifecyclePolicies {
pub fn default_tactical() -> Self {
Self {
contact_report: LifecyclePolicy::for_qos_class(QoSClass::Critical),
media: LifecyclePolicy::for_qos_class(QoSClass::High),
health_status: LifecyclePolicy::for_qos_class(QoSClass::Normal),
position: LifecyclePolicy::for_qos_class(QoSClass::Low),
bulk: LifecyclePolicy::for_qos_class(QoSClass::Bulk),
}
}
pub fn from_ttl_config(ttl_config: &TtlConfig) -> Self {
Self {
contact_report: LifecyclePolicy::custom(
QoSPolicy::critical(),
None, None,
false,
),
media: LifecyclePolicy::custom(
QoSPolicy::high(),
Some(Duration::from_secs(7 * 24 * 3600)),
Some(Duration::from_secs(24 * 3600)),
false,
),
health_status: LifecyclePolicy::custom(
QoSPolicy::default(),
Some(Duration::from_secs(24 * 3600)),
Some(Duration::from_secs(3600)),
false,
),
position: LifecyclePolicy::custom(
QoSPolicy::low(),
Some(ttl_config.position_ttl),
Some(Duration::from_secs(ttl_config.position_ttl.as_secs() / 2)),
true, ),
bulk: LifecyclePolicy::custom(
QoSPolicy::bulk(),
Some(Duration::from_secs(300)),
Some(Duration::from_secs(60)),
true, ),
}
}
pub fn get(&self, class: QoSClass) -> &LifecyclePolicy {
match class {
QoSClass::Critical => &self.contact_report,
QoSClass::High => &self.media,
QoSClass::Normal => &self.health_status,
QoSClass::Low => &self.position,
QoSClass::Bulk => &self.bulk,
}
}
}
impl Default for LifecyclePolicies {
fn default() -> Self {
Self::default_tactical()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LifecycleDecision {
Retain,
SoftDelete,
Evict,
TtlExpired,
}
impl LifecycleDecision {
pub fn removes_data(&self) -> bool {
matches!(self, Self::Evict | Self::TtlExpired)
}
pub fn retains_data(&self) -> bool {
matches!(self, Self::Retain)
}
}
impl std::fmt::Display for LifecycleDecision {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Retain => write!(f, "RETAIN"),
Self::SoftDelete => write!(f, "SOFT_DELETE"),
Self::Evict => write!(f, "EVICT"),
Self::TtlExpired => write!(f, "TTL_EXPIRED"),
}
}
}
pub fn make_lifecycle_decision(
policy: &LifecyclePolicy,
age_seconds: u64,
storage_pressure: f32,
) -> LifecycleDecision {
if let Some(ttl) = policy.ttl_seconds {
if age_seconds > ttl && policy.qos_policy.base_class != QoSClass::Critical {
return LifecycleDecision::TtlExpired;
}
}
if policy.should_soft_delete(age_seconds) {
return LifecycleDecision::SoftDelete;
}
if policy.should_evict(age_seconds, storage_pressure) {
return LifecycleDecision::Evict;
}
LifecycleDecision::Retain
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_lifecycle_policy_critical() {
let policy = LifecyclePolicy::for_qos_class(QoSClass::Critical);
assert!(!policy.should_evict(1_000_000, 0.99));
assert!(!policy.should_evict(10_000_000, 1.0));
assert!(policy.has_infinite_retention());
assert!(!policy.use_soft_delete);
}
#[test]
fn test_lifecycle_policy_bulk() {
let policy = LifecyclePolicy::for_qos_class(QoSClass::Bulk);
assert!(policy.use_soft_delete);
assert!(policy.soft_delete_after_seconds.is_some());
assert!(policy.should_evict(400, 0.70)); assert!(!policy.has_infinite_retention());
}
#[test]
fn test_ttl_based_eviction() {
let policy = LifecyclePolicy::custom(
QoSPolicy::default(),
Some(Duration::from_secs(3600)), None,
false,
);
assert!(!policy.should_evict(1800, 0.50));
assert!(policy.should_evict(4000, 0.50)); }
#[test]
fn test_pressure_based_eviction() {
let policy = LifecyclePolicy::for_qos_class(QoSClass::Normal);
assert!(!policy.should_evict(7200, 0.80));
assert!(policy.should_evict(7200, 0.90)); }
#[test]
fn test_soft_delete_decision() {
let policy = LifecyclePolicy::for_qos_class(QoSClass::Low);
assert!(!policy.should_soft_delete(200));
assert!(policy.should_soft_delete(400));
}
#[test]
fn test_make_lifecycle_decision() {
let critical = LifecyclePolicy::for_qos_class(QoSClass::Critical);
assert_eq!(
make_lifecycle_decision(&critical, 1_000_000, 0.99),
LifecycleDecision::Retain
);
let bulk = LifecyclePolicy::for_qos_class(QoSClass::Bulk);
assert_eq!(
make_lifecycle_decision(&bulk, 400, 0.50),
LifecycleDecision::TtlExpired
);
let low = LifecyclePolicy::for_qos_class(QoSClass::Low);
assert_eq!(
make_lifecycle_decision(&low, 400, 0.50),
LifecycleDecision::SoftDelete
);
}
#[test]
fn test_lifecycle_decision_helpers() {
assert!(LifecycleDecision::Evict.removes_data());
assert!(LifecycleDecision::TtlExpired.removes_data());
assert!(!LifecycleDecision::Retain.removes_data());
assert!(!LifecycleDecision::SoftDelete.removes_data());
assert!(LifecycleDecision::Retain.retains_data());
assert!(!LifecycleDecision::Evict.retains_data());
}
#[test]
fn test_lifecycle_policies_default() {
let policies = LifecyclePolicies::default_tactical();
assert_eq!(
policies.get(QoSClass::Critical).qos_class(),
QoSClass::Critical
);
assert_eq!(policies.get(QoSClass::High).qos_class(), QoSClass::High);
assert_eq!(policies.get(QoSClass::Normal).qos_class(), QoSClass::Normal);
assert_eq!(policies.get(QoSClass::Low).qos_class(), QoSClass::Low);
assert_eq!(policies.get(QoSClass::Bulk).qos_class(), QoSClass::Bulk);
}
#[test]
fn test_lifecycle_policies_from_ttl_config() {
let ttl_config = TtlConfig::tactical();
let policies = LifecyclePolicies::from_ttl_config(&ttl_config);
assert!(policies.position.use_soft_delete);
assert_eq!(
policies.position.ttl_seconds,
Some(ttl_config.position_ttl.as_secs())
);
}
#[test]
fn test_duration_getters() {
let policy = LifecyclePolicy::for_qos_class(QoSClass::Normal);
assert!(policy.ttl_duration().is_some());
assert_eq!(policy.ttl_duration(), Some(Duration::from_secs(24 * 3600)));
let critical = LifecyclePolicy::for_qos_class(QoSClass::Critical);
assert!(critical.ttl_duration().is_none());
}
#[test]
fn test_from_qos_and_ttl() {
let qos_policy = QoSPolicy::low();
let ttl_config = TtlConfig::tactical();
let lifecycle = LifecyclePolicy::from_qos_and_ttl(qos_policy.clone(), &ttl_config);
assert_eq!(lifecycle.qos_class(), QoSClass::Low);
assert!(lifecycle.use_soft_delete);
assert_eq!(
lifecycle.soft_delete_after_seconds,
Some(ttl_config.position_ttl.as_secs())
);
}
#[test]
fn test_decision_display() {
assert_eq!(LifecycleDecision::Retain.to_string(), "RETAIN");
assert_eq!(LifecycleDecision::SoftDelete.to_string(), "SOFT_DELETE");
assert_eq!(LifecycleDecision::Evict.to_string(), "EVICT");
assert_eq!(LifecycleDecision::TtlExpired.to_string(), "TTL_EXPIRED");
}
#[test]
fn test_eviction_order_by_pressure() {
let high = LifecyclePolicy::for_qos_class(QoSClass::High);
let normal = LifecyclePolicy::for_qos_class(QoSClass::Normal);
let low = LifecyclePolicy::for_qos_class(QoSClass::Low);
let bulk = LifecyclePolicy::for_qos_class(QoSClass::Bulk);
let age_high = 100_000; let age_normal = 7200; let age_low = 600; let age_bulk = 120;
assert!(!high.should_evict(age_high, 0.80));
assert!(!normal.should_evict(age_normal, 0.80));
assert!(low.should_evict(age_low, 0.80));
assert!(bulk.should_evict(age_bulk, 0.80));
assert!(!high.should_evict(age_high, 0.90));
assert!(normal.should_evict(age_normal, 0.90));
assert!(low.should_evict(age_low, 0.90));
assert!(bulk.should_evict(age_bulk, 0.90));
assert!(high.should_evict(age_high, 0.98));
assert!(normal.should_evict(age_normal, 0.98));
assert!(low.should_evict(age_low, 0.98));
assert!(bulk.should_evict(age_bulk, 0.98));
}
}