Skip to main content

kevy_store/
keyspace.rs

1//! Generic key operations + persistence hooks on [`Store`]:
2//! `del`/`exists`/`expire`/`persist`/`pttl`/`type_of`/`dbsize`/`flush`/
3//! `snapshot_each`/`load_*`/`collect_keys`. Type-agnostic; typed accessors
4//! live in the per-type modules (string/hash/list/set/zset).
5//!
6//! Split out of [`crate`] for file-size hygiene.
7
8use std::time::{Duration, Instant};
9
10use crate::value::{HashData, SetData, Value, ZSetData};
11use crate::{Entry, RenameOutcome, SmallBytes, Store, glob_match, pack_deadline, unpack_deadline};
12
13impl Store {
14    // ---- generic key ops (type-agnostic) -------------------------------
15
16    pub fn del(&mut self, keys: &[Vec<u8>]) -> usize {
17        let now = Instant::now();
18        let mut removed = 0;
19        for k in keys {
20            if self.reap(k, now) && self.remove_entry(k.as_slice()).is_some() {
21                removed += 1;
22            }
23        }
24        removed
25    }
26
27    pub fn exists(&mut self, keys: &[Vec<u8>]) -> usize {
28        keys.iter().filter(|k| self.live_entry(k).is_some()).count()
29    }
30
31    pub fn expire(&mut self, key: &[u8], ttl: Duration) -> bool {
32        let now = Instant::now();
33        if !self.reap(key, now) {
34            return false;
35        }
36        if let Some(e) = self.map.get_mut(key) {
37            e.expire_at_ns = pack_deadline(now + ttl);
38            true
39        } else {
40            false
41        }
42    }
43
44    /// `EXPIREAT`/`PEXPIREAT` semantics: set an **absolute** wall-clock
45    /// deadline (Unix epoch millis). This is the persistence-safe form —
46    /// a deadline survives restart unchanged, unlike the relative
47    /// [`Self::expire`] (whose duration is re-anchored to "now"). A
48    /// deadline already in the past deletes the key immediately (Redis
49    /// behaviour). Returns `true` iff the key existed (and was either
50    /// re-dated or deleted). The wall-clock → monotonic-`Instant`
51    /// conversion happens here so callers persist absolute time but the
52    /// hot path keeps its cheap monotonic deadline.
53    pub fn expire_at_unix_ms(&mut self, key: &[u8], deadline_ms: u64) -> bool {
54        let now = Instant::now();
55        if !self.reap(key, now) || !self.map.contains_key(key) {
56            return false;
57        }
58        let wall_now = crate::now_unix_ms();
59        if deadline_ms <= wall_now {
60            // Past deadline: delete now, just like Redis EXPIREAT in the past.
61            self.remove_entry(key);
62            return true;
63        }
64        let remaining = Duration::from_millis(deadline_ms - wall_now);
65        if let Some(e) = self.map.get_mut(key) {
66            e.expire_at_ns = pack_deadline(now + remaining);
67        }
68        true
69    }
70
71    /// Cross-shard RENAME step 1: atomically remove the entry at
72    /// `key` (if any), returning the `(value, ttl_ms_remaining)`. The
73    /// orchestrator on the origin shard ships the result into a
74    /// follow-up [`Self::put_with_ttl`] on the destination shard.
75    /// Lazy-reaps an expired entry before the take (so an expired
76    /// key is observed as `None`, not silently rehomed).
77    pub fn take_with_ttl(&mut self, key: &[u8]) -> Option<(Value, Option<u64>)> {
78        let now = Instant::now();
79        if !self.reap(key, now) {
80            return None;
81        }
82        let entry = self.remove_entry(key)?;
83        let ttl_ms = entry.expire_at_ns.map(|ns| {
84            unpack_deadline(ns).saturating_duration_since(now).as_millis() as u64
85        });
86        Some((entry.value, ttl_ms))
87    }
88
89    /// Cross-shard RENAME step 2: write `value` at `key` on this
90    /// shard, overwriting any prior entry. `ttl_ms` is set as a TTL
91    /// relative to *now* (i.e. the orchestrator should have computed
92    /// the remaining TTL on the source shard via `take_with_ttl` and
93    /// is shipping that exact remaining value here).
94    pub fn put_with_ttl(&mut self, key: Vec<u8>, value: Value, ttl_ms: Option<u64>) {
95        let expire_at = ttl_ms.map(|ms| Instant::now() + Duration::from_millis(ms));
96        let entry = Entry::new(value, expire_at);
97        // Overwrite — drop any existing entry first so the accounting
98        // doesn't double-count.
99        self.remove_entry(&key);
100        self.insert_entry(SmallBytes::from_vec(key), entry);
101    }
102
103    /// Whether a live (non-expired) entry exists at `key`. Reaps an
104    /// expired entry as a side effect. Used by the cross-shard RENAME
105    /// orchestrator's `nx` pre-check.
106    pub fn key_exists(&mut self, key: &[u8]) -> bool {
107        let now = Instant::now();
108        self.reap(key, now) && self.map.contains_key(key)
109    }
110
111    /// `RENAME` (or `RENAMENX` if `nx`). Atomic on this shard. Returns
112    /// the outcome so the dispatch layer can emit the right RESP frame
113    /// (RENAME: `+OK` or `-ERR no such key`; RENAMENX: `:1`/`:0`/error).
114    ///
115    /// Cross-shard rename is the runtime's job — by the time this is
116    /// called, both `src` and `dst` are guaranteed to live on the same
117    /// shard. See `kevy-rt::start_rename` for the cross-shard split.
118    pub fn rename(&mut self, src: &[u8], dst: &[u8], nx: bool) -> RenameOutcome {
119        let now = Instant::now();
120        if !self.reap(src, now) {
121            return RenameOutcome::NoSuchSrc;
122        }
123        if src == dst {
124            // Redis 6+ semantics: same-key rename is a no-op `+OK`.
125            // (RENAMENX same-key returns `:0` per Redis since dst
126            // technically already exists at src's address.)
127            return if nx {
128                RenameOutcome::DstExists
129            } else {
130                RenameOutcome::Renamed
131            };
132        }
133        if nx {
134            // Reap dst before the existence test so a TTL-expired dst
135            // doesn't block the rename.
136            let dst_live = self.reap(dst, now) && self.map.contains_key(dst);
137            if dst_live {
138                return RenameOutcome::DstExists;
139            }
140        }
141        // Take src's entry out. `remove_entry` returns the full Entry
142        // (value + TTL) — preserves TTL across rename, matching Redis.
143        let Some(entry) = self.remove_entry(src) else {
144            return RenameOutcome::NoSuchSrc;
145        };
146        // Drop any pre-existing dst (overwrite semantics). reap above
147        // already handled TTL-expired dst, but the live-dst case still
148        // needs removal.
149        self.remove_entry(dst);
150        self.insert_entry(SmallBytes::from_vec(dst.to_vec()), entry);
151        RenameOutcome::Renamed
152    }
153
154    pub fn persist(&mut self, key: &[u8]) -> bool {
155        let now = Instant::now();
156        if !self.reap(key, now) {
157            return false;
158        }
159        match self.map.get_mut(key) {
160            Some(e) if e.expire_at_ns.is_some() => {
161                e.expire_at_ns = None;
162                true
163            }
164            _ => false,
165        }
166    }
167
168    /// Remaining TTL in ms: `-2` no key, `-1` no expiry, else `>= 0`.
169    pub fn pttl(&mut self, key: &[u8]) -> i64 {
170        let now = Instant::now();
171        if !self.reap(key, now) {
172            return -2;
173        }
174        match self.map.get(key).and_then(|e| e.expire_at_ns) {
175            None => -1,
176            Some(ns) => unpack_deadline(ns)
177                .saturating_duration_since(now)
178                .as_millis() as i64,
179        }
180    }
181
182    pub fn type_of(&mut self, key: &[u8]) -> &'static str {
183        let now = Instant::now();
184        if !self.reap(key, now) {
185            return "none";
186        }
187        self.map.get(key).map_or("none", |e| e.value.type_name())
188    }
189
190    pub fn dbsize(&self) -> usize {
191        self.map.len()
192    }
193
194    pub fn flush(&mut self) {
195        self.map.clear();
196        self.used_memory = 0;
197        // peak is lifetime-cumulative; intentionally not reset.
198    }
199
200    /// Count live (non-expired) keys that carry a TTL — the size of the
201    /// "expire set" Redis tracks. Useful as an introspection signal for
202    /// confirming the TTL subsystem actually registered keys. O(n) over the
203    /// keyspace; call it for diagnostics, not on the hot path.
204    pub fn ttl_pending_count(&self) -> usize {
205        let now = Instant::now();
206        self.map
207            .values()
208            .filter(|e| e.expire_at_ns.is_some() && !e.is_expired_at(now))
209            .count()
210    }
211
212    // ---- persistence hooks ---------------------------------------------
213
214    /// Visit every live entry as `(key, &value, ttl_ms)` for snapshotting.
215    pub fn snapshot_each<F: FnMut(&[u8], &Value, Option<u64>)>(&self, mut f: F) {
216        let now = Instant::now();
217        for (k, e) in &self.map {
218            if e.is_expired_at(now) {
219                continue;
220            }
221            let ttl = e
222                .expire_at_ns
223                .map(|ns| unpack_deadline(ns).saturating_duration_since(now).as_millis() as u64);
224            f(k.as_slice(), &e.value, ttl);
225        }
226    }
227
228    fn insert_loaded(&mut self, key: Vec<u8>, value: Value, ttl_ms: Option<u64>) {
229        let expire_at = ttl_ms.map(|ms| Instant::now() + Duration::from_millis(ms));
230        self.insert_entry(SmallBytes::from_vec(key), Entry::new(value, expire_at));
231    }
232
233    pub fn load_str(&mut self, key: Vec<u8>, value: Vec<u8>, ttl_ms: Option<u64>) {
234        self.insert_loaded(key, Value::Str(SmallBytes::from_vec(value)), ttl_ms);
235    }
236
237    pub fn load_hash(
238        &mut self,
239        key: Vec<u8>,
240        fields: Vec<(Vec<u8>, Vec<u8>)>,
241        ttl_ms: Option<u64>,
242    ) {
243        // Hash keys are SmallBytes; values stay Vec<u8>. From-iter converts.
244        let hash_data: HashData = fields
245            .into_iter()
246            .map(|(f, v)| (SmallBytes::from_vec(f), v))
247            .collect();
248        self.insert_loaded(key, Value::Hash(Box::new(hash_data)), ttl_ms);
249    }
250
251    pub fn load_list(&mut self, key: Vec<u8>, items: Vec<Vec<u8>>, ttl_ms: Option<u64>) {
252        self.insert_loaded(key, Value::List(Box::new(items.into_iter().collect())), ttl_ms);
253    }
254
255    pub fn load_set(&mut self, key: Vec<u8>, members: Vec<Vec<u8>>, ttl_ms: Option<u64>) {
256        let set_data: SetData = members.into_iter().map(SmallBytes::from_vec).collect();
257        self.insert_loaded(key, Value::Set(Box::new(set_data)), ttl_ms);
258    }
259
260    /// Collect live keys (optionally matching a glob `pattern`, up to `limit`).
261    /// Used by KEYS/SCAN/RANDOMKEY. Treats expired keys as absent (no removal).
262    pub fn collect_keys(&self, pattern: Option<&[u8]>, limit: Option<usize>) -> Vec<Vec<u8>> {
263        let now = Instant::now();
264        let mut out = Vec::new();
265        for (k, e) in &self.map {
266            if e.is_expired_at(now) {
267                continue;
268            }
269            if let Some(p) = pattern
270                && !glob_match(p, k.as_slice())
271            {
272                continue;
273            }
274            out.push(k.to_vec());
275            if limit.is_some_and(|lim| out.len() >= lim) {
276                break;
277            }
278        }
279        out
280    }
281
282    pub fn load_zset(&mut self, key: Vec<u8>, pairs: Vec<(Vec<u8>, f64)>, ttl_ms: Option<u64>) {
283        let mut z = ZSetData::default();
284        for (m, score) in pairs {
285            z.insert(&m, score);
286        }
287        self.insert_loaded(key, Value::ZSet(Box::new(z)), ttl_ms);
288    }
289
290    /// Snapshot-load a stream: every entry plus the per-stream scalar
291    /// state (last_id, max_deleted_id, entries_added) is restored
292    /// verbatim. Caller passes already-decoded primitive tuples; this
293    /// fn does the [`SmallBytes`] / [`crate::StreamData`] conversion.
294    pub fn load_stream(
295        &mut self,
296        key: Vec<u8>,
297        entries: Vec<crate::stream::LoadedStreamEntry>,
298        last_id: (u64, u64),
299        max_deleted_id: (u64, u64),
300        entries_added: u64,
301        ttl_ms: Option<u64>,
302    ) {
303        let mut s = crate::stream::StreamData::default();
304        for (ms, seq, fv) in entries {
305            let id = crate::stream::StreamId { ms, seq };
306            let fv_small: Vec<(SmallBytes, SmallBytes)> = fv
307                .into_iter()
308                .map(|(f, v)| (SmallBytes::from_vec(f), SmallBytes::from_vec(v)))
309                .collect();
310            s.load_entry(id, fv_small);
311        }
312        s.set_loaded_state(
313            crate::stream::StreamId { ms: last_id.0, seq: last_id.1 },
314            crate::stream::StreamId { ms: max_deleted_id.0, seq: max_deleted_id.1 },
315            entries_added,
316        );
317        self.insert_loaded(key, Value::Stream(Box::new(s)), ttl_ms);
318    }
319}