prax_query/data_cache/
invalidation.rs

1//! Cache invalidation strategies.
2
3use std::fmt::{self, Display};
4use std::time::Instant;
5
6/// A tag for categorizing and invalidating cache entries.
7#[derive(Debug, Clone, PartialEq, Eq, Hash)]
8pub struct EntityTag {
9    /// The tag value.
10    value: String,
11}
12
13impl EntityTag {
14    /// Create a new tag.
15    pub fn new(value: impl Into<String>) -> Self {
16        Self {
17            value: value.into(),
18        }
19    }
20
21    /// Create an entity-type tag.
22    pub fn entity(entity: &str) -> Self {
23        Self::new(format!("entity:{}", entity))
24    }
25
26    /// Create a record-specific tag.
27    pub fn record<I: Display>(entity: &str, id: I) -> Self {
28        Self::new(format!("record:{}:{}", entity, id))
29    }
30
31    /// Create a tenant tag.
32    pub fn tenant(tenant: &str) -> Self {
33        Self::new(format!("tenant:{}", tenant))
34    }
35
36    /// Create a query tag.
37    pub fn query(name: &str) -> Self {
38        Self::new(format!("query:{}", name))
39    }
40
41    /// Create a relation tag.
42    pub fn relation(from: &str, to: &str) -> Self {
43        Self::new(format!("rel:{}:{}", from, to))
44    }
45
46    /// Get the tag value.
47    pub fn value(&self) -> &str {
48        &self.value
49    }
50
51    /// Check if this tag matches a pattern.
52    pub fn matches(&self, pattern: &str) -> bool {
53        if pattern.contains('*') {
54            super::key::KeyPattern::new(pattern).matches_str(&self.value)
55        } else {
56            self.value == pattern
57        }
58    }
59}
60
61impl Display for EntityTag {
62    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
63        write!(f, "{}", self.value)
64    }
65}
66
67impl From<&str> for EntityTag {
68    fn from(s: &str) -> Self {
69        Self::new(s)
70    }
71}
72
73impl From<String> for EntityTag {
74    fn from(s: String) -> Self {
75        Self::new(s)
76    }
77}
78
79/// An event that triggers cache invalidation.
80#[derive(Debug, Clone)]
81pub struct InvalidationEvent {
82    /// Type of event.
83    pub event_type: InvalidationEventType,
84    /// Entity affected.
85    pub entity: String,
86    /// Record ID if applicable.
87    pub record_id: Option<String>,
88    /// When the event occurred.
89    pub timestamp: Instant,
90    /// Tags to invalidate.
91    pub tags: Vec<EntityTag>,
92    /// Additional metadata.
93    pub metadata: Option<String>,
94}
95
96impl InvalidationEvent {
97    /// Create a new invalidation event.
98    pub fn new(event_type: InvalidationEventType, entity: impl Into<String>) -> Self {
99        Self {
100            event_type,
101            entity: entity.into(),
102            record_id: None,
103            timestamp: Instant::now(),
104            tags: Vec::new(),
105            metadata: None,
106        }
107    }
108
109    /// Create an insert event.
110    pub fn insert(entity: impl Into<String>) -> Self {
111        Self::new(InvalidationEventType::Insert, entity)
112    }
113
114    /// Create an update event.
115    pub fn update(entity: impl Into<String>) -> Self {
116        Self::new(InvalidationEventType::Update, entity)
117    }
118
119    /// Create a delete event.
120    pub fn delete(entity: impl Into<String>) -> Self {
121        Self::new(InvalidationEventType::Delete, entity)
122    }
123
124    /// Set the record ID.
125    pub fn with_record<I: Display>(mut self, id: I) -> Self {
126        self.record_id = Some(id.to_string());
127        self
128    }
129
130    /// Add a tag.
131    pub fn with_tag(mut self, tag: impl Into<EntityTag>) -> Self {
132        self.tags.push(tag.into());
133        self
134    }
135
136    /// Add multiple tags.
137    pub fn with_tags(mut self, tags: impl IntoIterator<Item = EntityTag>) -> Self {
138        self.tags.extend(tags);
139        self
140    }
141
142    /// Set metadata.
143    pub fn with_metadata(mut self, metadata: impl Into<String>) -> Self {
144        self.metadata = Some(metadata.into());
145        self
146    }
147
148    /// Get all tags that should be invalidated.
149    pub fn all_tags(&self) -> Vec<EntityTag> {
150        let mut tags = self.tags.clone();
151
152        // Add entity tag
153        tags.push(EntityTag::entity(&self.entity));
154
155        // Add record tag if present
156        if let Some(ref id) = self.record_id {
157            tags.push(EntityTag::record(&self.entity, id));
158        }
159
160        tags
161    }
162}
163
164/// Type of invalidation event.
165#[derive(Debug, Clone, Copy, PartialEq, Eq)]
166pub enum InvalidationEventType {
167    /// A new record was inserted.
168    Insert,
169    /// A record was updated.
170    Update,
171    /// A record was deleted.
172    Delete,
173    /// Multiple records were affected.
174    Bulk,
175    /// Schema changed (clear all for entity).
176    SchemaChange,
177    /// Manual invalidation.
178    Manual,
179}
180
181impl Display for InvalidationEventType {
182    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
183        match self {
184            Self::Insert => write!(f, "insert"),
185            Self::Update => write!(f, "update"),
186            Self::Delete => write!(f, "delete"),
187            Self::Bulk => write!(f, "bulk"),
188            Self::SchemaChange => write!(f, "schema_change"),
189            Self::Manual => write!(f, "manual"),
190        }
191    }
192}
193
194/// Strategy for cache invalidation.
195#[derive(Debug, Clone)]
196pub enum InvalidationStrategy {
197    /// Invalidate on every write.
198    Immediate,
199    
200    /// Invalidate after a delay (batching).
201    Delayed {
202        delay_ms: u64,
203    },
204    
205    /// Invalidate based on events.
206    EventBased {
207        events: Vec<InvalidationEventType>,
208    },
209    
210    /// Only invalidate specific tags.
211    TagBased {
212        tags: Vec<EntityTag>,
213    },
214    
215    /// Time-based expiration only (no explicit invalidation).
216    TtlOnly,
217    
218    /// Custom invalidation logic.
219    Custom {
220        name: String,
221    },
222}
223
224impl InvalidationStrategy {
225    /// Create an immediate invalidation strategy.
226    pub fn immediate() -> Self {
227        Self::Immediate
228    }
229
230    /// Create a delayed invalidation strategy.
231    pub fn delayed(delay_ms: u64) -> Self {
232        Self::Delayed { delay_ms }
233    }
234
235    /// Create an event-based strategy.
236    pub fn on_events(events: Vec<InvalidationEventType>) -> Self {
237        Self::EventBased { events }
238    }
239
240    /// Create a tag-based strategy.
241    pub fn for_tags(tags: Vec<EntityTag>) -> Self {
242        Self::TagBased { tags }
243    }
244
245    /// Create a TTL-only strategy.
246    pub fn ttl_only() -> Self {
247        Self::TtlOnly
248    }
249
250    /// Check if an event should trigger invalidation.
251    pub fn should_invalidate(&self, event: &InvalidationEvent) -> bool {
252        match self {
253            Self::Immediate => true,
254            Self::Delayed { .. } => true, // Will be batched
255            Self::EventBased { events } => events.contains(&event.event_type),
256            Self::TagBased { tags } => {
257                event.all_tags().iter().any(|t| tags.contains(t))
258            }
259            Self::TtlOnly => false,
260            Self::Custom { .. } => true, // Let custom logic decide
261        }
262    }
263}
264
265impl Default for InvalidationStrategy {
266    fn default() -> Self {
267        Self::Immediate
268    }
269}
270
271/// An invalidation handler that can be registered with the cache.
272pub trait InvalidationHandler: Send + Sync + 'static {
273    /// Handle an invalidation event.
274    fn handle(
275        &self,
276        event: &InvalidationEvent,
277    ) -> impl std::future::Future<Output = super::CacheResult<()>> + Send;
278}
279
280/// A simple function-based handler.
281pub struct FnHandler<F>(pub F);
282
283impl<F, Fut> InvalidationHandler for FnHandler<F>
284where
285    F: Fn(&InvalidationEvent) -> Fut + Send + Sync + 'static,
286    Fut: std::future::Future<Output = super::CacheResult<()>> + Send,
287{
288    async fn handle(&self, event: &InvalidationEvent) -> super::CacheResult<()> {
289        (self.0)(event).await
290    }
291}
292
293#[cfg(test)]
294mod tests {
295    use super::*;
296
297    #[test]
298    fn test_entity_tag() {
299        let tag = EntityTag::entity("User");
300        assert_eq!(tag.value(), "entity:User");
301
302        let record_tag = EntityTag::record("User", 123);
303        assert_eq!(record_tag.value(), "record:User:123");
304    }
305
306    #[test]
307    fn test_invalidation_event() {
308        let event = InvalidationEvent::insert("User")
309            .with_record(123)
310            .with_tag("custom_tag");
311
312        assert_eq!(event.entity, "User");
313        assert_eq!(event.record_id, Some("123".to_string()));
314        assert_eq!(event.event_type, InvalidationEventType::Insert);
315
316        let tags = event.all_tags();
317        assert!(tags.iter().any(|t| t.value() == "entity:User"));
318        assert!(tags.iter().any(|t| t.value() == "record:User:123"));
319    }
320
321    #[test]
322    fn test_invalidation_strategy() {
323        let immediate = InvalidationStrategy::immediate();
324        let event = InvalidationEvent::update("User");
325        assert!(immediate.should_invalidate(&event));
326
327        let events_only = InvalidationStrategy::on_events(vec![InvalidationEventType::Delete]);
328        assert!(!events_only.should_invalidate(&event));
329
330        let delete_event = InvalidationEvent::delete("User");
331        assert!(events_only.should_invalidate(&delete_event));
332    }
333
334    #[test]
335    fn test_tag_matching() {
336        let tag = EntityTag::new("entity:User");
337        assert!(tag.matches("entity:User"));
338        assert!(tag.matches("entity:*"));
339        assert!(!tag.matches("entity:Post"));
340    }
341}
342
343