Skip to main content

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 { delay_ms: u64 },
202
203    /// Invalidate based on events.
204    EventBased { events: Vec<InvalidationEventType> },
205
206    /// Only invalidate specific tags.
207    TagBased { tags: Vec<EntityTag> },
208
209    /// Time-based expiration only (no explicit invalidation).
210    TtlOnly,
211
212    /// Custom invalidation logic.
213    Custom { name: String },
214}
215
216impl InvalidationStrategy {
217    /// Create an immediate invalidation strategy.
218    pub fn immediate() -> Self {
219        Self::Immediate
220    }
221
222    /// Create a delayed invalidation strategy.
223    pub fn delayed(delay_ms: u64) -> Self {
224        Self::Delayed { delay_ms }
225    }
226
227    /// Create an event-based strategy.
228    pub fn on_events(events: Vec<InvalidationEventType>) -> Self {
229        Self::EventBased { events }
230    }
231
232    /// Create a tag-based strategy.
233    pub fn for_tags(tags: Vec<EntityTag>) -> Self {
234        Self::TagBased { tags }
235    }
236
237    /// Create a TTL-only strategy.
238    pub fn ttl_only() -> Self {
239        Self::TtlOnly
240    }
241
242    /// Check if an event should trigger invalidation.
243    pub fn should_invalidate(&self, event: &InvalidationEvent) -> bool {
244        match self {
245            Self::Immediate => true,
246            Self::Delayed { .. } => true, // Will be batched
247            Self::EventBased { events } => events.contains(&event.event_type),
248            Self::TagBased { tags } => event.all_tags().iter().any(|t| tags.contains(t)),
249            Self::TtlOnly => false,
250            Self::Custom { .. } => true, // Let custom logic decide
251        }
252    }
253}
254
255impl Default for InvalidationStrategy {
256    fn default() -> Self {
257        Self::Immediate
258    }
259}
260
261/// An invalidation handler that can be registered with the cache.
262pub trait InvalidationHandler: Send + Sync + 'static {
263    /// Handle an invalidation event.
264    fn handle(
265        &self,
266        event: &InvalidationEvent,
267    ) -> impl std::future::Future<Output = super::CacheResult<()>> + Send;
268}
269
270/// A simple function-based handler.
271pub struct FnHandler<F>(pub F);
272
273impl<F, Fut> InvalidationHandler for FnHandler<F>
274where
275    F: Fn(&InvalidationEvent) -> Fut + Send + Sync + 'static,
276    Fut: std::future::Future<Output = super::CacheResult<()>> + Send,
277{
278    async fn handle(&self, event: &InvalidationEvent) -> super::CacheResult<()> {
279        (self.0)(event).await
280    }
281}
282
283#[cfg(test)]
284mod tests {
285    use super::*;
286
287    #[test]
288    fn test_entity_tag() {
289        let tag = EntityTag::entity("User");
290        assert_eq!(tag.value(), "entity:User");
291
292        let record_tag = EntityTag::record("User", 123);
293        assert_eq!(record_tag.value(), "record:User:123");
294    }
295
296    #[test]
297    fn test_invalidation_event() {
298        let event = InvalidationEvent::insert("User")
299            .with_record(123)
300            .with_tag("custom_tag");
301
302        assert_eq!(event.entity, "User");
303        assert_eq!(event.record_id, Some("123".to_string()));
304        assert_eq!(event.event_type, InvalidationEventType::Insert);
305
306        let tags = event.all_tags();
307        assert!(tags.iter().any(|t| t.value() == "entity:User"));
308        assert!(tags.iter().any(|t| t.value() == "record:User:123"));
309    }
310
311    #[test]
312    fn test_invalidation_strategy() {
313        let immediate = InvalidationStrategy::immediate();
314        let event = InvalidationEvent::update("User");
315        assert!(immediate.should_invalidate(&event));
316
317        let events_only = InvalidationStrategy::on_events(vec![InvalidationEventType::Delete]);
318        assert!(!events_only.should_invalidate(&event));
319
320        let delete_event = InvalidationEvent::delete("User");
321        assert!(events_only.should_invalidate(&delete_event));
322    }
323
324    #[test]
325    fn test_tag_matching() {
326        let tag = EntityTag::new("entity:User");
327        assert!(tag.matches("entity:User"));
328        assert!(tag.matches("entity:*"));
329        assert!(!tag.matches("entity:Post"));
330    }
331}