Skip to main content

kevy_embedded/
ops_atomic_all.rs

1//! Cross-shard read-modify-write closure:
2//! `Store::atomic_all_shards`.
3//!
4//! `atomic_all_shards(|tx| { ... })` holds a write lock on every
5//! shard for the closure body. Operations inside the closure are
6//! routed to their owning shards, and AOF writes are batched
7//! per-shard with one fsync per shard at commit time.
8//!
9//! Heavier than [`Store::atomic`](crate::Store::atomic): every
10//! reader and writer on the affected shards blocks until the
11//! closure returns. Use it only when the closure genuinely needs
12//! more than one shard and atomicity across them is required.
13
14use crate::KevyResult;
15use std::sync::RwLockWriteGuard;
16
17use crate::shard::shard_idx;
18use crate::store::{Inner, Store, commit_write, store_err};
19
20use crate::store::ensure_writable;
21
22/// One key's pre-transaction state, plus the shard it lives on.
23/// `None` prior = the key did not exist.
24type ShardUndoEntry = (usize, Vec<u8>, Option<(kevy_store::Value, Option<u64>)>);
25
26/// Context handed to the `atomic_all_shards` closure body. Methods
27/// route to the right shard by hashing the key.
28///
29/// ```
30/// use kevy_embedded::{Config, Store};
31/// let s = Store::open(Config::default().with_shards(4))?;
32/// s.set(b"alice", b"10")?;
33/// // a transfer between keys that may live on different shards
34/// s.atomic_all_shards(|tx| {
35///     tx.incr_by(b"alice", -3)?;
36///     tx.incr_by(b"bob", 3)
37/// })?;
38/// assert_eq!((s.get(b"alice")?, s.get(b"bob")?), (Some(b"7".to_vec()), Some(b"3".to_vec())));
39/// # Ok::<(), kevy_embedded::KevyError>(())
40/// ```
41#[derive(Debug)]
42pub struct AtomicAllShards<'a> {
43    pub(crate) guards: Vec<RwLockWriteGuard<'a, Inner>>,
44    /// (shard_idx, serialised RESP-frame parts) queued for AOF commit.
45    log: Vec<(usize, Vec<Vec<u8>>)>,
46    /// `(shard_idx, key, prior)` captured on first touch; `None` prior
47    /// means the key did not exist. See [`Store::atomic`] — same
48    /// rollback contract, and worse to get wrong here because a
49    /// rejected transaction would otherwise diverge several shards at
50    /// once.
51    undo: Vec<ShardUndoEntry>,
52    touched: std::collections::HashSet<Vec<u8>>,
53    /// The index catalog, for the transaction-scoped index reads in
54    /// `ops_atomic_all_index.rs`. Held as a handle rather than reached
55    /// through `Store` because those reads must use the guards above,
56    /// not take the shard locks again.
57    #[cfg(feature = "index")]
58    pub(crate) indexes: std::sync::Arc<crate::ops_index::IndexReg>,
59}
60
61impl<'a> AtomicAllShards<'a> {
62    pub(crate) fn idx(&self, key: &[u8]) -> usize {
63        shard_idx(key, self.guards.len())
64    }
65
66    /// Record `key`'s prior state, once, before its first mutation.
67    fn snap(&mut self, key: &[u8]) {
68        if self.touched.contains(key) {
69            return;
70        }
71        let i = self.idx(key);
72        let prior = self.guards[i].store.clone_with_ttl(key);
73        self.touched.insert(key.to_vec());
74        self.undo.push((i, key.to_vec(), prior));
75    }
76
77    fn log_arg(&mut self, idx: usize, parts: &[&[u8]]) {
78        self.log.push((idx, parts.iter().map(|p| p.to_vec()).collect()));
79    }
80
81    // ---- string ops -----------------------------------------------
82
83    /// `SET key value` — always succeeds.
84    pub fn set(&mut self, key: &[u8], value: &[u8]) -> bool {
85        self.snap(key);
86        let i = self.idx(key);
87        let ok =
88            self.guards[i].store.set(key, value.to_vec(), None, kevy_store::SetCondition::Always);
89        self.log_arg(i, &[b"SET", key, value]);
90        ok
91    }
92
93    /// `GET key`.
94    pub fn get(&mut self, key: &[u8]) -> KevyResult<Option<Vec<u8>>> {
95        let i = self.idx(key);
96        self.guards[i]
97            .store
98            .get(key)
99            .map(|opt| opt.as_deref().map(<[u8]>::to_vec))
100            .map_err(store_err)
101    }
102
103    /// `INCR key`.
104    pub fn incr(&mut self, key: &[u8]) -> KevyResult<i64> {
105        self.snap(key);
106        let i = self.idx(key);
107        let n = self.guards[i].store.incr_by(key, 1).map_err(store_err)?;
108        self.log_arg(i, &[b"INCR", key]);
109        Ok(n)
110    }
111
112    /// `INCRBY key delta`.
113    pub fn incr_by(&mut self, key: &[u8], delta: i64) -> KevyResult<i64> {
114        self.snap(key);
115        let i = self.idx(key);
116        let n = self.guards[i].store.incr_by(key, delta).map_err(store_err)?;
117        let s = format!("{delta}");
118        self.log_arg(i, &[b"INCRBY", key, s.as_bytes()]);
119        Ok(n)
120    }
121
122    // ---- hash ops --------------------------------------------------
123
124    /// `HSET key field value [field value ...]`. Returns count newly
125    /// added (existing fields are overwritten but not counted).
126    pub fn hset(&mut self, key: &[u8], pairs: &[(&[u8], &[u8])]) -> KevyResult<usize> {
127        self.snap(key);
128        let i = self.idx(key);
129        let n = self.guards[i].store.hset(key, pairs).map_err(store_err)?;
130        let mut parts: Vec<&[u8]> = Vec::with_capacity(2 + pairs.len() * 2);
131        parts.push(b"HSET");
132        parts.push(key);
133        for (f, v) in pairs {
134            parts.push(f);
135            parts.push(v);
136        }
137        self.log_arg(i, &parts);
138        Ok(n)
139    }
140
141    /// `HGET key field` — `None` when the key or field is absent.
142    pub fn hget(&mut self, key: &[u8], field: &[u8]) -> KevyResult<Option<Vec<u8>>> {
143        let i = self.idx(key);
144        Ok(self.guards[i].store.hget(key, field).map_err(store_err)?.map(<[u8]>::to_vec))
145    }
146
147    /// `HINCRBY key field delta` — returns the field's new value.
148    pub fn hincrby(&mut self, key: &[u8], field: &[u8], delta: i64) -> KevyResult<i64> {
149        self.snap(key);
150        let i = self.idx(key);
151        let n = self.guards[i].store.hincrby(key, field, delta).map_err(store_err)?;
152        let s = format!("{delta}");
153        self.log_arg(i, &[b"HINCRBY", key, field, s.as_bytes()]);
154        Ok(n)
155    }
156
157    // ---- zset ops --------------------------------------------------
158
159    /// `ZADD key score member [score member ...]`. Returns count newly
160    /// added (score updates of existing members are not counted).
161    pub fn zadd(&mut self, key: &[u8], pairs: &[(f64, &[u8])]) -> KevyResult<usize> {
162        self.snap(key);
163        let i = self.idx(key);
164        let n = self.guards[i].store.zadd(key, pairs).map_err(store_err)?;
165        let score_strs: Vec<Vec<u8>> =
166            pairs.iter().map(|(s, _)| format!("{s}").into_bytes()).collect();
167        let mut parts: Vec<&[u8]> = Vec::with_capacity(2 + pairs.len() * 2);
168        parts.push(b"ZADD");
169        parts.push(key);
170        for (j, (_, m)) in pairs.iter().enumerate() {
171            parts.push(&score_strs[j]);
172            parts.push(m);
173        }
174        self.log_arg(i, &parts);
175        Ok(n)
176    }
177
178    /// `ZINCRBY key delta member` — returns the member's new score.
179    pub fn zincrby(&mut self, key: &[u8], delta: f64, member: &[u8]) -> KevyResult<f64> {
180        self.snap(key);
181        let i = self.idx(key);
182        let n = self.guards[i].store.zincrby(key, delta, member).map_err(store_err)?;
183        let s = format!("{delta}");
184        self.log_arg(i, &[b"ZINCRBY", key, s.as_bytes(), member]);
185        Ok(n)
186    }
187
188    /// `ZSCORE key member` (parity with [`super::ops_atomic::AtomicCtx`]).
189    pub fn zscore(&mut self, key: &[u8], member: &[u8]) -> KevyResult<Option<f64>> {
190        let i = self.idx(key);
191        self.guards[i].store.zscore(key, member).map_err(store_err)
192    }
193
194    // ---- keyspace ops (Pipeline write parity) ----------------------
195
196    /// `DEL key [key ...]` — keys may span shards; each key's delete
197    /// is applied and AOF-logged on its own shard.
198    pub fn del(&mut self, keys: &[&[u8]]) -> usize {
199        for k in keys {
200            self.snap(k);
201        }
202        let mut n = 0;
203        for k in keys {
204            let i = self.idx(k);
205            if self.guards[i].store.del(&[k]) > 0 {
206                n += 1;
207                self.log_arg(i, &[b"DEL", k]);
208            }
209        }
210        n
211    }
212
213    /// `EXISTS key [key ...]` — count of the given keys that exist.
214    pub fn exists(&mut self, keys: &[&[u8]]) -> usize {
215        keys.iter()
216            .filter(|k| {
217                let i = self.idx(k);
218                self.guards[i].store.key_exists(k)
219            })
220            .count()
221    }
222
223    // ---- hash ops --------------------------------------------------
224
225    /// `HDEL key field [field ...]`.
226    pub fn hdel(&mut self, key: &[u8], fields: &[&[u8]]) -> KevyResult<usize> {
227        self.snap(key);
228        let i = self.idx(key);
229        let removed = self.guards[i].store.hdel(key, fields).map_err(store_err)?;
230        if removed > 0 {
231            let mut argv: Vec<&[u8]> = Vec::with_capacity(2 + fields.len());
232            argv.push(b"HDEL");
233            argv.push(key);
234            argv.extend_from_slice(fields);
235            self.log_arg(i, &argv);
236        }
237        Ok(removed)
238    }
239
240    /// `HGETALL key` — `(field, value)` pairs.
241    pub fn hgetall(&mut self, key: &[u8]) -> KevyResult<Vec<(Vec<u8>, Vec<u8>)>> {
242        let i = self.idx(key);
243        let flat = self.guards[i].store.hgetall(key).map_err(store_err)?;
244        let mut out = Vec::with_capacity(flat.len() / 2);
245        let mut it = flat.into_iter();
246        while let (Some(f), Some(v)) = (it.next(), it.next()) {
247            out.push((f, v));
248        }
249        Ok(out)
250    }
251
252    /// `HMGET key field [field ...]` — `None` per absent field.
253    pub fn hmget(&mut self, key: &[u8], fields: &[&[u8]]) -> KevyResult<Vec<Option<Vec<u8>>>> {
254        let i = self.idx(key);
255        self.guards[i].store.hmget(key, fields).map_err(store_err)
256    }
257
258    /// `HEXISTS key field`.
259    pub fn hexists(&mut self, key: &[u8], field: &[u8]) -> KevyResult<bool> {
260        let i = self.idx(key);
261        self.guards[i].store.hexists(key, field).map_err(store_err)
262    }
263
264    // ---- set ops ---------------------------------------------------
265
266    /// `SADD key member [member ...]`.
267    pub fn sadd(&mut self, key: &[u8], members: &[&[u8]]) -> KevyResult<usize> {
268        self.snap(key);
269        let i = self.idx(key);
270        let added = self.guards[i].store.sadd(key, members).map_err(store_err)?;
271        if added > 0 {
272            let mut argv: Vec<&[u8]> = Vec::with_capacity(2 + members.len());
273            argv.push(b"SADD");
274            argv.push(key);
275            argv.extend_from_slice(members);
276            self.log_arg(i, &argv);
277        }
278        Ok(added)
279    }
280
281    /// `SREM key member [member ...]`.
282    pub fn srem(&mut self, key: &[u8], members: &[&[u8]]) -> KevyResult<usize> {
283        self.snap(key);
284        let i = self.idx(key);
285        let removed = self.guards[i].store.srem(key, members).map_err(store_err)?;
286        if removed > 0 {
287            let mut argv: Vec<&[u8]> = Vec::with_capacity(2 + members.len());
288            argv.push(b"SREM");
289            argv.push(key);
290            argv.extend_from_slice(members);
291            self.log_arg(i, &argv);
292        }
293        Ok(removed)
294    }
295
296    // ---- list ops --------------------------------------------------
297
298    /// `LPUSH key value [value ...]` — returns the new list length.
299    pub fn lpush(&mut self, key: &[u8], values: &[&[u8]]) -> KevyResult<usize> {
300        self.snap(key);
301        let i = self.idx(key);
302        let len = self.guards[i].store.lpush(key, values).map_err(store_err)?;
303        let mut argv: Vec<&[u8]> = Vec::with_capacity(2 + values.len());
304        argv.push(b"LPUSH");
305        argv.push(key);
306        argv.extend_from_slice(values);
307        self.log_arg(i, &argv);
308        Ok(len)
309    }
310
311    /// `RPUSH key value [value ...]` — returns the new list length.
312    pub fn rpush(&mut self, key: &[u8], values: &[&[u8]]) -> KevyResult<usize> {
313        self.snap(key);
314        let i = self.idx(key);
315        let len = self.guards[i].store.rpush(key, values).map_err(store_err)?;
316        let mut argv: Vec<&[u8]> = Vec::with_capacity(2 + values.len());
317        argv.push(b"RPUSH");
318        argv.push(key);
319        argv.extend_from_slice(values);
320        self.log_arg(i, &argv);
321        Ok(len)
322    }
323
324    // ---- zset ops --------------------------------------------------
325
326    /// `ZREM key member [member ...]`.
327    pub fn zrem(&mut self, key: &[u8], members: &[&[u8]]) -> KevyResult<usize> {
328        self.snap(key);
329        let i = self.idx(key);
330        let removed = self.guards[i].store.zrem(key, members).map_err(store_err)?;
331        if removed > 0 {
332            let mut argv: Vec<&[u8]> = Vec::with_capacity(2 + members.len());
333            argv.push(b"ZREM");
334            argv.push(key);
335            argv.extend_from_slice(members);
336            self.log_arg(i, &argv);
337        }
338        Ok(removed)
339    }
340
341    /// `ZCARD key` — member count; 0 when absent.
342    pub fn zcard(&mut self, key: &[u8]) -> KevyResult<usize> {
343        let i = self.idx(key);
344        self.guards[i].store.zcard(key).map_err(store_err)
345    }
346
347    /// Flags-aware `ZADD`. AOF logs the applied pairs as plain
348    /// `ZADD` — the effect, never the condition (deterministic replay).
349    pub fn zadd_flags(
350        &mut self,
351        key: &[u8],
352        pairs: &[(f64, &[u8])],
353        flags: kevy_store::ZaddFlags,
354    ) -> KevyResult<kevy_store::ZaddReport> {
355        let i = self.idx(key);
356        let rep = self.guards[i].store.zadd_flags(key, pairs, flags).map_err(store_err)?;
357        if !rep.applied.is_empty() {
358            let score_strs: Vec<Vec<u8>> =
359                rep.applied.iter().map(|(s, _)| format!("{s}").into_bytes()).collect();
360            let mut parts: Vec<&[u8]> = Vec::with_capacity(2 + rep.applied.len() * 2);
361            parts.push(b"ZADD");
362            parts.push(key);
363            for (j, (_, m)) in rep.applied.iter().enumerate() {
364                parts.push(&score_strs[j]);
365                parts.push(m);
366            }
367            self.log_arg(i, &parts);
368        }
369        Ok(rep)
370    }
371}
372
373impl Store {
374    /// Run `body` as a transaction holding write locks on EVERY
375    /// shard for the closure's duration. Reads inside the closure
376    /// see prior writes (full read-modify-write). On closure
377    /// return, AOF writes commit with one fsync per shard.
378    ///
379    /// Cost: blocks every other writer + reader on this Store for
380    /// the closure body. Use when atomic multi-shard semantics are
381    /// required; otherwise prefer the single-shard `atomic`.
382    pub fn atomic_all_shards<R>(
383        &self,
384        body: impl FnOnce(&mut AtomicAllShards<'_>) -> KevyResult<R>,
385    ) -> KevyResult<R> {
386        ensure_writable(self)?;
387        // Take every shard's write lock in shard-index order
388        // (deterministic order avoids deadlock).
389        let guards: Vec<RwLockWriteGuard<'_, Inner>> =
390            self.shards.iter().map(|s| s.write().expect("lock poisoned")).collect();
391        let mut ctx = AtomicAllShards {
392            guards,
393            log: Vec::new(),
394            undo: Vec::new(),
395            touched: std::collections::HashSet::new(),
396            #[cfg(feature = "index")]
397            indexes: std::sync::Arc::clone(&self.indexes),
398        };
399        let outcome = body(&mut ctx);
400        let log = std::mem::take(&mut ctx.log);
401        let undo = std::mem::take(&mut ctx.undo);
402        let r = match outcome {
403            Ok(r) => r,
404            Err(e) => {
405                rollback_all(&mut ctx.guards, undo);
406                return Err(e);
407            }
408        };
409        commit_group_all(&mut ctx.guards, log)?;
410        Ok(r)
411    }
412}
413
414/// Parity manifest: command names `AtomicAllShards` implements.
415/// MUST stay identical to `ops_atomic::ATOMIC_OPS` (the two ctxs
416/// drifted before — zscore was missing here).
417#[cfg_attr(not(test), allow(dead_code))]
418pub(crate) const ATOMIC_ALL_OPS: &[&str] = &[
419    "SET",
420    "GET",
421    "INCR",
422    "INCRBY",
423    "HSET",
424    "HGET",
425    "HINCRBY",
426    "ZADD",
427    "ZINCRBY",
428    "ZSCORE",
429    "DEL",
430    "EXISTS",
431    "HDEL",
432    "HGETALL",
433    "HMGET",
434    "HEXISTS",
435    "SADD",
436    "SREM",
437    "LPUSH",
438    "RPUSH",
439    "ZREM",
440    "ZCARD",
441    "SMEMBERS",
442    "SISMEMBER",
443    "LRANGE",
444    "LLEN",
445    "SCARD",
446    "ZRANGEBYSCORE",
447];
448
449/// Undo a rejected cross-shard transaction. See `Store::atomic`; reverse
450/// order so a key touched more than once lands on its earliest state.
451fn rollback_all(guards: &mut [RwLockWriteGuard<'_, Inner>], undo: Vec<ShardUndoEntry>) {
452    for (idx, key, prior) in undo.into_iter().rev() {
453        let g = &mut guards[idx];
454        match prior {
455            Some((value, ttl_ms)) => g.store.put_with_ttl(key, value, ttl_ms),
456            None => {
457                let k: &[u8] = &key;
458                g.store.del(&[k]);
459            }
460        }
461    }
462}
463
464/// Bracket and group-commit each shard's queued frames. The brackets make
465/// replay all-or-nothing at any size; the group makes `Fsync::Always`
466/// cost one sync per shard instead of one per frame.
467fn commit_group_all(
468    guards: &mut [RwLockWriteGuard<'_, Inner>],
469    log: Vec<(usize, Vec<Vec<u8>>)>,
470) -> KevyResult<()> {
471    #[cfg(feature = "persist")]
472    for g in guards.iter_mut() {
473        if let Some(aof) = g.aof.as_mut() {
474            aof.begin_group();
475        }
476    }
477    let mut commit = Ok(());
478    for (idx, parts) in log {
479        let g = &mut guards[idx];
480        let refs: Vec<&[u8]> = parts.iter().map(|v| v.as_slice()).collect();
481        commit = commit_write(g, &refs);
482        if commit.is_err() {
483            break;
484        }
485    }
486    #[cfg(feature = "persist")]
487    for g in guards.iter_mut() {
488        if let Some(aof) = g.aof.as_mut() {
489            let synced = aof.end_group().map_err(crate::KevyError::from);
490            if commit.is_ok() {
491                commit = synced;
492            }
493        }
494    }
495    commit
496}