use yo_common::{Code, Error, Result};
use crate::keyspace::Keyspace;
use crate::stream::groups::Filter;
use crate::stream::{Fate, Fields, Group, Id, Refs, Refused, Retry, Stream};
use crate::value::{self, Kind};
pub const BAD_ID: &str = "Invalid stream ID specified as stream command argument";
pub const NOT_GREATER: &str =
"The ID specified in XADD is equal or smaller than the target stream top item";
pub const ZERO_ID: &str = "The ID specified in XADD must be greater than 0-0";
pub const EXHAUSTED: &str =
"The stream has exhausted the last possible ID, unable to add more items";
pub const NO_KEY_FOR_GROUP: &str = "The XGROUP subcommand requires the key to exist. Note that for CREATE you may want to use the MKSTREAM option to create an empty stream automatically.";
pub const GROUP_EXISTS: &str = "Consumer Group name already exists";
pub const SETID_TOO_SMALL: &str =
"The ID specified in XSETID is smaller than the target stream top item";
pub const SETID_BELOW_MAX_DELETED: &str =
"The ID specified in XSETID is smaller than the provided max_deleted_entry_id";
pub const NO_SUCH_KEY: &str = "no such key";
#[must_use]
pub fn no_group(group: &[u8], key: &[u8]) -> String {
format!(
"No such consumer group '{}' for key name '{}'",
String::from_utf8_lossy(group),
String::from_utf8_lossy(key)
)
}
#[must_use]
pub fn no_key_or_group(key: &[u8], group: &[u8]) -> String {
format!(
"No such key '{}' or consumer group '{}'",
String::from_utf8_lossy(key),
String::from_utf8_lossy(group)
)
}
#[must_use]
pub fn no_group_for_read(key: &[u8], group: &[u8]) -> String {
format!(
"No such key '{}' or consumer group '{}' in XREADGROUP with GROUP option",
String::from_utf8_lossy(key),
String::from_utf8_lossy(group)
)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Add {
Auto,
Seq(u64),
At(Id),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Trim {
None,
MaxLen {
len: u64,
exact: bool,
limit: Option<u64>,
},
MinId {
id: Id,
exact: bool,
limit: Option<u64>,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Start {
Last,
At(Id),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum From {
New,
Pending(Id),
}
#[derive(Debug, Clone, Copy)]
pub struct Read<'a> {
pub group: &'a [u8],
pub consumer: &'a [u8],
pub from: From,
pub count: Option<usize>,
pub noack: bool,
}
#[derive(Debug, Clone, Copy)]
pub struct Claim<'a> {
pub group: &'a [u8],
pub consumer: &'a [u8],
pub min_idle: u64,
pub time: u64,
pub retry: Option<u64>,
pub bump: bool,
pub force: bool,
}
impl Default for Claim<'_> {
fn default() -> Claim<'static> {
Claim {
group: b"",
consumer: b"",
min_idle: 0,
time: 0,
retry: None,
bump: true,
force: false,
}
}
}
impl Keyspace {
pub fn stream(&mut self, key: &[u8]) -> Result<Option<&Stream>> {
let Some(at) = self.live_slot(key, Kind::Stream)? else {
return Ok(None);
};
Ok(self.streams.get(at))
}
pub fn stream_mut(&mut self, key: &[u8]) -> Result<Option<&mut Stream>> {
let Some(at) = self.live_slot(key, Kind::Stream)? else {
return Ok(None);
};
Ok(self.streams.get_mut(at))
}
pub fn xadd(
&mut self,
key: &[u8],
id: Add,
fields: &[(&[u8], &[u8])],
trim: Trim,
mkstream: bool,
now: u64,
) -> Result<Option<Id>> {
let limits = self.stream_limits;
let at = match self.live_slot(key, Kind::Stream)? {
Some(at) => at,
None if mkstream => self.new_stream(key),
None => return Ok(None),
};
let s = self.stream_at(at);
let want = match id {
Add::Auto => s.auto_id(now).ok_or_else(exhausted)?,
Add::Seq(ms) => s.auto_seq(ms).ok_or_else(|| {
if ms < s.last_id().ms {
Error::new(Code::Invalid, NOT_GREATER)
} else {
exhausted()
}
})?,
Add::At(id) => id,
};
s.append(want, fields, limits).map_err(refused)?;
cut(s, trim);
Ok(Some(want))
}
pub fn xdel(&mut self, key: &[u8], ids: impl Iterator<Item = Id>) -> Result<u64> {
let Some(at) = self.live_slot(key, Kind::Stream)? else {
return Ok(0);
};
let s = self.stream_at(at);
Ok(ids.filter(|&id| s.delete(id)).count() as u64)
}
pub fn xdelex<F>(
&mut self,
key: &[u8],
refs: Refs,
ids: impl Iterator<Item = Id>,
mut f: F,
) -> Result<()>
where
F: FnMut(Fate),
{
let Some(at) = self.live_slot(key, Kind::Stream)? else {
ids.for_each(|_| f(Fate::Missing));
return Ok(());
};
let s = self.stream_at(at);
ids.for_each(|id| f(s.delete_ref(id, refs)));
Ok(())
}
pub fn xackdel<F>(
&mut self,
key: &[u8],
group: &[u8],
refs: Refs,
ids: impl Iterator<Item = Id>,
mut f: F,
) -> Result<()>
where
F: FnMut(Fate),
{
let Some(at) = self.live_slot(key, Kind::Stream)? else {
ids.for_each(|_| f(Fate::Missing));
return Ok(());
};
let s = self.stream_at(at);
ids.for_each(|id| f(s.ack_delete(group, id, refs)));
Ok(())
}
pub fn xnack(
&mut self,
key: &[u8],
group: &[u8],
retry: Retry,
force: bool,
ids: impl Iterator<Item = Id>,
) -> Result<Option<u64>> {
let Some(at) = self.live_slot(key, Kind::Stream)? else {
return Ok(None);
};
let s = self.stream_at(at);
if s.group(group).is_none() {
return Ok(None);
}
let mut done = 0;
for id in ids {
done += u64::from(s.nack(group, id, retry, force).unwrap_or(false));
}
Ok(Some(done))
}
pub fn xtrim(&mut self, key: &[u8], trim: Trim) -> Result<u64> {
let Some(at) = self.live_slot(key, Kind::Stream)? else {
return Ok(0);
};
Ok(cut(self.stream_at(at), trim))
}
pub fn xsetid(
&mut self,
key: &[u8],
last: Id,
added: Option<u64>,
max_deleted: Option<Id>,
) -> Result<()> {
let Some(at) = self.live_slot(key, Kind::Stream)? else {
return Err(Error::new(Code::NotFound, NO_SUCH_KEY));
};
if max_deleted.is_some_and(|id| last < id) {
return Err(Error::new(Code::Invalid, SETID_BELOW_MAX_DELETED));
}
self.stream_at(at)
.set_id(last, added, max_deleted)
.map_err(|_| Error::new(Code::Invalid, SETID_TOO_SMALL))
}
pub fn xrange_into<F>(
&mut self,
key: &[u8],
start: Id,
end: Id,
count: Option<usize>,
rev: bool,
f: F,
) -> Result<usize>
where
F: FnMut(Id, Fields<'_>) -> bool,
{
let Some(s) = self.stream(key)? else {
return Ok(0);
};
Ok(if rev {
s.rev_range(start, end, count, f)
} else {
s.range(start, end, count, f)
})
}
pub fn xread_into<F>(
&mut self,
key: &[u8],
after: Id,
count: Option<usize>,
f: F,
) -> Result<usize>
where
F: FnMut(Id, Fields<'_>) -> bool,
{
let Some(from) = after.next() else {
return Ok(0);
};
self.xrange_into(key, from, Id::MAX, count, false, f)
}
pub fn xgroup_create(
&mut self,
key: &[u8],
group: &[u8],
at: Start,
mkstream: bool,
read: Option<u64>,
) -> Result<bool> {
let slot = match self.live_slot(key, Kind::Stream)? {
Some(slot) => slot,
None if mkstream => self.new_stream(key),
None => return Err(Error::new(Code::NotFound, NO_KEY_FOR_GROUP)),
};
let s = self.stream_at(slot);
let last = position(s, at);
let read = capped(read, s);
Ok(s.create_group(group, last, read))
}
pub fn xgroup_destroy(&mut self, key: &[u8], group: &[u8]) -> Result<bool> {
let Some(at) = self.live_slot(key, Kind::Stream)? else {
return Err(Error::new(Code::NotFound, NO_KEY_FOR_GROUP));
};
Ok(self.stream_at(at).destroy_group(group))
}
pub fn xgroup_setid(
&mut self,
key: &[u8],
group: &[u8],
at: Start,
read: Option<u64>,
) -> Result<Option<()>> {
let Some(slot) = self.live_slot(key, Kind::Stream)? else {
return Err(Error::new(Code::NotFound, NO_KEY_FOR_GROUP));
};
let s = self.stream_at(slot);
let last = position(s, at);
let read = capped(read, s);
let Some(g) = s.group_mut(group) else {
return Ok(None);
};
g.set_id(last, read);
Ok(Some(()))
}
pub fn xgroup_create_consumer(
&mut self,
key: &[u8],
group: &[u8],
consumer: &[u8],
now: u64,
) -> Result<Option<bool>> {
let Some(g) = self.group_mut_of(key, group)? else {
return Ok(None);
};
Ok(Some(g.create_consumer(consumer, now)))
}
pub fn xgroup_del_consumer(
&mut self,
key: &[u8],
group: &[u8],
consumer: &[u8],
) -> Result<Option<u64>> {
let Some(g) = self.group_mut_of(key, group)? else {
return Ok(None);
};
Ok(Some(g.delete_consumer(consumer)))
}
pub fn xreadgroup_into<F>(
&mut self,
key: &[u8],
want: Read<'_>,
now: u64,
mut f: F,
) -> Result<Option<usize>>
where
F: FnMut(Id, Option<Fields<'_>>) -> bool,
{
let Some(at) = self.live_slot(key, Kind::Stream)? else {
return Ok(None);
};
let s = self.stream_at(at);
Ok(match want.from {
From::New => s.read_group(
want.group,
want.consumer,
want.count,
want.noack,
now,
|id, fields| f(id, Some(fields)),
),
From::Pending(after) => {
s.read_group_pending(want.group, want.consumer, after, want.count, now, &mut f)
}
})
}
pub fn xack(&mut self, key: &[u8], group: &[u8], ids: impl Iterator<Item = Id>) -> Result<u64> {
let Some(at) = self.live_slot(key, Kind::Stream)? else {
return Ok(0);
};
let Some(g) = self.stream_at(at).group_mut(group) else {
return Ok(0);
};
Ok(ids.filter(|&id| g.ack(id)).count() as u64)
}
pub fn xpending_into<F>(
&mut self,
key: &[u8],
group: &[u8],
want: Filter,
now: u64,
f: F,
) -> Result<Option<usize>>
where
F: FnMut(Id, &crate::stream::Nack, Option<&crate::stream::Consumer>) -> bool,
{
let Some(s) = self.stream(key)? else {
return Ok(None);
};
let Some(g) = s.group(group) else {
return Ok(None);
};
Ok(Some(g.pending_range(want, now, f)))
}
pub fn xclaim(
&mut self,
key: &[u8],
ids: &[Id],
how: Claim<'_>,
now: u64,
gone: &mut Vec<Id>,
) -> Result<Option<Vec<Id>>> {
let Some(at) = self.live_slot(key, Kind::Stream)? else {
return Ok(None);
};
Ok(self.stream_at(at).claim(
how.group,
how.consumer,
ids,
how.min_idle,
how.time,
how.retry,
how.bump,
how.force,
now,
gone,
))
}
pub fn xautoclaim(
&mut self,
key: &[u8],
start: Id,
how: Claim<'_>,
count: usize,
now: u64,
gone: &mut Vec<Id>,
) -> Result<Option<(Option<Id>, Vec<Id>)>> {
let Some(at) = self.live_slot(key, Kind::Stream)? else {
return Ok(None);
};
Ok(self.stream_at(at).autoclaim(
how.group,
how.consumer,
start,
how.min_idle,
count,
how.bump,
now,
gone,
))
}
fn group_mut_of(&mut self, key: &[u8], group: &[u8]) -> Result<Option<&mut Group>> {
let Some(at) = self.live_slot(key, Kind::Stream)? else {
return Err(Error::new(Code::NotFound, NO_KEY_FOR_GROUP));
};
Ok(self.stream_at(at).group_mut(group))
}
fn stream_at(&mut self, at: u32) -> &mut Stream {
self.streams
.get_mut(at)
.expect("the record points at its body")
}
fn new_stream(&mut self, key: &[u8]) -> u32 {
let at = self.streams.insert(Stream::new());
let len = value::slot_record_len(false);
self.write_rec(key, len, |out| {
value::write_slot_record(out, Kind::Stream, at, None);
});
self.bodies += 1;
at
}
}
fn position(s: &Stream, at: Start) -> Id {
match at {
Start::Last => s.last_id(),
Start::At(id) => id,
}
}
fn capped(read: Option<u64>, s: &Stream) -> Option<u64> {
read.map(|n| n.min(s.added()))
}
fn cut(s: &mut Stream, trim: Trim) -> u64 {
match trim {
Trim::None => 0,
Trim::MaxLen { len, exact, limit } => s.trim_maxlen(len, exact, limit),
Trim::MinId { id, exact, limit } => s.trim_minid(id, exact, limit),
}
}
fn exhausted() -> Error {
Error::new(Code::Invalid, EXHAUSTED)
}
fn refused(why: Refused) -> Error {
match why {
Refused::Zero => Error::new(Code::Invalid, ZERO_ID),
Refused::NotGreater => Error::new(Code::Invalid, NOT_GREATER),
Refused::Full => exhausted(),
}
}
#[cfg(test)]
mod tests {
use super::*;
fn db() -> Keyspace {
Keyspace::new()
}
fn add(d: &mut Keyspace, key: &[u8], id: Add) -> Id {
d.xadd(
key,
id,
&[(b"sensor", b"a4"), (b"reading", b"21.5")],
Trim::None,
true,
1_000,
)
.expect("a stream")
.expect("an ID")
}
fn ids(d: &mut Keyspace, key: &[u8]) -> Vec<Id> {
let mut out = Vec::new();
d.xrange_into(key, Id::MIN, Id::MAX, None, false, |id, _| {
out.push(id);
true
})
.expect("a stream");
out
}
#[test]
fn a_write_makes_the_key_and_a_read_finds_it() {
let mut d = db();
assert_eq!(d.kind_of(b"s"), None);
let id = add(&mut d, b"s", Add::At(Id::new(5, 0)));
assert_eq!(id, Id::new(5, 0));
assert_eq!(d.kind_of(b"s"), Some(Kind::Stream));
assert_eq!(d.type_name(b"s"), Some("stream"));
assert_eq!(d.encoding_name(b"s"), Some("stream"));
assert_eq!(ids(&mut d, b"s"), vec![Id::new(5, 0)]);
}
#[test]
fn nomkstream_leaves_a_missing_key_missing() {
let mut d = db();
let got = d
.xadd(b"s", Add::Auto, &[(b"f", b"v")], Trim::None, false, 1_000)
.expect("a stream");
assert_eq!(got, None);
assert_eq!(d.kind_of(b"s"), None);
}
#[test]
fn an_auto_id_follows_the_clock_and_then_the_last_id() {
let mut d = db();
assert_eq!(add(&mut d, b"s", Add::Auto), Id::new(1_000, 0));
assert_eq!(add(&mut d, b"s", Add::Auto), Id::new(1_000, 1));
assert_eq!(add(&mut d, b"s", Add::Seq(1_000)), Id::new(1_000, 2));
assert_eq!(add(&mut d, b"s", Add::Seq(2_000)), Id::new(2_000, 0));
}
#[test]
fn an_id_that_is_not_above_the_last_one_is_refused() {
let mut d = db();
add(&mut d, b"s", Add::At(Id::new(5, 0)));
let e = d
.xadd(
b"s",
Add::At(Id::new(5, 0)),
&[(b"f", b"v")],
Trim::None,
true,
1_000,
)
.expect_err("not above the last one");
assert_eq!(e.message(), NOT_GREATER);
let e = d
.xadd(b"s", Add::Seq(4), &[(b"f", b"v")], Trim::None, true, 1_000)
.expect_err("a millisecond that has gone by");
assert_eq!(e.message(), NOT_GREATER);
}
#[test]
fn zero_is_refused_and_says_so_in_its_own_words() {
let mut d = db();
let e = d
.xadd(
b"s",
Add::At(Id::MIN),
&[(b"f", b"v")],
Trim::None,
true,
1_000,
)
.expect_err("nothing sorts below zero");
assert_eq!(e.message(), ZERO_ID);
}
#[test]
fn a_stream_is_not_deleted_when_the_last_entry_goes() {
let mut d = db();
let id = add(&mut d, b"s", Add::At(Id::new(5, 0)));
assert_eq!(d.xdel(b"s", [id].into_iter()).expect("a stream"), 1);
assert_eq!(d.xdel(b"s", [id].into_iter()).expect("a stream"), 0);
assert_eq!(d.kind_of(b"s"), Some(Kind::Stream), "the key is still here");
assert_eq!(add(&mut d, b"s", Add::Auto), Id::new(1_000, 0));
}
#[test]
fn a_trim_runs_after_the_append() {
let mut d = db();
for ms in 1..=10u64 {
add(&mut d, b"s", Add::At(Id::new(ms, 0)));
}
let trim = Trim::MaxLen {
len: 1,
exact: true,
limit: None,
};
let id = d
.xadd(
b"s",
Add::At(Id::new(11, 0)),
&[(b"f", b"v")],
trim,
true,
1,
)
.expect("a stream")
.expect("an ID");
assert_eq!(ids(&mut d, b"s"), vec![id]);
}
#[test]
fn a_limit_stops_a_trim_at_the_next_node_boundary() {
let mut d = db();
for ms in 1..=1_000u64 {
add(&mut d, b"s", Add::At(Id::new(ms, 0)));
}
let trim = Trim::MaxLen {
len: 0,
exact: false,
limit: Some(10),
};
assert_eq!(d.xtrim(b"s", trim).expect("a stream"), 100);
assert_eq!(
d.stream(b"s").expect("a stream").expect("the key").len(),
900
);
}
#[test]
fn setid_moves_the_bookmark_and_refuses_to_go_below_an_entry() {
let mut d = db();
add(&mut d, b"s", Add::At(Id::new(5, 0)));
let e = d
.xsetid(b"s", Id::new(4, 0), None, None)
.expect_err("below an entry that is still there");
assert_eq!(e.message(), SETID_TOO_SMALL);
d.xsetid(b"s", Id::new(9, 0), Some(41), None)
.expect("above it");
let s = d.stream(b"s").expect("a stream").expect("the key");
assert_eq!((s.last_id(), s.added()), (Id::new(9, 0), 41));
}
#[test]
fn setid_on_a_key_that_is_not_there_says_so() {
let mut d = db();
let e = d
.xsetid(b"s", Id::new(1, 0), None, None)
.expect_err("no key");
assert_eq!((e.code(), e.message()), (Code::NotFound, NO_SUCH_KEY));
}
#[test]
fn every_command_sees_the_wrong_type() {
let mut d = db();
d.set_plain(b"s", b"a string").expect("a string");
for e in [
d.xadd(b"s", Add::Auto, &[(b"f", b"v")], Trim::None, true, 1)
.expect_err("a string"),
d.xdel(b"s", [Id::new(1, 0)].into_iter())
.expect_err("a string"),
d.xtrim(b"s", Trim::None).expect_err("a string"),
d.xrange_into(b"s", Id::MIN, Id::MAX, None, false, |_, _| true)
.expect_err("a string"),
d.xack(b"s", b"g", [Id::new(1, 0)].into_iter())
.expect_err("a string"),
] {
assert_eq!(e.code(), Code::WrongType);
}
}
#[test]
fn a_new_group_has_no_read_counter() {
let mut d = db();
for ms in 1..=5 {
add(&mut d, b"s", Add::At(Id::new(ms, 0)));
}
assert!(
d.xgroup_create(b"s", b"early", Start::At(Id::MIN), false, None)
.expect("a stream")
);
assert!(
d.xgroup_create(b"s", b"late", Start::Last, false, None)
.expect("a stream")
);
let s = d.stream(b"s").expect("a stream").expect("the key");
let early = s.group(b"early").expect("the early group");
assert_eq!(early.entries_read(), None);
assert_eq!(s.lag(early), Some(5), "everything is still in front of it");
let late = s.group(b"late").expect("the late group");
assert_eq!(late.entries_read(), None);
assert_eq!(s.lag(late), Some(0), "and nothing is in front of this one");
}
#[test]
fn a_read_counter_that_is_too_big_is_brought_back_down() {
let mut d = db();
for ms in 1..=3 {
add(&mut d, b"s", Add::At(Id::new(ms, 0)));
}
d.xgroup_create(b"s", b"g", Start::At(Id::MIN), false, Some(99))
.expect("a stream");
assert_eq!(
counter(&mut d, b"g"),
Some(3),
"held down to what was added"
);
d.xgroup_setid(b"s", b"g", Start::At(Id::MIN), Some(2))
.expect("a stream")
.expect("the group");
assert_eq!(counter(&mut d, b"g"), Some(2), "and left alone below that");
d.xgroup_setid(b"s", b"g", Start::At(Id::MIN), None)
.expect("a stream")
.expect("the group");
assert_eq!(
counter(&mut d, b"g"),
None,
"and given up when none is named"
);
}
fn counter(d: &mut Keyspace, group: &[u8]) -> Option<u64> {
d.stream(b"s")
.expect("a stream")
.expect("the key")
.group(group)
.expect("the group")
.entries_read()
}
#[test]
fn setid_refuses_a_last_id_under_its_own_deletion_mark() {
let mut d = db();
add(&mut d, b"s", Add::At(Id::new(5, 0)));
let e = d
.xsetid(b"s", Id::new(9, 9), None, Some(Id::new(99, 99)))
.expect_err("the pair contradicts itself");
assert_eq!(e.message(), SETID_BELOW_MAX_DELETED);
d.xsetid(b"s", Id::new(99, 99), None, Some(Id::new(9, 9)))
.expect("the other way round is fine");
let s = d.stream(b"s").expect("a stream").expect("the key");
assert_eq!(s.last_id(), Id::new(99, 99));
assert_eq!(s.max_deleted_id(), Id::new(9, 9));
}
#[test]
fn a_history_read_by_a_name_nobody_has_used_makes_the_consumer() {
let mut d = db();
add(&mut d, b"s", Add::At(Id::new(1, 0)));
d.xgroup_create(b"s", b"g", Start::At(Id::MIN), false, None)
.expect("a stream");
let want = Read {
group: b"g",
consumer: b"newbie",
from: From::Pending(Id::MIN),
count: None,
noack: false,
};
let seen = d
.xreadgroup_into(b"s", want, 500, |_, _| true)
.expect("a stream")
.expect("the group is there");
assert_eq!(seen, 0, "nothing was ever handed to this name");
let s = d.stream(b"s").expect("a stream").expect("the key");
let c = s
.group(b"g")
.expect("the group")
.consumer_named(b"newbie")
.expect("the read made it");
assert_eq!(c.seen(), 500, "it was heard from");
assert_eq!(c.active(), None, "and it has never had anything");
}
#[test]
fn a_group_reads_what_arrives_after_it_was_made() {
let mut d = db();
add(&mut d, b"s", Add::At(Id::new(1, 0)));
assert!(
d.xgroup_create(b"s", b"workers", Start::Last, false, None)
.expect("a stream")
);
assert!(
!d.xgroup_create(b"s", b"workers", Start::Last, false, None)
.expect("a stream")
);
add(&mut d, b"s", Add::At(Id::new(2, 0)));
let mut got = Vec::new();
let seen = d
.xreadgroup_into(
b"s",
Read {
group: b"workers",
consumer: b"alice",
from: From::New,
count: None,
noack: false,
},
1_000,
|id, fields| {
got.push((id, fields.is_some()));
true
},
)
.expect("a stream")
.expect("a group");
assert_eq!(seen, 1, "only what arrived after the group was made");
assert_eq!(got, vec![(Id::new(2, 0), true)]);
assert_eq!(
d.xack(b"s", b"workers", [Id::new(2, 0)].into_iter())
.expect("a stream"),
1
);
}
#[test]
fn noack_hands_the_entry_over_without_writing_it_down() {
let mut d = db();
add(&mut d, b"s", Add::At(Id::new(1, 0)));
d.xgroup_create(b"s", b"workers", Start::At(Id::MIN), false, None)
.expect("a stream");
let seen = d
.xreadgroup_into(
b"s",
Read {
group: b"workers",
consumer: b"alice",
from: From::New,
count: None,
noack: true,
},
1_000,
|_, _| true,
)
.expect("a stream")
.expect("a group");
assert_eq!(seen, 1);
let s = d.stream(b"s").expect("a stream").expect("the key");
let g = s.group(b"workers").expect("the group");
assert_eq!(g.pending_len(), 0, "nothing was written down");
assert_eq!(g.last_id(), Id::new(1, 0), "the bookmark still moved");
assert_eq!(s.lag(g), Some(0), "and the group has caught up");
}
#[test]
fn a_missing_group_is_a_none_and_not_an_error() {
let mut d = db();
add(&mut d, b"s", Add::At(Id::new(1, 0)));
let got = d
.xreadgroup_into(
b"s",
Read {
group: b"nope",
consumer: b"alice",
from: From::New,
count: None,
noack: false,
},
1,
|_, _| true,
)
.expect("a stream");
assert_eq!(got, None);
assert!(!d.xgroup_destroy(b"s", b"nope").expect("a stream"));
assert_eq!(
d.xgroup_create_consumer(b"s", b"nope", b"alice", 1)
.expect("a stream"),
None
);
}
#[test]
fn xgroup_needs_the_key_and_says_which_option_makes_one() {
let mut d = db();
let e = d
.xgroup_create(b"s", b"workers", Start::Last, false, None)
.expect_err("no key");
assert_eq!((e.code(), e.message()), (Code::NotFound, NO_KEY_FOR_GROUP));
assert!(
d.xgroup_create(b"s", b"workers", Start::Last, true, None)
.expect("mkstream made one")
);
assert_eq!(d.kind_of(b"s"), Some(Kind::Stream));
}
#[test]
fn a_claim_moves_an_idle_entry_and_drops_one_that_has_gone() {
let mut d = db();
for ms in 1..=2u64 {
add(&mut d, b"s", Add::At(Id::new(ms, 0)));
}
d.xgroup_create(b"s", b"workers", Start::At(Id::MIN), false, None)
.expect("a stream");
d.xreadgroup_into(
b"s",
Read {
group: b"workers",
consumer: b"alice",
from: From::New,
count: None,
noack: false,
},
100,
|_, _| true,
)
.expect("a stream")
.expect("a group");
d.xdel(b"s", [Id::new(1, 0)].into_iter()).expect("a stream");
let mut gone = Vec::new();
let how = Claim {
group: b"workers",
consumer: b"bob",
min_idle: 500,
time: 1_000,
..Claim::default()
};
let took = d
.xclaim(b"s", &[Id::new(1, 0), Id::new(2, 0)], how, 1_000, &mut gone)
.expect("a stream")
.expect("a group");
assert_eq!(took, vec![Id::new(2, 0)]);
assert_eq!(gone, vec![Id::new(1, 0)], "no one can ever finish that one");
}
#[test]
fn an_autoclaim_sweeps_from_a_cursor() {
let mut d = db();
for ms in 1..=10u64 {
add(&mut d, b"s", Add::At(Id::new(ms, 0)));
}
d.xgroup_create(b"s", b"workers", Start::At(Id::MIN), false, None)
.expect("a stream");
d.xreadgroup_into(
b"s",
Read {
group: b"workers",
consumer: b"alice",
from: From::New,
count: None,
noack: false,
},
100,
|_, _| true,
)
.expect("a stream")
.expect("a group");
let mut gone = Vec::new();
let how = Claim {
group: b"workers",
consumer: b"bob",
min_idle: 500,
time: 1_000,
..Claim::default()
};
let (cursor, took) = d
.xautoclaim(b"s", Id::MIN, how, 4, 1_000, &mut gone)
.expect("a stream")
.expect("a group");
assert_eq!(took.len(), 4);
assert_eq!(cursor, Some(Id::new(5, 0)), "where the next call starts");
assert!(gone.is_empty());
}
#[test]
fn a_pending_window_carries_the_owner_and_the_counts() {
let mut d = db();
for ms in 1..=3u64 {
add(&mut d, b"s", Add::At(Id::new(ms, 0)));
}
d.xgroup_create(b"s", b"workers", Start::At(Id::MIN), false, None)
.expect("a stream");
d.xreadgroup_into(
b"s",
Read {
group: b"workers",
consumer: b"alice",
from: From::New,
count: None,
noack: false,
},
100,
|_, _| true,
)
.expect("a stream")
.expect("a group");
let mut out = Vec::new();
let seen = d
.xpending_into(b"s", b"workers", Filter::default(), 600, |id, nack, c| {
out.push((
id,
nack.count(),
nack.idle(600),
c.expect("an owner").name().to_vec(),
));
true
})
.expect("a stream")
.expect("a group");
assert_eq!(seen, 3);
assert_eq!(out[0], (Id::new(1, 0), 1, 500, b"alice".to_vec()));
}
#[test]
fn a_history_read_hands_back_a_null_for_an_entry_that_has_gone() {
let mut d = db();
for ms in 1..=2u64 {
add(&mut d, b"s", Add::At(Id::new(ms, 0)));
}
d.xgroup_create(b"s", b"workers", Start::At(Id::MIN), false, None)
.expect("a stream");
d.xreadgroup_into(
b"s",
Read {
group: b"workers",
consumer: b"alice",
from: From::New,
count: None,
noack: false,
},
100,
|_, _| true,
)
.expect("a stream")
.expect("a group");
d.xdel(b"s", [Id::new(1, 0)].into_iter()).expect("a stream");
let mut out = Vec::new();
d.xreadgroup_into(
b"s",
Read {
group: b"workers",
consumer: b"alice",
from: From::Pending(Id::MIN),
count: None,
noack: false,
},
2_000,
|id, fields| {
out.push((id, fields.is_some()));
true
},
)
.expect("a stream")
.expect("a group");
assert_eq!(out, vec![(Id::new(1, 0), false), (Id::new(2, 0), true)]);
}
#[test]
fn a_stream_counts_against_the_memory_total() {
let mut d = db();
let before = d.memory_bytes();
for ms in 1..=1_000u64 {
add(&mut d, b"s", Add::At(Id::new(ms, 0)));
}
let after = d.memory_bytes();
assert!(after > before + 1_000, "{before} then {after}");
assert!(d.del(b"s"), "the key was there");
assert_eq!(d.kind_of(b"s"), None);
}
#[test]
fn a_deadline_goes_on_a_stream_the_same_as_on_anything_else() {
let mut d = db();
add(&mut d, b"s", Add::At(Id::new(1, 0)));
assert!(d.set_expiry(b"s", Some(1 << 45)));
assert_eq!(d.kind_of(b"s"), Some(Kind::Stream));
assert_eq!(ids(&mut d, b"s"), vec![Id::new(1, 0)], "the body is intact");
}
}