use parking_lot::RwLock;
use std::collections::{HashMap, HashSet};
use std::sync::Arc;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum InvalidationStrategy {
Tag(String),
Event(String),
Dependency(String),
}
#[derive(Debug, Clone)]
pub struct InvalidationMetadata {
pub tags: Vec<String>,
pub events: Vec<String>,
pub dependencies: Vec<String>,
}
impl InvalidationMetadata {
pub fn new(tags: Vec<String>, events: Vec<String>, dependencies: Vec<String>) -> Self {
Self {
tags,
events,
dependencies,
}
}
pub fn is_empty(&self) -> bool {
self.tags.is_empty() && self.events.is_empty() && self.dependencies.is_empty()
}
}
pub struct InvalidationRegistry {
tag_to_caches: RwLock<HashMap<String, HashSet<String>>>,
event_to_caches: RwLock<HashMap<String, HashSet<String>>>,
dependency_to_caches: RwLock<HashMap<String, HashSet<String>>>,
cache_metadata: RwLock<HashMap<String, InvalidationMetadata>>,
clear_callbacks: RwLock<HashMap<String, Arc<dyn Fn() + Send + Sync>>>,
invalidation_check_callbacks:
RwLock<HashMap<String, Arc<dyn Fn(&dyn Fn(&str) -> bool) + Send + Sync>>>,
}
impl InvalidationRegistry {
fn new() -> Self {
Self {
tag_to_caches: RwLock::new(HashMap::new()),
event_to_caches: RwLock::new(HashMap::new()),
dependency_to_caches: RwLock::new(HashMap::new()),
cache_metadata: RwLock::new(HashMap::new()),
clear_callbacks: RwLock::new(HashMap::new()),
invalidation_check_callbacks: RwLock::new(HashMap::new()),
}
}
pub fn global() -> &'static InvalidationRegistry {
static INSTANCE: std::sync::OnceLock<InvalidationRegistry> = std::sync::OnceLock::new();
INSTANCE.get_or_init(InvalidationRegistry::new)
}
pub fn register(&self, cache_name: &str, metadata: InvalidationMetadata) {
{
let mut tag_map = self.tag_to_caches.write();
for tag in &metadata.tags {
tag_map
.entry(tag.clone())
.or_insert_with(HashSet::new)
.insert(cache_name.to_string());
}
}
{
let mut event_map = self.event_to_caches.write();
for event in &metadata.events {
event_map
.entry(event.clone())
.or_insert_with(HashSet::new)
.insert(cache_name.to_string());
}
}
{
let mut dep_map = self.dependency_to_caches.write();
for dep in &metadata.dependencies {
dep_map
.entry(dep.clone())
.or_insert_with(HashSet::new)
.insert(cache_name.to_string());
}
}
self.cache_metadata
.write()
.insert(cache_name.to_string(), metadata);
}
pub fn register_callback<F>(&self, cache_name: &str, callback: F)
where
F: Fn() + Send + Sync + 'static,
{
self.clear_callbacks
.write()
.insert(cache_name.to_string(), Arc::new(callback));
}
pub fn register_invalidation_callback<F>(&self, cache_name: &str, callback: F)
where
F: Fn(&dyn Fn(&str) -> bool) + Send + Sync + 'static,
{
self.invalidation_check_callbacks
.write()
.insert(cache_name.to_string(), Arc::new(callback));
}
pub fn invalidate_by_tag(&self, tag: &str) -> usize {
let cache_names = self
.tag_to_caches
.read()
.get(tag)
.cloned()
.unwrap_or_default();
self.invalidate_caches(&cache_names)
}
pub fn invalidate_by_event(&self, event: &str) -> usize {
let cache_names = self
.event_to_caches
.read()
.get(event)
.cloned()
.unwrap_or_default();
self.invalidate_caches(&cache_names)
}
pub fn invalidate_by_dependency(&self, dependency: &str) -> usize {
let cache_names = self
.dependency_to_caches
.read()
.get(dependency)
.cloned()
.unwrap_or_default();
self.invalidate_caches(&cache_names)
}
pub fn invalidate_cache(&self, cache_name: &str) -> bool {
if let Some(callback) = self.clear_callbacks.read().get(cache_name) {
callback();
true
} else {
false
}
}
fn invalidate_caches(&self, cache_names: &HashSet<String>) -> usize {
let callbacks = self.clear_callbacks.read();
let mut count = 0;
for name in cache_names {
if let Some(callback) = callbacks.get(name) {
callback();
count += 1;
}
}
count
}
pub fn get_caches_by_tag(&self, tag: &str) -> Vec<String> {
self.tag_to_caches
.read()
.get(tag)
.map(|set| set.iter().cloned().collect())
.unwrap_or_default()
}
pub fn get_caches_by_event(&self, event: &str) -> Vec<String> {
self.event_to_caches
.read()
.get(event)
.map(|set| set.iter().cloned().collect())
.unwrap_or_default()
}
pub fn get_dependent_caches(&self, dependency: &str) -> Vec<String> {
self.dependency_to_caches
.read()
.get(dependency)
.map(|set| set.iter().cloned().collect())
.unwrap_or_default()
}
pub fn invalidate_with<F>(&self, cache_name: &str, predicate: F) -> bool
where
F: Fn(&str) -> bool,
{
if let Some(callback) = self.invalidation_check_callbacks.read().get(cache_name) {
callback(&predicate);
true
} else {
false
}
}
pub fn invalidate_all_with<F>(&self, predicate: F) -> usize
where
F: Fn(&str, &str) -> bool,
{
let callbacks = self.invalidation_check_callbacks.read();
let mut count = 0;
for (cache_name, callback) in callbacks.iter() {
let cache_name_clone = cache_name.clone();
callback(&|key: &str| predicate(&cache_name_clone, key));
count += 1;
}
count
}
pub fn clear(&self) {
self.tag_to_caches.write().clear();
self.event_to_caches.write().clear();
self.dependency_to_caches.write().clear();
self.cache_metadata.write().clear();
self.clear_callbacks.write().clear();
self.invalidation_check_callbacks.write().clear();
}
}
impl Default for InvalidationRegistry {
fn default() -> Self {
Self::new()
}
}
pub fn invalidate_by_tag(tag: &str) -> usize {
InvalidationRegistry::global().invalidate_by_tag(tag)
}
pub fn invalidate_by_event(event: &str) -> usize {
InvalidationRegistry::global().invalidate_by_event(event)
}
pub fn invalidate_by_dependency(dependency: &str) -> usize {
InvalidationRegistry::global().invalidate_by_dependency(dependency)
}
pub fn invalidate_cache(cache_name: &str) -> bool {
InvalidationRegistry::global().invalidate_cache(cache_name)
}
pub fn invalidate_with<F>(cache_name: &str, predicate: F) -> bool
where
F: Fn(&str) -> bool,
{
InvalidationRegistry::global().invalidate_with(cache_name, predicate)
}
pub fn invalidate_all_with<F>(predicate: F) -> usize
where
F: Fn(&str, &str) -> bool,
{
InvalidationRegistry::global().invalidate_all_with(predicate)
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::atomic::{AtomicUsize, Ordering};
#[test]
fn test_tag_based_invalidation() {
let registry = InvalidationRegistry::new();
let counter1 = Arc::new(AtomicUsize::new(0));
let counter2 = Arc::new(AtomicUsize::new(0));
let c1 = counter1.clone();
let c2 = counter2.clone();
registry.register(
"cache1",
InvalidationMetadata::new(vec!["user_data".to_string()], vec![], vec![]),
);
registry.register(
"cache2",
InvalidationMetadata::new(vec!["user_data".to_string()], vec![], vec![]),
);
registry.register_callback("cache1", move || {
c1.fetch_add(1, Ordering::SeqCst);
});
registry.register_callback("cache2", move || {
c2.fetch_add(1, Ordering::SeqCst);
});
let count = registry.invalidate_by_tag("user_data");
assert_eq!(count, 2);
assert_eq!(counter1.load(Ordering::SeqCst), 1);
assert_eq!(counter2.load(Ordering::SeqCst), 1);
}
#[test]
fn test_event_based_invalidation() {
let registry = InvalidationRegistry::new();
let counter = Arc::new(AtomicUsize::new(0));
let c = counter.clone();
registry.register(
"cache1",
InvalidationMetadata::new(vec![], vec!["user_updated".to_string()], vec![]),
);
registry.register_callback("cache1", move || {
c.fetch_add(1, Ordering::SeqCst);
});
let count = registry.invalidate_by_event("user_updated");
assert_eq!(count, 1);
assert_eq!(counter.load(Ordering::SeqCst), 1);
}
#[test]
fn test_dependency_based_invalidation() {
let registry = InvalidationRegistry::new();
let counter = Arc::new(AtomicUsize::new(0));
let c = counter.clone();
registry.register(
"cache1",
InvalidationMetadata::new(vec![], vec![], vec!["get_user".to_string()]),
);
registry.register_callback("cache1", move || {
c.fetch_add(1, Ordering::SeqCst);
});
let count = registry.invalidate_by_dependency("get_user");
assert_eq!(count, 1);
assert_eq!(counter.load(Ordering::SeqCst), 1);
}
#[test]
fn test_get_caches_by_tag() {
let registry = InvalidationRegistry::new();
registry.register(
"cache1",
InvalidationMetadata::new(vec!["tag1".to_string()], vec![], vec![]),
);
registry.register(
"cache2",
InvalidationMetadata::new(vec!["tag1".to_string()], vec![], vec![]),
);
let caches = registry.get_caches_by_tag("tag1");
assert_eq!(caches.len(), 2);
assert!(caches.contains(&"cache1".to_string()));
assert!(caches.contains(&"cache2".to_string()));
}
#[test]
fn test_invalidate_specific_cache() {
let registry = InvalidationRegistry::new();
let counter = Arc::new(AtomicUsize::new(0));
let c = counter.clone();
registry.register_callback("cache1", move || {
c.fetch_add(1, Ordering::SeqCst);
});
assert!(registry.invalidate_cache("cache1"));
assert_eq!(counter.load(Ordering::SeqCst), 1);
assert!(!registry.invalidate_cache("cache2"));
}
#[test]
fn test_clear_registry() {
let registry = InvalidationRegistry::new();
registry.register("cache1", InvalidationMetadata::new(vec![], vec![], vec![]));
registry.clear();
assert!(registry.cache_metadata.read().is_empty());
}
#[test]
fn test_conditional_invalidation() {
use std::sync::Mutex;
let registry = InvalidationRegistry::new();
let removed_keys = Arc::new(Mutex::new(Vec::new()));
let removed_keys_clone = removed_keys.clone();
registry.register_invalidation_callback(
"cache1",
move |check_fn: &dyn Fn(&str) -> bool| {
let test_keys = vec!["key1", "key2", "key100", "key500", "key1001"];
let mut removed = removed_keys_clone.lock().unwrap();
removed.clear();
for key in test_keys {
if check_fn(key) {
removed.push(key.to_string());
}
}
},
);
registry.invalidate_with("cache1", |key: &str| {
key.strip_prefix("key")
.and_then(|s| s.parse::<u64>().ok())
.map(|n| n > 100)
.unwrap_or(false)
});
let removed = removed_keys.lock().unwrap();
assert_eq!(removed.len(), 2);
assert!(removed.contains(&"key500".to_string()));
assert!(removed.contains(&"key1001".to_string()));
assert!(!removed.contains(&"key1".to_string()));
assert!(!removed.contains(&"key2".to_string()));
assert!(!removed.contains(&"key100".to_string()));
}
#[test]
fn test_conditional_invalidation_nonexistent_cache() {
let registry = InvalidationRegistry::new();
let result = registry.invalidate_with("nonexistent", |_key: &str| true);
assert!(!result);
}
#[test]
fn test_invalidate_all_with_check_function() {
use std::sync::Mutex;
let registry = InvalidationRegistry::new();
let cache1_removed = Arc::new(Mutex::new(Vec::new()));
let cache2_removed = Arc::new(Mutex::new(Vec::new()));
let cache1_removed_clone = cache1_removed.clone();
let cache2_removed_clone = cache2_removed.clone();
registry.register_invalidation_callback(
"cache1",
move |check_fn: &dyn Fn(&str) -> bool| {
let test_keys = vec!["1", "2", "3", "4", "5"];
let mut removed = cache1_removed_clone.lock().unwrap();
removed.clear();
for key in test_keys {
if check_fn(key) {
removed.push(key.to_string());
}
}
},
);
registry.register_invalidation_callback(
"cache2",
move |check_fn: &dyn Fn(&str) -> bool| {
let test_keys = vec!["10", "20", "30"];
let mut removed = cache2_removed_clone.lock().unwrap();
removed.clear();
for key in test_keys {
if check_fn(key) {
removed.push(key.to_string());
}
}
},
);
let count = registry.invalidate_all_with(|_cache_name: &str, key: &str| {
key.parse::<u64>().unwrap_or(0) >= 3
});
assert_eq!(count, 2);
let cache1_removed = cache1_removed.lock().unwrap();
assert_eq!(cache1_removed.len(), 3); assert!(cache1_removed.contains(&"3".to_string()));
assert!(cache1_removed.contains(&"4".to_string()));
assert!(cache1_removed.contains(&"5".to_string()));
let cache2_removed = cache2_removed.lock().unwrap();
assert_eq!(cache2_removed.len(), 3); assert!(cache2_removed.contains(&"10".to_string()));
assert!(cache2_removed.contains(&"20".to_string()));
assert!(cache2_removed.contains(&"30".to_string()));
}
#[test]
fn test_complex_conditional_checks() {
use std::sync::Mutex;
let registry = InvalidationRegistry::new();
let removed_keys = Arc::new(Mutex::new(Vec::new()));
let removed_keys_clone = removed_keys.clone();
registry.register_invalidation_callback(
"cache1",
move |check_fn: &dyn Fn(&str) -> bool| {
let test_keys = vec!["user_10", "user_20", "user_30", "user_40", "user_50"];
let mut removed = removed_keys_clone.lock().unwrap();
removed.clear();
for key in test_keys {
if check_fn(key) {
removed.push(key.to_string());
}
}
},
);
registry.invalidate_with("cache1", |key: &str| {
key.strip_prefix("user_")
.and_then(|s| s.parse::<u64>().ok())
.map(|n| n % 20 == 0)
.unwrap_or(false)
});
let removed = removed_keys.lock().unwrap();
assert_eq!(removed.len(), 2);
assert!(removed.contains(&"user_20".to_string()));
assert!(removed.contains(&"user_40".to_string()));
assert!(!removed.contains(&"user_10".to_string()));
assert!(!removed.contains(&"user_30".to_string()));
}
}