use std::collections::HashMap;
use std::time::Duration;
#[derive(Debug, Clone)]
pub struct TtlConfig {
pub tombstone_ttl_hours: u32,
pub tombstone_reaping_enabled: bool,
pub days_between_reaping: u32,
pub beacon_ttl: Duration,
pub position_ttl: Duration,
pub capability_ttl: Duration,
pub evict_strategy: EvictionStrategy,
pub offline_policy: Option<OfflineRetentionPolicy>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[allow(clippy::upper_case_acronyms)]
pub enum EvictionStrategy {
OldestFirst,
StoragePressure {
threshold_pct: u8,
},
KeepLastN(usize),
None,
}
#[derive(Debug, Clone)]
pub struct OfflineRetentionPolicy {
pub online_ttl: Duration,
pub offline_ttl: Duration,
pub keep_last_n: usize,
}
impl TtlConfig {
pub fn new() -> Self {
Self {
tombstone_ttl_hours: 168, tombstone_reaping_enabled: true,
days_between_reaping: 1,
beacon_ttl: Duration::from_secs(600), position_ttl: Duration::from_secs(600), capability_ttl: Duration::from_secs(7200), evict_strategy: EvictionStrategy::None,
offline_policy: None,
}
}
pub fn tactical() -> Self {
Self {
tombstone_ttl_hours: 168, tombstone_reaping_enabled: true,
days_between_reaping: 1,
beacon_ttl: Duration::from_secs(300), position_ttl: Duration::from_secs(600), capability_ttl: Duration::from_secs(7200), evict_strategy: EvictionStrategy::OldestFirst,
offline_policy: Some(OfflineRetentionPolicy {
online_ttl: Duration::from_secs(600), offline_ttl: Duration::from_secs(60), keep_last_n: 10,
}),
}
}
pub fn long_duration() -> Self {
Self {
tombstone_ttl_hours: 168, tombstone_reaping_enabled: true,
days_between_reaping: 1,
beacon_ttl: Duration::from_secs(600), position_ttl: Duration::from_secs(3600), capability_ttl: Duration::from_secs(172800), evict_strategy: EvictionStrategy::StoragePressure { threshold_pct: 80 },
offline_policy: None, }
}
pub fn offline_node() -> Self {
Self {
tombstone_ttl_hours: 72, tombstone_reaping_enabled: true,
days_between_reaping: 1,
beacon_ttl: Duration::from_secs(30), position_ttl: Duration::from_secs(60), capability_ttl: Duration::from_secs(300), evict_strategy: EvictionStrategy::KeepLastN(10),
offline_policy: Some(OfflineRetentionPolicy {
online_ttl: Duration::from_secs(300), offline_ttl: Duration::from_secs(30), keep_last_n: 5, }),
}
}
pub fn with_beacon_ttl(mut self, ttl: Duration) -> Self {
self.beacon_ttl = ttl;
self
}
pub fn with_position_ttl(mut self, ttl: Duration) -> Self {
self.position_ttl = ttl;
self
}
pub fn with_capability_ttl(mut self, ttl: Duration) -> Self {
self.capability_ttl = ttl;
self
}
pub fn with_eviction(mut self, strategy: EvictionStrategy) -> Self {
self.evict_strategy = strategy;
self
}
pub fn with_offline_policy(mut self, policy: OfflineRetentionPolicy) -> Self {
self.offline_policy = Some(policy);
self
}
pub fn with_tombstone_ttl(mut self, hours: u32) -> Self {
self.tombstone_ttl_hours = hours;
self
}
pub fn get_collection_ttl(&self, collection: &str) -> Option<Duration> {
match collection {
"beacons" => Some(self.beacon_ttl),
"node_positions" => Some(self.position_ttl),
"capabilities" => Some(self.capability_ttl),
"hierarchical_commands" => None, "cells" => Some(Duration::from_secs(3600)), _ => None,
}
}
pub fn ditto_alter_system_commands(&self) -> Vec<String> {
vec![
format!(
"ALTER SYSTEM SET TOMBSTONE_TTL_ENABLED = {}",
self.tombstone_reaping_enabled
),
format!(
"ALTER SYSTEM SET TOMBSTONE_TTL_HOURS = {}",
self.tombstone_ttl_hours
),
format!(
"ALTER SYSTEM SET DAYS_BETWEEN_REAPING = {}",
self.days_between_reaping
),
]
}
pub fn ditto_env_vars(&self) -> HashMap<String, String> {
let mut vars = HashMap::new();
vars.insert(
"TOMBSTONE_TTL_ENABLED".to_string(),
self.tombstone_reaping_enabled.to_string(),
);
vars.insert(
"TOMBSTONE_TTL_HOURS".to_string(),
self.tombstone_ttl_hours.to_string(),
);
vars.insert(
"DAYS_BETWEEN_REAPING".to_string(),
self.days_between_reaping.to_string(),
);
vars
}
}
impl Default for TtlConfig {
fn default() -> Self {
Self::new()
}
}
impl OfflineRetentionPolicy {
pub fn minimal() -> Self {
Self {
online_ttl: Duration::from_secs(300), offline_ttl: Duration::from_secs(30), keep_last_n: 5,
}
}
pub fn conservative() -> Self {
Self {
online_ttl: Duration::from_secs(3600), offline_ttl: Duration::from_secs(300), keep_last_n: 20,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_tactical_preset() {
let config = TtlConfig::tactical();
assert_eq!(config.tombstone_ttl_hours, 168); assert_eq!(config.beacon_ttl, Duration::from_secs(300)); assert_eq!(config.position_ttl, Duration::from_secs(600)); assert!(config.offline_policy.is_some());
let offline = config.offline_policy.unwrap();
assert_eq!(offline.keep_last_n, 10);
}
#[test]
fn test_long_duration_preset() {
let config = TtlConfig::long_duration();
assert_eq!(config.beacon_ttl, Duration::from_secs(600)); assert_eq!(config.position_ttl, Duration::from_secs(3600)); assert_eq!(config.capability_ttl, Duration::from_secs(172800)); assert!(config.offline_policy.is_none());
}
#[test]
fn test_offline_node_preset() {
let config = TtlConfig::offline_node();
assert_eq!(config.tombstone_ttl_hours, 72); assert_eq!(config.beacon_ttl, Duration::from_secs(30)); assert_eq!(config.position_ttl, Duration::from_secs(60));
match config.evict_strategy {
EvictionStrategy::KeepLastN(n) => assert_eq!(n, 10),
_ => panic!("Expected KeepLastN strategy"),
}
}
#[test]
fn test_collection_ttl_lookup() {
let config = TtlConfig::tactical();
assert_eq!(
config.get_collection_ttl("beacons"),
Some(Duration::from_secs(300))
);
assert_eq!(
config.get_collection_ttl("node_positions"),
Some(Duration::from_secs(600))
);
assert_eq!(config.get_collection_ttl("hierarchical_commands"), None);
}
#[test]
fn test_builder_pattern() {
let config = TtlConfig::new()
.with_beacon_ttl(Duration::from_secs(120))
.with_eviction(EvictionStrategy::OldestFirst)
.with_tombstone_ttl(96);
assert_eq!(config.beacon_ttl, Duration::from_secs(120));
assert_eq!(config.tombstone_ttl_hours, 96);
assert!(matches!(
config.evict_strategy,
EvictionStrategy::OldestFirst
));
}
#[test]
fn test_alter_system_commands() {
let config = TtlConfig::tactical();
let commands = config.ditto_alter_system_commands();
assert_eq!(commands.len(), 3);
assert!(commands[0].contains("TOMBSTONE_TTL_ENABLED"));
assert!(commands[1].contains("TOMBSTONE_TTL_HOURS = 168"));
assert!(commands[2].contains("DAYS_BETWEEN_REAPING = 1"));
}
#[test]
fn test_env_vars() {
let config = TtlConfig::tactical();
let env_vars = config.ditto_env_vars();
assert_eq!(
env_vars.get("TOMBSTONE_TTL_HOURS"),
Some(&"168".to_string())
);
assert_eq!(
env_vars.get("TOMBSTONE_TTL_ENABLED"),
Some(&"true".to_string())
);
assert_eq!(env_vars.get("DAYS_BETWEEN_REAPING"), Some(&"1".to_string()));
}
#[test]
fn test_offline_retention_presets() {
let minimal = OfflineRetentionPolicy::minimal();
assert_eq!(minimal.offline_ttl, Duration::from_secs(30));
assert_eq!(minimal.keep_last_n, 5);
let conservative = OfflineRetentionPolicy::conservative();
assert_eq!(conservative.online_ttl, Duration::from_secs(3600));
assert_eq!(conservative.keep_last_n, 20);
}
#[test]
fn test_eviction_strategy_variants() {
let oldest = EvictionStrategy::OldestFirst;
let pressure = EvictionStrategy::StoragePressure { threshold_pct: 80 };
let keep_n = EvictionStrategy::KeepLastN(100);
let none = EvictionStrategy::None;
assert!(matches!(oldest, EvictionStrategy::OldestFirst));
assert!(matches!(
pressure,
EvictionStrategy::StoragePressure { threshold_pct: 80 }
));
assert!(matches!(keep_n, EvictionStrategy::KeepLastN(100)));
assert!(matches!(none, EvictionStrategy::None));
}
#[test]
fn test_tombstone_ttl_edge_constraint() {
let edge_config = TtlConfig::tactical();
assert!(edge_config.tombstone_ttl_hours <= 720);
let offline_config = TtlConfig::offline_node();
assert!(offline_config.tombstone_ttl_hours < edge_config.tombstone_ttl_hours);
}
}