Skip to content

fix(stream): align XINFO GROUPS entries-read and lag with Redis - #3581

Open
wengsht wants to merge 2 commits into
apache:unstablefrom
wengsht:fix-xgroup-entries-read-clamp
Open

fix(stream): align XINFO GROUPS entries-read and lag with Redis#3581
wengsht wants to merge 2 commits into
apache:unstablefrom
wengsht:fix-xgroup-entries-read-clamp

Conversation

@wengsht

@wengsht wengsht commented Aug 9, 2026

Copy link
Copy Markdown

Follow-up to #3578, split out per review (SRP + clean cherry-pick).

XINFO GROUPS can diverge from Redis in two related ways when a group's entries-read is out of range or its cursor is behind the stream head. Fixed here in one focused commit each.

1. Clamp entries-read to entries-added on write

XGROUP CREATE/SETID stored the ENTRIESREAD value verbatim, even past entries-added. XINFO GROUPS served it back, and lag = entries_added - entries_read (unsigned) underflowed to ~2^64, overflowing the signed-64 integer clients decode the RESP reply as. Redis clamps on write (t_stream.c L3663-3665); mirrored in Stream::CreateGroup/GroupSetId.

2. Report lag as stream length when the group is behind the first entry

Even with an in-range entries-read, if the group's last-delivered-id is behind the first live entry, computing lag from entries_read is wrong. Redis's streamReplyWithCGLag reports lag = length in that case (and 0 for an emptied stream); CheckLagValid was missing both branches. Added them.

Result: matches Redis exactly

Same repro against reference Redis 8.10 and this patch:

127.0.0.1:PORT> XADD repro-lag 1-0 f v ; XADD repro-lag 2-0 f v ; XADD repro-lag 3-0 f v
127.0.0.1:PORT> XGROUP CREATE repro-lag grp 0 ENTRIESREAD 1000000
OK
127.0.0.1:PORT> XINFO GROUPS repro-lag
    ...
    9) "entries-read"
   10) (integer) 3
   11) "lag"
   12) (integer) 3

Both report entries-read = 3, lag = 3. Before this patch kvrocks returned entries-read = 1000000.

Test

XGROUP CREATE/SETID with ENTRIESREAD beyond entries-added, asserting XINFO GROUPS stays decodable and reports entries-read = entries-added and lag = length. The unit/type/stream gocase suite passes.


AI assistance: diagnosis and drafting were done with AI help; I've reviewed the changes and tests and understand the behavior.

An ENTRIESREAD larger than the stream's entries-added was stored verbatim and
served back by XINFO GROUPS, and the group's lag (entries_added - entries_read,
an unsigned field) underflowed to a near-2^64 value that overflows the signed
64-bit integer clients decode the RESP reply as, breaking XINFO GROUPS.

Redis clamps entries-read down to entries-added on write in XGROUP CREATE and
SETID (t_stream.c). Mirror that so the stored counter can never exceed
entries-added: the reported entries-read now matches Redis and lag can no
longer underflow.
@github-actions

github-actions Bot commented Aug 9, 2026

Copy link
Copy Markdown

Hi @wengsht,

Thank you for your pull request. Please review our Contributing Guide.

Please make sure you understand your changes and explain your reasoning in this pull request. Low-quality pull requests may be closed.

…e stream head

lag is entries_added - entries_read on an unsigned field. When the group's
last-delivered-id is behind the first live entry (a group created at 0, or one
whose entries_read was set inconsistently via XGROUP SETID/ENTRIESREAD),
trusting entries_read yields a wrong or underflowing lag.

Mirror Redis streamReplyWithCGLag: when the cursor and the max tombstone are
both behind the first entry, report lag = current stream length; and report
lag = 0 for an emptied stream. XINFO GROUPS lag now matches Redis exactly.
@wengsht wengsht changed the title fix(stream): clamp XGROUP CREATE/SETID entries-read to entries-added fix(stream): align XINFO GROUPS entries-read and lag with Redis Aug 9, 2026
@wengsht

wengsht commented Aug 9, 2026

Copy link
Copy Markdown
Author

@LindaSummer can you look at this one when you get a chance?

` # kvrocks with PR #3581 (:6666)
127.0.0.1:6666> DEL repro-lag
(integer) 1
127.0.0.1:6666> XADD repro-lag 1-0 f v
"1-0"
127.0.0.1:6666> XADD repro-lag 2-0 f v
"2-0"
127.0.0.1:6666> XADD repro-lag 3-0 f v
"3-0"
127.0.0.1:6666> XGROUP CREATE repro-lag grp 0 ENTRIESREAD 1000000
OK
127.0.0.1:6666> XINFO GROUPS repro-lag

    1. "name"
    2. "grp"
    3. "consumers"
    4. (integer) 0
    5. "pending"
    6. (integer) 0
    7. "last-delivered-id"
    8. "0-0"
    9. "entries-read"
      10) (integer) 3
      11) "lag"
      12) (integer) 3`

@LindaSummer LindaSummer left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @wengsht ,

Thanks for your effort.

Left some comments, and we'd better fix the linter issue with ./x.py format before committing.

Comment thread src/types/redis_stream.cc
// clients decode the RESP reply as, breaking XINFO GROUPS.
static int64_t clampEntriesRead(int64_t entries_read, uint64_t entries_added) {
if (entries_read != -1 && static_cast<uint64_t>(entries_read) > entries_added) {
return static_cast<int64_t>(entries_added);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we have a risk of overflow here?

Comment thread src/types/redis_stream.cc
if (stream_metadata.entries_added == 0) {
group_metadata.lag = 0;
valid = true;
} else if (stream_metadata.size == 0) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could this be merged into the above branch?

Comment thread src/types/redis_stream.cc
// All entries deleted; the stream is empty.
group_metadata.lag = 0;
valid = true;
} else if (group_metadata.last_delivered_id < stream_metadata.first_entry_id &&

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we have a case to cover this branch?

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Aligns XINFO GROUPS entries-read and lag reporting with Redis.

Changes:

  • Clamps ENTRIESREAD to entries-added.
  • Handles empty streams and cursors behind the stream head.
  • Adds integration coverage for CREATE and SETID clamping.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

File Description
src/types/redis_stream.cc Updates entries-read clamping and lag calculation.
tests/gocase/unit/type/stream/stream_test.go Tests CREATE/SETID clamping behavior.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread src/types/redis_stream.cc
Comment on lines +1322 to +1325
} else if (stream_metadata.size == 0) {
// All entries deleted; the stream is empty.
group_metadata.lag = 0;
valid = true;
Comment thread src/types/redis_stream.cc
Comment on lines +691 to +695
static int64_t clampEntriesRead(int64_t entries_read, uint64_t entries_added) {
if (entries_read != -1 && static_cast<uint64_t>(entries_read) > entries_added) {
return static_cast<int64_t>(entries_added);
}
return entries_read;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants