kevy 6.0.0

kevy — a pure-Rust, zero-dependency, Redis-compatible KV server.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
//! Cross-shard BLOCK helpers that bridge `kevy_rt`'s arbiter to kevy's
//! command syntax: build the single-key **replay** command for one watched
//! key, and the non-destructive **readiness** peek for it. Lifted out of
//! `cmd_block.rs` to keep both files under the 500-LOC house rule.
//!
//! The runtime drives these via the [`kevy_rt::Commands`] hooks
//! `block_serve_argv` (origin, park time) and `block_ready` (target, arm
//! time) — see `kevy_rt::block_xshard` for the protocol.

use kevy_resp::{Argv, ArgvView};
use kevy_rt::{BlockKind, Store};

/// Build the single-key command the arbiter replays to satisfy one watched
/// `key`. `args` is the original (possibly multi-key) command; `$` is left
/// literal here and frozen later on the key's owning shard. The `BLOCK`
/// clause is preserved for the stream forms so the one-shot replay leaves
/// no output when the key is empty (the arbiter reads that as "raced").
pub(crate) fn block_serve_argv<A: ArgvView + ?Sized>(
    args: &A,
    kind: BlockKind,
    key: &[u8],
) -> Argv {
    match kind {
        BlockKind::Blpop => pop_serve(b"BLPOP", key),
        BlockKind::Brpop => pop_serve(b"BRPOP", key),
        BlockKind::Bzpopmin => pop_serve(b"BZPOPMIN", key),
        BlockKind::Brpoplpush => brpoplpush_serve(args, key),
        BlockKind::XReadBlock => xread_serve(args, key).unwrap_or_else(|| args.to_argv()),
        BlockKind::XReadGroupBlock => {
            xreadgroup_serve(args, key).unwrap_or_else(|| args.to_argv())
        }
    }
}

/// `BRPOPLPUSH src dst 0` — single-key form for the replay. `key`
/// (passed by the arbiter) is the source that woke; the destination
/// is at `args[2]` of the original `BRPOPLPUSH source destination
/// timeout` argv.
fn brpoplpush_serve<A: ArgvView + ?Sized>(args: &A, key: &[u8]) -> Argv {
    let mut a = Argv::default();
    a.push(b"BRPOPLPUSH");
    a.push(key);
    if let Some(dst) = args.get(2) {
        a.push(dst);
    } else {
        // Malformed original — fall back to a no-op key so dispatch
        // emits the args error rather than panicking.
        a.push(b"");
    }
    a.push(b"0");
    a
}

/// `BLPOP key 0` / `BRPOP key 0` — a single-key, block-forever replay; the
/// arbiter has already decided when to run it, so the embedded timeout is
/// inert (the dispatch is one-shot: pop on hit, no output on miss).
fn pop_serve(verb: &[u8], key: &[u8]) -> Argv {
    let mut a = Argv::default();
    a.push(verb);
    a.push(key);
    a.push(b"0");
    a
}

/// The command that undoes what replaying `serve_argv` is about to
/// consume, read from `store` **before** the serve runs.
///
/// A cross-shard serve pops on the target and ships the reply to the
/// origin; if the origin's client disconnected in that window, the
/// element would be lost. The target captures this undo first and holds
/// it until the origin confirms delivery. See
/// `kevy_rt::block_xshard` for the protocol.
///
/// Built from typed reads rather than by parsing the reply back apart.
/// A reply's shape depends on the kind *and* on whether the waiter
/// negotiated RESP2 or RESP3, so a parser would have to be right about
/// every combination forever; `LINDEX` means the same thing in both.
/// The peek runs on the owning shard immediately before the pop, with
/// nothing interleaved, so what it reads is what the pop takes.
pub(crate) fn block_restore_argv(
    store: &mut Store,
    kind: BlockKind,
    key: &[u8],
) -> Option<Argv> {
    match kind {
        // BLPOP takes the head, so putting it back is an LPUSH.
        BlockKind::Blpop => push_restore(store, b"LPUSH", key, 0),
        // BRPOP takes the tail — RPUSH, and index -1.
        BlockKind::Brpop => push_restore(store, b"RPUSH", key, -1),
        BlockKind::Bzpopmin => {
            let (member, score) = store.zrange(key, 0, 0).ok()?.into_iter().next()?;
            let mut a = Argv::default();
            a.push(b"ZADD");
            a.push(key);
            a.push(&crate::cmd::fmt_score(score));
            a.push(&member);
            Some(a)
        }
        // Cross-shard BRPOPLPUSH does not come through this path at all
        // — it is served by the list-move orchestrator
        // (`serve_via_list_move`), which owns its own recovery.
        BlockKind::Brpoplpush => None,
        // XREAD is non-destructive and XREADGROUP moves entries into a
        // PEL rather than consuming them. Nothing to put back.
        BlockKind::XReadBlock | BlockKind::XReadGroupBlock => None,
    }
}

/// `LPUSH`/`RPUSH key <elem at idx>` — None when the list is empty,
/// which means the serve is about to race and produce no reply anyway.
fn push_restore(store: &mut Store, verb: &[u8], key: &[u8], idx: i64) -> Option<Argv> {
    let elem = store.lindex(key, idx).ok()??;
    let mut a = Argv::default();
    a.push(verb);
    a.push(key);
    a.push(&elem);
    Some(a)
}

/// Options scanned out of an `XREAD` / `XREADGROUP` option preamble.
#[derive(Default)]
struct StreamOpts {
    count: Option<Vec<u8>>,
    block_ms: Option<Vec<u8>>,
    noack: bool,
    /// Index of the `STREAMS` token.
    streams_at: usize,
}

/// Scan the option preamble (`COUNT` / `BLOCK` / `NOACK`) starting at `from`
/// up to `STREAMS`. `None` on an unknown token or a missing operand.
fn scan_stream_opts<A: ArgvView + ?Sized>(args: &A, from: usize) -> Option<StreamOpts> {
    let mut o = StreamOpts::default();
    let mut i = from;
    loop {
        match args.get(i)?.to_ascii_uppercase().as_slice() {
            b"COUNT" => {
                o.count = Some(args.get(i + 1)?.to_vec());
                i += 2;
            }
            b"BLOCK" => {
                o.block_ms = Some(args.get(i + 1)?.to_vec());
                i += 2;
            }
            b"NOACK" => {
                o.noack = true;
                i += 1;
            }
            b"STREAMS" => {
                o.streams_at = i;
                return Some(o);
            }
            _ => return None,
        }
    }
}

/// Append `[COUNT n] [NOACK] [BLOCK ms] STREAMS key id` to a serve argv.
fn push_stream_tail(a: &mut Argv, o: &StreamOpts, key: &[u8], id: &[u8]) {
    if let Some(c) = &o.count {
        a.push(b"COUNT");
        a.push(c);
    }
    if o.noack {
        a.push(b"NOACK");
    }
    if let Some(b) = &o.block_ms {
        a.push(b"BLOCK");
        a.push(b);
    }
    a.push(b"STREAMS");
    a.push(key);
    a.push(id);
}

/// Reconstruct `XREAD [COUNT n] BLOCK ms STREAMS key id` for one stream of
/// a (possibly multi-stream) `XREAD`. `None` on malformed input.
fn xread_serve<A: ArgvView + ?Sized>(args: &A, key: &[u8]) -> Option<Argv> {
    let o = scan_stream_opts(args, 1)?;
    let id = id_for_key(args, o.streams_at + 1, key)?;
    let mut a = Argv::default();
    a.push(b"XREAD");
    push_stream_tail(&mut a, &o, key, &id);
    Some(a)
}

/// Reconstruct `XREADGROUP GROUP g c [COUNT n] [NOACK] BLOCK ms STREAMS
/// key id` for one stream of a multi-stream `XREADGROUP`. `None` on
/// malformed input.
fn xreadgroup_serve<A: ArgvView + ?Sized>(args: &A, key: &[u8]) -> Option<Argv> {
    if args.len() < 4 || !args[1].eq_ignore_ascii_case(b"GROUP") {
        return None;
    }
    let group = args[2].to_vec();
    let consumer = args[3].to_vec();
    let o = scan_stream_opts(args, 4)?;
    let id = id_for_key(args, o.streams_at + 1, key)?;
    let mut a = Argv::default();
    a.push(b"XREADGROUP");
    a.push(b"GROUP");
    a.push(&group);
    a.push(&consumer);
    push_stream_tail(&mut a, &o, key, &id);
    Some(a)
}

/// The ID paired with `key` in a `STREAMS k1 … kn id1 … idn` tail starting
/// at `keys_start`. `None` if unbalanced or `key` is absent.
fn id_for_key<A: ArgvView + ?Sized>(args: &A, keys_start: usize, key: &[u8]) -> Option<Vec<u8>> {
    let rest = args.len().checked_sub(keys_start)?;
    if rest == 0 || !rest.is_multiple_of(2) {
        return None;
    }
    let n = rest / 2;
    let pos = (keys_start..keys_start + n).position(|i| &args[i] == key)?;
    args.get(keys_start + n + pos).map(<[u8]>::to_vec)
}

/// Non-destructive readiness peek for a frozen single-key `serve_argv`:
/// would replaying it yield a reply right now?
/// - `BLPOP`/`BRPOP` → the list at `serve_argv[1]` is non-empty.
/// - `XREAD` → re-run the (read-only) replay and check it produced output.
/// - `XREADGROUP` → the group has entries past its last-delivered id.
pub(crate) fn block_ready<A: ArgvView + ?Sized>(
    ctx: &crate::state::Ctx<'_>,
    store: &mut Store,
    serve_argv: &A,
    kind: BlockKind,
) -> bool {
    match kind {
        BlockKind::Blpop | BlockKind::Brpop | BlockKind::Brpoplpush => serve_argv
            .get(1)
            .is_some_and(|k| store.llen(k).is_ok_and(|n| n > 0)),
        BlockKind::Bzpopmin => serve_argv
            .get(1)
            .is_some_and(|k| store.zcard(k).is_ok_and(|n| n > 0)),
        BlockKind::XReadBlock => {
            // XREAD is read-only, so dispatching the replay is itself a safe
            // peek. What it is NOT is empty when there is nothing: measured,
            // `XREAD COUNT 1 STREAMS st 0` against a stream with no new
            // entries writes `*-1\r\n`, the RESP nil array, five bytes. So
            // `!tmp.is_empty()` was true for every armed XREAD — the peek
            // always said ready.
            //
            // Nothing user-visible came of it: an end-to-end `XREAD BLOCK
            // 1000` still blocks its full second, because a waiter woken with
            // nothing to serve re-arms. The cost was a cross-shard signal and
            // a re-arm per armed waiter, every time, for a question that was
            // never actually being asked.
            let mut tmp = Vec::new();
            crate::dispatch::dispatch_into(ctx, store, serve_argv, &mut tmp);
            !tmp.is_empty() && tmp != b"*-1\r\n" && tmp != b"*0\r\n"
        }
        BlockKind::XReadGroupBlock => xreadgroup_ready(store, serve_argv),
    }
}

/// `XREADGROUP … >` readiness: locate the group name and STREAMS key in
/// the frozen replay, then ask the store (non-destructively) whether the
/// group has new entries.
fn xreadgroup_ready<A: ArgvView + ?Sized>(store: &mut Store, serve_argv: &A) -> bool {
    if serve_argv.len() < 3 || !serve_argv[1].eq_ignore_ascii_case(b"GROUP") {
        return false;
    }
    let group = serve_argv[2].to_vec();
    let mut i = 4usize;
    while i < serve_argv.len() {
        if serve_argv[i].eq_ignore_ascii_case(b"STREAMS") {
            let Some(key) = serve_argv.get(i + 1) else {
                return false;
            };
            return store.xreadgroup_has_new(key, &group).unwrap_or(false);
        }
        i += 1;
    }
    false
}

#[cfg(test)]
mod restore_tests {
    use super::*;

    fn argv_strings(a: &Argv) -> Vec<Vec<u8>> {
        (0..a.len()).filter_map(|i| a.get(i).map(<[u8]>::to_vec)).collect()
    }

    #[test]
    fn blpop_restores_the_head_with_lpush() {
        let mut s = Store::default();
        s.rpush(b"q", &[b"first" as &[u8], b"second"]).unwrap();
        let undo = block_restore_argv(&mut s, BlockKind::Blpop, b"q").unwrap();
        assert_eq!(argv_strings(&undo), vec![b"LPUSH".to_vec(), b"q".to_vec(), b"first".to_vec()]);
    }

    #[test]
    fn brpop_restores_the_tail_with_rpush() {
        let mut s = Store::default();
        s.rpush(b"q", &[b"first" as &[u8], b"second"]).unwrap();
        let undo = block_restore_argv(&mut s, BlockKind::Brpop, b"q").unwrap();
        assert_eq!(argv_strings(&undo), vec![b"RPUSH".to_vec(), b"q".to_vec(), b"second".to_vec()]);
    }

    /// The undo has to name the member the pop will actually take, not
    /// just any member — BZPOPMIN takes the lowest score.
    #[test]
    fn bzpopmin_restores_the_minimum_with_its_score() {
        let mut s = Store::default();
        s.zadd(b"z", &[(2.0, b"high" as &[u8]), (1.0, b"low")]).unwrap();
        let undo = block_restore_argv(&mut s, BlockKind::Bzpopmin, b"z").unwrap();
        assert_eq!(
            argv_strings(&undo),
            vec![b"ZADD".to_vec(), b"z".to_vec(), b"1".to_vec(), b"low".to_vec()]
        );
    }

    /// Peeking must not consume. If it did, the undo would be captured
    /// by removing the very element it exists to protect.
    #[test]
    fn capturing_the_undo_does_not_mutate() {
        let mut s = Store::default();
        s.rpush(b"q", &[b"a" as &[u8], b"b"]).unwrap();
        s.zadd(b"z", &[(1.0, b"m" as &[u8])]).unwrap();
        block_restore_argv(&mut s, BlockKind::Blpop, b"q").unwrap();
        block_restore_argv(&mut s, BlockKind::Brpop, b"q").unwrap();
        block_restore_argv(&mut s, BlockKind::Bzpopmin, b"z").unwrap();
        assert_eq!(s.llen(b"q").unwrap(), 2);
        assert_eq!(s.zcard(b"z").unwrap(), 1);
    }

    #[test]
    fn an_empty_key_has_nothing_to_restore() {
        let mut s = Store::default();
        assert!(block_restore_argv(&mut s, BlockKind::Blpop, b"missing").is_none());
        assert!(block_restore_argv(&mut s, BlockKind::Bzpopmin, b"missing").is_none());
    }

    /// XREAD is non-destructive and XREADGROUP moves entries to a PEL
    /// rather than consuming them; BRPOPLPUSH is served by the list-move
    /// orchestrator and recovers itself. None of them have an undo, and
    /// inventing one would put an element back that was never taken.
    #[test]
    fn kinds_that_consume_nothing_have_no_undo() {
        let mut s = Store::default();
        s.rpush(b"q", &[b"a" as &[u8]]).unwrap();
        for kind in [BlockKind::XReadBlock, BlockKind::XReadGroupBlock, BlockKind::Brpoplpush] {
            assert!(block_restore_argv(&mut s, kind, b"q").is_none(), "{kind:?}");
        }
    }
}

#[cfg(test)]
mod ready_tests {
    use super::*;

    /// Every `BlockKind` arm of [`block_ready`], asked both ways.
    ///
    /// The arms are reachable in the wider suite only when a blocked client
    /// of that particular kind happens to be served inside a test's window,
    /// so how many of them execute is a matter of timing — this symbol grew
    /// from 3 dead regions to 11 on a CI run that touched nothing near it.
    /// Which kinds exist is not a matter of timing, and asking each one
    /// directly costs nothing.
    fn argv(parts: &[&[u8]]) -> Argv {
        Argv::from(parts.iter().map(|p| p.to_vec()).collect::<Vec<_>>())
    }

    #[test]
    fn every_block_kind_answers_both_ways() {
        let kevy = crate::KevyCommands::default();
        let ctx = kevy.ctx();
        let mut s = Store::default();

        // List kinds: ready exactly when the key has elements.
        for kind in [BlockKind::Blpop, BlockKind::Brpop, BlockKind::Brpoplpush] {
            assert!(!block_ready(&ctx, &mut s, &argv(&[b"BLPOP", b"missing"]), kind));
        }
        s.rpush(b"q", &[b"one" as &[u8]]).unwrap();
        for kind in [BlockKind::Blpop, BlockKind::Brpop, BlockKind::Brpoplpush] {
            assert!(block_ready(&ctx, &mut s, &argv(&[b"BLPOP", b"q"]), kind));
        }

        // Sorted-set kind: ready exactly when the zset has members.
        assert!(!block_ready(&ctx, &mut s, &argv(&[b"BZPOPMIN", b"z"]), BlockKind::Bzpopmin));
        s.zadd(b"z", &[(1.0, b"m" as &[u8])]).unwrap();
        assert!(block_ready(&ctx, &mut s, &argv(&[b"BZPOPMIN", b"z"]), BlockKind::Bzpopmin));

        // XREAD peeks by dispatching the read-only replay: empty output is
        // "nothing yet", which is the only thing that makes the peek safe.
        let xread = argv(&[b"XREAD", b"COUNT", b"1", b"STREAMS", b"st", b"0"]);
        assert!(!block_ready(&ctx, &mut s, &xread, BlockKind::XReadBlock));
        kevy.dispatch(&mut s, &argv(&[b"XADD", b"st", b"1-1", b"f", b"v"]));
        assert!(block_ready(&ctx, &mut s, &xread, BlockKind::XReadBlock));

        // XREADGROUP: a call too short to name a group is not ready, and
        // cannot be — that guard is the first thing the arm does.
        assert!(!block_ready(&ctx, &mut s, &argv(&[b"XREADGROUP"]), BlockKind::XReadGroupBlock));
        let grouped = argv(&[
            b"XREADGROUP", b"GROUP", b"g", b"c", b"COUNT", b"1", b"STREAMS", b"st", b">",
        ]);
        assert!(!block_ready(&ctx, &mut s, &grouped, BlockKind::XReadGroupBlock));
        kevy.dispatch(&mut s, &argv(&[b"XGROUP", b"CREATE", b"st", b"g", b"0"]));
        assert!(block_ready(&ctx, &mut s, &grouped, BlockKind::XReadGroupBlock));
    }
}