Skip to main content

kevy_embedded/
ops_atomic.rs

1//! Single-shard read-modify-write closure: `Store::atomic`.
2//!
3//! `atomic(|tx| { ... })` holds the shard's write lock for the
4//! closure body. Reads inside the closure see prior writes inside
5//! the same closure, so read-modify-write loops work as expected.
6//! AOF writes are deferred and batched into a single fsync at
7//! commit time.
8//!
9//! Every key touched inside the closure must hash to the same
10//! shard. For closures that span shards use
11//! [`Store::atomic_all_shards`](crate::Store::atomic_all_shards).
12
13use crate::{KevyError, KevyResult};
14use std::sync::RwLockWriteGuard;
15
16use crate::store::ensure_writable;
17use crate::store::{Inner, Store, commit_write, store_err};
18
19/// One key's pre-transaction state: the key, and what was there before
20/// the transaction first touched it (`None` = the key did not exist).
21type UndoEntry = (Vec<u8>, Option<(kevy_store::Value, Option<u64>)>);
22
23/// Handle passed to the `atomic` closure body. Methods mirror the
24/// equivalent `Store` ops but operate on the already-held write
25/// lock, so reads inside the block see the closure's own writes.
26pub struct AtomicCtx<'a> {
27    inner: &'a mut Inner,
28    log: Vec<Vec<Vec<u8>>>,
29    /// Prior state of every key this transaction has touched, captured
30    /// on FIRST touch: `None` means the key did not exist. Replayed in
31    /// reverse by [`Store::atomic`] when the closure returns `Err`, so a
32    /// rejected transaction leaves neither memory nor the AOF changed.
33    undo: Vec<UndoEntry>,
34    /// Keys already in `undo` — a key is snapshotted once, before its
35    /// first mutation, never after.
36    touched: std::collections::HashSet<Vec<u8>>,
37}
38
39impl AtomicCtx<'_> {
40    // ---- string ops ------------------------------------------------
41
42    /// `SET key value`. Returns `true` (SET always succeeds without
43    /// `NX`/`XX` veto).
44    pub fn set(&mut self, key: &[u8], value: &[u8]) -> bool {
45        self.snap(key);
46        let ok = self.inner.store.set(key, value.to_vec(), None, false, false);
47        self.log_arg(&[b"SET", key, value]);
48        ok
49    }
50
51    /// `GET key`.
52    pub fn get(&mut self, key: &[u8]) -> KevyResult<Option<Vec<u8>>> {
53        self.inner.store.get(key).map(|opt| opt.as_deref().map(<[u8]>::to_vec)).map_err(store_err)
54    }
55
56    /// `INCR key` — by 1.
57    pub fn incr(&mut self, key: &[u8]) -> KevyResult<i64> {
58        self.snap(key);
59        let n = self.inner.store.incr_by(key, 1).map_err(store_err)?;
60        self.log_arg(&[b"INCR", key]);
61        Ok(n)
62    }
63
64    /// `INCRBY key delta`.
65    pub fn incr_by(&mut self, key: &[u8], delta: i64) -> KevyResult<i64> {
66        self.snap(key);
67        let n = self.inner.store.incr_by(key, delta).map_err(store_err)?;
68        let s = format!("{delta}");
69        self.log_arg(&[b"INCRBY", key, s.as_bytes()]);
70        Ok(n)
71    }
72
73    // ---- hash ops ---------------------------------------------------
74
75    /// `HSET key field value`.
76    pub fn hset(&mut self, key: &[u8], pairs: &[(&[u8], &[u8])]) -> KevyResult<usize> {
77        self.snap(key);
78        let n = self.inner.store.hset(key, pairs).map_err(store_err)?;
79        let mut parts: Vec<&[u8]> = Vec::with_capacity(2 + pairs.len() * 2);
80        parts.push(b"HSET");
81        parts.push(key);
82        for (f, v) in pairs {
83            parts.push(f);
84            parts.push(v);
85        }
86        self.log_arg(&parts);
87        Ok(n)
88    }
89
90    /// `HGET key field`.
91    pub fn hget(&mut self, key: &[u8], field: &[u8]) -> KevyResult<Option<Vec<u8>>> {
92        Ok(self.inner.store.hget(key, field).map_err(store_err)?.map(<[u8]>::to_vec))
93    }
94
95    /// `HINCRBY key field delta`.
96    pub fn hincrby(&mut self, key: &[u8], field: &[u8], delta: i64) -> KevyResult<i64> {
97        self.snap(key);
98        let n = self.inner.store.hincrby(key, field, delta).map_err(store_err)?;
99        let s = format!("{delta}");
100        self.log_arg(&[b"HINCRBY", key, field, s.as_bytes()]);
101        Ok(n)
102    }
103
104    // ---- zset ops ---------------------------------------------------
105
106    /// `ZADD key score member`.
107    pub fn zadd(&mut self, key: &[u8], pairs: &[(f64, &[u8])]) -> KevyResult<usize> {
108        self.snap(key);
109        let n = self.inner.store.zadd(key, pairs).map_err(store_err)?;
110        let score_strs: Vec<Vec<u8>> =
111            pairs.iter().map(|(s, _)| format!("{s}").into_bytes()).collect();
112        let mut parts: Vec<&[u8]> = Vec::with_capacity(2 + pairs.len() * 2);
113        parts.push(b"ZADD");
114        parts.push(key);
115        for (i, (_, m)) in pairs.iter().enumerate() {
116            parts.push(&score_strs[i]);
117            parts.push(m);
118        }
119        self.log_arg(&parts);
120        Ok(n)
121    }
122
123    /// `ZINCRBY key delta member`.
124    pub fn zincrby(&mut self, key: &[u8], delta: f64, member: &[u8]) -> KevyResult<f64> {
125        self.snap(key);
126        let n = self.inner.store.zincrby(key, delta, member).map_err(store_err)?;
127        let s = format!("{delta}");
128        self.log_arg(&[b"ZINCRBY", key, s.as_bytes(), member]);
129        Ok(n)
130    }
131
132    /// `ZSCORE key member`.
133    pub fn zscore(&mut self, key: &[u8], member: &[u8]) -> KevyResult<Option<f64>> {
134        self.inner.store.zscore(key, member).map_err(store_err)
135    }
136
137    // ---- helpers ----------------------------------------------------
138
139    // ---- keyspace ops (Pipeline write parity) ----------------------
140
141    /// `DEL key [key ...]` — every key must hash to this shard.
142    pub fn del(&mut self, keys: &[&[u8]]) -> usize {
143        for k in keys {
144            self.snap(k);
145        }
146        let n = self.inner.store.del(keys);
147        if n > 0 {
148            let mut argv: Vec<&[u8]> = Vec::with_capacity(1 + keys.len());
149            argv.push(b"DEL");
150            argv.extend_from_slice(keys);
151            self.log_arg(&argv);
152        }
153        n
154    }
155
156    /// `EXISTS key [key ...]` — count of the given keys that exist.
157    pub fn exists(&mut self, keys: &[&[u8]]) -> usize {
158        keys.iter().filter(|k| self.inner.store.key_exists(k)).count()
159    }
160
161    // ---- hash ops --------------------------------------------------
162
163    /// `HDEL key field [field ...]`.
164    pub fn hdel(&mut self, key: &[u8], fields: &[&[u8]]) -> KevyResult<usize> {
165        self.snap(key);
166        let removed = self.inner.store.hdel(key, fields).map_err(store_err)?;
167        if removed > 0 {
168            let mut argv: Vec<&[u8]> = Vec::with_capacity(2 + fields.len());
169            argv.push(b"HDEL");
170            argv.push(key);
171            argv.extend_from_slice(fields);
172            self.log_arg(&argv);
173        }
174        Ok(removed)
175    }
176
177    /// `HGETALL key` — `(field, value)` pairs; reads see the
178    /// closure's own prior writes.
179    pub fn hgetall(&mut self, key: &[u8]) -> KevyResult<Vec<(Vec<u8>, Vec<u8>)>> {
180        let flat = self.inner.store.hgetall(key).map_err(store_err)?;
181        let mut out = Vec::with_capacity(flat.len() / 2);
182        let mut it = flat.into_iter();
183        while let (Some(f), Some(v)) = (it.next(), it.next()) {
184            out.push((f, v));
185        }
186        Ok(out)
187    }
188
189    /// `HMGET key field [field ...]` — `None` per absent field.
190    pub fn hmget(&mut self, key: &[u8], fields: &[&[u8]]) -> KevyResult<Vec<Option<Vec<u8>>>> {
191        self.inner.store.hmget(key, fields).map_err(store_err)
192    }
193
194    /// `HEXISTS key field`.
195    pub fn hexists(&mut self, key: &[u8], field: &[u8]) -> KevyResult<bool> {
196        self.inner.store.hexists(key, field).map_err(store_err)
197    }
198
199    // ---- set ops ---------------------------------------------------
200
201    /// `SADD key member [member ...]`.
202    pub fn sadd(&mut self, key: &[u8], members: &[&[u8]]) -> KevyResult<usize> {
203        self.snap(key);
204        let added = self.inner.store.sadd(key, members).map_err(store_err)?;
205        if added > 0 {
206            let mut argv: Vec<&[u8]> = Vec::with_capacity(2 + members.len());
207            argv.push(b"SADD");
208            argv.push(key);
209            argv.extend_from_slice(members);
210            self.log_arg(&argv);
211        }
212        Ok(added)
213    }
214
215    /// `SREM key member [member ...]`.
216    pub fn srem(&mut self, key: &[u8], members: &[&[u8]]) -> KevyResult<usize> {
217        self.snap(key);
218        let removed = self.inner.store.srem(key, members).map_err(store_err)?;
219        if removed > 0 {
220            let mut argv: Vec<&[u8]> = Vec::with_capacity(2 + members.len());
221            argv.push(b"SREM");
222            argv.push(key);
223            argv.extend_from_slice(members);
224            self.log_arg(&argv);
225        }
226        Ok(removed)
227    }
228
229    // ---- list ops --------------------------------------------------
230
231    /// `LPUSH key value [value ...]` — returns the new list length.
232    pub fn lpush(&mut self, key: &[u8], values: &[&[u8]]) -> KevyResult<usize> {
233        self.snap(key);
234        let len = self.inner.store.lpush(key, values).map_err(store_err)?;
235        let mut argv: Vec<&[u8]> = Vec::with_capacity(2 + values.len());
236        argv.push(b"LPUSH");
237        argv.push(key);
238        argv.extend_from_slice(values);
239        self.log_arg(&argv);
240        Ok(len)
241    }
242
243    /// `RPUSH key value [value ...]` — returns the new list length.
244    pub fn rpush(&mut self, key: &[u8], values: &[&[u8]]) -> KevyResult<usize> {
245        self.snap(key);
246        let len = self.inner.store.rpush(key, values).map_err(store_err)?;
247        let mut argv: Vec<&[u8]> = Vec::with_capacity(2 + values.len());
248        argv.push(b"RPUSH");
249        argv.push(key);
250        argv.extend_from_slice(values);
251        self.log_arg(&argv);
252        Ok(len)
253    }
254
255    // ---- zset ops --------------------------------------------------
256
257    /// `ZREM key member [member ...]`.
258    pub fn zrem(&mut self, key: &[u8], members: &[&[u8]]) -> KevyResult<usize> {
259        self.snap(key);
260        let removed = self.inner.store.zrem(key, members).map_err(store_err)?;
261        if removed > 0 {
262            let mut argv: Vec<&[u8]> = Vec::with_capacity(2 + members.len());
263            argv.push(b"ZREM");
264            argv.push(key);
265            argv.extend_from_slice(members);
266            self.log_arg(&argv);
267        }
268        Ok(removed)
269    }
270
271    /// `ZCARD key` — member count; 0 when absent.
272    pub fn zcard(&mut self, key: &[u8]) -> KevyResult<usize> {
273        self.inner.store.zcard(key).map_err(store_err)
274    }
275
276    /// Flags-aware `ZADD`. AOF logs the applied pairs as plain
277    /// `ZADD` — the effect, never the condition (deterministic replay).
278    pub fn zadd_flags(
279        &mut self,
280        key: &[u8],
281        pairs: &[(f64, &[u8])],
282        flags: kevy_store::ZaddFlags,
283    ) -> KevyResult<kevy_store::ZaddReport> {
284        if !flags.valid() {
285            return Err(KevyError::InvalidInput("invalid ZADD flag combo".into()));
286        }
287        let rep = self.inner.store.zadd_flags(key, pairs, flags).map_err(store_err)?;
288        if !rep.applied.is_empty() {
289            let score_strs: Vec<Vec<u8>> =
290                rep.applied.iter().map(|(s, _)| format!("{s}").into_bytes()).collect();
291            let mut parts: Vec<&[u8]> = Vec::with_capacity(2 + rep.applied.len() * 2);
292            parts.push(b"ZADD");
293            parts.push(key);
294            for (i, (_, m)) in rep.applied.iter().enumerate() {
295                parts.push(&score_strs[i]);
296                parts.push(m);
297            }
298            self.log_arg(&parts);
299        }
300        Ok(rep)
301    }
302
303    // ---- collection reads --------------------------------------------
304    // Requested by a consumer: a set could be written inside a transaction but never read back
305    // inside one, so any child collection a cascade delete must
306    // enumerate had to be modelled as a hash — they reshaped a whole
307    // keyspace around the omission. These hold the shard write lock
308    // already, so there was never a consistency reason to withhold them.
309
310    /// `SMEMBERS key`.
311    pub fn smembers(&mut self, key: &[u8]) -> KevyResult<Vec<Vec<u8>>> {
312        self.inner.store.smembers(key).map_err(store_err)
313    }
314
315    /// `SISMEMBER key member`.
316    pub fn sismember(&mut self, key: &[u8], member: &[u8]) -> KevyResult<bool> {
317        self.inner.store.sismember(key, member).map_err(store_err)
318    }
319
320    /// `LRANGE key start stop` (inclusive, negatives count from the end).
321    pub fn lrange(&mut self, key: &[u8], start: i64, stop: i64) -> KevyResult<Vec<Vec<u8>>> {
322        self.inner.store.lrange(key, start, stop).map_err(store_err)
323    }
324
325    /// `LLEN key`.
326    pub fn llen(&mut self, key: &[u8]) -> KevyResult<usize> {
327        self.inner.store.llen(key).map_err(store_err)
328    }
329
330    /// `SCARD key`.
331    pub fn scard(&mut self, key: &[u8]) -> KevyResult<usize> {
332        self.inner.store.scard(key).map_err(store_err)
333    }
334
335    /// `ZRANGEBYSCORE key min max` — `(member, score)` in score order.
336    pub fn zrangebyscore(
337        &mut self,
338        key: &[u8],
339        min: kevy_store::ScoreBound,
340        max: kevy_store::ScoreBound,
341    ) -> KevyResult<Vec<(Vec<u8>, f64)>> {
342        self.inner.store.zrange_by_score(key, min, max).map_err(store_err)
343    }
344
345    /// Record `key`'s prior state, once, before its first mutation.
346    fn snap(&mut self, key: &[u8]) {
347        if self.touched.contains(key) {
348            return;
349        }
350        let prior = self.inner.store.clone_with_ttl(key);
351        self.touched.insert(key.to_vec());
352        self.undo.push((key.to_vec(), prior));
353    }
354
355    fn log_arg(&mut self, parts: &[&[u8]]) {
356        self.log.push(parts.iter().map(|p| p.to_vec()).collect());
357    }
358}
359
360impl Store {
361    /// Run `body` as a single-shard atomic transaction: it applies
362    /// entirely, or not at all.
363    ///
364    /// Inside the closure every read sees the closure's own previous
365    /// writes. On `Ok`, the queued AOF frames are committed as one
366    /// group — under `Fsync::Always` that is a single fsync for the
367    /// whole block, not one per mutation.
368    ///
369    /// On `Err`, **every write the closure made is rolled back** and
370    /// nothing is appended to the AOF. This is what lets the closure
371    /// act as the enforcement point for an invariant: read, decide,
372    /// write, and return `Err` to reject — the rejection leaves no
373    /// trace. (Before 4.0 the writes stayed live in memory while their
374    /// AOF frames were discarded, so a restarted process disagreed
375    /// with the running one.)
376    ///
377    /// Rollback restores each touched key to the value and TTL it had
378    /// before the transaction — including deleting keys the closure
379    /// created. It is a snapshot of the keys the closure touches, so
380    /// the cost scales with the transaction, not the keyspace.
381    ///
382    /// Constraint: every key touched inside the closure must hash to
383    /// the same shard. The default embedded config uses 1 shard, so
384    /// any key works.
385    pub fn atomic<R>(
386        &self,
387        body: impl FnOnce(&mut AtomicCtx<'_>) -> KevyResult<R>,
388    ) -> KevyResult<R> {
389        ensure_writable(self)?;
390        let mut g: RwLockWriteGuard<'_, Inner> = self.lock();
391        let mut ctx = AtomicCtx {
392            inner: &mut g,
393            log: Vec::new(),
394            undo: Vec::new(),
395            touched: std::collections::HashSet::new(),
396        };
397        let outcome = body(&mut ctx);
398        let log = std::mem::take(&mut ctx.log);
399        let undo = std::mem::take(&mut ctx.undo);
400        let r = match outcome {
401            Ok(r) => r,
402            Err(e) => {
403                rollback(&mut g, undo);
404                return Err(e);
405            }
406        };
407        commit_group(&mut g, log)?;
408        Ok(r)
409    }
410}
411
412/// Parity manifest: command names `AtomicCtx` implements.
413#[cfg_attr(not(test), allow(dead_code))]
414pub(crate) const ATOMIC_OPS: &[&str] = &[
415    "SET",
416    "GET",
417    "INCR",
418    "INCRBY",
419    "HSET",
420    "HGET",
421    "HINCRBY",
422    "ZADD",
423    "ZINCRBY",
424    "ZSCORE",
425    "DEL",
426    "EXISTS",
427    "HDEL",
428    "HGETALL",
429    "HMGET",
430    "HEXISTS",
431    "SADD",
432    "SREM",
433    "LPUSH",
434    "RPUSH",
435    "ZREM",
436    "ZCARD",
437    "SMEMBERS",
438    "SISMEMBER",
439    "LRANGE",
440    "LLEN",
441    "SCARD",
442    "ZRANGEBYSCORE",
443];
444
445/// Undo a rejected transaction.
446///
447/// The closure's writes hit the store as they were made — reads inside
448/// the block have to see them — so a rejected transaction must be undone
449/// here, or the rejected write stays live while its AOF frames are
450/// discarded and a restart disagrees with the running process. Reverse
451/// order so a key touched more than once lands on its earliest recorded
452/// state.
453fn rollback(g: &mut Inner, undo: Vec<UndoEntry>) {
454    for (key, prior) in undo.into_iter().rev() {
455        match prior {
456            Some((value, ttl_ms)) => g.store.put_with_ttl(key, value, ttl_ms),
457            None => {
458                let k: &[u8] = &key;
459                g.store.del(&[k]);
460            }
461        }
462    }
463}
464
465/// Commit the queued AOF frames as ONE bracketed group.
466///
467/// The brackets are what make replay all-or-nothing at any size, and the
468/// group is what makes `Fsync::Always` cost one sync instead of N. See
469/// `kevy_persist::Aof::begin_group`.
470fn commit_group(g: &mut Inner, log: Vec<Vec<Vec<u8>>>) -> KevyResult<()> {
471    #[cfg(feature = "persist")]
472    if let Some(aof) = g.aof.as_mut() {
473        aof.begin_group();
474    }
475    let mut commit = Ok(());
476    for entry in log {
477        let parts: Vec<&[u8]> = entry.iter().map(|v| v.as_slice()).collect();
478        commit = commit_write(g, &parts);
479        if commit.is_err() {
480            break;
481        }
482    }
483    #[cfg(feature = "persist")]
484    if let Some(aof) = g.aof.as_mut() {
485        let synced = aof.end_group().map_err(KevyError::from);
486        commit = commit.and(synced);
487    }
488    commit
489}