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}