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