Skip to content

XDELEX Command - #3467

Closed
nickiaq wants to merge 6 commits into
valkey-io:unstablefrom
nickiaq:command-xdelex
Closed

nickiaq wants to merge 6 commits into
valkey-io:unstablefrom
nickiaq:command-xdelex

Conversation

@nickiaq

@nickiaq nickiaq commented Apr 9, 2026

Copy link
Copy Markdown
Contributor

This PR adds a stream command XDELEX that deletes one or more stream messages with explicit control over how consumer groups' PEL references are handled. While implementing XACKDEL, it made sense to also add this as a complimentary command.

For more background on the problem space, see #2903. Similar to XACKDEL, this is compatible with the equivalent command introduced in Redis 8.2.0.

The command supports three deletion modes:

  • KEEPREF (default): Deletes the stream entry but leaves PEL references intact in all consumer groups
  • DELREF: Deletes the stream entry and forcibly removes it from all consumer group PELs
  • ACKED: Only deletes the entry once every consumer group has acknowledged or passed it — safe for fan-out topologies

The command returns a per-ID integer array: 1 for deleted, 2 for exists-but-not-yet-deletable (ACKED mode only), and -1 for not found.

I've included test coverage for all modes, multi-group scenarios, replication behavior, syntax, and edge cases.

This is my first PR (alongside XACKDEL #3466). And I've tried to follow the style guide & contributing guidelines. Happy to make any corrections if I've missed something.

It's possible to create a shared function for parsing the mode & id count between XACKDEL and XDELEX. So I can follow up with a PR to reduce that duplicated code. Or, I can merge these 2 PRs into one.

@zuiderkwast zuiderkwast added the major-decision-pending Major decision pending by TSC team label Apr 9, 2026
@nickiaq nickiaq mentioned this pull request May 4, 2026
@madolson madolson moved this to Todo in Valkey 9.2 May 4, 2026
@madolson madolson added major-decision-approved Major decision approved by TSC team and removed major-decision-pending Major decision pending by TSC team labels May 4, 2026
@codecov

codecov Bot commented May 4, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 99.00000% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 79.94%. Comparing base (79bca53) to head (8e5e202).
⚠️ Report is 184 commits behind head on unstable.

Files with missing lines Patch % Lines
src/t_stream.c 99.00% 1 Missing ⚠️
Additional details and impacted files
@@             Coverage Diff              @@
##           unstable    #3467      +/-   ##
============================================
+ Coverage     76.68%   79.94%   +3.26%     
============================================
  Files           162      187      +25     
  Lines         81021    95520   +14499     
============================================
+ Hits          62129    76363   +14234     
- Misses        18892    19157     +265     
Files with missing lines Coverage Δ
src/commands.def 100.00% <ø> (ø)
src/server.h 100.00% <ø> (ø)
src/t_stream.c 94.76% <99.00%> (+0.31%) ⬆️

... and 96 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@madolson madolson moved this from Todo to Needs Review in Valkey 9.2 May 10, 2026
Nick Iaquinto added 2 commits June 24, 2026 20:15
XDELEX provides extended deletion options for stream entries with three
modes for handling consumer group pending entry list (PEL) references:

- KEEPREF (default): Delete stream message(s) but not PEL references
- DELREF: Delete stream message(s) and all associated consumer group
  PEL entries
- ACKED: Only delete entries acknowledged by all consumer groups

The command returns an array of status codes for each requested ID:
- 1: Entry was deleted
- 2: Entry exists but has pending references (ACKED mode only)
- -1: Entry not found in stream

Includes test coverage for all modes, edge cases, replication behavior,
and syntax.

Signed-off-by: Nick Iaquinto <git+valkey@iaquinto.io>
Refactor the ACKED and DELREF modes to iterate over consumer groups first,
then messages, instead of the previous approach of iterating over messages
first with nested group loops. This provides performance improvements
b/c it:

- Opens consumer group iterators once instead of once per message
- Uses a response tracking array to skip already-finalized messages
- Reduces redundant raxStart/raxStop/raxSeek calls from O(ids * groups)
  to O(groups)

From local benchmarking, the additional allocation (of the response
array) is faster than multiple iterations of the consumer groups.

The KEEPREF mode is handled separately because it doesn't need to loop
over consumer groups at all.

Signed-off-by: Nick Iaquinto <git+valkey@iaquinto.io>
@coderabbitai

coderabbitai Bot commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds the XDELEX stream command, including command-spec wiring, KEEPREF/DELREF/ACKED execution, per-ID results, and unit and replication tests for deletion and validation behavior.

Changes

XDELEX stream command

Layer / File(s) Summary
Command contract and registration
src/commands/xdelex.json, src/commands.def, src/server.h
Defines XDELEX metadata, arguments, reply codes, prototype, and command-table registration.
Parsing and stream lookup
src/t_stream.c
Parses mode and IDS arguments, validates numids, allocates per-ID storage, parses IDs, and handles missing streams.
Consumer-group delete modes
src/t_stream.c
Implements DELREF and ACKED PEL processing, stream deletion, metadata updates, notifications, dirty-count updates, and replies.
Command unit tests
tests/unit/type/stream.tcl
Covers deletion results, mode semantics, consumer-group edge cases, validation errors, repeated deletes, and large-ID requests.
Replication tests
tests/unit/type/stream-cgroups.tcl
Tests KEEPREF, DELREF, and ACKED behavior across master/replica stream and PEL state.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant xdelexCommand
  participant Stream
  participant ConsumerGroups
  participant Server
  Client->>xdelexCommand: XDELEX key [MODE] IDS numids ids...
  xdelexCommand->>Stream: Parse IDs and load stream
  xdelexCommand->>ConsumerGroups: Process DELREF or ACKED PEL state
  xdelexCommand->>Stream: Delete eligible entries
  xdelexCommand->>Server: Signal modification and emit xdel event
  xdelexCommand->>Client: Return per-ID status array
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title names the new XDELEX command, which matches the main change in the PR.
Description check ✅ Passed The description directly matches the added XDELEX command, modes, and tests.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@nickiaq

nickiaq commented Jun 25, 2026

Copy link
Copy Markdown
Contributor Author

The force push a few seconds ago was just keeping this up with unstable.

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 2

🧹 Nitpick comments (1)
tests/unit/type/stream.tcl (1)

702-702: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Rename the DELREF test to match asserted behavior.

The test name says DELREF “keeps refs”, but the assertions verify PEL refs are removed. Please rename to avoid semantic confusion.

Suggested diff
-    test {XDELEX w/ DELREF deletes all but keeps refs in consumer group PELs} {
+    test {XDELEX w/ DELREF deletes entries and removes consumer group PEL refs} {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/unit/type/stream.tcl` at line 702, Rename the XDELEX DELREF test case
in stream.tcl so its name matches the asserted behavior: the current title in
the test block says DELREF “keeps refs,” but the assertions in that same test
verify the refs are removed from consumer group PELs. Update the test
description string in the test block identified by the XDELEX/DELREF scenario to
reflect that refs are deleted, using the surrounding test name and assertions as
the source of truth.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/t_stream.c`:
- Around line 3677-3741: DELREF can mutate the stream’s consumer-group state
without emitting WATCH/invalidation notifications because `signalModifiedKey()`
and `notifyKeyspaceEvent()` in the `stream` deletion flow are only tied to the
`deleted` counter. Update the logic around the DELREF/PEL-removal path in
`t_stream.c` so any successful removal of PEL/NACK state (not just
`streamDeleteItem`) sets a separate mutation flag and uses it to trigger the key
notifications and `server.dirty` handling; keep the existing `streamDeleteItem`,
`streamFreeNACK`, and `first_entry`/`max_deleted_entry_id` behavior intact.
- Around line 3633-3640: The missing-stream early return in the XDELEX path
leaks heap-allocated `ids` and `resps` when `id_count` exceeds
`STREAMID_STATIC_VECTOR_LEN`. Update the `o == NULL` branch in `t_stream.c` to
route through the existing `cleanup` path instead of returning directly, so the
allocations are always released before exiting. Keep the reply behavior the
same, but ensure the `cleanup:` label handles both stack and heap cases for
`ids`/`resps`.

---

Nitpick comments:
In `@tests/unit/type/stream.tcl`:
- Line 702: Rename the XDELEX DELREF test case in stream.tcl so its name matches
the asserted behavior: the current title in the test block says DELREF “keeps
refs,” but the assertions in that same test verify the refs are removed from
consumer group PELs. Update the test description string in the test block
identified by the XDELEX/DELREF scenario to reflect that refs are deleted, using
the surrounding test name and assertions as the source of truth.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 1a8ad017-59f1-446a-9af4-d11bf546ba5b

📥 Commits

Reviewing files that changed from the base of the PR and between 79bca53 and 20f2d99.

📒 Files selected for processing (6)
  • src/commands.def
  • src/commands/xdelex.json
  • src/server.h
  • src/t_stream.c
  • tests/unit/type/stream-cgroups.tcl
  • tests/unit/type/stream.tcl

Comment thread src/t_stream.c
Comment thread src/t_stream.c
- Use consistent exit cleanup to avoid leaking stream id's and resp
  array when stream not found.
- Emit keyspace event when removing an orphaned PEL entry, which is
  consistant with keyspace events for other operations and avoids
  adding another case of inconsistent behavior as described in issue
  valkey-io#3429.

Signed-off-by: Nick Iaquinto <git+valkey@iaquinto.io>
@hpatro hpatro self-assigned this Jul 20, 2026
@hpatro

hpatro commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

@valkey-review-bot Please review this change.

@valkey-review-bot

valkey-review-bot Bot commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

I found two user-visible issues in the new XDELEX path: the command metadata advertises syntax the parser doesn't accept, and the executor currently queues the reply before the mutation hooks run. Both should be small to fix, but they change how clients discover and observe the command.

Comment thread src/t_stream.c Outdated
Comment thread src/commands/xdelex.json
- Correct JSON DSL command definition for XDELEX to use the proper
  style for an optional one-of token: mode as
  `[KEEPREF | DELREF | ACKED ]`
- Delete stream messages, mark dirty, and signal before enqueueing
  response array so that we meet the module keyspace API contract

Signed-off-by: Nick Iaquinto <git+valkey@iaquinto.io>
@nickiaq

nickiaq commented Jul 24, 2026

Copy link
Copy Markdown
Contributor Author

Thank you for starting the review for this as well as XACKDEL! These were great comments from the bot. Added comments in thread on each. And, I'll add responses on the XACKDEL one hopefully shortly as well.

Signed-off-by: Nick Iaquinto <git+valkey@iaquinto.io>
nickiaq pushed a commit to nickiaq/valkey that referenced this pull request Jul 24, 2026
Mirroring the same concern with XDELEX, we need to defer response
enqueue-ing until after other modifications to preserve the module
keyspace event API contract. See issue valkey-io#3429 and this comment:
valkey-io#3467 (comment)

Signed-off-by: Nick Iaquinto <git+valkey@iaquinto.io>

@zuiderkwast zuiderkwast 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.

Looks good in general and very similar to the XACKDEL PR. The two commands looked identical to me, but finally I noticed that the group argument is absent in XDELEX.

Since it has much in common with the other PR and we prefer to avoid duplicated logic, consider putting both commands in one PR.

@coderabbitai Please check for duplicate code with XDEL and with the PR #3466 which adds XACKDEL. How can the code be de-duplicated, for example using a helper function?

Comment thread src/t_stream.c Outdated
Comment on lines +3583 to +3586
} else if (strcasecmp(objectGetVal(c->argv[argi]), "IDS") != 0) {
addReplyError(c, "xdelex mode not recognized");
return;
}

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.

Same comments as for the XACKDEL PR.

Suggested change
} else if (strcasecmp(objectGetVal(c->argv[argi]), "IDS") != 0) {
addReplyError(c, "xdelex mode not recognized");
return;
}
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Indeed, thanks for simplifying. Fixed in 8e5e202.

Comment thread src/t_stream.c Outdated

/* Expect IDS token. */
if (strcasecmp(objectGetVal(c->argv[argi]), "IDS") != 0) {
addReplyError(c, "syntax error");

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.

Suggested change
addReplyError(c, "syntax error");
addReplyErrorObject(c, shared.syntaxerr);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 8e5e202!

Comment thread src/t_stream.c Outdated
Comment on lines +3605 to +3609
if (id_count > actual_ids) {
addReplyError(c, "numids parameter must match the number of IDs provided");
return;
} else if (id_count < actual_ids) {
addReplyError(c, "syntax error");

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.

Suggested change
if (id_count > actual_ids) {
addReplyError(c, "numids parameter must match the number of IDs provided");
return;
} else if (id_count < actual_ids) {
addReplyError(c, "syntax error");
if (id_count != actual_ids) {
addReplyErrorObject(c, shared.syntaxerr);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 8e5e202; addressed as recommended here, but also needed to change tests to match.

Responding to [these review comments](valkey-io#3467 (review)),
this switches syntax errors to the standard shared helper and simplifies
error handling for the mode check and number of ID's matching remaining args.

Signed-off-by: Nick Iaquinto <git+valkey@iaquinto.io>
nickiaq pushed a commit to nickiaq/valkey that referenced this pull request Sep 9, 2026
Squashed commit of the following:

commit 8e5e202
Author: Nick Iaquinto <git+valkey@iaquinto.io>
Date:   Tue Sep 8 18:13:11 2026 -0400

    Simplify & Standardize Error Handling (RE: Review Comments)

    Responding to [these review comments](valkey-io#3467 (review)),
    this switches syntax errors to the standard shared helper and simplifies
    error handling for the mode check and number of ID's matching remaining args.

    Signed-off-by: Nick Iaquinto <git+valkey@iaquinto.io>

commit 27cf7bc
Author: Nick Iaquinto <git+valkey@iaquinto.io>
Date:   Thu Jul 23 21:13:38 2026 -0400

    Clang Format Fix (remove trailing newline)

    Signed-off-by: Nick Iaquinto <git+valkey@iaquinto.io>

commit 703506c
Author: Nick Iaquinto <git+valkey@iaquinto.io>
Date:   Thu Jul 23 20:42:40 2026 -0400

    Fixes from Valkey Bot & hpatro Code Review

    - Correct JSON DSL command definition for XDELEX to use the proper
      style for an optional one-of token: mode as
      `[KEEPREF | DELREF | ACKED ]`
    - Delete stream messages, mark dirty, and signal before enqueueing
      response array so that we meet the module keyspace API contract

    Signed-off-by: Nick Iaquinto <git+valkey@iaquinto.io>

commit 0fc54c5
Author: Nick Iaquinto <git+valkey@iaquinto.io>
Date:   Wed Jun 24 22:53:07 2026 -0400

    Corrections from XDELEX AI Review

    - Use consistent exit cleanup to avoid leaking stream id's and resp
      array when stream not found.
    - Emit keyspace event when removing an orphaned PEL entry, which is
      consistant with keyspace events for other operations and avoids
      adding another case of inconsistent behavior as described in issue
      valkey-io#3429.

    Signed-off-by: Nick Iaquinto <git+valkey@iaquinto.io>

commit 20f2d99
Author: Nick Iaquinto <git+valkey@iaquinto.io>
Date:   Thu Apr 9 00:21:37 2026 -0400

    Faster XDELEX

    Refactor the ACKED and DELREF modes to iterate over consumer groups first,
    then messages, instead of the previous approach of iterating over messages
    first with nested group loops. This provides performance improvements
    b/c it:

    - Opens consumer group iterators once instead of once per message
    - Uses a response tracking array to skip already-finalized messages
    - Reduces redundant raxStart/raxStop/raxSeek calls from O(ids * groups)
      to O(groups)

    From local benchmarking, the additional allocation (of the response
    array) is faster than multiple iterations of the consumer groups.

    The KEEPREF mode is handled separately because it doesn't need to loop
    over consumer groups at all.

    Signed-off-by: Nick Iaquinto <git+valkey@iaquinto.io>

commit 66a8555
Author: Nick Iaquinto <git+valkey@iaquinto.io>
Date:   Thu Apr 9 00:02:02 2026 -0400

    XDELEX Command

    XDELEX provides extended deletion options for stream entries with three
    modes for handling consumer group pending entry list (PEL) references:

    - KEEPREF (default): Delete stream message(s) but not PEL references
    - DELREF: Delete stream message(s) and all associated consumer group
      PEL entries
    - ACKED: Only delete entries acknowledged by all consumer groups

    The command returns an array of status codes for each requested ID:
    - 1: Entry was deleted
    - 2: Entry exists but has pending references (ACKED mode only)
    - -1: Entry not found in stream

    Includes test coverage for all modes, edge cases, replication behavior,
    and syntax.

    Signed-off-by: Nick Iaquinto <git+valkey@iaquinto.io>

Signed-off-by: Nick Iaquinto <git+valkey@iaquinto.io>
@nickiaq

nickiaq commented Sep 9, 2026

Copy link
Copy Markdown
Contributor Author

Same applies here as in this comment; added "since":"9.2.0" and setup a new PR with this plus #3466 and a new commit combining common code across all 3 commands into shared helpers.

@nickiaq

nickiaq commented Sep 9, 2026

Copy link
Copy Markdown
Contributor Author

Closing this in favor of #4629!

@nickiaq nickiaq closed this Sep 9, 2026
@nickiaq
nickiaq deleted the command-xdelex branch September 9, 2026 18:56
@zuiderkwast zuiderkwast removed this from Valkey 9.2 Sep 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

major-decision-approved Major decision approved by TSC team

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants