#[cfg(not(feature = "std"))]
use crate::nostd_prelude::*;
use alloc::collections::BTreeMap;
use super::group::ConsumerGroup;
use super::{StreamData, StreamId};
use crate::value::SmallBytes;
use crate::{Store, StoreError};
#[derive(Clone, Copy, Debug)]
pub(super) struct Tally {
added: u64,
length: u64,
max_deleted: StreamId,
last: StreamId,
first: Option<StreamId>,
}
impl Tally {
pub(super) fn of(
entries: &BTreeMap<StreamId, Vec<(SmallBytes, SmallBytes)>>,
added: u64,
max_deleted: StreamId,
last: StreamId,
) -> Tally {
let first = entries.first_key_value().map(|(id, _)| *id);
Tally { added, length: entries.len() as u64, max_deleted, last, first }
}
fn deleted_after(&self, id: StreamId) -> bool {
self.length > 0 && self.max_deleted != StreamId::MIN && self.max_deleted >= id
}
fn counter_at(&self, id: StreamId) -> Option<u64> {
if id > self.last {
return None;
}
if id == self.last {
return Some(self.added);
}
let gone = self.added.saturating_sub(self.length);
let Some(first) = self.first else {
return (id == StreamId::MIN || id >= self.max_deleted).then_some(gone);
};
if self.max_deleted != StreamId::MIN && self.max_deleted >= first {
return None;
}
if id < first {
return (id == StreamId::MIN || id >= self.max_deleted).then_some(gone);
}
(id == first).then_some(gone + 1)
}
pub(super) fn lag(&self, last_delivered: StreamId, entries_read: Option<u64>) -> Option<i64> {
if self.added == 0 {
return Some(0);
}
let read = match entries_read {
Some(n) if !self.deleted_after(last_delivered) => n,
None if self.length == 0 && last_delivered <= self.last => self.added,
_ => self.counter_at(last_delivered)?,
};
Some(difference(self.added, read))
}
pub(super) fn after_read(
&self,
from: StreamId,
entries_read: Option<u64>,
n: u64,
to: StreamId,
) -> Option<u64> {
if let Some(first) = self.first
&& from < first
&& (self.max_deleted == StreamId::MIN || self.max_deleted < first)
{
return Some(self.added.saturating_sub(self.length).saturating_add(n));
}
match entries_read {
Some(read) if !self.deleted_after(from) => Some(read.saturating_add(n)),
_ => self.counter_at(to),
}
}
}
fn difference(a: u64, b: u64) -> i64 {
let d = i128::from(a) - i128::from(b);
d.clamp(i128::from(i64::MIN), i128::from(i64::MAX)) as i64
}
impl StreamData {
pub(super) fn tally(&self) -> Tally {
Tally::of(&self.entries, self.entries_added, self.max_deleted_id, self.last_id)
}
pub fn group_lag(&self, group: &ConsumerGroup) -> Option<i64> {
self.tally().lag(group.last_delivered_id, group.entries_read)
}
pub fn group_set_entries_read(&mut self, group: &[u8], entries_read: Option<u64>) -> bool {
let Some(g) = self.groups.get_mut(group) else {
return false;
};
g.entries_read = entries_read;
true
}
}
impl Store {
pub fn xgroup_set_entries_read(
&mut self,
key: &[u8],
group: &[u8],
entries_read: Option<u64>,
) -> Result<bool, StoreError> {
let set = match self.stream_mut(key, false)? {
Some(s) => s.group_set_entries_read(group, entries_read),
None => false,
};
if set {
self.bump_if_watched(key);
}
Ok(set)
}
pub fn xgroup_set_delivery_count(
&mut self,
key: &[u8],
group: &[u8],
id: StreamId,
count: u64,
) -> Result<bool, StoreError> {
let Some(s) = self.stream_mut(key, false)? else {
return Ok(false);
};
let Some(p) = s.groups.get_mut(group).and_then(|g| g.pel.get_mut(&id)) else {
return Ok(false);
};
p.delivery_count = count;
Ok(true)
}
pub fn xgroup_consumer_active(
&mut self,
key: &[u8],
group: &[u8],
consumer: &[u8],
active_ms: Option<u64>,
) -> Result<bool, StoreError> {
let Some(s) = self.stream_mut(key, false)? else {
return Ok(false);
};
Ok(s.group_consumer_active(group, consumer, active_ms))
}
}
#[cfg(test)]
mod tests {
use super::*;
fn id(ms: u64) -> StreamId {
StreamId::new(ms, 0)
}
fn tally(live: &[u64], added: u64, deleted: u64, last: u64) -> Tally {
let entries: BTreeMap<StreamId, Vec<(SmallBytes, SmallBytes)>> =
live.iter().map(|ms| (id(*ms), Vec::new())).collect();
Tally::of(&entries, added, id(deleted), id(last))
}
#[test]
fn without_deletions_the_counter_is_exact_everywhere_it_is_known() {
let t = tally(&[1, 2, 3, 4, 5], 5, 0, 5);
assert_eq!(t.lag(id(2), Some(2)), Some(3));
assert_eq!(t.lag(StreamId::MIN, None), Some(5), "nothing read yet");
assert_eq!(t.lag(id(1), None), Some(4), "on the first entry");
assert_eq!(t.lag(id(2), None), None, "mid-stream with no counter");
assert_eq!(t.lag(id(5), None), Some(0), "at the last entry");
assert_eq!(t.lag(id(9), None), None, "past the last entry");
assert_eq!(t.lag(id(2), Some(10)), Some(-5), "a counter set too high");
}
#[test]
fn a_deletion_after_the_position_drops_the_counter() {
let t = tally(&[1, 2, 3, 5], 5, 4, 5);
assert_eq!(t.lag(id(2), Some(2)), None);
assert_eq!(t.lag(id(4), Some(4)), None, "a deletion at the position counts");
assert_eq!(t.lag(StreamId::MIN, None), None, "a deletion among the live entries");
assert_eq!(t.lag(id(5), None), Some(0));
assert_eq!(t.after_read(id(2), Some(2), 1, id(3)), None);
assert_eq!(t.after_read(StreamId::MIN, None, 4, id(5)), Some(5), "read to the end");
}
#[test]
fn deletions_before_the_first_live_entry_leave_the_head_known() {
let t = tally(&[4, 5], 5, 3, 5);
assert_eq!(t.lag(StreamId::MIN, None), Some(2));
assert_eq!(t.lag(id(1), None), None, "a deletion between it and the head");
assert_eq!(t.lag(id(3), None), Some(2));
assert_eq!(t.lag(id(4), None), Some(1));
assert_eq!(t.lag(StreamId::MIN, Some(0)), Some(2), "the counter is not trusted");
assert_eq!(t.after_read(StreamId::MIN, Some(0), 1, id(4)), Some(4));
assert_eq!(t.after_read(id(1), Some(1), 1, id(4)), Some(4), "landed on the head");
}
#[test]
fn a_read_from_before_the_head_counts_from_the_head() {
let t = tally(&[1, 2, 3], 3, 0, 3);
assert_eq!(t.after_read(StreamId::MIN, Some(3), 1, id(1)), Some(1));
assert_eq!(t.after_read(id(1), Some(7), 1, id(2)), Some(8));
assert_eq!(t.after_read(id(1), None, 1, id(2)), None);
}
#[test]
fn an_empty_stream() {
assert_eq!(tally(&[], 0, 0, 0).lag(StreamId::MIN, Some(3)), Some(0), "never written");
let t = tally(&[], 2, 2, 2);
assert_eq!(t.lag(StreamId::MIN, Some(5)), Some(-3));
assert_eq!(t.lag(StreamId::MIN, None), Some(0));
assert_eq!(t.lag(id(9), None), None);
}
#[test]
fn an_emptied_stream_leaves_nothing_to_hand_out_up_to_its_last_id() {
let t = tally(&[], 3, 2, 3);
assert_eq!(t.lag(id(1), None), Some(0), "before the last deletion");
assert_eq!(t.lag(id(2), None), Some(0), "at it");
assert_eq!(t.lag(id(4), None), None, "past the last ID");
assert_eq!(t.lag(id(1), Some(1)), Some(2), "a counter set by hand is taken as it is");
}
#[test]
fn the_read_setters_answer_every_missing_thing() {
use crate::{GroupCreateMode, MissingStream, SetCondition, Store, XAddIdSpec};
let mut s = Store::new();
assert_eq!(s.xgroup_set_entries_read(b"none", b"g", Some(1)), Ok(false));
assert_eq!(s.xgroup_consumer_active(b"none", b"g", b"c", Some(1)), Ok(false));
s.set(b"str", b"v".to_vec(), None, SetCondition::Always);
assert!(s.xgroup_set_entries_read(b"str", b"g", Some(1)).is_err());
assert!(s.xgroup_consumer_active(b"str", b"g", b"c", Some(1)).is_err());
let f = vec![(b"f".to_vec(), b"v".to_vec())];
s.xadd(b"s", XAddIdSpec::Explicit(id(1)), f, MissingStream::Create, 0).unwrap();
assert_eq!(s.xgroup_set_entries_read(b"s", b"g", Some(1)), Ok(false), "no group");
assert_eq!(s.xgroup_consumer_active(b"s", b"g", b"c", Some(1)), Ok(false), "no group");
let g = GroupCreateMode::AtId(StreamId::MIN);
s.xgroup_create(b"s", b"g", g, MissingStream::Refuse).unwrap();
assert_eq!(s.xgroup_consumer_active(b"s", b"g", b"new", Some(7)), Ok(true));
let group = s.stream_group_peek(b"s", b"g").unwrap();
let (_, made) = group.consumers().find(|(n, _)| *n == b"new").unwrap();
assert_eq!(made.last_active_ms(), Some(7), "made, with the time it was active");
}
#[test]
fn a_loaded_group_carries_its_reads() {
use crate::{LoadedGroup, StreamData};
let pel = vec![(1, 0, b"only-in-pel".to_vec(), 5, 1)];
let g = LoadedGroup::new(b"g".to_vec(), (1, 0), vec![(b"c".to_vec(), 5)], pel)
.with_reads(Some(1), vec![(b"c".to_vec(), 4), (b"gone".to_vec(), 3)]);
let mut s = StreamData::default();
s.import_groups(vec![g]);
let group = s.group(b"g").unwrap();
assert_eq!(group.entries_read(), Some(1));
let active: Vec<_> =
group.consumers().map(|(n, c)| (n.to_vec(), c.last_active_ms())).collect();
assert!(active.contains(&(b"c".to_vec(), Some(4))), "{active:?}");
assert!(active.contains(&(b"only-in-pel".to_vec(), None)), "{active:?}");
assert!(!active.iter().any(|(n, _)| n == b"gone"), "{active:?}");
let empty = crate::ConsumerGroup::default();
assert_eq!((empty.entries_read(), empty.consumers().count()), (None, 0));
}
}