Skip to main content

kevy_store/
notify.rs

1//! Store-origin keyspace-event capture: `new` (a key was created),
2//! `expired` (a TTL'd key was dropped — lazily on access or by the
3//! active reaper), and `evicted` (maxmemory pressure removed a key).
4//!
5//! These events originate INSIDE store operations, where no pub/sub
6//! machinery is in reach, so the store records them into a buffer the
7//! serving layer drains and publishes (after each write, and on the
8//! shard tick for reaper-origin batches). Capture is opt-in per kind
9//! — with the mask at its all-off default every hook is a single
10//! predicted-not-taken byte test, so embedders and disabled servers
11//! pay nothing.
12
13use crate::Store;
14#[cfg(not(feature = "std"))]
15use crate::nostd_prelude::*;
16
17/// One captured store-origin event kind. The serving layer maps these
18/// to the Redis event names (`new` / `expired` / `evicted`).
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20pub enum KeyspaceEvent {
21    /// A key was added to the keyspace.
22    New,
23    /// A TTL'd key was removed because its deadline passed.
24    Expired,
25    /// A key was removed by maxmemory eviction.
26    Evicted,
27}
28
29pub(crate) const CAPTURE_NEW: u8 = 1 << 0;
30pub(crate) const CAPTURE_EXPIRED: u8 = 1 << 1;
31pub(crate) const CAPTURE_EVICTED: u8 = 1 << 2;
32
33impl Store {
34    /// Choose which store-origin event kinds to capture. The serving
35    /// layer mirrors its notify-keyspace-events flags here; all-off
36    /// (the default) reduces every capture hook to one byte test.
37    pub fn set_notify_capture(&mut self, new_key: bool, expired: bool, evicted: bool) {
38        self.notify_capture = (u8::from(new_key) * CAPTURE_NEW)
39            | (u8::from(expired) * CAPTURE_EXPIRED)
40            | (u8::from(evicted) * CAPTURE_EVICTED);
41    }
42
43    /// Whether any events are waiting to be drained (one length read).
44    #[inline]
45    pub fn has_notify_events(&self) -> bool {
46        !self.notify_events.is_empty()
47    }
48
49    /// Whether any key has expired since the last drain.
50    #[inline]
51    pub fn has_expired_keys(&self) -> bool {
52        !self.expired_keys.is_empty()
53    }
54
55    /// Take the keys dropped by expiry since the last drain.
56    pub fn take_expired_keys(&mut self) -> Vec<Vec<u8>> {
57        core::mem::take(&mut self.expired_keys)
58    }
59
60    /// Take every captured event, in capture order.
61    pub fn take_notify_events(&mut self) -> Vec<(KeyspaceEvent, Vec<u8>)> {
62        core::mem::take(&mut self.notify_events)
63    }
64
65    #[inline]
66    pub(crate) fn note_expired(&mut self, key: &[u8]) {
67        // Always, whatever the notification flags say: the serving layer
68        // has to maintain derived state for this removal. Every expiry
69        // path — lazy `reap`, the single-lookup read, the active
70        // sampler — funnels through here, which is why the capture
71        // belongs here and not at each of them.
72        self.expired_keys.push(key.to_vec());
73        if self.notify_capture & CAPTURE_EXPIRED != 0 {
74            self.notify_events.push((KeyspaceEvent::Expired, key.to_vec()));
75        }
76    }
77
78    #[inline]
79    pub(crate) fn note_evicted(&mut self, key: &[u8]) {
80        if self.notify_capture & CAPTURE_EVICTED != 0 {
81            self.notify_events.push((KeyspaceEvent::Evicted, key.to_vec()));
82        }
83    }
84}