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