Skip to main content

yo_kv/stream/
groups.rs

1//! Consumer groups and the pending entries list (`08` section 7).
2//!
3//! A consumer group is a bookmark plus a ledger. The bookmark is one ID saying
4//! how far the group has read, and the ledger is every entry the group handed
5//! out and has not been told is finished with. Redis calls the ledger the PEL,
6//! the pending entries list, and an entry in it a NACK.
7//!
8//! ```text
9//! group "workers"  last 990-0  read 41823
10//! +----------------------------------------------------------+
11//! | PEL, in ID order                                          |
12//! | 971-0 -> owner c2, handed out 3 times, last at 09:14:02   |
13//! | 984-0 -> owner c1, handed out 1 time,  last at 09:14:07   |
14//! | 990-0 -> owner c1, handed out 1 time,  last at 09:14:07   |
15//! +----------------------------------------------------------+
16//!     consumer c1 holds 984-0, 990-0
17//!     consumer c2 holds 971-0
18//! ```
19//!
20//! The point of the ledger is that a consumer can die holding work. `XPENDING`
21//! finds entries nobody has touched in a while and `XCLAIM` moves them to a
22//! consumer that is still alive, which is the whole reason to use a group
23//! rather than plain `XREAD`.
24//!
25//! # A NACK is in two indexes and owned by neither
26//!
27//! Every pending entry has to be reachable two ways. `XPENDING` and `XAUTOCLAIM`
28//! walk the group's entries in ID order, and `XINFO CONSUMERS` and consumer
29//! deletion need everything one consumer holds. Redis keeps a rax per group and
30//! a rax per consumer holding pointers to the same NACK, which means a claim
31//! updates a pointer in two trees and the NACK belongs to whichever one frees it
32//! last.
33//!
34//! Here the NACK lives in the group's map and the consumer holds only IDs. A
35//! claim moves an ID between two [`BTreeSet`]s and rewrites one field, nothing
36//! is shared and nothing has to be freed carefully. It costs one extra lookup
37//! when going from a consumer's ID to its NACK, which happens on consumer
38//! deletion and nowhere on a hot path.
39//!
40//! # Why a B-tree and not a sorted deque
41//!
42//! The log next door is a sorted deque because entries are appended in order and
43//! trimmed from the front, and never touched in the middle. A PEL is the same
44//! shape most of the time: `XREADGROUP >` appends increasing IDs and `XACK`
45//! usually takes the oldest. But `XCLAIM` and a slow consumer both put holes in
46//! the middle, and an ack of an arbitrary ID is a normal thing to do rather than
47//! a pathology, so the middle is not the rare case here that it is in the log.
48//!
49//! A [`BTreeMap`] keyed by [`Id`] holds the key inline in sixteen bytes with no
50//! allocation per entry and about eleven entries a node, and it gives the
51//! ordered walk `XPENDING` and `XAUTOCLAIM` need. That is already well ahead of
52//! a rax over sixteen byte string keys. Whether the sorted deque with tombstones
53//! would beat it is a real question and the benchmark is there to answer it, but
54//! it is not worth guessing at before the feature works.
55//!
56//! # Consumers are a vector
57//!
58//! A group has a handful of consumers, usually as many as there are processes,
59//! and a name is looked up once per command rather than once per entry. A linear
60//! scan over a vector beats a hash map at that size and brings no dependency and
61//! no hashing with it. A slot is never reused while the group lives, so the
62//! index a NACK holds stays valid.
63
64use std::collections::{BTreeMap, BTreeSet, HashSet};
65
66use super::{Cursor, Id};
67use crate::frozen::{self, Broken};
68
69/// Which pending entries a caller wants, which is every filter `XPENDING` takes.
70///
71/// A struct rather than five more arguments because the command parses them as
72/// a group and they travel together from the wire to here. The default is the
73/// whole list, so a caller that only wants a window sets `start` and `end` and
74/// leaves the rest alone.
75#[derive(Debug, Clone, Copy)]
76pub struct Filter {
77    /// The low end of the ID window, included.
78    pub start: Id,
79    /// The high end, included.
80    pub end: Id,
81    /// At most this many, or every one in the window.
82    pub count: Option<usize>,
83    /// Only what this consumer is holding.
84    pub owner: Option<u32>,
85    /// Only what has been sitting at least this many milliseconds.
86    pub min_idle: u64,
87}
88
89impl Default for Filter {
90    fn default() -> Filter {
91        Filter {
92            start: Id::MIN,
93            end: Id::MAX,
94            count: None,
95            owner: None,
96            min_idle: 0,
97        }
98    }
99}
100
101/// One entry handed out and not yet acknowledged.
102///
103/// Redis calls this a NACK, for not acknowledged.
104#[derive(Debug, Clone, PartialEq, Eq)]
105pub struct Nack {
106    /// When it was last handed out, in milliseconds.
107    ///
108    /// Set on delivery and reset on a claim, because the point of it is how
109    /// long the entry has been sitting with somebody who is not finishing it.
110    time: u64,
111    /// How many times it has been handed out.
112    ///
113    /// `XCLAIM RETRYCOUNT` sets it and `XPENDING` reports it, so a consumer can
114    /// give up on a message that has killed several workers already.
115    count: u64,
116    /// Which consumer slot holds it, or [`Nack::NOBODY`].
117    owner: u32,
118}
119
120impl Nack {
121    /// The slot of an entry that is pending and that nobody holds.
122    ///
123    /// `XNACK` hands work back to the group without giving it to anybody, so the
124    /// pending list has to be able to hold an entry with no consumer against it.
125    /// Redis reports one as an empty consumer name and an idle time of minus
126    /// one, and treats it as idle for longer than any `min-idle-time` a claim can
127    /// name, which is what makes it the next thing `XAUTOCLAIM` picks up.
128    const NOBODY: u32 = u32::MAX;
129
130    /// When it was last handed out.
131    #[must_use]
132    #[inline]
133    pub fn time(&self) -> u64 {
134        self.time
135    }
136
137    /// How many times it has been handed out.
138    #[must_use]
139    #[inline]
140    pub fn count(&self) -> u64 {
141        self.count
142    }
143
144    /// Which consumer holds it, or `None` for one that has been released.
145    #[must_use]
146    #[inline]
147    pub fn owner(&self) -> Option<u32> {
148        (self.owner != Nack::NOBODY).then_some(self.owner)
149    }
150
151    /// How long it has been sitting, which is what `min-idle-time` is compared to.
152    ///
153    /// Saturating, because a NACK whose time was set forward by `XCLAIM TIME` is
154    /// something a caller is allowed to ask for and is not idle at all. An entry
155    /// nobody holds has been idle for as long as there is, so that every claim
156    /// and every `XPENDING IDLE` filter picks it up whatever they asked for.
157    #[must_use]
158    #[inline]
159    pub fn idle(&self, now: u64) -> u64 {
160        if self.owner == Nack::NOBODY {
161            return u64::MAX;
162        }
163        now.saturating_sub(self.time)
164    }
165}
166
167/// What releasing an entry does to its delivery count.
168///
169/// `XNACK` takes one of three words for this and they only differ here. A worker
170/// that could not do the job because the machine it was on went away wants the
171/// attempt not to count, one that failed the way work sometimes fails wants the
172/// count left alone, and one that has decided the message itself is the problem
173/// wants nobody to try it again.
174#[derive(Debug, Clone, Copy, PartialEq, Eq)]
175pub enum Retry {
176    /// `SILENT`: take one off the count, as if the delivery had not happened.
177    Down,
178    /// `FAIL`: leave the count where it is, and start a new entry at zero.
179    Keep,
180    /// `FATAL`: put the count as high as it goes.
181    Max,
182    /// `RETRYCOUNT n`: put the count at exactly this, whatever the word said.
183    At(u64),
184}
185
186impl Retry {
187    /// The count an entry that was on `had` ends up with.
188    ///
189    /// [`Retry::Down`] takes one off rather than putting the count back to zero,
190    /// which is worth saying because the two look the same on an entry that has
191    /// only been handed out once and that is the entry most people try it on. A
192    /// message that has killed four workers and is then released by a fifth for
193    /// a reason that was nothing to do with the message reads as three, not as
194    /// new. It saturates, so a released entry that is released again stays at
195    /// zero rather than wrapping.
196    ///
197    /// [`Retry::Max`] is [`i64::MAX`] and not [`u64::MAX`] because that is the
198    /// number Redis reports, and a client that reads the count into a signed
199    /// integer, which is what the protocol hands it, has to be able to hold it.
200    #[must_use]
201    pub fn applied(self, had: u64) -> u64 {
202        match self {
203            Retry::Down => had.saturating_sub(1),
204            Retry::Keep => had,
205            Retry::Max => i64::MAX as u64,
206            Retry::At(n) => n,
207        }
208    }
209}
210
211/// One consumer inside a group.
212#[derive(Debug, Clone, PartialEq, Eq)]
213pub struct Consumer {
214    name: Vec<u8>,
215    /// When this consumer was last heard from at all.
216    seen: u64,
217    /// When it last read something, as opposed to asking and getting nothing.
218    ///
219    /// Redis separates the two because a consumer polling an empty stream is
220    /// alive but idle, and telling those apart is the difference between a
221    /// worker that is stuck and one that has nothing to do.
222    ///
223    /// `None` for a consumer that has never had anything, which is what
224    /// `XGROUP CREATECONSUMER` makes and what an `XREADGROUP` that found nothing
225    /// leaves behind. Redis reports that as an active time of minus one rather
226    /// than as the moment the consumer turned up, and `XINFO CONSUMERS` passes
227    /// it through to `inactive`, so a fresh consumer reads as never active and
228    /// not as active a moment ago.
229    active: Option<u64>,
230    /// What it holds, in ID order.
231    pending: BTreeSet<Id>,
232}
233
234impl Consumer {
235    /// Its name.
236    #[must_use]
237    #[inline]
238    pub fn name(&self) -> &[u8] {
239        &self.name
240    }
241
242    /// When it was last heard from.
243    #[must_use]
244    #[inline]
245    pub fn seen(&self) -> u64 {
246        self.seen
247    }
248
249    /// When it last actually read something, or `None` if it never has.
250    #[must_use]
251    #[inline]
252    pub fn active(&self) -> Option<u64> {
253        self.active
254    }
255
256    /// How many entries it is holding.
257    #[must_use]
258    #[inline]
259    pub fn len(&self) -> usize {
260        self.pending.len()
261    }
262
263    /// Whether it is holding nothing.
264    #[must_use]
265    #[inline]
266    pub fn is_empty(&self) -> bool {
267        self.pending.is_empty()
268    }
269
270    /// What it is holding, oldest first.
271    pub fn pending(&self) -> impl Iterator<Item = Id> + '_ {
272        self.pending.iter().copied()
273    }
274}
275
276/// A consumer group over one stream.
277#[derive(Debug, Clone, Default)]
278pub struct Group {
279    /// The last ID handed out, which `XREADGROUP >` reads after.
280    last: Id,
281    /// How many entries the group has read, for the lag.
282    ///
283    /// An `Option` because it is not always knowable. `XSETID` without
284    /// `ENTRIESREAD` and a `SETID` to a point nobody can count from both leave
285    /// it unknown, and Redis reports a null lag rather than a made up one.
286    read: Option<u64>,
287    /// Everything handed out and not acknowledged, in ID order.
288    pending: BTreeMap<Id, Nack>,
289    /// The consumers. A slot is emptied on deletion and never reused.
290    consumers: Vec<Option<Consumer>>,
291    /// How many of the pending entries nobody holds.
292    ///
293    /// Kept rather than counted because `XINFO STREAM FULL` reports it and that
294    /// command takes a `COUNT` precisely so that it never walks a long pending
295    /// list. Every line that moves a NACK on or off [`Nack::NOBODY`] is in this
296    /// file and adjusts this, and a test at the bottom checks the number against
297    /// a full scan after a run of mixed operations.
298    nacked: usize,
299    /// Where the last read of this group stopped inside a node's blob.
300    ///
301    /// A note about the shape of the stream and not about the group, dropped by
302    /// anything that moves the bytes it counted past and rebuilt by the next
303    /// read. See [`Cursor`].
304    resume: Option<Cursor>,
305}
306
307/// Two groups are the same when they hold the same entries for the same
308/// consumers at the same place. The resume cursor is not part of that. It is a
309/// note about where a walk got to in a blob, a freeze and thaw drops it, and a
310/// group that has read something is not a different group from the same group
311/// before it did.
312impl PartialEq for Group {
313    fn eq(&self, other: &Group) -> bool {
314        self.last == other.last
315            && self.read == other.read
316            && self.pending == other.pending
317            && self.consumers == other.consumers
318            && self.nacked == other.nacked
319    }
320}
321
322impl Eq for Group {}
323
324impl Group {
325    /// A group reading after `last`, having read `read` entries.
326    #[must_use]
327    pub fn new(last: Id, read: Option<u64>) -> Group {
328        Group {
329            last,
330            read,
331            pending: BTreeMap::new(),
332            consumers: Vec::new(),
333            nacked: 0,
334            resume: None,
335        }
336    }
337
338    /// The node and byte offset the last read stopped at, when it is still good.
339    ///
340    /// Good means two things. The stream has not moved any bytes since, which is
341    /// what the epoch says, and this read is asking for exactly the ID the read
342    /// that left the mark would be asked for next. The second is what makes an
343    /// `XGROUP SETID` back to an older ID safe without the group having to know
344    /// about it: the bookmark is somewhere else, so the mark does not match and
345    /// the walk starts from the front.
346    #[must_use]
347    pub(crate) fn resume(&self, epoch: u64, from: Id) -> Option<(Id, usize)> {
348        let c = self.resume?;
349        (c.epoch == epoch && c.next == from).then_some((c.master, c.byte))
350    }
351
352    /// Keep where the read that just ran stopped, or forget the old mark.
353    pub(crate) fn set_resume(&mut self, at: Option<Cursor>) {
354        self.resume = at;
355    }
356
357    /// The last ID handed out.
358    #[must_use]
359    #[inline]
360    pub fn last_id(&self) -> Id {
361        self.last
362    }
363
364    /// How many entries the group has read, when that is known.
365    #[must_use]
366    #[inline]
367    pub fn entries_read(&self) -> Option<u64> {
368        self.read
369    }
370
371    /// Move the bookmark, which is `XGROUP SETID`.
372    ///
373    /// The PEL is left alone, because the entries in it were handed to somebody
374    /// who has not finished and moving the bookmark says nothing about them.
375    pub fn set_id(&mut self, last: Id, read: Option<u64>) {
376        self.last = last;
377        self.read = read;
378    }
379
380    /// How many entries are pending across the whole group.
381    #[must_use]
382    #[inline]
383    pub fn pending_len(&self) -> usize {
384        self.pending.len()
385    }
386
387    /// How many of those nobody is holding, which `XNACK` is what makes nonzero.
388    #[must_use]
389    #[inline]
390    pub fn nacked_len(&self) -> usize {
391        self.nacked
392    }
393
394    /// Every pending entry with what is known about it, oldest first.
395    ///
396    /// The whole ledger and no filter, which is what an RDB payload carries and
397    /// what nothing on the wire ever asks for, since `XPENDING` always has a
398    /// range and usually a count.
399    pub fn pending_all(&self) -> impl Iterator<Item = (Id, &Nack)> + '_ {
400        self.pending.iter().map(|(&id, nack)| (id, nack))
401    }
402
403    /// Put a pending entry on a group being built from a payload, unowned.
404    ///
405    /// An RDB writes the group's whole pending list first and its consumers
406    /// after it, so there is nobody to hand the entry to at the point it
407    /// arrives. [`Group::restore_owner`] gives it an owner when the consumer
408    /// holding it turns up, and an entry no consumer claims stays unowned. That
409    /// is not a hole in the format: Redis loads the same payload into a NACK
410    /// with a null consumer and leaves it there, so both servers end up with the
411    /// same released entry.
412    pub(crate) fn restore_nack(&mut self, id: Id, time: u64, count: u64) -> bool {
413        if self
414            .pending
415            .keys()
416            .next_back()
417            .is_some_and(|&had| had >= id)
418        {
419            return false;
420        }
421        self.pending.insert(
422            id,
423            Nack {
424                time,
425                count,
426                owner: Nack::NOBODY,
427            },
428        );
429        self.nacked += 1;
430        true
431    }
432
433    /// Make a consumer on a group being built from a payload, times and all.
434    ///
435    /// Not [`Group::create_consumer`], because that one sets both times to now
436    /// and a restored consumer has times of its own that a client can see.
437    pub(crate) fn restore_consumer(
438        &mut self,
439        name: &[u8],
440        seen: u64,
441        active: Option<u64>,
442    ) -> Option<u32> {
443        if self.slot(name).is_some() {
444            return None;
445        }
446        self.consumers.push(Some(Consumer {
447            name: name.to_vec(),
448            seen,
449            active,
450            pending: BTreeSet::new(),
451        }));
452        Some((self.consumers.len() - 1) as u32)
453    }
454
455    /// Hand a restored pending entry to the consumer that was holding it.
456    ///
457    /// Refuses an entry that is not pending or that somebody already holds,
458    /// since a payload naming the same entry under two consumers would leave the
459    /// second consumer holding one the ledger says belongs to the first.
460    pub(crate) fn restore_owner(&mut self, id: Id, slot: u32) -> bool {
461        if !matches!(self.pending.get(&id), Some(nack) if nack.owner == Nack::NOBODY) {
462            return false;
463        }
464        let Some(Some(c)) = self.consumers.get_mut(slot as usize) else {
465            return false;
466        };
467        c.pending.insert(id);
468        self.pending
469            .get_mut(&id)
470            .expect("the entry the check above just found")
471            .owner = slot;
472        self.nacked -= 1;
473        true
474    }
475
476    /// The lowest and highest pending IDs, which is the `XPENDING` summary.
477    #[must_use]
478    pub fn pending_bounds(&self) -> Option<(Id, Id)> {
479        let low = *self.pending.keys().next()?;
480        let high = *self.pending.keys().next_back()?;
481        Some((low, high))
482    }
483
484    /// One pending entry.
485    #[must_use]
486    #[inline]
487    pub fn nack(&self, id: Id) -> Option<&Nack> {
488        self.pending.get(&id)
489    }
490
491    /// The consumer slot for `name`, if there is one.
492    #[must_use]
493    pub fn slot(&self, name: &[u8]) -> Option<u32> {
494        self.consumers
495            .iter()
496            .position(|c| c.as_ref().is_some_and(|c| c.name == name))
497            .map(|at| at as u32)
498    }
499
500    /// A consumer by slot.
501    #[must_use]
502    #[inline]
503    pub fn consumer(&self, slot: u32) -> Option<&Consumer> {
504        self.consumers.get(slot as usize)?.as_ref()
505    }
506
507    /// A consumer by name.
508    #[must_use]
509    pub fn consumer_named(&self, name: &[u8]) -> Option<&Consumer> {
510        self.consumers
511            .iter()
512            .flatten()
513            .find(|c| c.name.as_slice() == name)
514    }
515
516    /// Every consumer, in the order they were created.
517    pub fn consumers(&self) -> impl Iterator<Item = &Consumer> + '_ {
518        self.consumers.iter().flatten()
519    }
520
521    /// The slot for `name`, making the consumer if it is not there yet.
522    ///
523    /// This is what `XREADGROUP` does, since a consumer exists because it turned
524    /// up rather than because anybody declared it.
525    pub fn consumer_or_create(&mut self, name: &[u8], now: u64) -> u32 {
526        if let Some(at) = self.slot(name) {
527            let c = self.consumers[at as usize]
528                .as_mut()
529                .expect("the slot the search just found");
530            c.seen = now;
531            return at;
532        }
533        self.consumers.push(Some(Consumer {
534            name: name.to_vec(),
535            seen: now,
536            active: None,
537            pending: BTreeSet::new(),
538        }));
539        (self.consumers.len() - 1) as u32
540    }
541
542    /// Make a consumer and say whether it was not already there.
543    ///
544    /// `XGROUP CREATECONSUMER`, which answers 1 when it made one.
545    pub fn create_consumer(&mut self, name: &[u8], now: u64) -> bool {
546        if self.slot(name).is_some() {
547            return false;
548        }
549        self.consumer_or_create(name, now);
550        true
551    }
552
553    /// Take a consumer out and say how many entries it was holding.
554    ///
555    /// Those entries stop being pending at all, which is Redis's behaviour and
556    /// is the point of the command: deleting a consumer is how you give up on
557    /// the work it was holding when you would rather lose it than claim it.
558    pub fn delete_consumer(&mut self, name: &[u8]) -> u64 {
559        let Some(at) = self.slot(name) else {
560            return 0;
561        };
562        let gone = self.consumers[at as usize]
563            .take()
564            .expect("the slot the search just found");
565        for id in &gone.pending {
566            self.pending.remove(id);
567        }
568        gone.pending.len() as u64
569    }
570
571    /// Mark that a consumer was heard from.
572    ///
573    /// `read` says whether it got anything, which is what separates seen from
574    /// active.
575    pub fn touch(&mut self, slot: u32, now: u64, read: bool) {
576        if let Some(Some(c)) = self.consumers.get_mut(slot as usize) {
577            c.seen = now;
578            if read {
579                c.active = Some(now);
580            }
581        }
582    }
583
584    /// Hand an entry to a consumer for the first time.
585    ///
586    /// The bookmark moves, since this is the `>` path and the entry is new to
587    /// the group. Answers false if the slot is empty, which a caller that got
588    /// its slot from [`Group::consumer_or_create`] cannot hit.
589    pub fn deliver(&mut self, slot: u32, id: Id, now: u64) -> bool {
590        let Some(Some(c)) = self.consumers.get_mut(slot as usize) else {
591            return false;
592        };
593        c.pending.insert(id);
594        self.pending.insert(
595            id,
596            Nack {
597                time: now,
598                count: 1,
599                owner: slot,
600            },
601        );
602        if id > self.last {
603            self.last = id;
604        }
605        true
606    }
607
608    /// Move the bookmark past an entry without writing it into the ledger.
609    ///
610    /// This is `XREADGROUP ... NOACK`, which is a consumer saying it does not
611    /// want the work tracked. The group still counts the entry as read, because
612    /// the lag is about how far behind the group is and not about how much of
613    /// it is outstanding, so a NOACK reader that has caught up reports a lag of
614    /// zero the same as any other.
615    pub fn skip(&mut self, id: Id) {
616        if id > self.last {
617            self.last = id;
618        }
619    }
620
621    /// Hand an entry to whoever already holds it, which is a history read.
622    ///
623    /// `XREADGROUP` with an ID rather than `>` is a consumer asking for what it
624    /// was already given, and Redis treats that as a real delivery: the time is
625    /// reset and the count goes up, exactly as if the entry had been handed out
626    /// again. Checked against Redis 8.10.1, where a history read of an entry
627    /// idle for 2006 milliseconds left it idle for 2 with its count up by one.
628    ///
629    /// It reads as surprising until you think about what the count is for. It
630    /// counts how many times a consumer has been told to do this work, and a
631    /// consumer re-reading its backlog after a restart has been told again.
632    pub fn redeliver(&mut self, id: Id, now: u64) -> bool {
633        let Some(nack) = self.pending.get_mut(&id) else {
634            return false;
635        };
636        nack.time = now;
637        nack.count += 1;
638        true
639    }
640
641    /// Put the read counter where the stream has worked out it belongs.
642    ///
643    /// The counter is a fact about the stream and not about the group, since
644    /// what a delivery does to it depends on whether anything has been deleted
645    /// ahead of the group. [`crate::stream::Stream::read_group`] is the one
646    /// caller, and it is the one that can see both.
647    pub fn set_read(&mut self, read: Option<u64>) {
648        self.read = read;
649    }
650
651    /// Finish with an entry, which is `XACK`.
652    ///
653    /// Answers whether it was pending. Acknowledging something twice is not an
654    /// error, it just does nothing the second time, because a consumer that
655    /// crashed between doing the work and sending the ack will send it again.
656    pub fn ack(&mut self, id: Id) -> bool {
657        let Some(nack) = self.pending.remove(&id) else {
658            return false;
659        };
660        match self.consumers.get_mut(nack.owner as usize) {
661            Some(Some(c)) => {
662                c.pending.remove(&id);
663            }
664            // Either the slot was emptied under it, which cannot happen because
665            // deleting a consumer takes its entries with it, or nobody held it.
666            _ => self.nacked -= usize::from(nack.owner == Nack::NOBODY),
667        }
668        true
669    }
670
671    /// Hand an entry back to the group without acknowledging it, which is `XNACK`.
672    ///
673    /// The entry stays pending and stops belonging to anybody, so it reads as
674    /// idle for longer than any claim can ask for and the next `XAUTOCLAIM`
675    /// takes it. `retry` is what the delivery count becomes, which is the whole
676    /// difference between the three words `XNACK` takes.
677    ///
678    /// Answers whether it was pending. The bookmark does not move, so a `>` read
679    /// will not hand it out again: releasing an entry offers it to a claim and
680    /// not to the group's next reader, which is Redis's behaviour and the only
681    /// one that keeps a released entry from being delivered twice over.
682    pub fn release(&mut self, id: Id, retry: Retry) -> bool {
683        let Some(nack) = self.pending.get_mut(&id) else {
684            return false;
685        };
686        let was = std::mem::replace(&mut nack.owner, Nack::NOBODY);
687        nack.count = retry.applied(nack.count);
688        // Zero rather than now, because the delivery time of an entry nobody
689        // holds is never read as a time: `XPENDING` reports minus one for it and
690        // `XINFO STREAM FULL` reports the zero.
691        nack.time = 0;
692        if was == Nack::NOBODY {
693            return true;
694        }
695        self.nacked += 1;
696        if let Some(Some(c)) = self.consumers.get_mut(was as usize) {
697            c.pending.remove(&id);
698        }
699        true
700    }
701
702    /// Make a released entry out of one that was not pending, which is
703    /// `XNACK ... FORCE`.
704    ///
705    /// The caller has to have checked that the entry is really in the stream,
706    /// for the same reason [`Group::force`] does. A count of zero is where a
707    /// released entry that has never been delivered starts, whatever word was
708    /// used, because there is no earlier count for `FAIL` to keep.
709    pub fn force_release(&mut self, id: Id, retry: Retry) {
710        if self.release(id, retry) {
711            return;
712        }
713        self.pending.insert(
714            id,
715            Nack {
716                time: 0,
717                count: retry.applied(0),
718                owner: Nack::NOBODY,
719            },
720        );
721        self.nacked += 1;
722    }
723
724    /// Drop a pending entry without it having been acknowledged.
725    ///
726    /// What happens to a NACK whose entry is no longer in the stream. `XCLAIM`
727    /// and `XAUTOCLAIM` both clear those out as they find them, because a
728    /// pending entry nobody can ever read is work no consumer can ever finish.
729    pub fn forget(&mut self, id: Id) -> bool {
730        self.ack(id)
731    }
732
733    /// Move an entry to another consumer, which is the middle of `XCLAIM`.
734    ///
735    /// `time` is when it should count as having been handed out, which is now
736    /// for a plain claim and something a caller chose for `IDLE` or `TIME`.
737    /// `count` replaces the delivery count when it is given, which is
738    /// `RETRYCOUNT`, and otherwise the count goes up by one unless `bump` says
739    /// not to, which is `JUSTID`.
740    ///
741    /// Answers false when the entry was not pending or the slot is empty.
742    pub fn claim(&mut self, id: Id, slot: u32, time: u64, count: Option<u64>, bump: bool) -> bool {
743        if !matches!(self.consumers.get(slot as usize), Some(Some(_))) {
744            return false;
745        }
746        let Some(nack) = self.pending.get_mut(&id) else {
747            return false;
748        };
749        let was = nack.owner;
750        nack.owner = slot;
751        nack.time = time;
752        if let Some(n) = count {
753            nack.count = n;
754        } else if bump {
755            nack.count += 1;
756        }
757        if was != slot {
758            if was == Nack::NOBODY {
759                // A claim is how a released entry gets an owner again, and it is
760                // the only way, since a `>` read never looks below the bookmark.
761                self.nacked -= 1;
762            } else if let Some(Some(c)) = self.consumers.get_mut(was as usize) {
763                c.pending.remove(&id);
764            }
765            if let Some(Some(c)) = self.consumers.get_mut(slot as usize) {
766                c.pending.insert(id);
767            }
768        }
769        true
770    }
771
772    /// Make a pending entry that was not pending, which is `XCLAIM FORCE`.
773    ///
774    /// The caller has to have checked that the entry is really in the stream,
775    /// because this cannot see the stream and creating a NACK for an entry that
776    /// is not there is exactly the state [`Group::forget`] exists to clean up.
777    pub fn force(&mut self, id: Id, slot: u32, time: u64, count: u64) -> bool {
778        let Some(Some(c)) = self.consumers.get_mut(slot as usize) else {
779            return false;
780        };
781        c.pending.insert(id);
782        self.pending.insert(
783            id,
784            Nack {
785                time,
786                count,
787                owner: slot,
788            },
789        );
790        true
791    }
792
793    /// Pending entries in `want`, oldest first.
794    ///
795    /// The callback answers whether to carry on, and gets `None` for the owner
796    /// of an entry that has been released, which `XPENDING` writes as an empty
797    /// name. A consumer filter never matches one of those, since asking what a
798    /// named consumer is holding is asking about entries that have an owner.
799    pub fn pending_range<F>(&self, want: Filter, now: u64, mut f: F) -> usize
800    where
801        F: FnMut(Id, &Nack, Option<&Consumer>) -> bool,
802    {
803        let mut seen = 0;
804        for (&id, nack) in self.pending.range(want.start..=want.end) {
805            if want.count.is_some_and(|n| seen >= n) {
806                break;
807            }
808            if want.owner.is_some_and(|c| Some(c) != nack.owner()) {
809                continue;
810            }
811            if nack.idle(now) < want.min_idle {
812                continue;
813            }
814            let who = match nack.owner() {
815                Some(slot) => match self.consumers.get(slot as usize) {
816                    Some(Some(c)) => Some(c),
817                    // A slot that has been emptied under a NACK, which deleting
818                    // a consumer cannot leave behind and nothing else can make.
819                    _ => continue,
820                },
821                None => None,
822            };
823            seen += 1;
824            if !f(id, nack, who) {
825                break;
826            }
827        }
828        seen
829    }
830
831    /// How many entries each consumer is holding, for the `XPENDING` summary.
832    pub fn pending_counts(&self) -> impl Iterator<Item = (&[u8], usize)> + '_ {
833        self.consumers
834            .iter()
835            .flatten()
836            .filter(|c| !c.pending.is_empty())
837            .map(|c| (c.name.as_slice(), c.pending.len()))
838    }
839
840    /// The IDs an `XAUTOCLAIM` would take, from `start` and idle at least
841    /// `min_idle`, and where a following call should carry on from.
842    ///
843    /// Only the scan, because deciding what to do with each one needs the
844    /// stream and this does not have it. The cursor is `None` when the scan
845    /// reached the end, which is the `0-0` Redis answers with.
846    #[must_use]
847    pub fn claimable(
848        &self,
849        start: Id,
850        min_idle: u64,
851        now: u64,
852        limit: usize,
853        out: &mut Vec<Id>,
854    ) -> Option<Id> {
855        // Redis charges attempts rather than hits, so a scan over a PEL full of
856        // entries that are not idle enough still ends and hands back a cursor
857        // instead of walking a million NACKs inside one command.
858        for (tried, (&id, nack)) in self.pending.range(start..).enumerate() {
859            if out.len() >= limit || tried >= limit * 10 {
860                return Some(id);
861            }
862            if nack.idle(now) >= min_idle {
863                out.push(id);
864            }
865        }
866        None
867    }
868
869    /// Write the group out as bytes, for [`super::Stream::freeze`].
870    ///
871    /// The consumer slots go out including the empty ones, because a slot is
872    /// emptied on deletion and never reused and a NACK names its owner by slot
873    /// number. Renumbering them on the way through would hand every pending
874    /// entry to the wrong consumer.
875    ///
876    /// What each consumer is holding is not written. It is a partition of the
877    /// pending list by owner, so it is rebuilt from the pending list on the way
878    /// back in and the two cannot come back disagreeing.
879    pub(super) fn freeze(&self, out: &mut Vec<u8>) {
880        frozen::put_uint(out, self.last.ms);
881        frozen::put_uint(out, self.last.seq);
882        put_opt(out, self.read);
883
884        frozen::put_uint(out, self.consumers.len() as u64);
885        for slot in &self.consumers {
886            match slot {
887                None => out.push(0),
888                Some(c) => {
889                    out.push(1);
890                    frozen::put_bytes(out, &c.name);
891                    frozen::put_uint(out, c.seen);
892                    put_opt(out, c.active);
893                }
894            }
895        }
896
897        frozen::put_uint(out, self.pending.len() as u64);
898        for (id, nack) in &self.pending {
899            frozen::put_uint(out, id.ms);
900            frozen::put_uint(out, id.seq);
901            frozen::put_uint(out, nack.time);
902            frozen::put_uint(out, nack.count);
903            // The owner goes out as itself rather than as an index with a spare
904            // value for nobody, because [`Nack::NOBODY`] already is one.
905            frozen::put_uint(out, u64::from(nack.owner));
906        }
907    }
908
909    /// Read back a group [`Group::freeze`] wrote.
910    pub(super) fn thaw(cut: &mut frozen::Cut<'_>) -> Result<Group, Broken> {
911        let last = Id::new(cut.uint()?, cut.uint()?);
912        let read = take_opt(cut)?;
913
914        let n = usize::try_from(cut.uint()?).map_err(|_| Broken::Short)?;
915        // A slot is a byte at the very least, so a count past what is left is a
916        // short body and not a reason to reserve that many.
917        if n > cut.rest().len() {
918            return Err(Broken::Short);
919        }
920        let mut consumers: Vec<Option<Consumer>> = Vec::with_capacity(n);
921        let mut names = HashSet::with_capacity(n);
922        for _ in 0..n {
923            match cut.byte()? {
924                0 => consumers.push(None),
925                1 => {
926                    let name = cut.bytes()?;
927                    // Two consumers under one name would make every lookup find
928                    // the first and leave the second unreachable, holding
929                    // entries nothing can claim back.
930                    if !names.insert(name) {
931                        return Err(Broken::Body);
932                    }
933                    consumers.push(Some(Consumer {
934                        name: name.to_vec(),
935                        seen: cut.uint()?,
936                        active: take_opt(cut)?,
937                        pending: BTreeSet::new(),
938                    }));
939                }
940                _ => return Err(Broken::Body),
941            }
942        }
943
944        let n = usize::try_from(cut.uint()?).map_err(|_| Broken::Short)?;
945        // Five numbers each, so one byte apiece is already generous.
946        if n > cut.rest().len() {
947            return Err(Broken::Short);
948        }
949        let mut group = Group {
950            last,
951            read,
952            pending: BTreeMap::new(),
953            consumers,
954            nacked: 0,
955            resume: None,
956        };
957        let mut prev = None;
958        for _ in 0..n {
959            let id = Id::new(cut.uint()?, cut.uint()?);
960            // Written in ID order out of a map, so anything else is bytes that
961            // did not come from `freeze`, and a repeat would silently drop an
962            // entry somebody is holding.
963            if prev.is_some_and(|p| p >= id) {
964                return Err(Broken::Body);
965            }
966            prev = Some(id);
967            let nack = Nack {
968                time: cut.uint()?,
969                count: cut.uint()?,
970                owner: u32::try_from(cut.uint()?).map_err(|_| Broken::Body)?,
971            };
972            if nack.owner == Nack::NOBODY {
973                group.nacked += 1;
974            } else {
975                match group.consumers.get_mut(nack.owner as usize) {
976                    Some(Some(c)) => {
977                        c.pending.insert(id);
978                    }
979                    // An owner that is off the end or an emptied slot would be
980                    // an entry held by a consumer that cannot be named, so
981                    // neither `XPENDING` nor a claim would ever reach it.
982                    _ => return Err(Broken::Body),
983                }
984            }
985            group.pending.insert(id, nack);
986        }
987        Ok(group)
988    }
989
990    /// How many bytes this group takes, not counting the struct itself.
991    ///
992    /// The pending map is counted per entry at the size of a key and a value
993    /// plus a share of the node around them, rather than exactly, because a
994    /// [`BTreeMap`] does not say how many nodes it has and the answer is only
995    /// ever read by `MEMORY USAGE` and the eviction total. A B-tree node here
996    /// holds eleven entries and some overhead, and a sixteenth of an entry is
997    /// close enough for both.
998    #[must_use]
999    pub fn memory_bytes(&self) -> usize {
1000        let each = std::mem::size_of::<(Id, Nack)>();
1001        let pending = self.pending.len() * (each + each / 16);
1002        let consumers: usize = self
1003            .consumers
1004            .iter()
1005            .map(|slot| {
1006                std::mem::size_of::<Option<Consumer>>()
1007                    + slot.as_ref().map_or(0, |c| {
1008                        let each = std::mem::size_of::<Id>();
1009                        c.name.capacity() + c.pending.len() * (each + each / 16)
1010                    })
1011            })
1012            .sum();
1013        pending + consumers
1014    }
1015}
1016
1017/// Append a count that may not be known, as a flag byte and then the number.
1018///
1019/// A byte rather than the usual trick of writing one more than the number and
1020/// keeping zero for nothing, because both of the counts this is used for are a
1021/// `u64` and adding one to the top of the range wraps. The flag costs a byte
1022/// and is right everywhere.
1023fn put_opt(out: &mut Vec<u8>, v: Option<u64>) {
1024    match v {
1025        None => out.push(0),
1026        Some(n) => {
1027            out.push(1);
1028            frozen::put_uint(out, n);
1029        }
1030    }
1031}
1032
1033/// Read back what [`put_opt`] wrote.
1034fn take_opt(cut: &mut frozen::Cut<'_>) -> Result<Option<u64>, Broken> {
1035    match cut.byte()? {
1036        0 => Ok(None),
1037        1 => Ok(Some(cut.uint()?)),
1038        _ => Err(Broken::Body),
1039    }
1040}
1041
1042#[cfg(test)]
1043mod tests {
1044    use super::*;
1045
1046    fn group() -> Group {
1047        Group::new(Id::MIN, Some(0))
1048    }
1049
1050    #[test]
1051    fn a_consumer_appears_by_turning_up() {
1052        let mut g = group();
1053        assert_eq!(g.slot(b"alice"), None);
1054        let at = g.consumer_or_create(b"alice", 100);
1055        assert_eq!(g.slot(b"alice"), Some(at));
1056        assert_eq!(g.consumer_or_create(b"alice", 200), at);
1057        assert_eq!(g.consumers().count(), 1);
1058        assert_eq!(g.consumer(at).expect("alice").seen(), 200);
1059    }
1060
1061    #[test]
1062    fn creating_a_consumer_twice_says_so() {
1063        let mut g = group();
1064        assert!(g.create_consumer(b"alice", 1));
1065        assert!(!g.create_consumer(b"alice", 2));
1066    }
1067
1068    #[test]
1069    fn delivering_moves_the_bookmark_and_fills_both_indexes() {
1070        let mut g = group();
1071        let a = g.consumer_or_create(b"alice", 10);
1072        assert!(g.deliver(a, Id::new(5, 0), 10));
1073        assert!(g.deliver(a, Id::new(7, 0), 12));
1074
1075        assert_eq!(g.last_id(), Id::new(7, 0));
1076        assert_eq!(g.pending_len(), 2);
1077        assert_eq!(
1078            g.consumer(a).expect("alice").pending().collect::<Vec<_>>(),
1079            vec![Id::new(5, 0), Id::new(7, 0)]
1080        );
1081        let nack = g.nack(Id::new(5, 0)).expect("a nack");
1082        assert_eq!((nack.count(), nack.time()), (1, 10));
1083    }
1084
1085    #[test]
1086    fn acking_takes_it_out_of_both_indexes() {
1087        let mut g = group();
1088        let a = g.consumer_or_create(b"alice", 1);
1089        g.deliver(a, Id::new(5, 0), 1);
1090        assert!(g.ack(Id::new(5, 0)));
1091        assert_eq!(g.pending_len(), 0);
1092        assert!(g.consumer(a).expect("alice").is_empty());
1093        // Twice is not an error, because a consumer that crashed after doing the
1094        // work and before sending the ack will send it again.
1095        assert!(!g.ack(Id::new(5, 0)));
1096    }
1097
1098    #[test]
1099    fn acking_does_not_move_the_bookmark() {
1100        let mut g = group();
1101        let a = g.consumer_or_create(b"alice", 1);
1102        g.deliver(a, Id::new(5, 0), 1);
1103        g.ack(Id::new(5, 0));
1104        assert_eq!(g.last_id(), Id::new(5, 0));
1105    }
1106
1107    #[test]
1108    fn a_claim_moves_it_between_consumers() {
1109        let mut g = group();
1110        let a = g.consumer_or_create(b"alice", 1);
1111        let b = g.consumer_or_create(b"bob", 1);
1112        g.deliver(a, Id::new(5, 0), 100);
1113
1114        assert!(g.claim(Id::new(5, 0), b, 500, None, true));
1115        assert!(g.consumer(a).expect("alice").is_empty());
1116        assert_eq!(
1117            g.consumer(b).expect("bob").pending().collect::<Vec<_>>(),
1118            vec![Id::new(5, 0)]
1119        );
1120        let nack = g.nack(Id::new(5, 0)).expect("a nack");
1121        assert_eq!((nack.count(), nack.time()), (2, 500));
1122    }
1123
1124    #[test]
1125    fn a_claim_that_does_not_bump_is_justid() {
1126        let mut g = group();
1127        let a = g.consumer_or_create(b"alice", 1);
1128        let b = g.consumer_or_create(b"bob", 1);
1129        g.deliver(a, Id::new(5, 0), 100);
1130        g.claim(Id::new(5, 0), b, 500, None, false);
1131        assert_eq!(g.nack(Id::new(5, 0)).expect("a nack").count(), 1);
1132    }
1133
1134    #[test]
1135    fn a_retry_count_replaces_rather_than_adds() {
1136        let mut g = group();
1137        let a = g.consumer_or_create(b"alice", 1);
1138        g.deliver(a, Id::new(5, 0), 100);
1139        g.claim(Id::new(5, 0), a, 500, Some(9), true);
1140        assert_eq!(g.nack(Id::new(5, 0)).expect("a nack").count(), 9);
1141    }
1142
1143    #[test]
1144    fn claiming_back_to_the_same_consumer_keeps_it_there() {
1145        let mut g = group();
1146        let a = g.consumer_or_create(b"alice", 1);
1147        g.deliver(a, Id::new(5, 0), 100);
1148        assert!(g.claim(Id::new(5, 0), a, 500, None, true));
1149        assert_eq!(
1150            g.consumer(a).expect("alice").pending().collect::<Vec<_>>(),
1151            vec![Id::new(5, 0)]
1152        );
1153    }
1154
1155    #[test]
1156    fn nothing_pending_cannot_be_claimed_without_force() {
1157        let mut g = group();
1158        let a = g.consumer_or_create(b"alice", 1);
1159        assert!(!g.claim(Id::new(5, 0), a, 500, None, true));
1160        assert!(g.force(Id::new(5, 0), a, 500, 1));
1161        assert_eq!(g.pending_len(), 1);
1162    }
1163
1164    #[test]
1165    fn deleting_a_consumer_gives_up_its_work() {
1166        let mut g = group();
1167        let a = g.consumer_or_create(b"alice", 1);
1168        let b = g.consumer_or_create(b"bob", 1);
1169        g.deliver(a, Id::new(5, 0), 1);
1170        g.deliver(a, Id::new(6, 0), 1);
1171        g.deliver(b, Id::new(7, 0), 1);
1172
1173        assert_eq!(g.delete_consumer(b"alice"), 2);
1174        assert_eq!(g.pending_len(), 1);
1175        assert!(g.nack(Id::new(5, 0)).is_none());
1176        assert!(g.nack(Id::new(7, 0)).is_some());
1177        assert_eq!(g.delete_consumer(b"alice"), 0);
1178        // The bookmark is untouched, so the entries are not handed out again.
1179        assert_eq!(g.last_id(), Id::new(7, 0));
1180    }
1181
1182    #[test]
1183    fn a_deleted_slot_is_not_reused() {
1184        let mut g = group();
1185        let a = g.consumer_or_create(b"alice", 1);
1186        g.delete_consumer(b"alice");
1187        let b = g.consumer_or_create(b"bob", 1);
1188        assert_ne!(a, b);
1189        assert_eq!(g.consumers().count(), 1);
1190    }
1191
1192    #[test]
1193    fn idle_is_measured_from_the_last_hand_out() {
1194        let mut g = group();
1195        let a = g.consumer_or_create(b"alice", 1);
1196        g.deliver(a, Id::new(5, 0), 1_000);
1197        assert_eq!(g.nack(Id::new(5, 0)).expect("a nack").idle(4_000), 3_000);
1198        // A time set into the future is something XCLAIM TIME allows, and it is
1199        // not idle rather than idle by a negative amount.
1200        assert_eq!(g.nack(Id::new(5, 0)).expect("a nack").idle(500), 0);
1201    }
1202
1203    #[test]
1204    fn the_summary_is_the_two_ends_and_the_counts() {
1205        let mut g = group();
1206        let a = g.consumer_or_create(b"alice", 1);
1207        let b = g.consumer_or_create(b"bob", 1);
1208        for ms in [3u64, 5, 9] {
1209            g.deliver(a, Id::new(ms, 0), 1);
1210        }
1211        g.deliver(b, Id::new(11, 0), 1);
1212
1213        assert_eq!(g.pending_bounds(), Some((Id::new(3, 0), Id::new(11, 0))));
1214        let counts: Vec<_> = g
1215            .pending_counts()
1216            .map(|(n, c)| (String::from_utf8_lossy(n).into_owned(), c))
1217            .collect();
1218        assert_eq!(counts, vec![("alice".into(), 3), ("bob".into(), 1)]);
1219    }
1220
1221    #[test]
1222    fn a_consumer_with_nothing_is_left_out_of_the_summary() {
1223        let mut g = group();
1224        g.consumer_or_create(b"alice", 1);
1225        assert_eq!(g.pending_counts().count(), 0);
1226        assert_eq!(g.pending_bounds(), None);
1227    }
1228
1229    #[test]
1230    fn the_pending_range_takes_both_ends_and_the_filters() {
1231        let mut g = group();
1232        let a = g.consumer_or_create(b"alice", 1);
1233        let b = g.consumer_or_create(b"bob", 1);
1234        g.deliver(a, Id::new(3, 0), 100);
1235        g.deliver(b, Id::new(5, 0), 100);
1236        g.deliver(a, Id::new(9, 0), 900);
1237
1238        let seen = |g: &Group, start, end, owner, idle| {
1239            let mut out = Vec::new();
1240            let want = Filter {
1241                start,
1242                end,
1243                owner,
1244                min_idle: idle,
1245                ..Filter::default()
1246            };
1247            g.pending_range(want, 1_000, |id, _, c| {
1248                out.push((
1249                    id,
1250                    String::from_utf8_lossy(c.expect("an owner").name()).into_owned(),
1251                ));
1252                true
1253            });
1254            out
1255        };
1256
1257        assert_eq!(seen(&g, Id::MIN, Id::MAX, None, 0).len(), 3);
1258        assert_eq!(seen(&g, Id::new(4, 0), Id::new(9, 0), None, 0).len(), 2);
1259        assert_eq!(
1260            seen(&g, Id::MIN, Id::MAX, Some(a), 0),
1261            vec![
1262                (Id::new(3, 0), "alice".into()),
1263                (Id::new(9, 0), "alice".into())
1264            ]
1265        );
1266        // Only the two handed out at 100 have been sitting 500 milliseconds.
1267        assert_eq!(seen(&g, Id::MIN, Id::MAX, None, 500).len(), 2);
1268    }
1269
1270    #[test]
1271    fn a_count_stops_the_pending_range() {
1272        let mut g = group();
1273        let a = g.consumer_or_create(b"alice", 1);
1274        for ms in 1..=10u64 {
1275            g.deliver(a, Id::new(ms, 0), 1);
1276        }
1277        let mut out = Vec::new();
1278        let want = Filter {
1279            count: Some(4),
1280            ..Filter::default()
1281        };
1282        let seen = g.pending_range(want, 1, |id, _, _| {
1283            out.push(id);
1284            true
1285        });
1286        assert_eq!((seen, out.len()), (4, 4));
1287    }
1288
1289    #[test]
1290    fn the_callback_can_stop_the_pending_range() {
1291        let mut g = group();
1292        let a = g.consumer_or_create(b"alice", 1);
1293        for ms in 1..=10u64 {
1294            g.deliver(a, Id::new(ms, 0), 1);
1295        }
1296        let mut out = Vec::new();
1297        g.pending_range(Filter::default(), 1, |id, _, _| {
1298            out.push(id);
1299            out.len() < 3
1300        });
1301        assert_eq!(out.len(), 3);
1302    }
1303
1304    #[test]
1305    fn claimable_takes_the_idle_ones_and_says_where_to_carry_on() {
1306        let mut g = group();
1307        let a = g.consumer_or_create(b"alice", 1);
1308        for ms in 1..=10u64 {
1309            g.deliver(a, Id::new(ms, 0), if ms <= 5 { 100 } else { 900 });
1310        }
1311        let mut out = Vec::new();
1312        let cursor = g.claimable(Id::MIN, 500, 1_000, 100, &mut out);
1313        assert_eq!(cursor, None, "the scan reached the end");
1314        assert_eq!(out, (1..=5).map(|ms| Id::new(ms, 0)).collect::<Vec<_>>());
1315
1316        // A limit hands back where the next call starts.
1317        out.clear();
1318        let cursor = g.claimable(Id::MIN, 500, 1_000, 3, &mut out);
1319        assert_eq!(out.len(), 3);
1320        assert_eq!(cursor, Some(Id::new(4, 0)));
1321    }
1322
1323    /// The read counter is set from outside and a delivery does not touch it.
1324    ///
1325    /// It looks like something the group should keep for itself, and it is not:
1326    /// what a delivery does to it depends on whether anything has been deleted
1327    /// ahead of the entry being handed over, which is a fact about the stream.
1328    /// The rule lives in [`crate::stream::Stream::read_group`] and this only
1329    /// holds the number.
1330    #[test]
1331    fn a_delivery_leaves_the_read_counter_to_the_stream() {
1332        let mut g = group();
1333        let a = g.consumer_or_create(b"alice", 1);
1334        g.deliver(a, Id::new(1, 0), 1);
1335        assert_eq!(g.entries_read(), Some(0), "the group did not count it");
1336
1337        g.set_read(Some(1));
1338        assert_eq!(g.entries_read(), Some(1));
1339        g.set_read(None);
1340        assert_eq!(g.entries_read(), None, "and it can be given up on");
1341    }
1342
1343    #[test]
1344    fn setting_the_id_leaves_the_pending_list_alone() {
1345        let mut g = group();
1346        let a = g.consumer_or_create(b"alice", 1);
1347        g.deliver(a, Id::new(5, 0), 1);
1348        g.set_id(Id::MIN, Some(0));
1349        assert_eq!(g.last_id(), Id::MIN);
1350        assert_eq!(g.pending_len(), 1, "somebody is still holding it");
1351    }
1352
1353    /// A released entry is pending, owned by nobody, and idle for ever.
1354    #[test]
1355    fn releasing_takes_the_entry_out_of_the_consumers_hands() {
1356        let mut g = group();
1357        let a = g.consumer_or_create(b"alice", 1);
1358        g.deliver(a, Id::new(5, 0), 100);
1359
1360        assert!(g.release(Id::new(5, 0), Retry::Keep));
1361        assert_eq!(g.pending_len(), 1, "it is still the group's problem");
1362        assert_eq!(
1363            g.consumer(a).expect("alice").pending().count(),
1364            0,
1365            "and no longer alice's"
1366        );
1367        let nack = g.nack(Id::new(5, 0)).expect("a nack");
1368        assert_eq!(nack.owner(), None);
1369        assert_eq!(nack.count(), 1, "Keep left the count where it was");
1370        // Idle for longer than any min-idle-time a claim can name, which is what
1371        // puts it at the front of the next sweep.
1372        assert_eq!(nack.idle(100), u64::MAX);
1373        let mut out = Vec::new();
1374        assert_eq!(g.claimable(Id::MIN, u64::MAX, 100, 10, &mut out), None);
1375        assert_eq!(out, vec![Id::new(5, 0)]);
1376
1377        // Releasing it again is still true and does not count it twice.
1378        assert!(g.release(Id::new(5, 0), Retry::Keep));
1379        assert_eq!(g.nacked_len(), 1);
1380        // And nothing pending is false, however it is asked.
1381        assert!(!g.release(Id::new(9, 0), Retry::Keep));
1382    }
1383
1384    #[test]
1385    fn the_three_words_differ_only_in_the_delivery_count() {
1386        let count = |retry| {
1387            let mut g = group();
1388            let a = g.consumer_or_create(b"alice", 1);
1389            g.deliver(a, Id::new(5, 0), 1);
1390            g.claim(Id::new(5, 0), a, 2, None, true);
1391            g.release(Id::new(5, 0), retry);
1392            g.nack(Id::new(5, 0)).expect("a nack").count()
1393        };
1394        assert_eq!(count(Retry::Down), 1, "one off, not back to nothing");
1395        assert_eq!(count(Retry::Keep), 2);
1396        assert_eq!(count(Retry::Max), i64::MAX as u64);
1397        assert_eq!(count(Retry::At(7)), 7);
1398    }
1399
1400    /// Forcing makes the pending entry when there is not one, and does not make
1401    /// a second one when there is.
1402    #[test]
1403    fn forcing_a_release_is_the_same_call_twice() {
1404        let mut g = group();
1405        g.force_release(Id::new(5, 0), Retry::Keep);
1406        assert_eq!(g.pending_len(), 1);
1407        assert_eq!(
1408            g.nack(Id::new(5, 0)).expect("a nack").count(),
1409            0,
1410            "there was no earlier count to keep"
1411        );
1412
1413        g.force_release(Id::new(5, 0), Retry::At(4));
1414        assert_eq!(g.pending_len(), 1);
1415        assert_eq!(g.nacked_len(), 1);
1416        assert_eq!(g.nack(Id::new(5, 0)).expect("a nack").count(), 4);
1417    }
1418
1419    /// The counter behind `XINFO STREAM FULL`'s `nacked-count`, which is a field
1420    /// and not a walk, so every line that moves an entry on or off `NOBODY` has
1421    /// to keep it right. This is the walk, run against the field.
1422    #[test]
1423    fn the_nacked_count_matches_a_full_scan() {
1424        let mut g = group();
1425        let a = g.consumer_or_create(b"alice", 1);
1426        let b = g.consumer_or_create(b"bob", 1);
1427        for ms in 1..=6u64 {
1428            g.deliver(a, Id::new(ms, 0), 100);
1429        }
1430
1431        let scan = |g: &Group| {
1432            (1..=9u64)
1433                .filter(|&ms| g.nack(Id::new(ms, 0)).is_some_and(|n| n.owner().is_none()))
1434                .count()
1435        };
1436        let agrees = |g: &Group| assert_eq!(g.nacked_len(), scan(g), "the field drifted");
1437
1438        agrees(&g);
1439        g.release(Id::new(1, 0), Retry::Keep);
1440        g.release(Id::new(2, 0), Retry::Keep);
1441        agrees(&g);
1442
1443        // A claim takes one back into somebody's hands.
1444        g.claim(Id::new(1, 0), b, 200, None, true);
1445        agrees(&g);
1446        // An ack takes one out of the list altogether, released or not.
1447        assert!(g.ack(Id::new(2, 0)));
1448        assert!(g.ack(Id::new(3, 0)));
1449        agrees(&g);
1450        // And a forced release on an entry nobody was ever handed.
1451        g.force_release(Id::new(9, 0), Retry::Down);
1452        agrees(&g);
1453        assert_eq!(g.nacked_len(), 1);
1454    }
1455
1456    /// A consumer filter skips released entries, because a released entry has no
1457    /// consumer to match and `XPENDING key group - + n consumer` is a question
1458    /// about one consumer's work.
1459    #[test]
1460    fn a_released_entry_is_not_anybodys_pending_work() {
1461        let mut g = group();
1462        let a = g.consumer_or_create(b"alice", 1);
1463        g.deliver(a, Id::new(3, 0), 100);
1464        g.deliver(a, Id::new(5, 0), 100);
1465        g.release(Id::new(3, 0), Retry::Keep);
1466
1467        let seen = |g: &Group, owner| {
1468            let mut out = Vec::new();
1469            let want = Filter {
1470                owner,
1471                ..Filter::default()
1472            };
1473            g.pending_range(want, 1_000, |id, _, c| {
1474                out.push((id, c.map(|c| c.name().to_vec())));
1475                true
1476            });
1477            out
1478        };
1479
1480        assert_eq!(
1481            seen(&g, None),
1482            vec![
1483                (Id::new(3, 0), None),
1484                (Id::new(5, 0), Some(b"alice".to_vec()))
1485            ]
1486        );
1487        assert_eq!(
1488            seen(&g, Some(a)),
1489            vec![(Id::new(5, 0), Some(b"alice".to_vec()))]
1490        );
1491        // The summary counts it against nobody, so alice is down to one.
1492        assert_eq!(
1493            g.pending_counts()
1494                .map(|(name, n)| (name.to_vec(), n))
1495                .collect::<Vec<_>>(),
1496            vec![(b"alice".to_vec(), 1)]
1497        );
1498    }
1499
1500    #[test]
1501    fn a_frozen_group_with_a_pending_entry_nobody_could_hold_is_refused() {
1502        let mut g = group();
1503        let slot = g.consumer_or_create(b"alice", 1_000);
1504        g.deliver(slot, Id::new(1, 0), 1_000);
1505        let mut bytes = Vec::new();
1506        g.freeze(&mut bytes);
1507        assert_eq!(Group::thaw(&mut frozen::Cut::new(&bytes)), Ok(g));
1508
1509        // The owner is the last number in the body, and a slot past the end
1510        // would be an entry no consumer could ever be told to finish.
1511        let mut bad = bytes.clone();
1512        *bad.last_mut().expect("a body") = 7;
1513        assert_eq!(Group::thaw(&mut frozen::Cut::new(&bad)), Err(Broken::Body));
1514
1515        for cut in 0..bytes.len() {
1516            assert!(
1517                Group::thaw(&mut frozen::Cut::new(&bytes[..cut])).is_err(),
1518                "cut at {cut}"
1519            );
1520        }
1521    }
1522
1523    #[test]
1524    fn a_frozen_group_that_names_one_consumer_twice_is_refused() {
1525        let mut g = group();
1526        g.consumer_or_create(b"alice", 1_000);
1527        g.consumer_or_create(b"carol", 1_000);
1528        let mut bytes = Vec::new();
1529        g.freeze(&mut bytes);
1530        // Both names are five letters, so one name becomes the other without
1531        // the length in front of it moving.
1532        let at = bytes
1533            .windows(5)
1534            .position(|w| w == b"carol")
1535            .expect("the second name");
1536        bytes[at..at + 5].copy_from_slice(b"alice");
1537        assert_eq!(
1538            Group::thaw(&mut frozen::Cut::new(&bytes)),
1539            Err(Broken::Body)
1540        );
1541    }
1542}