use yo_common::num::{DIGITS_MAX, u64_digits};
use yo_common::{Code, Error, Result, num};
use yo_kv::lookups;
use yo_kv::stream::{Consumer, Fate, Fields, Filter, Group, Id, Refs, Retry, Stream};
use yo_kv::streams::{self as kv, Add, Claim, Read, Start, Trim};
use yo_kv::{Db, Entry, Holds};
use super::args::{self, Args};
use super::notify::{self, class};
use super::table::Spec;
use crate::reply::Out;
const MAXLEN_NEGATIVE: &str = "The MAXLEN argument must be >= 0.";
const LIMIT_NEGATIVE: &str = "The LIMIT argument must be >= 0.";
const LIMIT_NO_APPROX: &str = "syntax error, LIMIT cannot be used without the special ~ option";
const LIMIT_NO_STRATEGY: &str =
"syntax error, LIMIT cannot be used without specifying a trimming strategy";
const BOTH_STRATEGIES: &str =
"syntax error, MAXLEN and MINID options at the same time are not compatible";
const ADDED_NOT_POSITIVE: &str = "entries_added must be positive";
const READ_NOT_POSITIVE: &str = "value for ENTRIESREAD must be positive or -1";
const AUTOCLAIM_COUNT: &str = "COUNT must be > 0";
const BAD_MIN_IDLE: &str = "Invalid min-idle-time argument for XCLAIM";
const BAD_MIN_IDLE_AUTO: &str = "Invalid min-idle-time argument for XAUTOCLAIM";
const BAD_IDLE: &str = "Invalid IDLE option argument for XCLAIM";
const BAD_TIME: &str = "Invalid TIME option argument for XCLAIM";
const BAD_RETRY: &str = "Invalid RETRYCOUNT option argument for XCLAIM";
const BAD_INTERVAL_START: &str = "invalid start ID for the interval";
const BAD_INTERVAL_END: &str = "invalid end ID for the interval";
const TIMEOUT_NOT_AN_INT: &str = "timeout is not an integer or out of range";
const TIMEOUT_NEGATIVE: &str = "timeout is negative";
const DOLLAR_MEANINGLESS: &str = "The $ ID is meaningless in the context of XREADGROUP: you want to read the history of this consumer by specifying a proper ID, or use the > ID to get new messages. The $ ID would just return an empty result set.";
const PLUS_MEANINGLESS: &str = "The + ID is meaningless in the context of XREADGROUP: you want to read the history of this consumer by specifying a proper ID, or use the + ID to get new messages. The + ID would just return an empty result set.";
const MISSING_GROUP: &str = "Missing GROUP option for XREADGROUP";
const IDS_NOT_POSITIVE: &str = "Number of IDs must be a positive integer";
const IDS_MISMATCH: &str = "The `numids` parameter must match the number of arguments";
const NACK_IDS_NOT_POSITIVE: &str = "numids must be a positive integer";
const NACK_IDS_MISMATCH: &str = "number of IDs doesn't match numids";
const NACK_MODE: &str = "mode must be SILENT, FAIL, or FATAL";
const NACK_RETRY_NEGATIVE: &str = "Invalid RETRYCOUNT value, must be >= 0";
const GROUP_IS_NOT_XREAD: &str =
"The GROUP option is only supported by XREADGROUP. You called XREAD instead.";
const NOACK_IS_NOT_XREAD: &str =
"The NOACK option is only supported by XREADGROUP. You called XREAD instead.";
const INLINE_FIELDS: usize = 32;
pub(super) fn execute(
db: &Db,
on: usize,
spec: &Spec,
args: Args<'_>,
now: u64,
out: &mut Out,
) -> Result<()> {
match spec.name {
"xadd" => xadd(db, on, args, now, out)?,
"xlen" => {
let key = args.get(1);
out.uint(db.hold(key).stream(key)?.map_or(0, Stream::len));
}
"xdel" => {
let key = args.get(1);
let gone = db.hold(key).xdel(key, ids(args, 2)?)?;
out.uint(gone);
if gone > 0 {
notify::fire(on, class::STREAM, "xdel", key);
}
}
"xdelex" => delex(db, on, args, out)?,
"xackdel" => ackdel(db, on, args, out)?,
"xnack" => nack(db, args, out)?,
"xtrim" => xtrim(db, on, args, out)?,
"xrange" => range(db, args, false, out)?,
"xrevrange" => range(db, args, true, out)?,
"xack" => {
let (key, group) = (args.get(1), args.get(2));
out.uint(db.hold(key).xack(key, group, ids(args, 3)?)?);
}
"xsetid" => setid(db, on, args, out)?,
"xgroup" => group(db, on, args, now, out)?,
"xinfo" => info(db, args, now, out)?,
"xpending" => pending(db, args, now, out)?,
"xclaim" => claim(db, on, args, now, out)?,
"xautoclaim" => autoclaim(db, on, args, now, out)?,
other => unreachable!("the table sent {other} to the stream group"),
}
Ok(())
}
fn xadd(db: &Db, on: usize, args: Args<'_>, now: u64, out: &mut Out) -> Result<()> {
let mut stripe = db.hold(args.get(1));
let node = stripe.stream_limits().max_node_entries;
let opts = trimming(args, 2, true, node)?;
let at = opts.at;
if at + 2 >= args.len() || !(args.len() - at - 1).is_multiple_of(2) {
return Err(args::wrong_arity("xadd"));
}
let id = add_id(args.get(at))?;
let key = args.get(1);
let pairs = (args.len() - at - 1) / 2;
let field = |i: usize| (args.get(at + 1 + i * 2), args.get(at + 2 + i * 2));
let written = if pairs <= INLINE_FIELDS {
let mut buf: [(&[u8], &[u8]); INLINE_FIELDS] = [(b"", b""); INLINE_FIELDS];
for (i, slot) in buf[..pairs].iter_mut().enumerate() {
*slot = field(i);
}
stripe.xadd_trimmed(key, id, &buf[..pairs], opts.trim, opts.mkstream, now)?
} else {
let fields: Vec<(&[u8], &[u8])> = yo_alloc::allow(|| (0..pairs).map(field).collect());
stripe.xadd_trimmed(key, id, &fields, opts.trim, opts.mkstream, now)?
};
drop(stripe);
match written {
Some((id, cut)) => {
id_out(out, id);
notify::fire(on, class::STREAM, "xadd", key);
if cut > 0 {
notify::fire(on, class::STREAM, "xtrim", key);
}
}
None => out.nil(),
}
Ok(())
}
fn xtrim(db: &Db, on: usize, args: Args<'_>, out: &mut Out) -> Result<()> {
let mut stripe = db.hold(args.get(1));
let node = stripe.stream_limits().max_node_entries;
let opts = trimming(args, 2, false, node)?;
if opts.at != args.len() || matches!(opts.trim, Trim::None) {
return Err(args::syntax());
}
let key = args.get(1);
let gone = stripe.xtrim(key, opts.trim)?;
drop(stripe);
out.uint(gone);
if gone > 0 {
notify::fire(on, class::STREAM, "xtrim", key);
}
Ok(())
}
struct Trimmed {
trim: Trim,
mkstream: bool,
at: usize,
}
fn trimming(args: Args<'_>, at: usize, xadd: bool, node: usize) -> Result<Trimmed> {
let mut maxlen: Option<u64> = None;
let mut minid: Option<Id> = None;
let mut approx = false;
let mut limit: Option<u64> = None;
let mut limited = false;
let mut mkstream = true;
let mut i = at;
while i < args.len() {
let opt = args.get(i);
let more = args.len() - i - 1;
if xadd && args::is(opt, b"nomkstream") {
mkstream = false;
i += 1;
} else if (args::is(opt, b"maxlen") || args::is(opt, b"minid")) && more > 0 {
if maxlen.is_some() || minid.is_some() {
return Err(Error::new(Code::Invalid, BOTH_STRATEGIES));
}
let len = args::is(opt, b"maxlen");
let mut n = i + 1;
let next = args.get(n);
if more >= 2 && (next == b"~" || next == b"=") {
approx = next == b"~";
n += 1;
}
if len {
maxlen = Some(non_negative(args.int(n)?, MAXLEN_NEGATIVE)?);
} else {
minid = Some(strict_id(args.get(n))?);
}
i = n + 1;
} else if args::is(opt, b"limit") && more > 0 {
limit = Some(non_negative(args.int(i + 1)?, LIMIT_NEGATIVE)?);
limited = true;
i += 2;
} else {
break;
}
}
if limited {
if maxlen.is_none() && minid.is_none() {
return Err(Error::new(Code::Invalid, LIMIT_NO_STRATEGY));
}
if !approx {
return Err(Error::new(Code::Invalid, LIMIT_NO_APPROX));
}
}
let limit = match (approx, limit) {
(false, _) => None,
(true, Some(0)) => None,
(true, Some(n)) => Some(n),
(true, None) => (node > 0).then(|| 100 * node as u64),
};
let trim = match (maxlen, minid) {
(Some(len), _) => Trim::MaxLen {
len,
exact: !approx,
limit,
},
(_, Some(id)) => Trim::MinId {
id,
exact: !approx,
limit,
},
_ => Trim::None,
};
Ok(Trimmed {
trim,
mkstream,
at: i,
})
}
fn setid(db: &Db, on: usize, args: Args<'_>, out: &mut Out) -> Result<()> {
let last = strict_id(args.get(2))?;
let mut added = None;
let mut deleted = None;
let mut i = 3;
while i < args.len() {
if i + 1 >= args.len() {
return Err(args::syntax());
}
let opt = args.get(i);
if args::is(opt, b"entriesadded") {
added = Some(non_negative(args.int(i + 1)?, ADDED_NOT_POSITIVE)?);
} else if args::is(opt, b"maxdeletedid") {
deleted = Some(strict_id(args.get(i + 1))?);
} else {
return Err(args::syntax());
}
i += 2;
}
let key = args.get(1);
db.hold(key).xsetid(key, last, added, deleted)?;
out.ok();
notify::fire(on, class::STREAM, "xsetid", key);
Ok(())
}
fn range(db: &Db, args: Args<'_>, rev: bool, out: &mut Out) -> Result<()> {
let (lo, hi) = if rev { (3, 2) } else { (2, 3) };
let start = bound(args.get(lo), true)?;
let end = bound(args.get(hi), false)?;
let mut count: Option<usize> = None;
let mut i = 4;
while i < args.len() {
if !args::is(args.get(i), b"count") || i + 1 >= args.len() {
return Err(args::syntax());
}
count = Some(args.int(i + 1)?.max(0).unsigned_abs() as usize);
i += 2;
}
let key = args.get(1);
let mut stripe = db.hold(key);
if stripe.stream(key)?.is_none() {
out.array(0);
return Ok(());
}
let _quiet = lookups::quiet();
if count == Some(0) {
out.nil_array();
return Ok(());
}
let mark = out.len();
let n = stripe.xrange_into(key, start, end, count, rev, |id, fields| {
entry(out, id, fields);
true
})?;
out.close_array(mark, n);
Ok(())
}
fn pending(db: &Db, args: Args<'_>, now: u64, out: &mut Out) -> Result<()> {
let (key, name) = (args.get(1), args.get(2));
if args.len() == 3 {
return summary(db, key, name, out);
}
if args.len() > 9 {
return Err(args::syntax());
}
let idle = args::is(args.get(3), b"idle");
let at = if idle { 5 } else { 3 };
if args.len() < at + 3 {
return Err(args::syntax());
}
let want = Filter {
min_idle: if idle {
args.int(4)?.max(0).unsigned_abs()
} else {
0
},
start: bound(args.get(at), true)?,
end: bound(args.get(at + 1), false)?,
count: Some(args.int(at + 2)?.max(0).unsigned_abs() as usize),
owner: None,
};
let mut want = want;
if args.len() == at + 4 {
let who = args.get(at + 3);
let mut stripe = db.hold(key);
let Some(s) = stripe.stream(key)? else {
nogroup(out, &kv::no_key_or_group(key, name));
return Ok(());
};
let Some(g) = s.group(name) else {
nogroup(out, &kv::no_key_or_group(key, name));
return Ok(());
};
match g.slot(who) {
Some(slot) => want.owner = Some(slot),
None => {
out.array(0);
return Ok(());
}
}
}
let mark = out.len();
let seen = db
.hold(key)
.xpending_into(key, name, want, now, |id, nack, c| {
out.array(4);
id_out(out, id);
match c {
Some(c) => {
out.bulk(c.name());
out.uint(nack.idle(now));
}
None => {
out.bulk(b"");
out.int(-1);
}
}
out.uint(nack.count());
true
})?;
match seen {
Some(n) => out.close_array(mark, n),
None => nogroup(out, &kv::no_key_or_group(key, name)),
}
Ok(())
}
fn summary(db: &Db, key: &[u8], name: &[u8], out: &mut Out) -> Result<()> {
let mut stripe = db.hold(key);
let Some(s) = stripe.stream(key)? else {
nogroup(out, &kv::no_key_or_group(key, name));
return Ok(());
};
let Some(g) = s.group(name) else {
nogroup(out, &kv::no_key_or_group(key, name));
return Ok(());
};
out.array(4);
out.uint(g.pending_len() as u64);
match g.pending_bounds() {
Some((low, high)) => {
id_out(out, low);
id_out(out, high);
let mark = out.len();
let mut n = 0;
let mut prev: Option<&[u8]> = None;
while let Some(who) = next_name(g.pending_counts().map(|(name, _)| name), prev) {
let held = g
.pending_counts()
.find(|(name, _)| *name == who)
.map_or(0, |(_, n)| n);
out.array(2);
out.bulk(who);
out.bulk_u64(held as u64);
prev = Some(who);
n += 1;
}
out.close_array(mark, n);
}
None => {
out.nil();
out.nil();
out.nil_array();
}
}
Ok(())
}
fn delex(db: &Db, on: usize, args: Args<'_>, out: &mut Out) -> Result<()> {
let key = args.get(1);
let here = db.hold(key).stream(key)?.is_some();
let (refs, at) = refs_and_ids(args, 2)?;
let n = args.len() - at;
if !here {
missing(out, n);
return Ok(());
}
let ids = ids_in(args, at, args.len())?;
out.array(n);
let gone = db
.hold(key)
.xdelex(key, refs, ids, |fate| out.int(fate.code()))?;
if gone > 0 {
notify::fire(on, class::STREAM, "xdel", key);
}
Ok(())
}
fn ackdel(db: &Db, on: usize, args: Args<'_>, out: &mut Out) -> Result<()> {
let (key, name) = (args.get(1), args.get(2));
let here = db.hold(key).stream(key)?.is_some();
let (refs, at) = refs_and_ids(args, 3)?;
let n = args.len() - at;
if !here {
missing(out, n);
return Ok(());
}
let ids = ids_in(args, at, args.len())?;
out.array(n);
let gone = db
.hold(key)
.xackdel(key, name, refs, ids, |fate| out.int(fate.code()))?;
if gone > 0 {
notify::fire(on, class::STREAM, "xdel", key);
}
Ok(())
}
fn nack(db: &Db, args: Args<'_>, out: &mut Out) -> Result<()> {
let (key, name) = (args.get(1), args.get(2));
if !db
.hold(key)
.stream(key)?
.is_some_and(|s| s.group(name).is_some())
{
nogroup(out, &kv::no_key_or_group(key, name));
return Ok(());
}
let mode = match args.get(3) {
w if args::is(w, b"silent") => Retry::Down,
w if args::is(w, b"fail") => Retry::Keep,
w if args::is(w, b"fatal") => Retry::Max,
_ => return Err(bad(NACK_MODE)),
};
if !args::is(args.get(4), b"ids") {
return Err(args::syntax());
}
let want = count(args, 5, NACK_IDS_NOT_POSITIVE)?;
let (at, left) = (6, args.len().saturating_sub(6));
if left < want {
return Err(bad(NACK_IDS_MISMATCH));
}
let end = at + want;
let ids = ids_in(args, at, end)?;
let mut retry = None;
let mut force = false;
let mut i = end;
while i < args.len() {
let opt = args.get(i);
let more = args.len() - i - 1;
if args::is(opt, b"force") {
force = true;
i += 1;
} else if args::is(opt, b"retrycount") && more > 0 {
retry = Some(Retry::At(non_negative(
args.int(i + 1)?,
NACK_RETRY_NEGATIVE,
)?));
i += 2;
} else {
return Err(unrecognised("XNACK", opt));
}
}
let done = db
.hold(key)
.xnack(key, name, retry.unwrap_or(mode), force, ids)?;
out.uint(done.expect("the group was there a moment ago"));
Ok(())
}
fn refs_and_ids(args: Args<'_>, at: usize) -> Result<(Refs, usize)> {
let word = args.get(at);
let (refs, at) = if args::is(word, b"keepref") {
(Refs::Keep, at + 1)
} else if args::is(word, b"delref") {
(Refs::Drop, at + 1)
} else if args::is(word, b"acked") {
(Refs::Acked, at + 1)
} else {
(Refs::Keep, at)
};
if !args::is(args.get(at), b"ids") {
return Err(args::syntax());
}
let want = count(args, at + 1, IDS_NOT_POSITIVE)?;
let start = at + 2;
let left = args.len().saturating_sub(start);
if left < want {
return Err(bad(IDS_MISMATCH));
}
if left > want {
return Err(args::syntax());
}
Ok((refs, start))
}
fn count(args: Args<'_>, at: usize, msg: &'static str) -> Result<usize> {
match num::parse_i64(args.get(at)) {
Some(n) if n > 0 => Ok(usize::try_from(n).unwrap_or(usize::MAX)),
_ => Err(bad(msg)),
}
}
fn missing(out: &mut Out, n: usize) {
out.array(n);
for _ in 0..n {
out.int(Fate::Missing.code());
}
}
fn unrecognised(name: &str, opt: &[u8]) -> Error {
yo_alloc::allow(|| {
Error::fmt(
Code::Invalid,
format_args!(
"Unrecognized {name} option '{}'",
String::from_utf8_lossy(opt)
),
)
})
}
fn claim(db: &Db, on: usize, args: Args<'_>, now: u64, out: &mut Out) -> Result<()> {
let (key, name, who) = (args.get(1), args.get(2), args.get(3));
let min_idle = millis(args.int(4).map_err(|_| bad(BAD_MIN_IDLE))?);
let mut at = 5;
while at < args.len() && Id::parse(args.get(at), 0).is_some() {
at += 1;
}
let count = at - 5;
let mut how = Claim {
group: name,
consumer: who,
min_idle,
time: now,
..Claim::default()
};
let mut justid = false;
let mut last: Option<Id> = None;
let mut i = at;
while i < args.len() {
let opt = args.get(i);
let more = args.len() - i - 1;
if args::is(opt, b"force") {
how.force = true;
i += 1;
} else if args::is(opt, b"justid") {
justid = true;
i += 1;
} else if args::is(opt, b"idle") && more > 0 {
let ms = millis(args.int(i + 1).map_err(|_| bad(BAD_IDLE))?);
how.time = now.saturating_sub(ms);
i += 2;
} else if args::is(opt, b"time") && more > 0 {
how.time = millis(args.int(i + 1).map_err(|_| bad(BAD_TIME))?);
i += 2;
} else if args::is(opt, b"retrycount") && more > 0 {
how.retry = Some(millis(args.int(i + 1).map_err(|_| bad(BAD_RETRY))?));
i += 2;
} else if args::is(opt, b"lastid") && more > 0 {
last = Some(strict_id(args.get(i + 1))?);
i += 2;
} else {
return Err(unrecognised("XCLAIM", opt));
}
}
how.bump = !justid;
let fresh = fresh_consumer(db, key, name, who);
let (took, mut gone) = yo_alloc::allow(|| {
let mut gone = Vec::new();
let ids: Vec<Id> = (5..5 + count)
.filter_map(|j| Id::parse(args.get(j), 0))
.collect();
(db.hold(key).xclaim(key, &ids, how, now, &mut gone), gone)
});
gone.clear();
let Some(took) = took? else {
nogroup(out, &kv::no_key_or_group(key, name));
return Ok(());
};
if fresh {
notify::fire(on, class::STREAM, "xgroup-createconsumer", key);
}
if let Some(id) = last {
move_bookmark(db, key, name, id)?;
}
entries(db, key, &took, justid, out)
}
fn autoclaim(db: &Db, on: usize, args: Args<'_>, now: u64, out: &mut Out) -> Result<()> {
let (key, name, who) = (args.get(1), args.get(2), args.get(3));
let min_idle = millis(args.int(4).map_err(|_| bad(BAD_MIN_IDLE_AUTO))?);
let start = bound(args.get(5), true)?;
let mut count = 100;
let mut justid = false;
let mut i = 6;
while i < args.len() {
let opt = args.get(i);
if args::is(opt, b"justid") {
justid = true;
i += 1;
} else if args::is(opt, b"count") && i + 1 < args.len() {
let n = args.int(i + 1)?;
if n <= 0 {
return Err(Error::new(Code::Invalid, AUTOCLAIM_COUNT));
}
count = usize::try_from(n).unwrap_or(usize::MAX);
i += 2;
} else {
return Err(args::syntax());
}
}
let how = Claim {
group: name,
consumer: who,
min_idle,
time: now,
bump: !justid,
..Claim::default()
};
let fresh = fresh_consumer(db, key, name, who);
let (claimed, gone) = yo_alloc::allow(|| {
let mut gone = Vec::new();
(
db.hold(key)
.xautoclaim(key, start, how, count, now, &mut gone),
gone,
)
});
let Some((cursor, took)) = claimed? else {
nogroup(out, &kv::no_key_or_group(key, name));
return Ok(());
};
if fresh {
notify::fire(on, class::STREAM, "xgroup-createconsumer", key);
}
out.array(3);
id_out(out, cursor.unwrap_or(Id::MIN));
entries(db, key, &took, justid, out)?;
out.array(gone.len());
for &id in &gone {
id_out(out, id);
}
Ok(())
}
fn fresh_consumer(db: &Db, key: &[u8], group: &[u8], who: &[u8]) -> bool {
notify::armed()
&& db
.hold(key)
.stream(key)
.ok()
.flatten()
.and_then(|s| s.group(group))
.is_some_and(|g| g.slot(who).is_none())
}
fn entries(db: &Db, key: &[u8], took: &[Id], justid: bool, out: &mut Out) -> Result<()> {
if justid {
out.array(took.len());
for &id in took {
id_out(out, id);
}
return Ok(());
}
let mark = out.len();
let mut n = 0;
for &id in took {
let found = db
.hold(key)
.xrange_into(key, id, id, Some(1), false, |id, fields| {
entry(out, id, fields);
true
})?;
n += found;
}
out.close_array(mark, n);
Ok(())
}
fn move_bookmark(db: &Db, key: &[u8], name: &[u8], id: Id) -> Result<()> {
let mut stripe = db.hold(key);
let Some(s) = stripe.stream_mut(key)? else {
return Ok(());
};
if let Some(g) = s.group_mut(name)
&& id > g.last_id()
{
let read = g.entries_read();
g.set_id(id, read);
}
Ok(())
}
fn group(db: &Db, on: usize, args: Args<'_>, now: u64, out: &mut Out) -> Result<()> {
let sub = args.get(1);
let n = args.len();
if args::is(sub, b"create") {
arity(n, -5, "create")?;
return create(db, on, args, out);
}
if args::is(sub, b"setid") {
arity(n, -5, "setid")?;
if n != 5 && n != 7 {
return Err(unknown_or_arity(sub, "XGROUP"));
}
let read = entries_read(args, 5, n)?;
let at = start_at(args.get(4))?;
let key = args.get(2);
return match db.hold(key).xgroup_setid(key, args.get(3), at, read)? {
Some(()) => {
out.ok();
notify::fire(on, class::STREAM, "xgroup-setid", key);
Ok(())
}
None => {
nogroup(out, &kv::no_group(args.get(3), args.get(2)));
Ok(())
}
};
}
if args::is(sub, b"destroy") {
arity(n, 4, "destroy")?;
let key = args.get(2);
let gone = db.hold(key).xgroup_destroy(key, args.get(3))?;
out.int(i64::from(gone));
if gone {
notify::fire(on, class::STREAM, "xgroup-destroy", key);
}
return Ok(());
}
if args::is(sub, b"createconsumer") {
arity(n, 5, "createconsumer")?;
let key = args.get(2);
let made = db
.hold(key)
.xgroup_create_consumer(key, args.get(3), args.get(4), now)?;
return match made {
Some(made) => {
out.int(i64::from(made));
if made {
notify::fire(on, class::STREAM, "xgroup-createconsumer", key);
}
Ok(())
}
None => {
nogroup(out, &kv::no_group(args.get(3), args.get(2)));
Ok(())
}
};
}
if args::is(sub, b"delconsumer") {
arity(n, 5, "delconsumer")?;
let (key, name, who) = (args.get(2), args.get(3), args.get(4));
let mut stripe = db.hold(key);
let there = notify::armed()
&& stripe
.stream(key)?
.and_then(|s| s.group(name))
.is_some_and(|g| g.slot(who).is_some());
let held = stripe.xgroup_del_consumer(key, name, who)?;
drop(stripe);
return match held {
Some(held) => {
out.uint(held);
if there {
notify::fire(on, class::STREAM, "xgroup-delconsumer", key);
}
Ok(())
}
None => {
nogroup(out, &kv::no_group(args.get(3), args.get(2)));
Ok(())
}
};
}
if args::is(sub, b"help") {
arity(n, 2, "help")?;
super::server::help(out, GROUP_HELP);
return Ok(());
}
Err(args::unknown_subcommand(sub, "XGROUP"))
}
fn create(db: &Db, on: usize, args: Args<'_>, out: &mut Out) -> Result<()> {
let mut mkstream = false;
let mut read = None;
let mut i = 5;
while i < args.len() {
let opt = args.get(i);
if args::is(opt, b"mkstream") {
mkstream = true;
i += 1;
} else if args::is(opt, b"entriesread") && i + 1 < args.len() {
read = entries_read(args, i + 1, i + 2)?;
i += 2;
} else {
return Err(unknown_or_arity(args.get(1), "XGROUP"));
}
}
let at = start_at(args.get(4))?;
let key = args.get(2);
if db
.hold(key)
.xgroup_create(key, args.get(3), at, mkstream, read)?
{
out.ok();
notify::fire(on, class::STREAM, "xgroup-create", key);
} else {
out.error_line(b"BUSYGROUP ", kv::GROUP_EXISTS.as_bytes());
}
Ok(())
}
fn entries_read(args: Args<'_>, at: usize, end: usize) -> Result<Option<u64>> {
if at >= end {
return Ok(None);
}
if !args::is(args.get(at - 1), b"entriesread") {
return Err(unknown_or_arity(args.get(1), "XGROUP"));
}
let n = args.int(at)?;
if n < -1 {
return Err(Error::new(Code::Invalid, READ_NOT_POSITIVE));
}
Ok((n >= 0).then(|| n.unsigned_abs()))
}
fn start_at(arg: &[u8]) -> Result<Start> {
if arg == b"$" {
return Ok(Start::Last);
}
Ok(Start::At(strict_id(arg)?))
}
fn info(db: &Db, args: Args<'_>, now: u64, out: &mut Out) -> Result<()> {
let sub = args.get(1);
let n = args.len();
if args::is(sub, b"stream") {
arity(n, -3, "stream")?;
let mut count = 10;
let full = n > 3;
if full {
if !args::is(args.get(3), b"full") || (n != 4 && n != 6) {
return Err(unknown_or_arity(sub, "XINFO"));
}
if n == 6 {
if !args::is(args.get(4), b"count") {
return Err(unknown_or_arity(sub, "XINFO"));
}
let want = args.int(5)?;
count = if want <= 0 {
usize::MAX
} else {
usize::try_from(want).unwrap_or(usize::MAX)
};
}
}
let key = args.get(2);
let mut stripe = db.hold(key);
let Some(s) = stripe.stream(key)? else {
return Err(Error::new(Code::NotFound, kv::NO_SUCH_KEY));
};
if full {
full_info(s, count, out);
} else {
stream_info(s, out);
}
return Ok(());
}
if args::is(sub, b"groups") {
arity(n, 3, "groups")?;
let key = args.get(2);
let mut stripe = db.hold(key);
let Some(s) = stripe.stream(key)? else {
return Err(Error::new(Code::NotFound, kv::NO_SUCH_KEY));
};
groups_info(s, out);
return Ok(());
}
if args::is(sub, b"consumers") {
arity(n, 4, "consumers")?;
let (key, name) = (args.get(2), args.get(3));
let mut stripe = db.hold(key);
let Some(s) = stripe.stream(key)? else {
return Err(Error::new(Code::NotFound, kv::NO_SUCH_KEY));
};
let Some(g) = s.group(name) else {
nogroup(out, &kv::no_group(name, key));
return Ok(());
};
consumers_info(g, now, out);
return Ok(());
}
if args::is(sub, b"help") {
arity(n, 2, "help")?;
super::server::help(out, INFO_HELP);
return Ok(());
}
Err(args::unknown_subcommand(sub, "XINFO"))
}
fn stream_info(s: &Stream, out: &mut Out) {
out.map(10);
header(s, out);
out.bulk(b"groups");
out.uint(s.groups().count() as u64);
out.bulk(b"first-entry");
one(s, s.first_id(), out);
out.bulk(b"last-entry");
one(s, s.top_id(), out);
}
fn full_info(s: &Stream, count: usize, out: &mut Out) {
out.map(9);
header(s, out);
out.bulk(b"entries");
let mark = out.len();
let n = s.range(Id::MIN, Id::MAX, Some(count), |id, fields| {
entry(out, id, fields);
true
});
out.close_array(mark, n);
out.bulk(b"groups");
let mark = out.len();
let mut wrote = 0;
let mut prev: Option<&[u8]> = None;
while let Some(name) = next_name(s.groups().map(|(name, _)| name), prev) {
let g = s.group(name).expect("a name the walk just found");
out.map(8);
out.bulk(b"name");
out.bulk(name);
out.bulk(b"last-delivered-id");
id_out(out, g.last_id());
out.bulk(b"entries-read");
maybe_uint(out, g.entries_read());
out.bulk(b"lag");
maybe_uint(out, s.lag(g));
out.bulk(b"pel-count");
out.uint(g.pending_len() as u64);
out.bulk(b"nacked-count");
out.uint(g.nacked_len() as u64);
out.bulk(b"pending");
let at = out.len();
let want = Filter {
count: Some(count),
..Filter::default()
};
let seen = g.pending_range(want, 0, |id, nack, c| {
out.array(4);
id_out(out, id);
out.bulk(c.map_or(&b""[..], Consumer::name));
out.uint(nack.time());
out.uint(nack.count());
true
});
out.close_array(at, seen);
out.bulk(b"consumers");
let at = out.len();
let mut consumers = 0;
let mut before: Option<&[u8]> = None;
while let Some(who) = next_name(g.consumers().map(Consumer::name), before) {
let c = g
.consumer(g.slot(who).expect("a name the walk just found"))
.expect("the slot that name is on");
out.map(5);
out.bulk(b"name");
out.bulk(who);
out.bulk(b"seen-time");
out.uint(c.seen());
out.bulk(b"active-time");
active(out, c);
out.bulk(b"pel-count");
out.uint(c.len() as u64);
out.bulk(b"pending");
let here = out.len();
let mut held = 0;
for id in c.pending().take(count) {
let Some(nack) = g.nack(id) else { continue };
out.array(3);
id_out(out, id);
out.uint(nack.time());
out.uint(nack.count());
held += 1;
}
out.close_array(here, held);
before = Some(who);
consumers += 1;
}
out.close_array(at, consumers);
prev = Some(name);
wrote += 1;
}
out.close_array(mark, wrote);
}
fn header(s: &Stream, out: &mut Out) {
out.bulk(b"length");
out.uint(s.len());
out.bulk(b"radix-tree-keys");
out.uint(s.nodes() as u64);
out.bulk(b"radix-tree-nodes");
out.uint(s.nodes().max(1) as u64);
out.bulk(b"last-generated-id");
id_out(out, s.last_id());
out.bulk(b"max-deleted-entry-id");
id_out(out, s.max_deleted_id());
out.bulk(b"entries-added");
out.uint(s.added());
out.bulk(b"recorded-first-entry-id");
id_out(out, s.first_id().unwrap_or(Id::MIN));
}
fn groups_info(s: &Stream, out: &mut Out) {
let mark = out.len();
let mut n = 0;
let mut prev: Option<&[u8]> = None;
while let Some(name) = next_name(s.groups().map(|(name, _)| name), prev) {
let g = s.group(name).expect("a name the walk just found");
out.map(6);
out.bulk(b"name");
out.bulk(name);
out.bulk(b"consumers");
out.uint(g.consumers().count() as u64);
out.bulk(b"pending");
out.uint(g.pending_len() as u64);
out.bulk(b"last-delivered-id");
id_out(out, g.last_id());
out.bulk(b"entries-read");
maybe_uint(out, g.entries_read());
out.bulk(b"lag");
maybe_uint(out, s.lag(g));
prev = Some(name);
n += 1;
}
out.close_array(mark, n);
}
fn consumers_info(g: &Group, now: u64, out: &mut Out) {
let mark = out.len();
let mut n = 0;
let mut prev: Option<&[u8]> = None;
while let Some(who) = next_name(g.consumers().map(Consumer::name), prev) {
let c = g
.consumer(g.slot(who).expect("a name the walk just found"))
.expect("the slot that name is on");
out.map(4);
out.bulk(b"name");
out.bulk(who);
out.bulk(b"pending");
out.uint(c.len() as u64);
out.bulk(b"idle");
out.uint(now.saturating_sub(c.seen()));
out.bulk(b"inactive");
match c.active() {
Some(at) => out.uint(now.saturating_sub(at)),
None => out.int(-1),
}
prev = Some(who);
n += 1;
}
out.close_array(mark, n);
}
fn next_name<'a, I>(names: I, prev: Option<&[u8]>) -> Option<&'a [u8]>
where
I: Iterator<Item = &'a [u8]>,
{
names.filter(|name| prev.is_none_or(|p| *name > p)).min()
}
fn one(s: &Stream, id: Option<Id>, out: &mut Out) {
let mut wrote = false;
if let Some(id) = id {
s.range(id, id, Some(1), |id, fields| {
entry(out, id, fields);
wrote = true;
true
});
}
if !wrote {
out.nil();
}
}
fn maybe_uint(out: &mut Out, n: Option<u64>) {
match n {
Some(n) => out.uint(n),
None => out.nil(),
}
}
fn active(out: &mut Out, c: &Consumer) {
match c.active() {
Some(at) => out.uint(at),
None => out.int(-1),
}
}
pub(super) enum At {
After(Id),
New,
Mine(Id),
Meaningless(&'static str),
}
pub(super) struct Reads {
at: Vec<At>,
count: Option<usize>,
group: Option<(Vec<u8>, Vec<u8>)>,
noack: bool,
}
pub(super) struct Parsed {
pub wait: Option<Option<u64>>,
pub keys: Vec<Vec<u8>>,
pub reads: Reads,
}
pub(super) fn parse_read(name: &str, args: Args<'_>, db: &Db, now: u64) -> Result<Parsed> {
let grouped = name == "xreadgroup";
let mut count = None;
let mut wait = None;
let mut group = None;
let mut noack = false;
let mut streams = None;
let mut i = 1;
while i < args.len() {
let opt = args.get(i);
let more = args.len() - i - 1;
if args::is(opt, b"count") && more > 0 {
let n = args.int(i + 1)?;
count = (n > 0).then(|| usize::try_from(n).unwrap_or(usize::MAX));
i += 2;
} else if args::is(opt, b"block") && more > 0 {
let ms = num::parse_i64(args.get(i + 1)).ok_or_else(|| bad(TIMEOUT_NOT_AN_INT))?;
if ms < 0 {
return Err(bad(TIMEOUT_NEGATIVE));
}
wait = Some((ms > 0).then(|| now.saturating_add(ms.unsigned_abs())));
i += 2;
} else if args::is(opt, b"group") && more > 1 {
if !grouped {
return Err(bad(GROUP_IS_NOT_XREAD));
}
group = Some((args.get(i + 1), args.get(i + 2)));
i += 3;
} else if args::is(opt, b"noack") {
if !grouped {
return Err(bad(NOACK_IS_NOT_XREAD));
}
noack = true;
i += 1;
} else if args::is(opt, b"streams") {
streams = Some(i + 1);
break;
} else {
return Err(args::syntax());
}
}
let Some(at) = streams else {
return Err(args::syntax());
};
if grouped && group.is_none() {
return Err(bad(MISSING_GROUP));
}
let left = args.len() - at;
if left == 0 || !left.is_multiple_of(2) {
return Err(unbalanced(name));
}
let n = left / 2;
let mut where_from = yo_alloc::allow(|| Vec::with_capacity(n));
for j in 0..n {
let key = args.get(at + j);
let arg = args.get(at + n + j);
where_from.push(if grouped {
match arg {
b">" => At::New,
b"$" => At::Meaningless(DOLLAR_MEANINGLESS),
b"+" => At::Meaningless(PLUS_MEANINGLESS),
_ => At::Mine(strict_id(arg)?),
}
} else {
At::After(match arg {
b"$" => db.hold(key).stream(key)?.map_or(Id::MIN, Stream::last_id),
b"+" => {
let mut stripe = db.hold(key);
let s = stripe.stream(key)?;
let last = s.as_ref().map_or(Id::MIN, |s| s.last_id());
s.and_then(Stream::top_id)
.and_then(Id::prev)
.unwrap_or(last)
}
_ => strict_id(arg)?,
})
});
}
let keys = yo_alloc::allow(|| (0..n).map(|j| args.get(at + j).to_vec()).collect());
let group = yo_alloc::allow(|| group.map(|(g, c)| (g.to_vec(), c.to_vec())));
Ok(Parsed {
wait,
keys,
reads: Reads {
at: where_from,
count,
group,
noack,
},
})
}
#[derive(Clone, Copy)]
pub(super) struct On<'a> {
pub(super) db: &'a Db,
pub(super) at: usize,
}
pub(super) fn read(
on: On<'_>,
keys: &[Vec<u8>],
r: &Reads,
now: u64,
strict: bool,
out: &mut Out,
) -> Result<bool> {
let db = on.db;
let mut held = db.hold_keys(keys.iter().map(Vec::as_slice));
if let Some((name, _)) = &r.group {
for key in keys {
let missing = match held.stripe_mut(db.stripe_of(key)).stream(key) {
Ok(Some(s)) => s.group(name).is_none(),
Ok(None) => true,
Err(e) if strict => return Err(e),
Err(_) => true,
};
if missing {
nogroup(out, &kv::no_group_for_read(key, name));
return Ok(true);
}
}
}
for at in &r.at {
if let At::Meaningless(msg) = at {
return Err(bad(msg));
}
}
let mark = out.len();
let mut wrote = 0;
for (key, at) in keys.iter().zip(&r.at) {
let here = out.len();
if !out.proto().is_resp3() {
out.array(2);
}
out.bulk(key);
let body = out.len();
let got = match one_stream(on, &mut held, key, at, r, now, out) {
Ok(n) => n,
Err(e) if strict => return Err(e),
Err(_) => {
out.truncate(here);
continue;
}
};
match got {
Some(n) => {
out.close_array(body, n);
wrote += 1;
}
None => out.truncate(here),
}
}
if wrote == 0 {
out.truncate(mark);
return Ok(false);
}
out.close_map(mark, wrote);
Ok(true)
}
fn one_stream(
on: On<'_>,
held: &mut Holds<'_>,
key: &[u8],
at: &At,
r: &Reads,
now: u64,
out: &mut Out,
) -> Result<Option<usize>> {
let db = on.db;
match at {
At::After(after) => {
let n = held.stripe_mut(db.stripe_of(key)).xread_into(
key,
*after,
r.count,
|id, fields| {
entry(out, id, fields);
true
},
)?;
Ok((n > 0).then_some(n))
}
At::Meaningless(_) => unreachable!("read refuses these before it walks"),
At::New | At::Mine(_) => {
let (group, consumer) = r.group.as_ref().expect("a group read names its group");
let history = matches!(at, At::Mine(_));
let want = Read {
group,
consumer,
from: match at {
At::Mine(after) => kv::From::Pending(*after),
_ => kv::From::New,
},
count: r.count,
noack: r.noack,
};
let stripe = held.stripe_mut(db.stripe_of(key));
let fresh = notify::armed()
&& stripe
.stream(key)
.ok()
.flatten()
.and_then(|s| s.group(group))
.is_some_and(|g| g.slot(consumer).is_none());
let n = stripe.xreadgroup_into(key, want, now, |id, fields| {
match fields {
Some(fields) => entry(out, id, fields),
None => {
out.array(2);
id_out(out, id);
out.nil();
}
}
true
})?;
if fresh {
notify::fire(on.at, class::STREAM, "xgroup-createconsumer", key);
}
Ok(match n {
Some(n) if history || n > 0 => Some(n),
_ => None,
})
}
}
}
fn unbalanced(name: &str) -> Error {
if name == "xreadgroup" {
return Error::new(
Code::Invalid,
"Unbalanced 'xreadgroup' list of streams: for each stream key an ID or '>' must be specified.",
);
}
Error::new(
Code::Invalid,
"Unbalanced 'xread' list of streams: for each stream key an ID, '+', or '$' must be specified.",
)
}
fn add_id(arg: &[u8]) -> Result<Add> {
if arg == b"*" {
return Ok(Add::Auto);
}
if let Some(ms) = arg.strip_suffix(b"-*") {
return match Id::parse(ms, 0) {
Some(id) => Ok(Add::Seq(id.ms)),
None => Err(bad(kv::BAD_ID)),
};
}
Ok(Add::At(strict_id(arg)?))
}
fn strict_id(arg: &[u8]) -> Result<Id> {
Id::parse(arg, 0).ok_or_else(|| bad(kv::BAD_ID))
}
fn bound(arg: &[u8], low: bool) -> Result<Id> {
let (arg, open) = match arg.strip_prefix(b"(") {
Some(rest) => (rest, true),
None => (arg, false),
};
if !open {
if arg == b"-" {
return Ok(Id::MIN);
}
if arg == b"+" {
return Ok(Id::MAX);
}
}
let id = Id::parse(arg, if low { 0 } else { u64::MAX }).ok_or_else(|| bad(kv::BAD_ID))?;
if !open {
return Ok(id);
}
let stepped = if low { id.next() } else { id.prev() };
stepped.ok_or_else(|| {
bad(if low {
BAD_INTERVAL_START
} else {
BAD_INTERVAL_END
})
})
}
fn ids<'a>(args: Args<'a>, at: usize) -> Result<impl Iterator<Item = Id> + 'a> {
ids_in(args, at, args.len())
}
fn ids_in<'a>(args: Args<'a>, at: usize, end: usize) -> Result<impl Iterator<Item = Id> + 'a> {
for i in at..end {
strict_id(args.get(i))?;
}
Ok((at..end).filter_map(move |i| Id::parse(args.get(i), 0)))
}
fn entry(out: &mut Out, id: Id, fields: Fields<'_>) {
out.array(2);
id_out(out, id);
out.array(fields.len() * 2);
for (name, value) in fields {
element(out, name);
element(out, value);
}
}
#[inline]
fn element(out: &mut Out, e: Entry<'_>) {
match e {
Entry::Int(n) => out.bulk_int(n),
Entry::Str(s) => out.bulk(s),
}
}
fn id_out(out: &mut Out, id: Id) {
let mut buf = [0u8; DIGITS_MAX * 2 + 1];
let mut digits = [0u8; DIGITS_MAX];
let ms = u64_digits(&mut digits, id.ms);
let mut n = ms.len();
buf[..n].copy_from_slice(ms);
buf[n] = b'-';
n += 1;
let mut digits = [0u8; DIGITS_MAX];
let seq = u64_digits(&mut digits, id.seq);
buf[n..n + seq.len()].copy_from_slice(seq);
out.bulk(&buf[..n + seq.len()]);
}
fn nogroup(out: &mut Out, msg: &str) {
out.error_line(b"NOGROUP ", msg.as_bytes());
}
fn bad(msg: &'static str) -> Error {
Error::new(Code::Invalid, msg)
}
fn non_negative(n: i64, msg: &'static str) -> Result<u64> {
if n < 0 {
return Err(Error::new(Code::Invalid, msg));
}
Ok(n.unsigned_abs())
}
fn millis(n: i64) -> u64 {
if n < 0 { 0 } else { n.unsigned_abs() }
}
fn arity(n: usize, want: i32, sub: &'static str) -> Result<()> {
let n = n as i32;
let ok = if want < 0 { n >= -want } else { n == want };
if ok {
return Ok(());
}
Err(args::wrong_arity_sub("xgroup", sub))
}
fn unknown_or_arity(sub: &[u8], container: &str) -> Error {
yo_alloc::allow(|| {
Error::fmt(
Code::Unsupported,
format_args!(
"unknown subcommand or wrong number of arguments for '{}'. Try {container} HELP.",
String::from_utf8_lossy(sub)
),
)
})
}
const GROUP_HELP: &[&str] = &[
"XGROUP <subcommand> [<arg> [value] [opt] ...]. Subcommands are:",
"CREATE <key> <groupname> <id|$> [option]",
" Create a new consumer group. Options are:",
" * MKSTREAM",
" Create the empty stream if it does not exist.",
" * ENTRIESREAD entries_read",
" Set the group's entries_read counter (internal use).",
"CREATECONSUMER <key> <groupname> <consumer>",
" Create a new consumer in the specified group.",
"DELCONSUMER <key> <groupname> <consumer>",
" Remove the specified consumer.",
"DESTROY <key> <groupname>",
" Remove the specified group.",
"SETID <key> <groupname> <id|$> [ENTRIESREAD entries_read]",
" Set the current group ID and entries_read counter.",
"HELP",
" Print this help.",
];
const INFO_HELP: &[&str] = &[
"XINFO <subcommand> [<arg> [value] [opt] ...]. Subcommands are:",
"CONSUMERS <key> <groupname>",
" Show consumers of <groupname>.",
"GROUPS <key>",
" Show the stream consumer groups.",
"STREAM <key> [FULL [COUNT <count>]",
" Show information about the stream.",
"HELP",
" Print this help.",
];