use kevy_resp::{Argv, ArgvView};
use kevy_rt::{BlockKind, Store};
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()),
}
}
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 {
a.push(b"");
}
a.push(b"0");
a
}
fn pop_serve(verb: &[u8], key: &[u8]) -> Argv {
let mut a = Argv::default();
a.push(verb);
a.push(key);
a.push(b"0");
a
}
pub(crate) fn block_restore_argv(store: &mut Store, kind: BlockKind, key: &[u8]) -> Option<Argv> {
match kind {
BlockKind::Blpop => push_restore(store, b"LPUSH", key, 0),
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)
}
BlockKind::Brpoplpush => None,
BlockKind::XReadBlock | BlockKind::XReadGroupBlock => None,
}
}
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)
}
#[derive(Default)]
struct StreamOpts {
count: Option<Vec<u8>>,
block_ms: Option<Vec<u8>>,
noack: bool,
streams_at: usize,
}
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,
}
}
}
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);
}
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)
}
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)
}
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)
}
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 => {
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),
}
}
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()]);
}
#[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()]
);
}
#[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());
}
#[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::*;
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();
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));
}
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));
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));
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));
}
}