cachelito_core/
invalidation.rs

1//! # Cache Invalidation
2//!
3//! Smart cache invalidation mechanisms beyond simple TTL expiration.
4//!
5//! This module provides fine-grained control over cache invalidation through:
6//! - **Tag-based invalidation**: Group related entries and invalidate them together
7//! - **Event-driven invalidation**: Trigger invalidation based on events
8//! - **Dependency-based invalidation**: Cascade invalidation to dependent caches
9//! - **Conditional invalidation**: Custom check functions for invalidation logic (v0.13.0)
10//!
11//! # Examples
12//!
13//! ```rust
14//! use cachelito_core::invalidation::{InvalidationRegistry, InvalidationMetadata};
15//!
16//! // Register a cache with tags
17//! let registry = InvalidationRegistry::global();
18//! let metadata = InvalidationMetadata::new(
19//!     vec!["user_data".to_string(), "profile".to_string()],
20//!     vec![],
21//!     vec![]
22//! );
23//! registry.register("user_profile", metadata);
24//!
25//! // Invalidate all caches tagged with "user_data"
26//! registry.invalidate_by_tag("user_data");
27//! ```
28
29use parking_lot::RwLock;
30use std::collections::{HashMap, HashSet};
31use std::sync::Arc;
32
33/// Strategy for cache invalidation
34#[derive(Debug, Clone, PartialEq, Eq)]
35pub enum InvalidationStrategy {
36    /// Invalidate by tag
37    Tag(String),
38    /// Invalidate by event
39    Event(String),
40    /// Invalidate by dependency
41    Dependency(String),
42}
43
44/// Metadata about cache invalidation configuration
45#[derive(Debug, Clone)]
46pub struct InvalidationMetadata {
47    /// Tags associated with this cache
48    pub tags: Vec<String>,
49    /// Events that trigger invalidation
50    pub events: Vec<String>,
51    /// Dependencies that trigger cascade invalidation
52    pub dependencies: Vec<String>,
53}
54
55impl InvalidationMetadata {
56    /// Create new invalidation metadata
57    pub fn new(tags: Vec<String>, events: Vec<String>, dependencies: Vec<String>) -> Self {
58        Self {
59            tags,
60            events,
61            dependencies,
62        }
63    }
64
65    /// Check if metadata has any invalidation rules
66    pub fn is_empty(&self) -> bool {
67        self.tags.is_empty() && self.events.is_empty() && self.dependencies.is_empty()
68    }
69}
70
71/// Registry for managing cache invalidation
72///
73/// This struct maintains mappings between tags/events/dependencies and cache names,
74/// allowing efficient invalidation of related caches.
75pub struct InvalidationRegistry {
76    /// Map from tag to set of cache names
77    tag_to_caches: RwLock<HashMap<String, HashSet<String>>>,
78    /// Map from event to set of cache names
79    event_to_caches: RwLock<HashMap<String, HashSet<String>>>,
80    /// Map from dependency to set of dependent cache names
81    dependency_to_caches: RwLock<HashMap<String, HashSet<String>>>,
82    /// Map from cache name to its metadata
83    cache_metadata: RwLock<HashMap<String, InvalidationMetadata>>,
84    /// Callbacks for full invalidation actions (cache_name -> clear function)
85    clear_callbacks: RwLock<HashMap<String, Arc<dyn Fn() + Send + Sync>>>,
86    /// Callbacks for selective invalidation checks (cache_name -> check function)
87    /// These callbacks receive a check function and invalidate entries that match it
88    invalidation_check_callbacks:
89        RwLock<HashMap<String, Arc<dyn Fn(&dyn Fn(&str) -> bool) + Send + Sync>>>,
90}
91
92impl InvalidationRegistry {
93    /// Create a new empty invalidation registry
94    fn new() -> Self {
95        Self {
96            tag_to_caches: RwLock::new(HashMap::new()),
97            event_to_caches: RwLock::new(HashMap::new()),
98            dependency_to_caches: RwLock::new(HashMap::new()),
99            cache_metadata: RwLock::new(HashMap::new()),
100            clear_callbacks: RwLock::new(HashMap::new()),
101            invalidation_check_callbacks: RwLock::new(HashMap::new()),
102        }
103    }
104
105    /// Get the global invalidation registry
106    pub fn global() -> &'static InvalidationRegistry {
107        static INSTANCE: std::sync::OnceLock<InvalidationRegistry> = std::sync::OnceLock::new();
108        INSTANCE.get_or_init(InvalidationRegistry::new)
109    }
110
111    /// Register a cache with its invalidation metadata
112    ///
113    /// # Arguments
114    ///
115    /// * `cache_name` - Unique name of the cache
116    /// * `metadata` - Invalidation metadata (tags, events, dependencies)
117    pub fn register(&self, cache_name: &str, metadata: InvalidationMetadata) {
118        // Register tags
119        {
120            let mut tag_map = self.tag_to_caches.write();
121            for tag in &metadata.tags {
122                tag_map
123                    .entry(tag.clone())
124                    .or_insert_with(HashSet::new)
125                    .insert(cache_name.to_string());
126            }
127        }
128
129        // Register events
130        {
131            let mut event_map = self.event_to_caches.write();
132            for event in &metadata.events {
133                event_map
134                    .entry(event.clone())
135                    .or_insert_with(HashSet::new)
136                    .insert(cache_name.to_string());
137            }
138        }
139
140        // Register dependencies
141        {
142            let mut dep_map = self.dependency_to_caches.write();
143            for dep in &metadata.dependencies {
144                dep_map
145                    .entry(dep.clone())
146                    .or_insert_with(HashSet::new)
147                    .insert(cache_name.to_string());
148            }
149        }
150
151        // Store metadata
152        self.cache_metadata
153            .write()
154            .insert(cache_name.to_string(), metadata);
155    }
156
157    /// Register an invalidation callback for a cache
158    ///
159    /// This callback will be invoked when the cache needs to be invalidated.
160    ///
161    /// # Arguments
162    ///
163    /// * `cache_name` - Name of the cache
164    /// * `callback` - Function to call when invalidating
165    pub fn register_callback<F>(&self, cache_name: &str, callback: F)
166    where
167        F: Fn() + Send + Sync + 'static,
168    {
169        self.clear_callbacks
170            .write()
171            .insert(cache_name.to_string(), Arc::new(callback));
172    }
173
174    /// Register an invalidation callback for a cache
175    ///
176    /// This callback will be invoked when invalidating entries based on a check function.
177    /// The check function receives the cache key and returns true if the entry
178    /// should be invalidated.
179    ///
180    /// # Arguments
181    ///
182    /// * `cache_name` - Name of the cache
183    /// * `callback` - Function that takes a check function and invalidates matching entries
184    pub fn register_invalidation_callback<F>(&self, cache_name: &str, callback: F)
185    where
186        F: Fn(&dyn Fn(&str) -> bool) + Send + Sync + 'static,
187    {
188        self.invalidation_check_callbacks
189            .write()
190            .insert(cache_name.to_string(), Arc::new(callback));
191    }
192
193    /// Invalidate all caches associated with a tag
194    ///
195    /// # Arguments
196    ///
197    /// * `tag` - The tag to invalidate
198    ///
199    /// # Returns
200    ///
201    /// Number of caches invalidated
202    pub fn invalidate_by_tag(&self, tag: &str) -> usize {
203        let cache_names = self
204            .tag_to_caches
205            .read()
206            .get(tag)
207            .cloned()
208            .unwrap_or_default();
209
210        self.invalidate_caches(&cache_names)
211    }
212
213    /// Invalidate all caches associated with an event
214    ///
215    /// # Arguments
216    ///
217    /// * `event` - The event that occurred
218    ///
219    /// # Returns
220    ///
221    /// Number of caches invalidated
222    pub fn invalidate_by_event(&self, event: &str) -> usize {
223        let cache_names = self
224            .event_to_caches
225            .read()
226            .get(event)
227            .cloned()
228            .unwrap_or_default();
229
230        self.invalidate_caches(&cache_names)
231    }
232
233    /// Invalidate all dependent caches when a dependency changes
234    ///
235    /// # Arguments
236    ///
237    /// * `dependency` - The dependency that changed
238    ///
239    /// # Returns
240    ///
241    /// Number of caches invalidated
242    pub fn invalidate_by_dependency(&self, dependency: &str) -> usize {
243        let cache_names = self
244            .dependency_to_caches
245            .read()
246            .get(dependency)
247            .cloned()
248            .unwrap_or_default();
249
250        self.invalidate_caches(&cache_names)
251    }
252
253    /// Invalidate a specific cache by name
254    ///
255    /// # Arguments
256    ///
257    /// * `cache_name` - Name of the cache to invalidate
258    ///
259    /// # Returns
260    ///
261    /// `true` if the cache was found and invalidated
262    pub fn invalidate_cache(&self, cache_name: &str) -> bool {
263        if let Some(callback) = self.clear_callbacks.read().get(cache_name) {
264            callback();
265            true
266        } else {
267            false
268        }
269    }
270
271    /// Invalidate multiple caches
272    ///
273    /// # Arguments
274    ///
275    /// * `cache_names` - Set of cache names to invalidate
276    ///
277    /// # Returns
278    ///
279    /// Number of caches successfully invalidated
280    fn invalidate_caches(&self, cache_names: &HashSet<String>) -> usize {
281        let callbacks = self.clear_callbacks.read();
282        let mut count = 0;
283
284        for name in cache_names {
285            if let Some(callback) = callbacks.get(name) {
286                callback();
287                count += 1;
288            }
289        }
290
291        count
292    }
293
294    /// Get all caches associated with a tag
295    pub fn get_caches_by_tag(&self, tag: &str) -> Vec<String> {
296        self.tag_to_caches
297            .read()
298            .get(tag)
299            .map(|set| set.iter().cloned().collect())
300            .unwrap_or_default()
301    }
302
303    /// Get all caches associated with an event
304    pub fn get_caches_by_event(&self, event: &str) -> Vec<String> {
305        self.event_to_caches
306            .read()
307            .get(event)
308            .map(|set| set.iter().cloned().collect())
309            .unwrap_or_default()
310    }
311
312    /// Get all dependent caches
313    pub fn get_dependent_caches(&self, dependency: &str) -> Vec<String> {
314        self.dependency_to_caches
315            .read()
316            .get(dependency)
317            .map(|set| set.iter().cloned().collect())
318            .unwrap_or_default()
319    }
320
321    /// Invalidate entries in a specific cache based on a check function
322    ///
323    /// # Arguments
324    ///
325    /// * `cache_name` - Name of the cache to invalidate
326    /// * `predicate` - Check function that returns true for keys that should be invalidated
327    ///
328    /// # Returns
329    ///
330    /// `true` if the cache was found and the check function was applied
331    ///
332    /// # Examples
333    ///
334    /// ```ignore
335    /// use cachelito_core::InvalidationRegistry;
336    ///
337    /// let registry = InvalidationRegistry::global();
338    ///
339    /// // Invalidate all entries where user_id > 1000
340    /// registry.invalidate_with("get_user", |key| {
341    ///     key.parse::<u64>().unwrap_or(0) > 1000
342    /// });
343    /// ```
344    pub fn invalidate_with<F>(&self, cache_name: &str, predicate: F) -> bool
345    where
346        F: Fn(&str) -> bool,
347    {
348        if let Some(callback) = self.invalidation_check_callbacks.read().get(cache_name) {
349            callback(&predicate);
350            true
351        } else {
352            false
353        }
354    }
355
356    /// Invalidate entries across all caches based on a check function
357    ///
358    /// # Arguments
359    ///
360    /// * `predicate` - Check function that returns true for keys that should be invalidated
361    ///
362    /// # Returns
363    ///
364    /// Number of caches that had the check function applied
365    ///
366    /// # Examples
367    ///
368    /// ```ignore
369    /// use cachelito_core::InvalidationRegistry;
370    ///
371    /// let registry = InvalidationRegistry::global();
372    ///
373    /// // Invalidate all entries with numeric keys > 1000 across all caches
374    /// registry.invalidate_all_with(|_cache_name, key| {
375    ///     key.parse::<u64>().unwrap_or(0) > 1000
376    /// });
377    /// ```
378    pub fn invalidate_all_with<F>(&self, predicate: F) -> usize
379    where
380        F: Fn(&str, &str) -> bool,
381    {
382        let callbacks = self.invalidation_check_callbacks.read();
383        let mut count = 0;
384
385        for (cache_name, callback) in callbacks.iter() {
386            let cache_name_clone = cache_name.clone();
387            callback(&|key: &str| predicate(&cache_name_clone, key));
388            count += 1;
389        }
390
391        count
392    }
393
394    /// Clear all registrations
395    pub fn clear(&self) {
396        self.tag_to_caches.write().clear();
397        self.event_to_caches.write().clear();
398        self.dependency_to_caches.write().clear();
399        self.cache_metadata.write().clear();
400        self.clear_callbacks.write().clear();
401        self.invalidation_check_callbacks.write().clear();
402    }
403}
404
405impl Default for InvalidationRegistry {
406    fn default() -> Self {
407        Self::new()
408    }
409}
410
411/// Global convenience function to invalidate all caches with a given tag
412///
413/// # Arguments
414///
415/// * `tag` - The tag to match
416///
417/// # Returns
418///
419/// Number of caches invalidated
420///
421/// # Example
422///
423/// ```ignore
424/// use cachelito_core::invalidate_by_tag;
425///
426/// let count = invalidate_by_tag("user_data");
427/// println!("Invalidated {} caches", count);
428/// ```
429pub fn invalidate_by_tag(tag: &str) -> usize {
430    InvalidationRegistry::global().invalidate_by_tag(tag)
431}
432
433/// Global convenience function to invalidate all caches listening to an event
434///
435/// # Arguments
436///
437/// * `event` - The event that occurred
438///
439/// # Returns
440///
441/// Number of caches invalidated
442///
443/// # Example
444///
445/// ```ignore
446/// use cachelito_core::invalidate_by_event;
447///
448/// let count = invalidate_by_event("user_updated");
449/// println!("Invalidated {} caches", count);
450/// ```
451pub fn invalidate_by_event(event: &str) -> usize {
452    InvalidationRegistry::global().invalidate_by_event(event)
453}
454
455/// Global convenience function to invalidate all dependent caches
456///
457/// # Arguments
458///
459/// * `dependency` - The dependency that changed
460///
461/// # Returns
462///
463/// Number of caches invalidated
464///
465/// # Example
466///
467/// ```ignore
468/// use cachelito_core::invalidate_by_dependency;
469///
470/// let count = invalidate_by_dependency("get_user");
471/// println!("Invalidated {} caches", count);
472/// ```
473pub fn invalidate_by_dependency(dependency: &str) -> usize {
474    InvalidationRegistry::global().invalidate_by_dependency(dependency)
475}
476
477/// Invalidate a specific cache by its name
478///
479/// This function invalidates a single cache identified by its name.
480///
481/// # Arguments
482///
483/// * `cache_name` - The name of the cache to invalidate
484///
485/// # Returns
486///
487/// `true` if the cache was found and invalidated, `false` otherwise
488///
489/// # Examples
490///
491/// ```ignore
492/// use cachelito_core::invalidate_cache;
493///
494/// // Invalidate a specific cache:
495/// invalidate_cache("get_user_profile");
496/// ```
497pub fn invalidate_cache(cache_name: &str) -> bool {
498    InvalidationRegistry::global().invalidate_cache(cache_name)
499}
500
501/// Invalidate entries in a specific cache based on a check function
502///
503/// This function allows conditional invalidation of cache entries based on their keys.
504///
505/// # Arguments
506///
507/// * `cache_name` - Name of the cache to invalidate
508/// * `predicate` - Check function that returns true for keys that should be invalidated
509///
510/// # Returns
511///
512/// `true` if the cache was found and the check function was applied
513///
514/// # Examples
515///
516/// ```ignore
517/// use cachelito_core::invalidate_with;
518///
519/// // Invalidate all entries where user_id > 1000
520/// invalidate_with("get_user", |key| {
521///     key.parse::<u64>().unwrap_or(0) > 1000
522/// });
523/// ```
524pub fn invalidate_with<F>(cache_name: &str, predicate: F) -> bool
525where
526    F: Fn(&str) -> bool,
527{
528    InvalidationRegistry::global().invalidate_with(cache_name, predicate)
529}
530
531/// Invalidate entries across all caches based on a check function
532///
533/// This function applies a check function to all registered caches.
534///
535/// # Arguments
536///
537/// * `predicate` - Check function that receives cache name and key, returns true for entries to invalidate
538///
539/// # Returns
540///
541/// Number of caches that had the check function applied
542///
543/// # Examples
544///
545/// ```ignore
546/// use cachelito_core::invalidate_all_with;
547///
548/// // Invalidate all entries with numeric keys > 1000 across all caches
549/// invalidate_all_with(|_cache_name, key| {
550///     key.parse::<u64>().unwrap_or(0) > 1000
551/// });
552/// ```
553pub fn invalidate_all_with<F>(predicate: F) -> usize
554where
555    F: Fn(&str, &str) -> bool,
556{
557    InvalidationRegistry::global().invalidate_all_with(predicate)
558}
559
560#[cfg(test)]
561mod tests {
562    use super::*;
563    use std::sync::atomic::{AtomicUsize, Ordering};
564
565    #[test]
566    fn test_tag_based_invalidation() {
567        let registry = InvalidationRegistry::new();
568        let counter1 = Arc::new(AtomicUsize::new(0));
569        let counter2 = Arc::new(AtomicUsize::new(0));
570
571        let c1 = counter1.clone();
572        let c2 = counter2.clone();
573
574        // Register two caches with same tag
575        registry.register(
576            "cache1",
577            InvalidationMetadata::new(vec!["user_data".to_string()], vec![], vec![]),
578        );
579        registry.register(
580            "cache2",
581            InvalidationMetadata::new(vec!["user_data".to_string()], vec![], vec![]),
582        );
583
584        registry.register_callback("cache1", move || {
585            c1.fetch_add(1, Ordering::SeqCst);
586        });
587        registry.register_callback("cache2", move || {
588            c2.fetch_add(1, Ordering::SeqCst);
589        });
590
591        // Invalidate by tag
592        let count = registry.invalidate_by_tag("user_data");
593        assert_eq!(count, 2);
594        assert_eq!(counter1.load(Ordering::SeqCst), 1);
595        assert_eq!(counter2.load(Ordering::SeqCst), 1);
596    }
597
598    #[test]
599    fn test_event_based_invalidation() {
600        let registry = InvalidationRegistry::new();
601        let counter = Arc::new(AtomicUsize::new(0));
602        let c = counter.clone();
603
604        registry.register(
605            "cache1",
606            InvalidationMetadata::new(vec![], vec!["user_updated".to_string()], vec![]),
607        );
608        registry.register_callback("cache1", move || {
609            c.fetch_add(1, Ordering::SeqCst);
610        });
611
612        let count = registry.invalidate_by_event("user_updated");
613        assert_eq!(count, 1);
614        assert_eq!(counter.load(Ordering::SeqCst), 1);
615    }
616
617    #[test]
618    fn test_dependency_based_invalidation() {
619        let registry = InvalidationRegistry::new();
620        let counter = Arc::new(AtomicUsize::new(0));
621        let c = counter.clone();
622
623        registry.register(
624            "cache1",
625            InvalidationMetadata::new(vec![], vec![], vec!["get_user".to_string()]),
626        );
627        registry.register_callback("cache1", move || {
628            c.fetch_add(1, Ordering::SeqCst);
629        });
630
631        let count = registry.invalidate_by_dependency("get_user");
632        assert_eq!(count, 1);
633        assert_eq!(counter.load(Ordering::SeqCst), 1);
634    }
635
636    #[test]
637    fn test_get_caches_by_tag() {
638        let registry = InvalidationRegistry::new();
639
640        registry.register(
641            "cache1",
642            InvalidationMetadata::new(vec!["tag1".to_string()], vec![], vec![]),
643        );
644        registry.register(
645            "cache2",
646            InvalidationMetadata::new(vec!["tag1".to_string()], vec![], vec![]),
647        );
648
649        let caches = registry.get_caches_by_tag("tag1");
650        assert_eq!(caches.len(), 2);
651        assert!(caches.contains(&"cache1".to_string()));
652        assert!(caches.contains(&"cache2".to_string()));
653    }
654
655    #[test]
656    fn test_invalidate_specific_cache() {
657        let registry = InvalidationRegistry::new();
658        let counter = Arc::new(AtomicUsize::new(0));
659        let c = counter.clone();
660
661        registry.register_callback("cache1", move || {
662            c.fetch_add(1, Ordering::SeqCst);
663        });
664
665        assert!(registry.invalidate_cache("cache1"));
666        assert_eq!(counter.load(Ordering::SeqCst), 1);
667
668        // Non-existent cache
669        assert!(!registry.invalidate_cache("cache2"));
670    }
671
672    #[test]
673    fn test_clear_registry() {
674        let registry = InvalidationRegistry::new();
675        registry.register("cache1", InvalidationMetadata::new(vec![], vec![], vec![]));
676        registry.clear();
677        assert!(registry.cache_metadata.read().is_empty());
678    }
679
680    #[test]
681    fn test_conditional_invalidation() {
682        use std::sync::Mutex;
683
684        let registry = InvalidationRegistry::new();
685        let removed_keys = Arc::new(Mutex::new(Vec::new()));
686        let removed_keys_clone = removed_keys.clone();
687
688        // Register a check function callback that tracks removed keys
689        registry.register_invalidation_callback(
690            "cache1",
691            move |check_fn: &dyn Fn(&str) -> bool| {
692                let test_keys = vec!["key1", "key2", "key100", "key500", "key1001"];
693                let mut removed = removed_keys_clone.lock().unwrap();
694                removed.clear();
695
696                for key in test_keys {
697                    if check_fn(key) {
698                        removed.push(key.to_string());
699                    }
700                }
701            },
702        );
703
704        // Invalidate keys that parse to numbers > 100
705        registry.invalidate_with("cache1", |key: &str| {
706            key.strip_prefix("key")
707                .and_then(|s| s.parse::<u64>().ok())
708                .map(|n| n > 100)
709                .unwrap_or(false)
710        });
711
712        let removed = removed_keys.lock().unwrap();
713        assert_eq!(removed.len(), 2);
714        assert!(removed.contains(&"key500".to_string()));
715        assert!(removed.contains(&"key1001".to_string()));
716        assert!(!removed.contains(&"key1".to_string()));
717        assert!(!removed.contains(&"key2".to_string()));
718        assert!(!removed.contains(&"key100".to_string()));
719    }
720
721    #[test]
722    fn test_conditional_invalidation_nonexistent_cache() {
723        let registry = InvalidationRegistry::new();
724
725        // Should return false for non-registered cache
726        let result = registry.invalidate_with("nonexistent", |_key: &str| true);
727        assert!(!result);
728    }
729
730    #[test]
731    fn test_invalidate_all_with_check_function() {
732        use std::sync::Mutex;
733
734        let registry = InvalidationRegistry::new();
735
736        // Track invalidations for multiple caches
737        let cache1_removed = Arc::new(Mutex::new(Vec::new()));
738        let cache2_removed = Arc::new(Mutex::new(Vec::new()));
739
740        let cache1_removed_clone = cache1_removed.clone();
741        let cache2_removed_clone = cache2_removed.clone();
742
743        // Register check function callbacks for two caches
744        registry.register_invalidation_callback(
745            "cache1",
746            move |check_fn: &dyn Fn(&str) -> bool| {
747                let test_keys = vec!["1", "2", "3", "4", "5"];
748                let mut removed = cache1_removed_clone.lock().unwrap();
749                removed.clear();
750
751                for key in test_keys {
752                    if check_fn(key) {
753                        removed.push(key.to_string());
754                    }
755                }
756            },
757        );
758
759        registry.register_invalidation_callback(
760            "cache2",
761            move |check_fn: &dyn Fn(&str) -> bool| {
762                let test_keys = vec!["10", "20", "30"];
763                let mut removed = cache2_removed_clone.lock().unwrap();
764                removed.clear();
765
766                for key in test_keys {
767                    if check_fn(key) {
768                        removed.push(key.to_string());
769                    }
770                }
771            },
772        );
773
774        // Invalidate all entries with numeric keys >= 3 across all caches
775        let count = registry.invalidate_all_with(|_cache_name: &str, key: &str| {
776            key.parse::<u64>().unwrap_or(0) >= 3
777        });
778
779        assert_eq!(count, 2); // Two caches processed
780
781        let cache1_removed = cache1_removed.lock().unwrap();
782        assert_eq!(cache1_removed.len(), 3); // 3, 4, 5
783        assert!(cache1_removed.contains(&"3".to_string()));
784        assert!(cache1_removed.contains(&"4".to_string()));
785        assert!(cache1_removed.contains(&"5".to_string()));
786
787        let cache2_removed = cache2_removed.lock().unwrap();
788        assert_eq!(cache2_removed.len(), 3); // 10, 20, 30
789        assert!(cache2_removed.contains(&"10".to_string()));
790        assert!(cache2_removed.contains(&"20".to_string()));
791        assert!(cache2_removed.contains(&"30".to_string()));
792    }
793
794    #[test]
795    fn test_complex_conditional_checks() {
796        use std::sync::Mutex;
797
798        let registry = InvalidationRegistry::new();
799        let removed_keys = Arc::new(Mutex::new(Vec::new()));
800        let removed_keys_clone = removed_keys.clone();
801
802        registry.register_invalidation_callback(
803            "cache1",
804            move |check_fn: &dyn Fn(&str) -> bool| {
805                let test_keys = vec!["user_10", "user_20", "user_30", "user_40", "user_50"];
806                let mut removed = removed_keys_clone.lock().unwrap();
807                removed.clear();
808
809                for key in test_keys {
810                    if check_fn(key) {
811                        removed.push(key.to_string());
812                    }
813                }
814            },
815        );
816
817        // Invalidate keys where ID is divisible by 20
818        registry.invalidate_with("cache1", |key: &str| {
819            key.strip_prefix("user_")
820                .and_then(|s| s.parse::<u64>().ok())
821                .map(|n| n % 20 == 0)
822                .unwrap_or(false)
823        });
824
825        let removed = removed_keys.lock().unwrap();
826        assert_eq!(removed.len(), 2);
827        assert!(removed.contains(&"user_20".to_string()));
828        assert!(removed.contains(&"user_40".to_string()));
829        assert!(!removed.contains(&"user_10".to_string()));
830        assert!(!removed.contains(&"user_30".to_string()));
831    }
832}