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