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
13use crate::models::ThingsId;
14
15#[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#[non_exhaustive]
30#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
31pub enum InvalidationEventType {
32 Created,
34 Updated,
36 Deleted,
38 Completed,
40 BulkOperation,
42 ManualInvalidation,
44 Expired,
46 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#[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#[non_exhaustive]
82#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
83pub enum InvalidationStrategy {
84 InvalidateAll,
86 InvalidateSpecific(Vec<String>),
88 InvalidateByEntity,
90 InvalidateByPattern(String),
92 CascadeInvalidation,
94}
95
96pub struct CacheInvalidationMiddleware {
98 rules: Arc<RwLock<HashMap<String, InvalidationRule>>>,
100 events: Arc<RwLock<Vec<InvalidationEvent>>>,
102 handlers: Arc<RwLock<HashMap<String, Box<dyn CacheInvalidationHandler + Send + Sync>>>>,
104 config: InvalidationConfig,
106 stats: Arc<RwLock<InvalidationStats>>,
108}
109
110pub trait CacheInvalidationHandler {
112 fn invalidate(&self, event: &InvalidationEvent) -> Result<()>;
118
119 fn cache_type(&self) -> &str;
121
122 fn can_handle(&self, event: &InvalidationEvent) -> bool;
124}
125
126#[derive(Debug, Clone, Serialize, Deserialize)]
128pub struct InvalidationConfig {
129 pub max_events: usize,
131 pub event_retention: Duration,
133 pub enable_cascade: bool,
135 pub cascade_depth: u32,
137 pub enable_batching: bool,
139 pub batch_size: usize,
141 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), 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#[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 #[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 #[must_use]
187 pub fn new_default() -> Self {
188 Self::new(InvalidationConfig::default())
189 }
190
191 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 pub fn add_rule(&self, rule: InvalidationRule) {
199 let mut rules = self.rules.write();
200 rules.insert(rule.name.clone(), rule);
201 }
202
203 pub async fn process_event(&self, event: InvalidationEvent) -> Result<()> {
209 let start_time = std::time::Instant::now();
210
211 self.store_event(&event);
213
214 let applicable_rules = self.find_applicable_rules(&event);
216
217 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 if self.config.enable_cascade {
229 self.handle_cascade_invalidation(&event).await?;
230 }
231
232 #[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 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 #[must_use]
282 pub fn get_stats(&self) -> InvalidationStats {
283 self.stats.read().clone()
284 }
285
286 #[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 #[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 fn store_event(&self, event: &InvalidationEvent) {
306 let mut events = self.events.write();
307 events.push(event.clone());
308
309 if events.len() > self.config.max_events {
311 let excess = events.len() - self.config.max_events;
312 events.drain(0..excess);
313 }
314
315 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 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 async fn process_rule(&self, event: &InvalidationEvent, rule: &InvalidationRule) -> Result<()> {
337 match &rule.invalidation_strategy {
338 InvalidationStrategy::InvalidateAll => {
339 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 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 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 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 self.handle_cascade_invalidation(event).await?;
381 }
382 }
383
384 Ok(())
385 }
386
387 async fn handle_cascade_invalidation(&self, event: &InvalidationEvent) -> Result<()> {
389 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 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 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 fn matches_pattern(event: &InvalidationEvent, pattern: &str) -> bool {
484 event.entity_type.contains(pattern) || event.operation.contains(pattern)
486 }
487
488 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 fn record_failed_invalidation(&self) {
497 let mut stats = self.stats.write();
498 stats.failed_invalidations += 1;
499 }
500
501 fn record_cascade_invalidation(&self) {
503 let mut stats = self.stats.write();
504 stats.cascade_invalidations += 1;
505 }
506
507 fn record_manual_invalidation(&self) {
509 let mut stats = self.stats.write();
510 stats.manual_invalidations += 1;
511 }
512
513 fn update_processing_time(&self, processing_time: f64) {
515 let mut stats = self.stats.write();
516
517 #[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#[derive(Debug, Clone)]
528struct DependentEntity {
529 entity_type: String,
530 entity_id: Option<ThingsId>,
531 affected_caches: Vec<String>,
532}
533
534pub struct ThingsCacheInvalidationHandler {
551 cache: Arc<crate::cache::ThingsCache>,
552 cache_type: String,
553}
554
555impl ThingsCacheInvalidationHandler {
556 #[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 #[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 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#[non_exhaustive]
600#[derive(Debug, Clone, Serialize, Deserialize)]
601pub enum CascadeInvalidationEvent {
602 InvalidateAll,
604 InvalidateSpecific(Vec<String>),
606 InvalidateByLevel(u32),
608}
609
610#[cfg(test)]
611mod tests {
612 use super::*;
613 use std::collections::HashMap;
614
615 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 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 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 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 middleware.process_event(event).await.unwrap();
718
719 let stats = middleware.get_stats();
721 assert_eq!(stats.total_events, 3); 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 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 }
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 }
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 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); 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 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 let recent_events = middleware.get_recent_events(3);
920 assert_eq!(recent_events.len(), 3);
921
922 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 tokio::time::sleep(std::time::Duration::from_millis(10)).await;
1016
1017 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}