Skip to main content

things3_core/
cache_invalidation_middleware.rs

1//! Cache invalidation middleware for data consistency
2
3use anyhow::Result;
4use chrono::{DateTime, Utc};
5use parking_lot::RwLock;
6use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8use std::sync::Arc;
9use std::time::Duration;
10use tracing::{debug, warn};
11use uuid::Uuid;
12
13/// Cache invalidation event
14#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct InvalidationEvent {
16    pub event_id: Uuid,
17    pub event_type: InvalidationEventType,
18    pub entity_type: String,
19    pub entity_id: Option<Uuid>,
20    pub operation: String,
21    pub timestamp: DateTime<Utc>,
22    pub affected_caches: Vec<String>,
23    pub metadata: HashMap<String, serde_json::Value>,
24}
25
26/// Types of invalidation events
27#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
28pub enum InvalidationEventType {
29    /// Entity was created
30    Created,
31    /// Entity was updated
32    Updated,
33    /// Entity was deleted
34    Deleted,
35    /// Entity was completed
36    Completed,
37    /// Bulk operation occurred
38    BulkOperation,
39    /// Cache was manually invalidated
40    ManualInvalidation,
41    /// Cache expired
42    Expired,
43    /// Cascade invalidation
44    CascadeInvalidation,
45}
46
47impl std::fmt::Display for InvalidationEventType {
48    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
49        match self {
50            InvalidationEventType::Created => write!(f, "Created"),
51            InvalidationEventType::Updated => write!(f, "Updated"),
52            InvalidationEventType::Deleted => write!(f, "Deleted"),
53            InvalidationEventType::Completed => write!(f, "Completed"),
54            InvalidationEventType::BulkOperation => write!(f, "BulkOperation"),
55            InvalidationEventType::ManualInvalidation => write!(f, "ManualInvalidation"),
56            InvalidationEventType::Expired => write!(f, "Expired"),
57            InvalidationEventType::CascadeInvalidation => write!(f, "CascadeInvalidation"),
58        }
59    }
60}
61
62/// Cache invalidation rule
63#[derive(Debug, Clone, Serialize, Deserialize)]
64pub struct InvalidationRule {
65    pub rule_id: Uuid,
66    pub name: String,
67    pub description: String,
68    pub entity_type: String,
69    pub operations: Vec<String>,
70    pub affected_cache_types: Vec<String>,
71    pub invalidation_strategy: InvalidationStrategy,
72    pub enabled: bool,
73    pub created_at: DateTime<Utc>,
74    pub updated_at: DateTime<Utc>,
75}
76
77/// Invalidation strategies
78#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
79pub enum InvalidationStrategy {
80    /// Invalidate all caches
81    InvalidateAll,
82    /// Invalidate specific cache types
83    InvalidateSpecific(Vec<String>),
84    /// Invalidate by entity ID
85    InvalidateByEntity,
86    /// Invalidate by pattern
87    InvalidateByPattern(String),
88    /// Cascade invalidation (invalidate dependent entities)
89    CascadeInvalidation,
90}
91
92/// Cache invalidation middleware
93pub struct CacheInvalidationMiddleware {
94    /// Invalidation rules
95    rules: Arc<RwLock<HashMap<String, InvalidationRule>>>,
96    /// Event history
97    events: Arc<RwLock<Vec<InvalidationEvent>>>,
98    /// Cache invalidation handlers
99    handlers: Arc<RwLock<HashMap<String, Box<dyn CacheInvalidationHandler + Send + Sync>>>>,
100    /// Configuration
101    config: InvalidationConfig,
102    /// Statistics
103    stats: Arc<RwLock<InvalidationStats>>,
104}
105
106/// Cache invalidation handler trait
107pub trait CacheInvalidationHandler {
108    /// Handle cache invalidation
109    ///
110    /// # Errors
111    ///
112    /// This function will return an error if the invalidation fails
113    fn invalidate(&self, event: &InvalidationEvent) -> Result<()>;
114
115    /// Get cache type name
116    fn cache_type(&self) -> &str;
117
118    /// Check if this handler can handle the event
119    fn can_handle(&self, event: &InvalidationEvent) -> bool;
120}
121
122/// Invalidation configuration
123#[derive(Debug, Clone, Serialize, Deserialize)]
124pub struct InvalidationConfig {
125    /// Maximum number of events to keep in history
126    pub max_events: usize,
127    /// Event retention duration
128    pub event_retention: Duration,
129    /// Enable cascade invalidation
130    pub enable_cascade: bool,
131    /// Cascade invalidation depth
132    pub cascade_depth: u32,
133    /// Enable event batching
134    pub enable_batching: bool,
135    /// Batch size
136    pub batch_size: usize,
137    /// Batch timeout
138    pub batch_timeout: Duration,
139}
140
141impl Default for InvalidationConfig {
142    fn default() -> Self {
143        Self {
144            max_events: 10000,
145            event_retention: Duration::from_secs(86400), // 24 hours
146            enable_cascade: true,
147            cascade_depth: 3,
148            enable_batching: true,
149            batch_size: 100,
150            batch_timeout: Duration::from_secs(5),
151        }
152    }
153}
154
155/// Invalidation statistics
156#[derive(Debug, Clone, Default, Serialize, Deserialize)]
157pub struct InvalidationStats {
158    pub total_events: u64,
159    pub successful_invalidations: u64,
160    pub failed_invalidations: u64,
161    pub cascade_invalidations: u64,
162    pub manual_invalidations: u64,
163    pub expired_invalidations: u64,
164    pub average_processing_time_ms: f64,
165    pub last_invalidation: Option<DateTime<Utc>>,
166}
167
168impl CacheInvalidationMiddleware {
169    /// Create a new cache invalidation middleware
170    #[must_use]
171    pub fn new(config: InvalidationConfig) -> Self {
172        Self {
173            rules: Arc::new(RwLock::new(HashMap::new())),
174            events: Arc::new(RwLock::new(Vec::new())),
175            handlers: Arc::new(RwLock::new(HashMap::new())),
176            config,
177            stats: Arc::new(RwLock::new(InvalidationStats::default())),
178        }
179    }
180
181    /// Create a new middleware with default configuration
182    #[must_use]
183    pub fn new_default() -> Self {
184        Self::new(InvalidationConfig::default())
185    }
186
187    /// Register a cache invalidation handler
188    pub fn register_handler(&self, handler: Box<dyn CacheInvalidationHandler + Send + Sync>) {
189        let mut handlers = self.handlers.write();
190        handlers.insert(handler.cache_type().to_string(), handler);
191    }
192
193    /// Add an invalidation rule
194    pub fn add_rule(&self, rule: InvalidationRule) {
195        let mut rules = self.rules.write();
196        rules.insert(rule.name.clone(), rule);
197    }
198
199    /// Process an invalidation event
200    ///
201    /// # Errors
202    ///
203    /// This function will return an error if the event processing fails
204    pub async fn process_event(&self, event: InvalidationEvent) -> Result<()> {
205        let start_time = std::time::Instant::now();
206
207        // Store the event
208        self.store_event(&event);
209
210        // Find applicable rules
211        let applicable_rules = self.find_applicable_rules(&event);
212
213        // Process invalidation for each rule
214        for rule in applicable_rules {
215            if let Err(e) = self.process_rule(&event, &rule).await {
216                warn!("Failed to process invalidation rule {}: {}", rule.name, e);
217                self.record_failed_invalidation();
218            } else {
219                self.record_successful_invalidation();
220            }
221        }
222
223        // Handle cascade invalidation if enabled
224        if self.config.enable_cascade {
225            self.handle_cascade_invalidation(&event).await?;
226        }
227
228        // Update statistics
229        #[allow(clippy::cast_precision_loss)]
230        let processing_time = start_time.elapsed().as_millis().min(u128::from(u64::MAX)) as f64;
231        {
232            let mut stats = self.stats.write();
233            stats.total_events += 1;
234        }
235        self.update_processing_time(processing_time);
236
237        debug!(
238            "Processed invalidation event: {} for entity: {}:{}",
239            event.event_type,
240            event.entity_type,
241            event
242                .entity_id
243                .map_or_else(|| "none".to_string(), |id| id.to_string())
244        );
245
246        Ok(())
247    }
248
249    /// Manually invalidate caches
250    ///
251    /// # Errors
252    ///
253    /// This function will return an error if the manual invalidation fails
254    pub async fn manual_invalidate(
255        &self,
256        entity_type: &str,
257        entity_id: Option<Uuid>,
258        cache_types: Option<Vec<String>>,
259    ) -> Result<()> {
260        let event = InvalidationEvent {
261            event_id: Uuid::new_v4(),
262            event_type: InvalidationEventType::ManualInvalidation,
263            entity_type: entity_type.to_string(),
264            entity_id,
265            operation: "manual_invalidation".to_string(),
266            timestamp: Utc::now(),
267            affected_caches: cache_types.unwrap_or_default(),
268            metadata: HashMap::new(),
269        };
270
271        self.process_event(event).await?;
272        self.record_manual_invalidation();
273        Ok(())
274    }
275
276    /// Get invalidation statistics
277    #[must_use]
278    pub fn get_stats(&self) -> InvalidationStats {
279        self.stats.read().clone()
280    }
281
282    /// Get recent invalidation events
283    #[must_use]
284    pub fn get_recent_events(&self, limit: usize) -> Vec<InvalidationEvent> {
285        let events = self.events.read();
286        events.iter().rev().take(limit).cloned().collect()
287    }
288
289    /// Get events by entity type
290    #[must_use]
291    pub fn get_events_by_entity_type(&self, entity_type: &str) -> Vec<InvalidationEvent> {
292        let events = self.events.read();
293        events
294            .iter()
295            .filter(|event| event.entity_type == entity_type)
296            .cloned()
297            .collect()
298    }
299
300    /// Store an invalidation event
301    fn store_event(&self, event: &InvalidationEvent) {
302        let mut events = self.events.write();
303        events.push(event.clone());
304
305        // Trim events if we exceed max_events
306        if events.len() > self.config.max_events {
307            let excess = events.len() - self.config.max_events;
308            events.drain(0..excess);
309        }
310
311        // Remove old events based on retention policy
312        let cutoff_time = Utc::now()
313            - chrono::Duration::from_std(self.config.event_retention).unwrap_or_default();
314        events.retain(|event| event.timestamp > cutoff_time);
315    }
316
317    /// Find applicable invalidation rules
318    fn find_applicable_rules(&self, event: &InvalidationEvent) -> Vec<InvalidationRule> {
319        let rules = self.rules.read();
320        rules
321            .values()
322            .filter(|rule| {
323                rule.enabled
324                    && rule.entity_type == event.entity_type
325                    && (rule.operations.is_empty() || rule.operations.contains(&event.operation))
326            })
327            .cloned()
328            .collect()
329    }
330
331    /// Process an invalidation rule
332    async fn process_rule(&self, event: &InvalidationEvent, rule: &InvalidationRule) -> Result<()> {
333        match &rule.invalidation_strategy {
334            InvalidationStrategy::InvalidateAll => {
335                // Invalidate all registered caches
336                let handlers_guard = self.handlers.read();
337                for handler in handlers_guard.values() {
338                    if handler.can_handle(event) {
339                        handler.invalidate(event)?;
340                    }
341                }
342            }
343            InvalidationStrategy::InvalidateSpecific(cache_types) => {
344                // Invalidate specific cache types
345                let handlers_guard = self.handlers.read();
346                for cache_type in cache_types {
347                    if let Some(handler) = handlers_guard.get(cache_type) {
348                        if handler.can_handle(event) {
349                            handler.invalidate(event)?;
350                        }
351                    }
352                }
353            }
354            InvalidationStrategy::InvalidateByEntity => {
355                // Invalidate by entity ID
356                if let Some(_entity_id) = event.entity_id {
357                    let handlers_guard = self.handlers.read();
358                    for handler in handlers_guard.values() {
359                        if handler.can_handle(event) {
360                            handler.invalidate(event)?;
361                        }
362                    }
363                }
364            }
365            InvalidationStrategy::InvalidateByPattern(pattern) => {
366                // Invalidate by pattern matching
367                let handlers_guard = self.handlers.read();
368                for handler in handlers_guard.values() {
369                    if handler.can_handle(event) && Self::matches_pattern(event, pattern) {
370                        handler.invalidate(event)?;
371                    }
372                }
373            }
374            InvalidationStrategy::CascadeInvalidation => {
375                // Handle cascade invalidation
376                self.handle_cascade_invalidation(event).await?;
377            }
378        }
379
380        Ok(())
381    }
382
383    /// Handle cascade invalidation
384    async fn handle_cascade_invalidation(&self, event: &InvalidationEvent) -> Result<()> {
385        // Find dependent entities and invalidate them
386        let dependent_entities = Self::find_dependent_entities(event);
387
388        for dependent_entity in dependent_entities {
389            let dependent_event = InvalidationEvent {
390                event_id: Uuid::new_v4(),
391                event_type: InvalidationEventType::CascadeInvalidation,
392                entity_type: dependent_entity.entity_type.clone(),
393                entity_id: dependent_entity.entity_id,
394                operation: "cascade_invalidation".to_string(),
395                timestamp: Utc::now(),
396                affected_caches: dependent_entity.affected_caches.clone(),
397                metadata: HashMap::new(),
398            };
399
400            Box::pin(self.process_event(dependent_event)).await?;
401            self.record_cascade_invalidation();
402        }
403
404        Ok(())
405    }
406
407    /// Find dependent entities for cascade invalidation.
408    ///
409    /// The fan-out is type-driven (a task event implies project + area
410    /// dependents; an area event implies project + task dependents). When the
411    /// caller populates `event.metadata` with structured hints, the dependent
412    /// events carry concrete entity IDs instead of `None`. Recognised keys:
413    ///
414    /// - `"project_uuid"` — UUID string of the project the entity belongs to.
415    /// - `"area_uuid"` — UUID string of the area the entity belongs to.
416    ///
417    /// Missing or unparseable metadata falls back to `entity_id: None`
418    /// (back-compatible with pre-#93 callers).
419    fn find_dependent_entities(event: &InvalidationEvent) -> Vec<DependentEntity> {
420        let mut dependent_entities = Vec::new();
421        let project_uuid = Self::metadata_uuid(event, "project_uuid");
422        let area_uuid = Self::metadata_uuid(event, "area_uuid");
423
424        match event.entity_type.as_str() {
425            "task" if event.entity_id.is_some() => {
426                dependent_entities.push(DependentEntity {
427                    entity_type: "project".to_string(),
428                    entity_id: project_uuid,
429                    affected_caches: vec![],
430                });
431                dependent_entities.push(DependentEntity {
432                    entity_type: "area".to_string(),
433                    entity_id: area_uuid,
434                    affected_caches: vec![],
435                });
436            }
437            "project" if event.entity_id.is_some() => {
438                dependent_entities.push(DependentEntity {
439                    entity_type: "task".to_string(),
440                    entity_id: None,
441                    affected_caches: vec![],
442                });
443                dependent_entities.push(DependentEntity {
444                    entity_type: "area".to_string(),
445                    entity_id: area_uuid,
446                    affected_caches: vec![],
447                });
448            }
449            "area" if event.entity_id.is_some() => {
450                dependent_entities.push(DependentEntity {
451                    entity_type: "project".to_string(),
452                    entity_id: None,
453                    affected_caches: vec![],
454                });
455                dependent_entities.push(DependentEntity {
456                    entity_type: "task".to_string(),
457                    entity_id: None,
458                    affected_caches: vec![],
459                });
460            }
461            _ => {}
462        }
463
464        dependent_entities
465    }
466
467    /// Extract a UUID from `event.metadata[key]`, returning `None` if missing
468    /// or unparseable.
469    fn metadata_uuid(event: &InvalidationEvent, key: &str) -> Option<Uuid> {
470        event
471            .metadata
472            .get(key)
473            .and_then(serde_json::Value::as_str)
474            .and_then(|s| Uuid::parse_str(s).ok())
475    }
476
477    /// Check if event matches a pattern
478    fn matches_pattern(event: &InvalidationEvent, pattern: &str) -> bool {
479        // Simple pattern matching - in production, use regex or more sophisticated matching
480        event.entity_type.contains(pattern) || event.operation.contains(pattern)
481    }
482
483    /// Record successful invalidation
484    fn record_successful_invalidation(&self) {
485        let mut stats = self.stats.write();
486        stats.successful_invalidations += 1;
487        stats.last_invalidation = Some(Utc::now());
488    }
489
490    /// Record failed invalidation
491    fn record_failed_invalidation(&self) {
492        let mut stats = self.stats.write();
493        stats.failed_invalidations += 1;
494    }
495
496    /// Record cascade invalidation
497    fn record_cascade_invalidation(&self) {
498        let mut stats = self.stats.write();
499        stats.cascade_invalidations += 1;
500    }
501
502    /// Record manual invalidation
503    fn record_manual_invalidation(&self) {
504        let mut stats = self.stats.write();
505        stats.manual_invalidations += 1;
506    }
507
508    /// Update average processing time
509    fn update_processing_time(&self, processing_time: f64) {
510        let mut stats = self.stats.write();
511
512        // Update running average
513        #[allow(clippy::cast_precision_loss)]
514        let total_events = stats.total_events as f64;
515        stats.average_processing_time_ms =
516            (stats.average_processing_time_ms * (total_events - 1.0) + processing_time)
517                / total_events;
518    }
519}
520
521/// Dependent entity for cascade invalidation
522#[derive(Debug, Clone)]
523struct DependentEntity {
524    entity_type: String,
525    entity_id: Option<Uuid>,
526    affected_caches: Vec<String>,
527}
528
529/// Bridge handler that routes [`InvalidationEvent`]s into a [`ThingsCache`]'s
530/// dependency-aware invalidation methods.
531///
532/// Register one of these on a [`CacheInvalidationMiddleware`] to make
533/// `process_event(...)` actually evict cached entries:
534///
535/// ```ignore
536/// let cache = Arc::new(ThingsCache::new_default());
537/// let handler = ThingsCacheInvalidationHandler::new(Arc::clone(&cache));
538/// middleware.register_handler(Box::new(handler));
539/// ```
540///
541/// Spawns the actual eviction onto the current Tokio runtime so the
542/// synchronous trait method can return immediately. Callers that need to
543/// observe the post-invalidation state (e.g. tests) should yield once with
544/// `tokio::task::yield_now().await` after `process_event` returns.
545pub struct ThingsCacheInvalidationHandler {
546    cache: Arc<crate::cache::ThingsCache>,
547    cache_type: String,
548}
549
550impl ThingsCacheInvalidationHandler {
551    /// Construct a handler with the default cache type label `"things_cache"`.
552    #[must_use]
553    pub fn new(cache: Arc<crate::cache::ThingsCache>) -> Self {
554        Self {
555            cache,
556            cache_type: "things_cache".to_string(),
557        }
558    }
559
560    /// Construct a handler with a caller-supplied cache type label.
561    #[must_use]
562    pub fn with_cache_type(cache: Arc<crate::cache::ThingsCache>, cache_type: String) -> Self {
563        Self { cache, cache_type }
564    }
565}
566
567impl CacheInvalidationHandler for ThingsCacheInvalidationHandler {
568    fn invalidate(&self, event: &InvalidationEvent) -> Result<()> {
569        // Route only by `(entity_type, entity_id)`. Operation matching is a
570        // category-level fallback that would over-evict siblings of the
571        // mutated entity (every task entry lists `task_updated`), so we leave
572        // it for callers that explicitly want coarse invalidation.
573        let cache = Arc::clone(&self.cache);
574        let entity_type = event.entity_type.clone();
575        let entity_id = event.entity_id;
576        tokio::spawn(async move {
577            cache
578                .invalidate_by_entity(&entity_type, entity_id.as_ref())
579                .await;
580        });
581        Ok(())
582    }
583
584    fn cache_type(&self) -> &str {
585        &self.cache_type
586    }
587
588    fn can_handle(&self, _event: &InvalidationEvent) -> bool {
589        true
590    }
591}
592
593/// Cascade invalidation event type
594#[derive(Debug, Clone, Serialize, Deserialize)]
595pub enum CascadeInvalidationEvent {
596    /// Invalidate all dependent entities
597    InvalidateAll,
598    /// Invalidate specific dependent entities
599    InvalidateSpecific(Vec<String>),
600    /// Invalidate by dependency level
601    InvalidateByLevel(u32),
602}
603
604#[cfg(test)]
605mod tests {
606    use super::*;
607    use std::collections::HashMap;
608
609    // Mock cache invalidation handler for testing
610    struct MockCacheHandler {
611        cache_type: String,
612        invalidated_events: Arc<RwLock<Vec<InvalidationEvent>>>,
613    }
614
615    impl MockCacheHandler {
616        fn new(cache_type: &str) -> Self {
617            Self {
618                cache_type: cache_type.to_string(),
619                invalidated_events: Arc::new(RwLock::new(Vec::new())),
620            }
621        }
622
623        fn _get_invalidated_events(&self) -> Vec<InvalidationEvent> {
624            self.invalidated_events.read().clone()
625        }
626    }
627
628    impl CacheInvalidationHandler for MockCacheHandler {
629        fn invalidate(&self, event: &InvalidationEvent) -> Result<()> {
630            let mut events = self.invalidated_events.write();
631            events.push(event.clone());
632            Ok(())
633        }
634
635        fn cache_type(&self) -> &str {
636            &self.cache_type
637        }
638
639        fn can_handle(&self, event: &InvalidationEvent) -> bool {
640            event.affected_caches.is_empty() || event.affected_caches.contains(&self.cache_type)
641        }
642    }
643
644    #[tokio::test]
645    async fn test_invalidation_middleware_basic() {
646        let middleware = CacheInvalidationMiddleware::new_default();
647
648        // Register mock handlers
649        let _l1_handler = Arc::new(MockCacheHandler::new("l1"));
650        let _l2_handler = Arc::new(MockCacheHandler::new("l2"));
651
652        middleware.register_handler(Box::new(MockCacheHandler::new("l1")));
653        middleware.register_handler(Box::new(MockCacheHandler::new("l2")));
654
655        // Add rules for task, project, and area entities
656        let task_rule = InvalidationRule {
657            rule_id: Uuid::new_v4(),
658            name: "task_rule".to_string(),
659            description: "Rule for task invalidation".to_string(),
660            entity_type: "task".to_string(),
661            operations: vec!["updated".to_string()],
662            affected_cache_types: vec!["l1".to_string(), "l2".to_string()],
663            invalidation_strategy: InvalidationStrategy::InvalidateAll,
664            enabled: true,
665            created_at: Utc::now(),
666            updated_at: Utc::now(),
667        };
668        middleware.add_rule(task_rule);
669
670        let project_rule = InvalidationRule {
671            rule_id: Uuid::new_v4(),
672            name: "project_rule".to_string(),
673            description: "Rule for project invalidation".to_string(),
674            entity_type: "project".to_string(),
675            operations: vec!["cascade_invalidation".to_string()],
676            affected_cache_types: vec!["l1".to_string(), "l2".to_string()],
677            invalidation_strategy: InvalidationStrategy::InvalidateAll,
678            enabled: true,
679            created_at: Utc::now(),
680            updated_at: Utc::now(),
681        };
682        middleware.add_rule(project_rule);
683
684        let area_rule = InvalidationRule {
685            rule_id: Uuid::new_v4(),
686            name: "area_rule".to_string(),
687            description: "Rule for area invalidation".to_string(),
688            entity_type: "area".to_string(),
689            operations: vec!["cascade_invalidation".to_string()],
690            affected_cache_types: vec!["l1".to_string(), "l2".to_string()],
691            invalidation_strategy: InvalidationStrategy::InvalidateAll,
692            enabled: true,
693            created_at: Utc::now(),
694            updated_at: Utc::now(),
695        };
696        middleware.add_rule(area_rule);
697
698        // Create an invalidation event
699        let event = InvalidationEvent {
700            event_id: Uuid::new_v4(),
701            event_type: InvalidationEventType::Updated,
702            entity_type: "task".to_string(),
703            entity_id: Some(Uuid::new_v4()),
704            operation: "updated".to_string(),
705            timestamp: Utc::now(),
706            affected_caches: vec!["l1".to_string(), "l2".to_string()],
707            metadata: HashMap::new(),
708        };
709
710        // Process the event
711        middleware.process_event(event).await.unwrap();
712
713        // Check statistics
714        let stats = middleware.get_stats();
715        assert_eq!(stats.total_events, 3); // 1 original + 2 cascade events
716        assert_eq!(stats.successful_invalidations, 3);
717    }
718
719    #[tokio::test]
720    async fn test_manual_invalidation() {
721        let middleware = CacheInvalidationMiddleware::new_default();
722
723        middleware.register_handler(Box::new(MockCacheHandler::new("l1")));
724
725        // Manual invalidation
726        middleware
727            .manual_invalidate("task", Some(Uuid::new_v4()), None)
728            .await
729            .unwrap();
730
731        let stats = middleware.get_stats();
732        assert_eq!(stats.manual_invalidations, 1);
733    }
734
735    #[tokio::test]
736    async fn test_event_storage() {
737        let middleware = CacheInvalidationMiddleware::new_default();
738
739        let event = InvalidationEvent {
740            event_id: Uuid::new_v4(),
741            event_type: InvalidationEventType::Created,
742            entity_type: "task".to_string(),
743            entity_id: Some(Uuid::new_v4()),
744            operation: "created".to_string(),
745            timestamp: Utc::now(),
746            affected_caches: vec![],
747            metadata: HashMap::new(),
748        };
749
750        middleware.store_event(&event);
751
752        let recent_events = middleware.get_recent_events(1);
753        assert_eq!(recent_events.len(), 1);
754        assert_eq!(recent_events[0].entity_type, "task");
755    }
756
757    #[tokio::test]
758    async fn test_invalidation_middleware_creation() {
759        let middleware = CacheInvalidationMiddleware::new_default();
760        let stats = middleware.get_stats();
761
762        assert_eq!(stats.total_events, 0);
763        assert_eq!(stats.successful_invalidations, 0);
764        assert_eq!(stats.failed_invalidations, 0);
765        assert_eq!(stats.manual_invalidations, 0);
766    }
767
768    #[tokio::test]
769    async fn test_invalidation_middleware_with_config() {
770        let config = InvalidationConfig {
771            enable_cascade: true,
772            max_events: 1000,
773            event_retention: Duration::from_secs(3600),
774            batch_size: 10,
775            batch_timeout: Duration::from_secs(30),
776            cascade_depth: 3,
777            enable_batching: true,
778        };
779
780        let middleware = CacheInvalidationMiddleware::new(config);
781        let stats = middleware.get_stats();
782
783        assert_eq!(stats.total_events, 0);
784    }
785
786    #[tokio::test]
787    async fn test_add_rule() {
788        let middleware = CacheInvalidationMiddleware::new_default();
789
790        let rule = InvalidationRule {
791            rule_id: Uuid::new_v4(),
792            name: "test_rule".to_string(),
793            description: "Test rule".to_string(),
794            entity_type: "task".to_string(),
795            operations: vec!["updated".to_string()],
796            affected_cache_types: vec!["task_cache".to_string()],
797            invalidation_strategy: InvalidationStrategy::InvalidateAll,
798            enabled: true,
799            created_at: Utc::now(),
800            updated_at: Utc::now(),
801        };
802
803        middleware.add_rule(rule);
804
805        // Rules are stored internally, we can't directly test them
806        // but we can test that the method doesn't panic
807    }
808
809    #[tokio::test]
810    async fn test_register_handler() {
811        let middleware = CacheInvalidationMiddleware::new_default();
812        let handler = Box::new(MockCacheHandler::new("test_cache"));
813
814        middleware.register_handler(handler);
815
816        // Handler is stored internally, we can't directly test it
817        // but we can test that the method doesn't panic
818    }
819
820    #[tokio::test]
821    async fn test_process_event_with_handler() {
822        let middleware = CacheInvalidationMiddleware::new_default();
823        let handler = Box::new(MockCacheHandler::new("test_cache"));
824        let l1_handler = Box::new(MockCacheHandler::new("l1"));
825        let l2_handler = Box::new(MockCacheHandler::new("l2"));
826
827        middleware.register_handler(handler);
828        middleware.register_handler(l1_handler);
829        middleware.register_handler(l2_handler);
830
831        let rule = InvalidationRule {
832            rule_id: Uuid::new_v4(),
833            name: "test_rule".to_string(),
834            description: "Test rule".to_string(),
835            entity_type: "task".to_string(),
836            operations: vec!["created".to_string(), "updated".to_string()],
837            affected_cache_types: vec!["test_cache".to_string()],
838            invalidation_strategy: InvalidationStrategy::InvalidateAll,
839            enabled: true,
840            created_at: Utc::now(),
841            updated_at: Utc::now(),
842        };
843        middleware.add_rule(rule);
844
845        // Add rules for project and area entities to handle cascade events
846        // Note: cascade events use "l1" and "l2" as affected caches
847        let project_rule = InvalidationRule {
848            rule_id: Uuid::new_v4(),
849            name: "project_rule".to_string(),
850            description: "Rule for project invalidation".to_string(),
851            entity_type: "project".to_string(),
852            operations: vec!["cascade_invalidation".to_string()],
853            affected_cache_types: vec!["l1".to_string(), "l2".to_string()],
854            invalidation_strategy: InvalidationStrategy::InvalidateAll,
855            enabled: true,
856            created_at: Utc::now(),
857            updated_at: Utc::now(),
858        };
859        middleware.add_rule(project_rule);
860
861        let area_rule = InvalidationRule {
862            rule_id: Uuid::new_v4(),
863            name: "area_rule".to_string(),
864            description: "Rule for area invalidation".to_string(),
865            entity_type: "area".to_string(),
866            operations: vec!["cascade_invalidation".to_string()],
867            affected_cache_types: vec!["l1".to_string(), "l2".to_string()],
868            invalidation_strategy: InvalidationStrategy::InvalidateAll,
869            enabled: true,
870            created_at: Utc::now(),
871            updated_at: Utc::now(),
872        };
873        middleware.add_rule(area_rule);
874
875        let event = InvalidationEvent {
876            event_id: Uuid::new_v4(),
877            event_type: InvalidationEventType::Created,
878            entity_type: "task".to_string(),
879            entity_id: Some(Uuid::new_v4()),
880            operation: "created".to_string(),
881            timestamp: Utc::now(),
882            affected_caches: vec!["test_cache".to_string()],
883            metadata: HashMap::new(),
884        };
885
886        let _ = middleware.process_event(event).await;
887
888        let stats = middleware.get_stats();
889        assert_eq!(stats.total_events, 3); // 1 original + 2 cascade events
890        assert_eq!(stats.successful_invalidations, 3);
891    }
892
893    #[tokio::test]
894    async fn test_get_recent_events() {
895        let middleware = CacheInvalidationMiddleware::new_default();
896
897        // Add multiple events
898        for i in 0..5 {
899            let event = InvalidationEvent {
900                event_id: Uuid::new_v4(),
901                event_type: InvalidationEventType::Created,
902                entity_type: format!("task_{i}"),
903                entity_id: Some(Uuid::new_v4()),
904                operation: "created".to_string(),
905                timestamp: Utc::now(),
906                affected_caches: vec![],
907                metadata: HashMap::new(),
908            };
909            middleware.store_event(&event);
910        }
911
912        // Get recent events
913        let recent_events = middleware.get_recent_events(3);
914        assert_eq!(recent_events.len(), 3);
915
916        // Get all events
917        let all_events = middleware.get_recent_events(10);
918        assert_eq!(all_events.len(), 5);
919    }
920
921    fn task_event_with_metadata(
922        entity_id: Uuid,
923        metadata: HashMap<String, serde_json::Value>,
924    ) -> InvalidationEvent {
925        InvalidationEvent {
926            event_id: Uuid::new_v4(),
927            event_type: InvalidationEventType::Updated,
928            entity_type: "task".to_string(),
929            entity_id: Some(entity_id),
930            operation: "task_updated".to_string(),
931            timestamp: Utc::now(),
932            affected_caches: vec![],
933            metadata,
934        }
935    }
936
937    #[test]
938    fn test_cascade_uses_project_uuid_metadata() {
939        let task_id = Uuid::new_v4();
940        let project_id = Uuid::new_v4();
941        let mut metadata = HashMap::new();
942        metadata.insert(
943            "project_uuid".to_string(),
944            serde_json::Value::String(project_id.to_string()),
945        );
946        let event = task_event_with_metadata(task_id, metadata);
947
948        let dependents = CacheInvalidationMiddleware::find_dependent_entities(&event);
949        let project_dep = dependents
950            .iter()
951            .find(|d| d.entity_type == "project")
952            .expect("task event must fan out to project");
953        assert_eq!(project_dep.entity_id, Some(project_id));
954        let area_dep = dependents
955            .iter()
956            .find(|d| d.entity_type == "area")
957            .expect("task event must fan out to area");
958        assert_eq!(area_dep.entity_id, None, "no area_uuid in metadata");
959    }
960
961    #[test]
962    fn test_cascade_falls_back_when_metadata_missing() {
963        let event = task_event_with_metadata(Uuid::new_v4(), HashMap::new());
964        let dependents = CacheInvalidationMiddleware::find_dependent_entities(&event);
965        assert!(dependents.iter().all(|d| d.entity_id.is_none()));
966    }
967
968    #[tokio::test]
969    async fn test_things_cache_handler_routes_to_invalidate_by_entity() {
970        use crate::cache::ThingsCache;
971        use crate::test_utils::create_mock_tasks;
972
973        let cache = Arc::new(ThingsCache::new_default());
974        let target_id = Uuid::new_v4();
975        let other_id = Uuid::new_v4();
976
977        let mut target_task = create_mock_tasks().into_iter().next().unwrap();
978        target_task.uuid = target_id;
979        target_task.project_uuid = None;
980        target_task.area_uuid = None;
981        let mut other_task = target_task.clone();
982        other_task.uuid = other_id;
983
984        cache
985            .get_tasks("target_key", || async { Ok(vec![target_task]) })
986            .await
987            .unwrap();
988        cache
989            .get_tasks("other_key", || async { Ok(vec![other_task]) })
990            .await
991            .unwrap();
992
993        let handler = ThingsCacheInvalidationHandler::new(Arc::clone(&cache));
994        let event = InvalidationEvent {
995            event_id: Uuid::new_v4(),
996            event_type: InvalidationEventType::Updated,
997            entity_type: "task".to_string(),
998            entity_id: Some(target_id),
999            operation: "task_updated".to_string(),
1000            timestamp: Utc::now(),
1001            affected_caches: vec![],
1002            metadata: HashMap::new(),
1003        };
1004        handler.invalidate(&event).unwrap();
1005
1006        // The handler spawns the eviction; give the runtime enough time to
1007        // complete it before asserting. yield_now is insufficient on loaded
1008        // machines — a brief sleep is more robust.
1009        tokio::time::sleep(std::time::Duration::from_millis(10)).await;
1010
1011        // Re-fetch via the cache: the target key should miss (returning the
1012        // empty fetcher result), the other key should still hit its cached row.
1013        let target = cache
1014            .get_tasks("target_key", || async { Ok(vec![]) })
1015            .await
1016            .unwrap();
1017        let other = cache
1018            .get_tasks("other_key", || async { Ok(vec![]) })
1019            .await
1020            .unwrap();
1021        assert!(target.is_empty(), "target should have been invalidated");
1022        assert_eq!(other.len(), 1, "other task should still be cached");
1023        assert_eq!(other[0].uuid, other_id);
1024    }
1025}