use std::io::{self, Read, Write};
use kevy_store::{Store, StreamData, StreamId, Value};
use crate::snapshot_fmt::{OP_GROUP_READS, read_bytes, read_u8, read_u32, read_u64, write_bytes};
pub(crate) struct GroupReads {
key: Vec<u8>,
group: Vec<u8>,
entries_read: Option<u64>,
active: Vec<(Vec<u8>, u64)>,
wide: Vec<(StreamId, u64)>,
}
pub(crate) fn collect(key: &[u8], value: &Value, into: &mut Vec<GroupReads>) {
let Value::Stream(s) = value else { return };
collect_stream(key, s, into);
}
fn collect_stream(key: &[u8], s: &StreamData, into: &mut Vec<GroupReads>) {
for (name, g) in s.groups() {
let active: Vec<(Vec<u8>, u64)> = g
.consumers()
.filter_map(|(c, cs)| cs.last_active_ms().map(|at| (c.to_vec(), at)))
.collect();
let wide: Vec<(StreamId, u64)> = g
.pending_range(..)
.filter(|(_, p)| p.delivery_count > u64::from(u32::MAX))
.map(|(id, p)| (id, p.delivery_count))
.collect();
if g.entries_read().is_none() && active.is_empty() && wide.is_empty() {
continue;
}
into.push(GroupReads {
key: key.to_vec(),
group: name.to_vec(),
entries_read: g.entries_read(),
active,
wide,
});
}
}
pub(crate) fn write<W: Write>(w: &mut W, r: &GroupReads) -> io::Result<()> {
w.write_all(&[OP_GROUP_READS])?;
write_bytes(w, &r.key)?;
write_bytes(w, &r.group)?;
match r.entries_read {
Some(n) => {
w.write_all(&[1])?;
w.write_all(&n.to_le_bytes())?;
}
None => w.write_all(&[0])?,
}
w.write_all(&(r.active.len() as u32).to_le_bytes())?;
for (consumer, at) in &r.active {
write_bytes(w, consumer)?;
w.write_all(&at.to_le_bytes())?;
}
w.write_all(&(r.wide.len() as u32).to_le_bytes())?;
for (id, count) in &r.wide {
w.write_all(&id.ms.to_le_bytes())?;
w.write_all(&id.seq.to_le_bytes())?;
w.write_all(&count.to_le_bytes())?;
}
Ok(())
}
pub(crate) fn read_and_apply<R: Read>(
r: &mut R,
store: &mut Store,
keep: &impl Fn(&[u8]) -> bool,
) -> io::Result<()> {
let key = read_bytes(r)?;
let group = read_bytes(r)?;
let entries_read = match read_u8(r)? {
0 => None,
_ => Some(read_u64(r)?),
};
let n = read_u32(r)? as usize;
let mut active = Vec::with_capacity(n.min(1024));
for _ in 0..n {
let consumer = read_bytes(r)?;
active.push((consumer, read_u64(r)?));
}
let m = read_u32(r)? as usize;
let mut wide = Vec::with_capacity(m.min(1024));
for _ in 0..m {
let id = StreamId::new(read_u64(r)?, read_u64(r)?);
wide.push((id, read_u64(r)?));
}
if !keep(&key) {
return Ok(());
}
store
.xgroup_set_entries_read(&key, &group, entries_read)
.and_then(|_| {
active.into_iter().try_for_each(|(consumer, at)| {
store.xgroup_consumer_active(&key, &group, &consumer, Some(at)).map(drop)
})
})
.and_then(|_| {
wide.into_iter().try_for_each(|(id, count)| {
store.xgroup_set_delivery_count(&key, &group, id, count).map(drop)
})
})
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e.as_wire()))
}
#[cfg(test)]
mod tests {
use kevy_store::{AckMode, GroupCreateMode, MissingStream, ReadGroupId, XAddIdSpec};
use super::*;
fn stream_with_reads() -> Store {
let mut s = Store::new();
for ms in 1..=3 {
let f = vec![(b"f".to_vec(), b"v".to_vec())];
s.xadd(b"s", XAddIdSpec::Explicit(StreamId::new(ms, 0)), f, MissingStream::Create, 0)
.unwrap();
}
let at = GroupCreateMode::AtId(StreamId::MIN);
for g in [&b"g"[..], b"idle", b"set"] {
s.xgroup_create(b"s", g, at, MissingStream::Refuse).unwrap();
}
s.xreadgroup(b"s", b"g", b"c", ReadGroupId::New, Some(2), AckMode::Pending, 40).unwrap();
s.xgroup_create_consumer(b"s", b"g", b"never", 50).unwrap();
s.xgroup_set_entries_read(b"s", b"set", Some(9)).unwrap();
s
}
type Reads = (Vec<u8>, Option<u64>, Vec<(Vec<u8>, Option<u64>)>);
fn reads(s: &Store) -> Vec<Reads> {
let mut groups = Vec::new();
for name in [&b"g"[..], b"idle", b"set"] {
let g = s.stream_group_peek(b"s", name).unwrap();
let mut cs: Vec<_> =
g.consumers().map(|(n, c)| (n.to_vec(), c.last_active_ms())).collect();
cs.sort();
groups.push((name.to_vec(), g.entries_read(), cs));
}
groups
}
#[test]
fn the_counter_and_active_times_come_back_from_a_snapshot() {
let s = stream_with_reads();
let want = reads(&s);
assert_eq!(want[0].1, Some(2));
assert_eq!(want[0].2, [(b"c".to_vec(), Some(40)), (b"never".to_vec(), None)]);
let mut image = Vec::new();
crate::write_snapshot_to(&s, &mut image).unwrap();
let mut back = Store::new();
crate::load_snapshot_from(&mut back, image.as_slice()).unwrap();
assert_eq!(reads(&back), want);
let mut none = Store::new();
crate::load_snapshot_filtered(&mut none, image.as_slice(), |_| false).unwrap();
assert_eq!(none.dbsize(), 0);
}
#[test]
fn a_cut_or_mismatched_record_fails_the_load() {
let mut s = stream_with_reads();
s.xgroup_set_entries_read(b"s", b"g", None).unwrap();
s.set(b"t", b"v".to_vec(), None, kevy_store::SetCondition::Always);
let want = reads(&s);
assert_eq!(want[0].1, None, "g's counter is unknown and its consumer active");
let mut image = Vec::new();
crate::write_snapshot_to(&s, &mut image).unwrap();
let mut back = Store::new();
crate::load_snapshot_from(&mut back, image.as_slice()).unwrap();
assert_eq!(reads(&back), want);
let mut records = Vec::new();
crate::SnapshotSource::for_each_entry(&s, |k, v, _| collect(k, v, &mut records));
let mut tail = Vec::new();
for r in &records {
write(&mut tail, r).unwrap();
}
let start = image.len() - tail.len();
let mut bounds = vec![start];
let mut at = start;
for r in &records {
let mut one = Vec::new();
write(&mut one, r).unwrap();
at += one.len();
bounds.push(at);
}
for cut in start..image.len() {
if bounds.contains(&cut) {
continue;
}
let loaded = crate::load_snapshot_from(&mut Store::new(), &image[..cut]);
assert!(loaded.is_err(), "a cut at {cut} of {} loaded", image.len());
}
let mut wrong = image.clone();
let key_at = start + 1 + 4;
assert_eq!(wrong[key_at], b's');
wrong[key_at] = b't';
let err = crate::load_snapshot_from(&mut Store::new(), wrong.as_slice()).unwrap_err();
assert!(err.to_string().contains("WRONGTYPE"), "{err}");
}
#[test]
fn the_records_follow_the_end_of_the_keyspace() {
let s = stream_with_reads();
let mut image = Vec::new();
crate::write_snapshot_to(&s, &mut image).unwrap();
let mut records = Vec::new();
crate::SnapshotSource::for_each_entry(&s, |k, v, _| collect(k, v, &mut records));
assert_eq!(records.len(), 2, "g has a counter and an active consumer, set a counter");
let mut tail = Vec::new();
for r in &records {
write(&mut tail, r).unwrap();
}
assert!(image.ends_with(&tail));
assert_eq!(image[image.len() - tail.len() - 1], crate::snapshot_fmt::OP_EOF);
}
}