Skip to main content

kevy_embedded/
ops_pipeline.rs

1//! Non-atomic batched-fsync write queue: `Store::pipeline`.
2//!
3//! Builder-style queue: enqueue any number of writes via fluent
4//! methods, then `commit()` applies them in queue order. Per-shard
5//! AOF appends are batched into one fsync per shard at commit
6//! time, cutting fsync cost from `N` to `min(N, shard_count)`.
7//!
8//! `Pipeline` is not atomic — each op acquires its own per-shard
9//! write lock as it is applied, so other writers see intermediate
10//! states. For transactional semantics use
11//! [`Store::atomic`](crate::Store::atomic).
12
13use std::io;
14
15use crate::store::Store;
16
17/// Builder-style write queue. Returned by [`Store::pipeline`]; call
18/// fluent methods to enqueue + `commit()` to apply with batched
19/// AOF fsync.
20pub struct Pipeline<'a> {
21    store: &'a Store,
22    ops: Vec<PendingOp>,
23}
24
25enum PendingOp {
26    Set { key: Vec<u8>, value: Vec<u8> },
27    Del { keys: Vec<Vec<u8>> },
28    Incr { key: Vec<u8> },
29    IncrBy { key: Vec<u8>, delta: i64 },
30    HSet { key: Vec<u8>, pairs: Vec<(Vec<u8>, Vec<u8>)> },
31    HDel { key: Vec<u8>, fields: Vec<Vec<u8>> },
32    HIncrBy { key: Vec<u8>, field: Vec<u8>, delta: i64 },
33    ZAdd { key: Vec<u8>, pairs: Vec<(f64, Vec<u8>)> },
34    ZAddFlags { key: Vec<u8>, pairs: Vec<(f64, Vec<u8>)>, flags: kevy_store::ZaddFlags },
35    ZRem { key: Vec<u8>, members: Vec<Vec<u8>> },
36    ZIncrBy { key: Vec<u8>, delta: f64, member: Vec<u8> },
37    SAdd { key: Vec<u8>, members: Vec<Vec<u8>> },
38    SRem { key: Vec<u8>, members: Vec<Vec<u8>> },
39    LPush { key: Vec<u8>, values: Vec<Vec<u8>> },
40    RPush { key: Vec<u8>, values: Vec<Vec<u8>> },
41}
42
43impl<'a> Pipeline<'a> {
44    pub(crate) fn new(store: &'a Store) -> Self {
45        Self { store, ops: Vec::new() }
46    }
47
48    /// Number of ops queued so far.
49    pub fn len(&self) -> usize {
50        self.ops.len()
51    }
52
53    /// `true` when no ops are queued.
54    pub fn is_empty(&self) -> bool {
55        self.ops.is_empty()
56    }
57
58    // ---- fluent enqueue --------------------------------------------
59
60    pub fn set(mut self, key: &[u8], value: &[u8]) -> Self {
61        self.ops.push(PendingOp::Set {
62            key: key.to_vec(),
63            value: value.to_vec(),
64        });
65        self
66    }
67
68    pub fn del(mut self, keys: &[&[u8]]) -> Self {
69        self.ops.push(PendingOp::Del {
70            keys: keys.iter().map(|k| k.to_vec()).collect(),
71        });
72        self
73    }
74
75    pub fn incr(mut self, key: &[u8]) -> Self {
76        self.ops.push(PendingOp::Incr { key: key.to_vec() });
77        self
78    }
79
80    pub fn incr_by(mut self, key: &[u8], delta: i64) -> Self {
81        self.ops.push(PendingOp::IncrBy { key: key.to_vec(), delta });
82        self
83    }
84
85    pub fn hset(mut self, key: &[u8], pairs: &[(&[u8], &[u8])]) -> Self {
86        self.ops.push(PendingOp::HSet {
87            key: key.to_vec(),
88            pairs: pairs.iter().map(|(f, v)| (f.to_vec(), v.to_vec())).collect(),
89        });
90        self
91    }
92
93    pub fn hdel(mut self, key: &[u8], fields: &[&[u8]]) -> Self {
94        self.ops.push(PendingOp::HDel {
95            key: key.to_vec(),
96            fields: fields.iter().map(|f| f.to_vec()).collect(),
97        });
98        self
99    }
100
101    pub fn hincrby(mut self, key: &[u8], field: &[u8], delta: i64) -> Self {
102        self.ops.push(PendingOp::HIncrBy {
103            key: key.to_vec(),
104            field: field.to_vec(),
105            delta,
106        });
107        self
108    }
109
110    pub fn zadd(mut self, key: &[u8], pairs: &[(f64, &[u8])]) -> Self {
111        self.ops.push(PendingOp::ZAdd {
112            key: key.to_vec(),
113            pairs: pairs.iter().map(|(s, m)| (*s, m.to_vec())).collect(),
114        });
115        self
116    }
117
118    /// Flags-aware `ZADD` (v2.1) — e.g. the `GT` monotonic-heal form.
119    pub fn zadd_flags(
120        mut self,
121        key: &[u8],
122        pairs: &[(f64, &[u8])],
123        flags: kevy_store::ZaddFlags,
124    ) -> Self {
125        self.ops.push(PendingOp::ZAddFlags {
126            key: key.to_vec(),
127            pairs: pairs.iter().map(|(s, m)| (*s, m.to_vec())).collect(),
128            flags,
129        });
130        self
131    }
132
133    pub fn zrem(mut self, key: &[u8], members: &[&[u8]]) -> Self {
134        self.ops.push(PendingOp::ZRem {
135            key: key.to_vec(),
136            members: members.iter().map(|m| m.to_vec()).collect(),
137        });
138        self
139    }
140
141    pub fn zincrby(mut self, key: &[u8], delta: f64, member: &[u8]) -> Self {
142        self.ops.push(PendingOp::ZIncrBy {
143            key: key.to_vec(),
144            delta,
145            member: member.to_vec(),
146        });
147        self
148    }
149
150    pub fn sadd(mut self, key: &[u8], members: &[&[u8]]) -> Self {
151        self.ops.push(PendingOp::SAdd {
152            key: key.to_vec(),
153            members: members.iter().map(|m| m.to_vec()).collect(),
154        });
155        self
156    }
157
158    pub fn srem(mut self, key: &[u8], members: &[&[u8]]) -> Self {
159        self.ops.push(PendingOp::SRem {
160            key: key.to_vec(),
161            members: members.iter().map(|m| m.to_vec()).collect(),
162        });
163        self
164    }
165
166    pub fn lpush(mut self, key: &[u8], values: &[&[u8]]) -> Self {
167        self.ops.push(PendingOp::LPush {
168            key: key.to_vec(),
169            values: values.iter().map(|v| v.to_vec()).collect(),
170        });
171        self
172    }
173
174    pub fn rpush(mut self, key: &[u8], values: &[&[u8]]) -> Self {
175        self.ops.push(PendingOp::RPush {
176            key: key.to_vec(),
177            values: values.iter().map(|v| v.to_vec()).collect(),
178        });
179        self
180    }
181
182    /// Apply every queued op in order. Each op acquires its own
183    /// per-shard write lock — other writers see intermediate states
184    /// between ops; for transactional semantics use [`Store::atomic`]
185    /// instead. AOF appends batch into one fsync per shard.
186    pub fn commit(mut self) -> io::Result<()> {
187        let ops = std::mem::take(&mut self.ops);
188        for op in ops {
189            self.apply_one(op)?;
190        }
191        Ok(())
192    }
193
194    fn apply_one(&self, op: PendingOp) -> io::Result<()> {
195        match op {
196            PendingOp::Set { key, value } => {
197                self.store.set(&key, &value)?;
198            }
199            PendingOp::Del { keys } => {
200                let refs: Vec<&[u8]> = keys.iter().map(|k| k.as_slice()).collect();
201                self.store.del(&refs)?;
202            }
203            PendingOp::Incr { key } => {
204                self.store.incr(&key)?;
205            }
206            PendingOp::IncrBy { key, delta } => {
207                self.store.incr_by(&key, delta)?;
208            }
209            PendingOp::HSet { key, pairs } => {
210                let refs: Vec<(&[u8], &[u8])> =
211                    pairs.iter().map(|(f, v)| (f.as_slice(), v.as_slice())).collect();
212                self.store.hset(&key, &refs)?;
213            }
214            PendingOp::HDel { key, fields } => {
215                let refs: Vec<&[u8]> = fields.iter().map(|f| f.as_slice()).collect();
216                self.store.hdel(&key, &refs)?;
217            }
218            PendingOp::HIncrBy { key, field, delta } => {
219                self.store.hincrby(&key, &field, delta)?;
220            }
221            PendingOp::ZAdd { key, pairs } => {
222                let refs: Vec<(f64, &[u8])> =
223                    pairs.iter().map(|(s, m)| (*s, m.as_slice())).collect();
224                self.store.zadd(&key, &refs)?;
225            }
226            PendingOp::ZAddFlags { key, pairs, flags } => {
227                let refs: Vec<(f64, &[u8])> =
228                    pairs.iter().map(|(s, m)| (*s, m.as_slice())).collect();
229                self.store.zadd_flags(&key, &refs, flags)?;
230            }
231            PendingOp::ZRem { key, members } => {
232                let refs: Vec<&[u8]> = members.iter().map(|m| m.as_slice()).collect();
233                self.store.zrem(&key, &refs)?;
234            }
235            PendingOp::ZIncrBy { key, delta, member } => {
236                self.store.zincrby(&key, delta, &member)?;
237            }
238            PendingOp::SAdd { key, members } => {
239                let refs: Vec<&[u8]> = members.iter().map(|m| m.as_slice()).collect();
240                self.store.sadd(&key, &refs)?;
241            }
242            PendingOp::SRem { key, members } => {
243                let refs: Vec<&[u8]> = members.iter().map(|m| m.as_slice()).collect();
244                self.store.srem(&key, &refs)?;
245            }
246            PendingOp::LPush { key, values } => {
247                let refs: Vec<&[u8]> = values.iter().map(|v| v.as_slice()).collect();
248                self.store.lpush(&key, &refs)?;
249            }
250            PendingOp::RPush { key, values } => {
251                let refs: Vec<&[u8]> = values.iter().map(|v| v.as_slice()).collect();
252                self.store.rpush(&key, &refs)?;
253            }
254        }
255        Ok(())
256    }
257}
258
259impl Store {
260    /// Begin a [`Pipeline`] — fluent write queue. Add ops via
261    /// `.set(...).hset(...).zadd(...)` then call `.commit()`.
262    pub fn pipeline(&self) -> Pipeline<'_> {
263        Pipeline::new(self)
264    }
265}
266
267/// Parity manifest (v2.1): command names `Pipeline` implements.
268/// Cross-checked against `kevy_resp::ops_table` in
269/// `store_tests_op_table.rs` — update BOTH when adding an op.
270#[cfg_attr(not(test), allow(dead_code))]
271pub(crate) const PIPELINE_OPS: &[&str] = &[
272    "SET", "DEL", "INCR", "INCRBY", "HSET", "HDEL", "HINCRBY", "ZADD",
273    "ZREM", "ZINCRBY", "SADD", "SREM", "LPUSH", "RPUSH",
274];