Skip to main content

hydracache_core/
events.rs

1use std::time::SystemTime;
2
3use crate::CacheKey;
4
5/// Kind of cache event emitted by a HydraCache runtime.
6///
7/// Access and loader events are intentionally separate from mutation events so
8/// applications can keep high-volume hit/miss reporting disabled until needed.
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
10pub enum CacheEventKind {
11    /// A lookup returned a cached value.
12    Hit,
13    /// A lookup did not find a usable cached value.
14    Miss,
15    /// A caller joined an already running single-flight load.
16    SingleFlightJoined,
17    /// The cache owner started a loader for a missing key.
18    LoadStarted,
19    /// A loader completed and its result was accepted into the cache.
20    LoadCompleted,
21    /// A loader returned an error or failed to encode the loaded value.
22    LoadFailed,
23    /// A value was stored.
24    Stored,
25    /// A key was explicitly removed.
26    Removed,
27    /// A key was explicitly invalidated.
28    KeyInvalidated,
29    /// A tag was invalidated.
30    TagInvalidated,
31    /// The cache was flushed.
32    Flushed,
33    /// A loader result was discarded because an invalidation made it stale.
34    StaleLoadDiscarded,
35    /// An entry expired and was removed during cache access.
36    Expired,
37    /// An entry was evicted by the backend.
38    Evicted,
39}
40
41impl CacheEventKind {
42    /// Return whether this event belongs to the high-volume access/load group.
43    pub fn is_access(self) -> bool {
44        matches!(
45            self,
46            Self::Hit
47                | Self::Miss
48                | Self::SingleFlightJoined
49                | Self::LoadStarted
50                | Self::LoadCompleted
51                | Self::LoadFailed
52        )
53    }
54
55    /// Return whether this event describes a cache mutation or invalidation.
56    pub fn is_mutation(self) -> bool {
57        !self.is_access()
58    }
59}
60
61/// Logical scope affected by a cache event.
62#[derive(Debug, Clone, PartialEq, Eq)]
63pub enum CacheEventScope {
64    /// Event for one cache key.
65    Key {
66        /// Physical cache key.
67        key: CacheKey<'static>,
68    },
69    /// Event for one invalidation tag.
70    Tag {
71        /// Invalidated tag.
72        tag: String,
73        /// Number of keys affected by the operation.
74        affected_keys: u64,
75    },
76    /// Event for the whole cache.
77    Cache {
78        /// Approximate number of keys affected, when known.
79        affected_keys: Option<u64>,
80    },
81}
82
83impl CacheEventScope {
84    /// Return the event key when this is a key-scoped event.
85    pub fn key(&self) -> Option<&str> {
86        match self {
87            Self::Key { key } => Some(key.as_str()),
88            Self::Tag { .. } | Self::Cache { .. } => None,
89        }
90    }
91
92    /// Return the event tag when this is a tag-scoped event.
93    pub fn tag(&self) -> Option<&str> {
94        match self {
95            Self::Tag { tag, .. } => Some(tag),
96            Self::Key { .. } | Self::Cache { .. } => None,
97        }
98    }
99
100    /// Return the affected-key count when the scope carries one.
101    pub fn affected_keys(&self) -> Option<u64> {
102        match self {
103            Self::Key { .. } => Some(1),
104            Self::Tag { affected_keys, .. } => Some(*affected_keys),
105            Self::Cache { affected_keys } => *affected_keys,
106        }
107    }
108}
109
110/// Origin of a cache event.
111#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
112pub enum CacheEventOrigin {
113    /// Event was caused by a direct local API call.
114    LocalApi,
115    /// Event was caused by a loader owner.
116    Loader,
117    /// Event was caused by single-flight coordination.
118    SingleFlight,
119    /// Event was caused by backend expiration or eviction.
120    Backend,
121    /// Event was received from a future distributed bus.
122    DistributedBus,
123}
124
125/// Value payload mode requested by event subscribers.
126///
127/// The first implementation emits metadata-only events. The enum exists so the
128/// public filter shape can grow toward encoded-value delivery without changing
129/// subscription options later.
130#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
131pub enum CacheEventValueMode {
132    /// Do not include cached values in events.
133    #[default]
134    MetadataOnly,
135    /// Reserve space for a future encoded-value event mode.
136    EncodedBytes,
137}
138
139/// Metadata-only cache event.
140#[derive(Debug, Clone, PartialEq, Eq)]
141pub struct CacheEvent {
142    kind: CacheEventKind,
143    scope: CacheEventScope,
144    origin: CacheEventOrigin,
145    tags: Vec<String>,
146    timestamp: SystemTime,
147}
148
149impl CacheEvent {
150    /// Create a key-scoped event.
151    pub fn for_key<I, S>(
152        kind: CacheEventKind,
153        key: impl Into<String>,
154        origin: CacheEventOrigin,
155        tags: I,
156    ) -> Self
157    where
158        I: IntoIterator<Item = S>,
159        S: Into<String>,
160    {
161        Self {
162            kind,
163            scope: CacheEventScope::Key {
164                key: CacheKey::from(key.into()),
165            },
166            origin,
167            tags: tags.into_iter().map(Into::into).collect(),
168            timestamp: SystemTime::now(),
169        }
170    }
171
172    /// Create a tag-scoped event.
173    pub fn for_tag(
174        kind: CacheEventKind,
175        tag: impl Into<String>,
176        affected_keys: u64,
177        origin: CacheEventOrigin,
178    ) -> Self {
179        let tag = tag.into();
180        Self {
181            kind,
182            scope: CacheEventScope::Tag {
183                tag: tag.clone(),
184                affected_keys,
185            },
186            origin,
187            tags: vec![tag],
188            timestamp: SystemTime::now(),
189        }
190    }
191
192    /// Create a cache-wide event.
193    pub fn for_cache(
194        kind: CacheEventKind,
195        affected_keys: Option<u64>,
196        origin: CacheEventOrigin,
197    ) -> Self {
198        Self {
199            kind,
200            scope: CacheEventScope::Cache { affected_keys },
201            origin,
202            tags: Vec::new(),
203            timestamp: SystemTime::now(),
204        }
205    }
206
207    /// Return the event kind.
208    pub fn kind(&self) -> CacheEventKind {
209        self.kind
210    }
211
212    /// Return the event scope.
213    pub fn scope(&self) -> &CacheEventScope {
214        &self.scope
215    }
216
217    /// Return the event origin.
218    pub fn origin(&self) -> CacheEventOrigin {
219        self.origin
220    }
221
222    /// Return the event key when this is a key-scoped event.
223    pub fn key(&self) -> Option<&str> {
224        self.scope.key()
225    }
226
227    /// Return the event tag when this is a tag-scoped event.
228    pub fn tag(&self) -> Option<&str> {
229        self.scope.tag()
230    }
231
232    /// Return event tags associated with the key or invalidation.
233    pub fn tags(&self) -> &[String] {
234        &self.tags
235    }
236
237    /// Return the affected-key count when known.
238    pub fn affected_keys(&self) -> Option<u64> {
239        self.scope.affected_keys()
240    }
241
242    /// Return the event timestamp.
243    pub fn timestamp(&self) -> SystemTime {
244        self.timestamp
245    }
246}
247
248/// Subscription filters for cache events.
249///
250/// Filters are intentionally metadata-only. They are cheap enough to apply in
251/// the subscriber wrapper and avoid coupling event publication to decoded
252/// values.
253#[derive(Debug, Clone, Default, PartialEq, Eq)]
254pub struct CacheEventOptions {
255    include_kinds: Option<Vec<CacheEventKind>>,
256    exclude_kinds: Vec<CacheEventKind>,
257    key: Option<String>,
258    key_prefix: Option<String>,
259    tag: Option<String>,
260    origin: Option<CacheEventOrigin>,
261    value_mode: CacheEventValueMode,
262}
263
264impl CacheEventOptions {
265    /// Create event options that accept all published events.
266    pub fn new() -> Self {
267        Self::default()
268    }
269
270    /// Create event options for mutation/invalidation events.
271    pub fn mutations() -> Self {
272        Self::new().include_kinds([
273            CacheEventKind::Stored,
274            CacheEventKind::Removed,
275            CacheEventKind::KeyInvalidated,
276            CacheEventKind::TagInvalidated,
277            CacheEventKind::Flushed,
278            CacheEventKind::StaleLoadDiscarded,
279            CacheEventKind::Expired,
280            CacheEventKind::Evicted,
281        ])
282    }
283
284    /// Create event options for high-volume access/load events.
285    pub fn access() -> Self {
286        Self::new().include_kinds([
287            CacheEventKind::Hit,
288            CacheEventKind::Miss,
289            CacheEventKind::SingleFlightJoined,
290            CacheEventKind::LoadStarted,
291            CacheEventKind::LoadCompleted,
292            CacheEventKind::LoadFailed,
293        ])
294    }
295
296    /// Include one event kind.
297    pub fn include_kind(self, kind: CacheEventKind) -> Self {
298        self.include_kinds([kind])
299    }
300
301    /// Include several event kinds.
302    pub fn include_kinds<I>(mut self, kinds: I) -> Self
303    where
304        I: IntoIterator<Item = CacheEventKind>,
305    {
306        self.include_kinds
307            .get_or_insert_with(Vec::new)
308            .extend(kinds);
309        self
310    }
311
312    /// Exclude one event kind.
313    pub fn exclude_kind(self, kind: CacheEventKind) -> Self {
314        self.exclude_kinds([kind])
315    }
316
317    /// Exclude several event kinds.
318    pub fn exclude_kinds<I>(mut self, kinds: I) -> Self
319    where
320        I: IntoIterator<Item = CacheEventKind>,
321    {
322        self.exclude_kinds.extend(kinds);
323        self
324    }
325
326    /// Restrict events to one exact key.
327    pub fn key(mut self, key: impl Into<String>) -> Self {
328        self.key = Some(key.into());
329        self
330    }
331
332    /// Restrict events to key-scoped events whose key starts with the prefix.
333    pub fn key_prefix(mut self, key_prefix: impl Into<String>) -> Self {
334        self.key_prefix = Some(key_prefix.into());
335        self
336    }
337
338    /// Restrict events to a tag.
339    pub fn tag(mut self, tag: impl Into<String>) -> Self {
340        self.tag = Some(tag.into());
341        self
342    }
343
344    /// Restrict events to one origin.
345    pub fn origin(mut self, origin: CacheEventOrigin) -> Self {
346        self.origin = Some(origin);
347        self
348    }
349
350    /// Set the value mode requested by this subscription.
351    pub fn value_mode(mut self, value_mode: CacheEventValueMode) -> Self {
352        self.value_mode = value_mode;
353        self
354    }
355
356    /// Return the requested value mode.
357    pub fn value_mode_value(&self) -> CacheEventValueMode {
358        self.value_mode
359    }
360
361    /// Return whether this event passes all filters.
362    pub fn matches(&self, event: &CacheEvent) -> bool {
363        if let Some(include_kinds) = &self.include_kinds {
364            if !include_kinds.contains(&event.kind()) {
365                return false;
366            }
367        }
368
369        if self.exclude_kinds.contains(&event.kind()) {
370            return false;
371        }
372
373        if let Some(key) = &self.key {
374            if event.key() != Some(key.as_str()) {
375                return false;
376            }
377        }
378
379        if let Some(key_prefix) = &self.key_prefix {
380            let Some(key) = event.key() else {
381                return false;
382            };
383            if !key.starts_with(key_prefix) {
384                return false;
385            }
386        }
387
388        if let Some(tag) = &self.tag {
389            let scope_matches = event.tag() == Some(tag.as_str());
390            let tags_match = event.tags().iter().any(|event_tag| event_tag == tag);
391            if !scope_matches && !tags_match {
392                return false;
393            }
394        }
395
396        if let Some(origin) = self.origin {
397            if event.origin() != origin {
398                return false;
399            }
400        }
401
402        true
403    }
404}
405
406#[cfg(test)]
407mod tests {
408    use super::{
409        CacheEvent, CacheEventKind, CacheEventOptions, CacheEventOrigin, CacheEventScope,
410        CacheEventValueMode,
411    };
412
413    #[test]
414    fn event_kind_groups_access_and_mutation_events() {
415        assert!(CacheEventKind::Hit.is_access());
416        assert!(CacheEventKind::LoadFailed.is_access());
417        assert!(!CacheEventKind::Stored.is_access());
418        assert!(CacheEventKind::Stored.is_mutation());
419    }
420
421    #[test]
422    fn event_constructors_capture_scope_tags_and_origin() {
423        let key_event = CacheEvent::for_key(
424            CacheEventKind::Stored,
425            "user:42",
426            CacheEventOrigin::LocalApi,
427            ["users", "user:42"],
428        );
429        let tag_event = CacheEvent::for_tag(
430            CacheEventKind::TagInvalidated,
431            "users",
432            3,
433            CacheEventOrigin::LocalApi,
434        );
435        let cache_event = CacheEvent::for_cache(
436            CacheEventKind::Flushed,
437            Some(10),
438            CacheEventOrigin::LocalApi,
439        );
440
441        assert_eq!(key_event.key(), Some("user:42"));
442        assert_eq!(
443            key_event.tags(),
444            &["users".to_owned(), "user:42".to_owned()]
445        );
446        assert_eq!(key_event.origin(), CacheEventOrigin::LocalApi);
447        assert_eq!(tag_event.tag(), Some("users"));
448        assert_eq!(tag_event.affected_keys(), Some(3));
449        assert_eq!(
450            cache_event.scope(),
451            &CacheEventScope::Cache {
452                affected_keys: Some(10)
453            }
454        );
455    }
456
457    #[test]
458    fn event_options_filter_by_kind_key_prefix_tag_and_origin() {
459        let stored = CacheEvent::for_key(
460            CacheEventKind::Stored,
461            "users:42",
462            CacheEventOrigin::LocalApi,
463            ["users"],
464        );
465        let removed = CacheEvent::for_key(
466            CacheEventKind::Removed,
467            "orders:7",
468            CacheEventOrigin::LocalApi,
469            ["orders"],
470        );
471
472        let options = CacheEventOptions::new()
473            .include_kind(CacheEventKind::Stored)
474            .key_prefix("users:")
475            .tag("users")
476            .origin(CacheEventOrigin::LocalApi);
477
478        assert!(options.matches(&stored));
479        assert!(!options.matches(&removed));
480        assert!(!options
481            .clone()
482            .exclude_kind(CacheEventKind::Stored)
483            .matches(&stored));
484    }
485
486    #[test]
487    fn event_options_mutations_and_access_presets_are_distinct() {
488        let stored = CacheEvent::for_key(
489            CacheEventKind::Stored,
490            "k",
491            CacheEventOrigin::LocalApi,
492            ["t"],
493        );
494        let hit = CacheEvent::for_key(CacheEventKind::Hit, "k", CacheEventOrigin::LocalApi, ["t"]);
495
496        assert!(CacheEventOptions::mutations().matches(&stored));
497        assert!(!CacheEventOptions::mutations().matches(&hit));
498        assert!(CacheEventOptions::access().matches(&hit));
499        assert!(!CacheEventOptions::access().matches(&stored));
500    }
501
502    #[test]
503    fn event_options_keep_requested_value_mode() {
504        let options = CacheEventOptions::new().value_mode(CacheEventValueMode::EncodedBytes);
505
506        assert_eq!(
507            options.value_mode_value(),
508            CacheEventValueMode::EncodedBytes
509        );
510    }
511}