1use 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#[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#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
28pub enum InvalidationEventType {
29 Created,
31 Updated,
33 Deleted,
35 Completed,
37 BulkOperation,
39 ManualInvalidation,
41 Expired,
43 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#[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#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
79pub enum InvalidationStrategy {
80 InvalidateAll,
82 InvalidateSpecific(Vec<String>),
84 InvalidateByEntity,
86 InvalidateByPattern(String),
88 CascadeInvalidation,
90}
91
92pub struct CacheInvalidationMiddleware {
94 rules: Arc<RwLock<HashMap<String, InvalidationRule>>>,
96 events: Arc<RwLock<Vec<InvalidationEvent>>>,
98 handlers: Arc<RwLock<HashMap<String, Box<dyn CacheInvalidationHandler + Send + Sync>>>>,
100 config: InvalidationConfig,
102 stats: Arc<RwLock<InvalidationStats>>,
104}
105
106pub trait CacheInvalidationHandler {
108 fn invalidate(&self, event: &InvalidationEvent) -> Result<()>;
114
115 fn cache_type(&self) -> &str;
117
118 fn can_handle(&self, event: &InvalidationEvent) -> bool;
120}
121
122#[derive(Debug, Clone, Serialize, Deserialize)]
124pub struct InvalidationConfig {
125 pub max_events: usize,
127 pub event_retention: Duration,
129 pub enable_cascade: bool,
131 pub cascade_depth: u32,
133 pub enable_batching: bool,
135 pub batch_size: usize,
137 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), 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#[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 #[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 #[must_use]
183 pub fn new_default() -> Self {
184 Self::new(InvalidationConfig::default())
185 }
186
187 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 pub fn add_rule(&self, rule: InvalidationRule) {
195 let mut rules = self.rules.write();
196 rules.insert(rule.name.clone(), rule);
197 }
198
199 pub async fn process_event(&self, event: InvalidationEvent) -> Result<()> {
205 let start_time = std::time::Instant::now();
206
207 self.store_event(&event);
209
210 let applicable_rules = self.find_applicable_rules(&event);
212
213 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 if self.config.enable_cascade {
225 self.handle_cascade_invalidation(&event).await?;
226 }
227
228 #[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 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 #[must_use]
278 pub fn get_stats(&self) -> InvalidationStats {
279 self.stats.read().clone()
280 }
281
282 #[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 #[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 fn store_event(&self, event: &InvalidationEvent) {
302 let mut events = self.events.write();
303 events.push(event.clone());
304
305 if events.len() > self.config.max_events {
307 let excess = events.len() - self.config.max_events;
308 events.drain(0..excess);
309 }
310
311 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 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 async fn process_rule(&self, event: &InvalidationEvent, rule: &InvalidationRule) -> Result<()> {
333 match &rule.invalidation_strategy {
334 InvalidationStrategy::InvalidateAll => {
335 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 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 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 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 self.handle_cascade_invalidation(event).await?;
377 }
378 }
379
380 Ok(())
381 }
382
383 async fn handle_cascade_invalidation(&self, event: &InvalidationEvent) -> Result<()> {
385 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 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 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 fn matches_pattern(event: &InvalidationEvent, pattern: &str) -> bool {
479 event.entity_type.contains(pattern) || event.operation.contains(pattern)
481 }
482
483 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 fn record_failed_invalidation(&self) {
492 let mut stats = self.stats.write();
493 stats.failed_invalidations += 1;
494 }
495
496 fn record_cascade_invalidation(&self) {
498 let mut stats = self.stats.write();
499 stats.cascade_invalidations += 1;
500 }
501
502 fn record_manual_invalidation(&self) {
504 let mut stats = self.stats.write();
505 stats.manual_invalidations += 1;
506 }
507
508 fn update_processing_time(&self, processing_time: f64) {
510 let mut stats = self.stats.write();
511
512 #[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#[derive(Debug, Clone)]
523struct DependentEntity {
524 entity_type: String,
525 entity_id: Option<Uuid>,
526 affected_caches: Vec<String>,
527}
528
529pub struct ThingsCacheInvalidationHandler {
546 cache: Arc<crate::cache::ThingsCache>,
547 cache_type: String,
548}
549
550impl ThingsCacheInvalidationHandler {
551 #[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 #[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 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#[derive(Debug, Clone, Serialize, Deserialize)]
595pub enum CascadeInvalidationEvent {
596 InvalidateAll,
598 InvalidateSpecific(Vec<String>),
600 InvalidateByLevel(u32),
602}
603
604#[cfg(test)]
605mod tests {
606 use super::*;
607 use std::collections::HashMap;
608
609 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 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 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 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 middleware.process_event(event).await.unwrap();
712
713 let stats = middleware.get_stats();
715 assert_eq!(stats.total_events, 3); 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 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 }
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 }
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 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); 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 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 let recent_events = middleware.get_recent_events(3);
914 assert_eq!(recent_events.len(), 3);
915
916 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 tokio::time::sleep(std::time::Duration::from_millis(10)).await;
1010
1011 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}