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}