use crate::config::{CacheStrategy, LegacyEvictionPolicy as EvictionPolicy, OxcacheConfig};
use dashmap::DashMap;
use std::sync::Arc;
use tokio::sync::broadcast;
#[derive(Debug, Clone)]
pub struct ConfigChangeEvent {
pub service_name: String,
pub old_strategy: Option<CacheStrategy>,
pub new_strategy: CacheStrategy,
pub timestamp: chrono::DateTime<chrono::Utc>,
}
#[derive(Debug, Clone)]
pub struct ConfigDynamicManager {
strategies: Arc<DashMap<String, CacheStrategy>>,
event_sender: broadcast::Sender<ConfigChangeEvent>,
}
impl Default for ConfigDynamicManager {
fn default() -> Self {
Self::new()
}
}
impl ConfigDynamicManager {
pub fn new() -> Self {
let (sender, _) = broadcast::channel(100);
Self {
strategies: Arc::new(DashMap::new()),
event_sender: sender,
}
}
pub fn update_strategy(&self, strategy: CacheStrategy) {
if let Err(e) = self.validate_strategy(&strategy) {
tracing::error!(
"Invalid strategy for service '{}': {}",
strategy.service_name,
e
);
return;
}
let old_strategy = self
.strategies
.get(&strategy.service_name)
.map(|s| s.clone());
let service_name = strategy.service_name.clone();
self.strategies
.insert(service_name.clone(), strategy.clone());
let _ = self.event_sender.send(ConfigChangeEvent {
service_name,
old_strategy,
new_strategy: strategy,
timestamp: chrono::Utc::now(),
});
}
pub fn get_strategy(&self, service_name: &str) -> Option<CacheStrategy> {
self.strategies.get(service_name).map(|s| s.clone())
}
pub fn has_strategy(&self, service_name: &str) -> bool {
self.strategies.contains_key(service_name)
}
pub fn remove_strategy(&self, service_name: &str) -> bool {
self.strategies.remove(service_name).is_some()
}
pub fn switch_eviction_policy(&self, service_name: &str, policy: EvictionPolicy) {
if let Some(mut entry) = self.strategies.get_mut(service_name) {
entry.l1_eviction_policy = policy;
entry.updated_at = chrono::Utc::now();
}
}
pub fn switch_cache_mode(&self, service_name: &str, _promote_on_hit: bool) {
if let Some(mut entry) = self.strategies.get_mut(service_name) {
entry.updated_at = chrono::Utc::now();
}
}
pub fn subscribe(&self) -> broadcast::Receiver<ConfigChangeEvent> {
self.event_sender.subscribe()
}
fn validate_strategy(&self, strategy: &CacheStrategy) -> Result<(), String> {
if strategy.service_name.is_empty() {
return Err("Service name cannot be empty".to_string());
}
if strategy.service_name.len() > 64 {
return Err("Service name exceeds maximum length of 64 characters".to_string());
}
if strategy.ttl == 0 {
return Err("TTL cannot be zero".to_string());
}
if strategy.ttl > 86400 * 30 {
return Err("TTL cannot exceed 30 days".to_string());
}
Ok(())
}
pub fn clear(&self) {
self.strategies.clear();
}
}
pub static GLOBAL_CONFIG_MANAGER: once_cell::sync::Lazy<ConfigDynamicManager> =
once_cell::sync::Lazy::new(ConfigDynamicManager::new);
#[cfg(feature = "config-dynamic")]
pub async fn hot_reload_config(config_path: &str) -> Result<(), String> {
use crate::config::confers_macro::confers_load;
let config = confers_load(config_path).map_err(|e| e.to_string())?;
apply_runtime_config(&config)?;
Ok(())
}
#[cfg(feature = "config-dynamic")]
fn apply_runtime_config(config: &OxcacheConfig) -> Result<(), String> {
for (name, service) in &config.services {
let mut strategy = CacheStrategy::new(name);
if let Some(ttl) = service.ttl {
strategy = strategy.with_ttl(ttl);
}
GLOBAL_CONFIG_MANAGER.update_strategy(strategy);
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_config_dynamic_manager() {
let manager = ConfigDynamicManager::new();
assert!(!manager.has_strategy("test"));
let strategy = CacheStrategy::new("test").with_ttl(600);
manager.update_strategy(strategy.clone());
assert!(manager.has_strategy("test"));
assert_eq!(manager.get_strategy("test"), Some(strategy.clone()));
assert!(manager.remove_strategy("test"));
assert!(!manager.has_strategy("test"));
}
#[test]
fn test_config_dynamic_manager_switch_policy() {
let manager = ConfigDynamicManager::new();
let strategy = CacheStrategy::new("test");
manager.update_strategy(strategy);
manager.switch_eviction_policy("test", EvictionPolicy::Lru);
let updated = manager.get_strategy("test").unwrap();
assert_eq!(updated.l1_eviction_policy, EvictionPolicy::Lru);
}
#[tokio::test]
async fn test_config_change_event() {
let manager = ConfigDynamicManager::new();
let mut receiver = manager.subscribe();
let strategy = CacheStrategy::new("test");
manager.update_strategy(strategy.clone());
let event = receiver.recv().await.unwrap();
assert_eq!(event.service_name, "test");
assert_eq!(event.new_strategy, strategy);
assert!(event.old_strategy.is_none());
}
#[test]
fn test_global_config_manager() {
let manager = &GLOBAL_CONFIG_MANAGER;
assert!(!manager.has_strategy("global_test"));
let strategy = CacheStrategy::new("global_test");
manager.update_strategy(strategy.clone());
assert!(manager.has_strategy("global_test"));
assert_eq!(manager.get_strategy("global_test"), Some(strategy));
manager.remove_strategy("global_test");
}
}