Skip to main content

kevy_embedded/
ops_p3.rs

1//! Multi-key string operations, keyspace scan, atomic `getex`, set
2//! algebra (`sinter` / `sunion` / `sdiff`), and absolute-time TTL
3//! variants (`expireat` / `pexpire`).
4//!
5//! The set algebra is implemented at the embedded layer (compose
6//! `smembers` per key + Rust set operations) instead of touching
7//! `kevy_store::Store` — over N small sets that is faster than
8//! serialising N RESP arrays.
9
10use crate::KevyResult;
11use std::collections::BTreeSet;
12use std::time::Duration;
13
14use crate::store::ensure_writable;
15use crate::store::{Store, commit_write, store_err};
16
17impl Store {
18    // ---- multi-key string ops ---------------------------------------
19
20    /// `MSET key value [key value ...]` — set every pair atomically
21    /// per-key. Each pair is logged independently to its shard's
22    /// AOF (no cross-shard atomic guarantee — a crash mid-call may
23    /// leave a prefix applied; matches Redis Cluster semantics).
24    pub fn mset(&self, pairs: &[(&[u8], &[u8])]) -> KevyResult<()> {
25        ensure_writable(self)?;
26        for (k, v) in pairs {
27            let mut g = self.wshard(k);
28            g.store.set(k, v.to_vec(), None, false, false);
29            commit_write(&mut g, &[b"SET", k, v])?;
30        }
31        Ok(())
32    }
33
34    /// `MGET key [key ...]` — return `Some(value)` per requested key
35    /// that's present, `None` per absent / wrong-type.
36    ///
37    /// The wrong-type half of that sentence was prose only: the read
38    /// propagated the store's `WrongType` with `?`, so one list among
39    /// the keys turned the whole call into an error. Redis returns nil
40    /// for a key that does not hold a string and never errors here —
41    /// which is what the server's gather already did, and how the two
42    /// surfaces came to disagree in `differential_wire_vs_embedded`.
43    pub fn mget(&self, keys: &[&[u8]]) -> KevyResult<Vec<Option<Vec<u8>>>> {
44        let mut out = Vec::with_capacity(keys.len());
45        for k in keys {
46            let got = match self.wshard(k).store.get(k) {
47                Ok(v) => v.as_deref().map(<[u8]>::to_vec),
48                Err(kevy_store::StoreError::WrongType) => None,
49                // Not reachable through a hot keyspace, and it stays:
50                // `Store::get` propagates the tiering path's errors
51                // too, and a cold-tier read that failed must not be
52                // answered as "this key holds nothing".
53                Err(e) => return Err(store_err(e)),
54            };
55            out.push(got);
56        }
57        Ok(out)
58    }
59
60    // ---- keyspace introspection -------------------------------------
61
62    /// `KEYS pattern` — glob-match every key in the keyspace
63    /// (across all shards). `pattern = None` matches everything.
64    /// `limit = None` is unbounded; otherwise bounds the TOTAL
65    /// returned across shards. Glob syntax matches Redis (`*` /
66    /// `?` / `[abc]` / escape).
67    pub fn keys(&self, pattern: Option<&[u8]>, limit: Option<usize>) -> Vec<Vec<u8>> {
68        self.collect_keys(pattern, limit)
69    }
70
71    // ---- atomic get + TTL -------------------------------------------
72
73    /// `GETEX key TTL` — get the value and update the TTL atomically
74    /// (single lock cycle on the owning shard). Returns the value;
75    /// `None` when absent. AOF-logged as an absolute `PEXPIREAT` — a
76    /// relative frame re-anchors on every replay, handing the key its
77    /// full TTL back at each restart (the incident class the absolute
78    /// form exists to prevent; this was its last relative holdout).
79    pub fn getex(&self, key: &[u8], ttl: Duration) -> KevyResult<Option<Vec<u8>>> {
80        ensure_writable(self)?;
81        let mut g = self.wshard(key);
82        let val = g.store.get(key).map_err(store_err)?.as_deref().map(<[u8]>::to_vec);
83        if val.is_some() {
84            g.store.expire(key, ttl);
85            let ms = ttl.as_millis().min(u128::from(u64::MAX)) as u64;
86            let deadline = kevy_store::now_unix_ms().saturating_add(ms);
87            commit_write(&mut g, &[b"PEXPIREAT", key, deadline.to_string().as_bytes()])?;
88        }
89        Ok(val)
90    }
91
92    // ---- set algebra (compose-side, not Store-side) ------------------
93
94    /// `SINTER key [key ...]` — set intersection. Reads each key's
95    /// members, computes the intersection in BTreeSet order
96    /// (sorted, no duplicates).
97    pub fn sinter(&self, keys: &[&[u8]]) -> KevyResult<Vec<Vec<u8>>> {
98        if keys.is_empty() {
99            return Ok(Vec::new());
100        }
101        let first: BTreeSet<Vec<u8>> = self.smembers(keys[0])?.into_iter().collect();
102        let mut acc = first;
103        for k in &keys[1..] {
104            if acc.is_empty() {
105                break;
106            }
107            let next: BTreeSet<Vec<u8>> = self.smembers(k)?.into_iter().collect();
108            acc.retain(|m| next.contains(m));
109        }
110        Ok(acc.into_iter().collect())
111    }
112
113    /// `SUNION key [key ...]` — set union over N sets.
114    pub fn sunion(&self, keys: &[&[u8]]) -> KevyResult<Vec<Vec<u8>>> {
115        let mut acc: BTreeSet<Vec<u8>> = BTreeSet::new();
116        for k in keys {
117            for m in self.smembers(k)? {
118                acc.insert(m);
119            }
120        }
121        Ok(acc.into_iter().collect())
122    }
123
124    /// `SDIFF key [key ...]` — `keys[0]` minus the union of every
125    /// subsequent set.
126    pub fn sdiff(&self, keys: &[&[u8]]) -> KevyResult<Vec<Vec<u8>>> {
127        if keys.is_empty() {
128            return Ok(Vec::new());
129        }
130        let mut acc: BTreeSet<Vec<u8>> = self.smembers(keys[0])?.into_iter().collect();
131        for k in &keys[1..] {
132            let next: BTreeSet<Vec<u8>> = self.smembers(k)?.into_iter().collect();
133            acc.retain(|m| !next.contains(m));
134        }
135        Ok(acc.into_iter().collect())
136    }
137
138    // ---- absolute-time TTL variants ----------------------------------
139
140    /// `EXPIREAT key unix_secs` — schedule expiry for the given
141    /// absolute UNIX wall-clock time. Returns `true` when the key
142    /// existed and the deadline was set; `false` when absent.
143    pub fn expireat(&self, key: &[u8], unix_secs: u64) -> KevyResult<bool> {
144        ensure_writable(self)?;
145        let mut g = self.wshard(key);
146        let unix_ms = unix_secs.saturating_mul(1000);
147        let ok = g.store.expire_at_unix_ms(key, unix_ms);
148        if ok {
149            let ts_str = format!("{unix_ms}");
150            commit_write(&mut g, &[b"PEXPIREAT", key, ts_str.as_bytes()])?;
151        }
152        Ok(ok)
153    }
154
155    /// `PEXPIREAT key unix_ms` — same as `expireat` but in
156    /// milliseconds.
157    pub fn pexpireat(&self, key: &[u8], unix_ms: u64) -> KevyResult<bool> {
158        ensure_writable(self)?;
159        let mut g = self.wshard(key);
160        let ok = g.store.expire_at_unix_ms(key, unix_ms);
161        if ok {
162            let ts_str = format!("{unix_ms}");
163            commit_write(&mut g, &[b"PEXPIREAT", key, ts_str.as_bytes()])?;
164        }
165        Ok(ok)
166    }
167
168    /// `PEXPIRE key ms` — relative TTL in milliseconds. (`expire`
169    /// takes `Duration`; this is the integer-ms variant matching
170    /// the Redis wire command.)
171    pub fn pexpire(&self, key: &[u8], ms: u64) -> KevyResult<bool> {
172        self.expire(key, Duration::from_millis(ms))
173    }
174
175    // ---- hash float increment ----------------------------------------
176
177    /// `HINCRBYFLOAT key field delta` — atomic float increment of a
178    /// hash field. Returns the post-increment value. Errors on
179    /// `NotFloat` when the field is present but not parseable.
180    pub fn hincrbyfloat(&self, key: &[u8], field: &[u8], delta: f64) -> KevyResult<f64> {
181        ensure_writable(self)?;
182        let mut g = self.wshard(key);
183        let new_val = g.store.hincrbyfloat(key, field, delta).map_err(store_err)?;
184        let delta_str = format!("{delta}");
185        commit_write(&mut g, &[b"HINCRBYFLOAT", key, field, delta_str.as_bytes()])?;
186        Ok(new_val)
187    }
188
189    // ---- list positional insert --------------------------------------
190
191    /// `LINSERT key BEFORE|AFTER pivot value` — insert `value` before
192    /// or after the first occurrence of `pivot` in the list. Returns:
193    /// - `Ok(new_len)` on success (`>= 1`);
194    /// - `Ok(0)` when `key` does not exist;
195    /// - `Ok(-1)` when `pivot` was not found in the list.
196    ///
197    /// `before = true` matches Redis `LINSERT … BEFORE`, `false`
198    /// matches `LINSERT … AFTER`.
199    pub fn linsert(&self, key: &[u8], before: bool, pivot: &[u8], value: &[u8]) -> KevyResult<i64> {
200        ensure_writable(self)?;
201        let mut g = self.wshard(key);
202        let new_len = g.store.linsert(key, before, pivot, value).map_err(store_err)?;
203        if new_len > 0 {
204            let dir = if before { b"BEFORE".as_slice() } else { b"AFTER".as_slice() };
205            commit_write(&mut g, &[b"LINSERT", key, dir, pivot, value])?;
206        }
207        Ok(new_len)
208    }
209
210    // ---- observability ----------------------------------------------
211
212    /// `Store::ping_us()` — return the round-trip duration of a
213    /// shard-0 read-lock acquire + release in **nanoseconds**, for
214    /// perfgate observability. Always returns immediately; the
215    /// duration reflects current shard-0 contention (= shorter when
216    /// idle, longer when many readers/writers compete).
217    pub fn ping_ns(&self) -> u128 {
218        let t = std::time::Instant::now();
219        let _g = self.lock();
220        t.elapsed().as_nanos()
221    }
222}