Skip to content

XACKDEL Command - #3466

Closed
nickiaq wants to merge 7 commits into
valkey-io:unstablefrom
nickiaq:command-xackdel
Closed

nickiaq wants to merge 7 commits into
valkey-io:unstablefrom
nickiaq:command-xackdel

Conversation

@nickiaq

@nickiaq nickiaq commented Apr 9, 2026

Copy link
Copy Markdown
Contributor

This PR adds a stream command XACKDEL that acknowledges one or more messages and (conditionally) deletes them from the stream. The command is especially valuable for teams using Valkey streams as a Kafka alternative. This implementation is compatible with the equivalent command introduced in Redis 8.2.0.

For more background on why XACKDEL is necessary, see #2903. This article describes a common pattern when processing a stream in Valkey's current state. To cleanup stream message, a separate "janitor" process tracks each consumer group's progress and periodically calls XTRIM to reclaim space once all messages up to some high water mark have been ack'd by all groups.

Another alternative is to use a Lua script that replicates the XACKDEL logic. Consumer groups can use the script to acknowledge and (conditionally) delete each batch of messages. But, iterating over consumer groups in Lua is slower. And in both the Lua script solution and the janitor process method, there's the risk that the developer introduces a race condition or logic error leading to an unbounded accumulation of stream messages.

XACKDEL with ACKED mode makes this janitor process or Lua script unnecessary.

For full Redis compatibility, the command supports the same three deletion modes with the same semantics, command syntax, and reply format:

  • KEEPREF (default, implicit) acks and deletes immediately, leaving PEL references in other groups
  • ACKED only deletes once every consumer group has acknowledged or passed the message, making it safe for fan-out stream topologies
    • Here, if the message was ack'd but not deleted, the response is 2
  • DELREF acks, deletes, and forcibly removes PEL entries from all other groups

I've got tests for each mode and possible state included along with other tests for replication, syntax, and other edge cases.

This is my first PR (alongside XDELEX). 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.

@nickiaq nickiaq mentioned this pull request Apr 9, 2026
@codecov

codecov Bot commented Apr 9, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 79.87%. Comparing base (79bca53) to head (1dcd67c).
⚠️ Report is 183 commits behind head on unstable.

Additional details and impacted files
@@             Coverage Diff              @@
##           unstable    #3466      +/-   ##
============================================
+ Coverage     76.68%   79.87%   +3.19%     
============================================
  Files           162      187      +25     
  Lines         81021    95554   +14533     
============================================
+ Hits          62129    76325   +14196     
- Misses        18892    19229     +337     
Files with missing lines Coverage Δ
src/commands.def 100.00% <ø> (ø)
src/server.h 100.00% <ø> (ø)
src/t_stream.c 94.89% <100.00%> (+0.44%) ⬆️

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

@zuiderkwast zuiderkwast added the major-decision-pending Major decision pending by TSC team label Apr 9, 2026
@nickiaq

nickiaq commented May 4, 2026

Copy link
Copy Markdown
Contributor Author

@zuiderkwast Thanks for updating this to the latest unstable. If it helps for me to rebase or make any other changes, I'm happy to. (Same for #3467)

@zuiderkwast

Copy link
Copy Markdown
Contributor

@zuiderkwast Thanks for updating this to the latest unstable. If it helps for me to rebase or make any other changes, I'm happy to. (Same for #3467)

I did it just to trigger the "Provenance Guard" CI job. It checks for code from Redis, which is under an incompatible license. It looks green. 👍

I hope we'll get some time to review it in the next weeks. Now, we're finishing the 9.1 release, so this will target the next version.

@madolson madolson moved this to Todo in Valkey 9.2 May 4, 2026
@nickiaq

nickiaq commented May 4, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for the update. I'll watch out for any review comments to address whenever you have a chance to look.

I don't see a 9.2 project on the projects tab, just 10. So it looks like "since": "10.0.0" is right for that in src/commands/xackdel.json. But, that's an easy change if there's a 9.1.1 or 9.2 coming.

@madolson
madolson requested a review from hpatro May 4, 2026 16:09
@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
@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:14
Initial implementation of XACKDEL command, which combines ACK'ing a
stream message with deleting the message and several modes for how to
manage other consumer group's PELs. Modes include:

- KEEPREF (default): leave references in other consumer groups' PELs
- DELREF: delete references from other consumer groups' PELs
- ACKED: only delete the message if all consumer groups have ACKED

The syntax is as follows:

XACKDEL <key> <group> [ KEEPREF | DELREF | ACKED ] IDS <numids> <id> [<id> ...]

The response is an array of status codes. One status code for each ID
passed in to XACKDEL. The codes are as follows:

- -1 for when the message does not exist or is not in the consumer
  group's PEL.
- 1 for acked and deleted
- 2 for acked and not deleted (in ACKED mode)

Signed-off-by: Nick Iaquinto <git+valkey@iaquinto.io>
Optimize XACKDEL implementation by iterating per-consumer first then
per-message for ACKED and DELREF modes.

This is faster because looping over consumers is slower than looping
over messages. So doing consumers first means we only loop over those
once. And it allows skipping the inner-loop iterations once a final
response is determined for a message

For KEEPREF, there's a separate branch that skips the consumer loop
entirely, since KEEPREF only needs to clean up the target group's PEL.

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

coderabbitai Bot commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

Review Change StackReview 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

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Advanced

Run ID: 9b60ed7e-45e7-4bf2-8d26-d8acaf1ca2ba

📥 Commits

Reviewing files that changed from the base of the PR and between 0c4618a and bf72de1.

📒 Files selected for processing (1)
  • src/t_stream.c

Included review availability: Your plan provides up to 10 included reviews per hour; 8 remain after this review.


📝 Walkthrough

Walkthrough

Adds the XACKDEL stream command, its command metadata and handler, and unit and replication tests. The command supports KEEPREF, ACKED, and DELREF modes, validates IDs and counts, updates stream and consumer-group state, and returns per-ID status codes.

Changes

XACKDEL stream command

Layer / File(s) Summary
Command contract and wiring
src/commands/xackdel.json, src/commands.def, src/server.h
Adds the XACKDEL schema, generated argument tables, handler declaration, and server command-table entry.
Handler implementation
src/t_stream.c
Implements mode parsing, ID validation, stream deletion, PEL cleanup, metadata updates, notifications, and per-ID replies.
Stream command tests
tests/unit/type/stream.tcl
Adds syntax, mode, error-handling, allocation, edge-state, and dangling-PEL coverage.
Replication tests
tests/unit/type/stream-cgroups.tcl
Adds master/replica coverage for ACKED and DELREF behavior across consumer groups.

Priority: ⬇️ Low

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

Merge Risk: ⚪ Minimal · up to bf72d

XACKDEL adds conditional stream acknowledgment and deletion modes with coverage for parsing, consumer-group state changes, and replication behavior. No concrete current-head merge-blocking risk remains.

Sequence Diagram(s)

sequenceDiagram
  participant client
  participant xackdelCommand
  participant consumerGroupPELs
  participant streamState
  participant keyspaceNotifications
  client->>xackdelCommand: Submit XACKDEL arguments and IDs
  xackdelCommand->>consumerGroupPELs: Remove or inspect pending entries
  xackdelCommand->>streamState: Delete eligible entries and update stream edges
  xackdelCommand->>keyspaceNotifications: Signal stream deletion changes
  xackdelCommand->>client: Return per-ID status codes
Loading

Suggested reviewers: jsoref

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the primary change: adding the XACKDEL stream command.
Description check ✅ Passed The description directly explains the XACKDEL implementation, supported deletion modes, compatibility goals, and test coverage.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 3 functions across 1 files.
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: 3

🤖 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/commands/xackdel.json`:
- Around line 48-70: The mode argument definition for XACKDEL is advertising an
extra MODE keyword that does not exist in the handler or Redis 8.2 syntax.
Update the `mode` oneof in the `xackdel` command spec to remove the `token`
value and keep only `KEEPREF`, `DELREF`, and `ACKED`, then regenerate the
generated command artifacts so `XACKDEL_Args` and the docs/definitions match
`xackdelCommand` and no longer expose MODE.

In `@src/t_stream.c`:
- Around line 3799-3804: The XACKDEL path in t_stream.c is currently emitting
the same keyspace notification event as XDEL, so subscribers cannot tell the two
commands apart. Update the notification in the deleted-branch around
signalModifiedKey and notifyKeyspaceEvent to use a dedicated "xackdel" event for
XACKDEL, keeping the rest of the propagation logic unchanged.

In `@tests/unit/type/stream.tcl`:
- Around line 1001-1020: The test comment in the XACKDEL/ACKED scenario is
inconsistent with the actual assertion and should be updated. In the stream test
case around XACKDEL ACKED, replace the stale “despite the -1 response code”
wording with text that matches the asserted return value of 1, so the comments
around XDEL, XPENDING, and XACKDEL all describe the same behavior clearly.
🪄 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: 37dd68bb-cbcf-4401-8293-2c51db6a428b

📥 Commits

Reviewing files that changed from the base of the PR and between 79bca53 and 6ae512d.

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

Comment thread src/commands/xackdel.json
Comment thread src/t_stream.c
Comment thread tests/unit/type/stream.tcl
- Remove `MODE` token from `XACKDEL` command definition
- Correct comment on XACKDEL ACKED test case covering removal of
  dangling PEL entry

Deliberately ignoring the AI review comment on keyspace notifications,
because to the best of my digging in the code, it is incorrect. Using
keyspace notification `xdel` is valid here, because it is similar to
other usage like `del`, which is emitted in many commands not just
`DEL`. For example, `GETDEL` emits `del`. So, I'm thinking that `xdel`
is the comparable event to emit here to match `XDEL`.

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 one correctness issue in the DELREF path where the target group is validated too late; see the inline comment.

Comment thread src/t_stream.c Outdated

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

Thanks for the patience @nickiaq. Got to this finally.

The code mostly looks good. Have some nitpicks. There is a quite few branching across the code block due to the nature of the command. Need to look at the tests next. Would love additional eyes on this. @soloestoy would you be interested in taking a pass at this?

Comment thread src/t_stream.c Outdated
Comment thread src/t_stream.c Outdated
Comment thread src/t_stream.c
Comment thread src/t_stream.c Outdated
Comment thread src/t_stream.c Outdated
Comment thread src/t_stream.c Outdated
Comment thread src/t_stream.c Outdated
Comment thread src/t_stream.c Outdated
Comment on lines +3726 to +3727
if (!streamEntryExists(s, id)) {
resps[j] = -1;

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.

Why do we check if the entry exists after the streamCompareID check? Shouldn't we flip it?

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.

Initially, I set this up to have the streamCompareID(id, &cg->last_id) check first here because it’s a cheaper check than the other. From what I could tell reviewing the stream state tracking, cg->last_id is the ID of the latest stream message delivered to that consumer group. So I was thinking that means it’s not possible that the stream message is in the PEL of the group if its ID is larger than the latest thing delivered. Whereas streamEntryExists seems to walk the radix tree of stream messages to check if it exists.

But in researching your comment here, I realized the one exception to this is if you run XGROUP SETID to manually move the last_id to before a message in the PEL. In that case, the cheaper check and continue would produce incorrect result because the message in PEL is actually after the last_id.

So I’ve included a fix as you’ve described to be more defensive in commit: 75983f7

Comment thread src/t_stream.c
Comment thread src/t_stream.c Outdated
- Split `IDS` integer parsing into 2 steps: 1) parse to int, 2) check
  positive; and use same error message for step 1 as rest of codebase
- (Styleguide) Move comments out of conditional expressions to end of
  line
- Move checking PEL before the `cg->last_id` check b/c manually
  adjusting the `last_id` with `XGROUP SETID` can result in a stream
  message after the `last_id` that's left in PEL.

Signed-off-by: Nick Iaquinto <git+valkey@iaquinto.io>
Nick Iaquinto added 2 commits July 23, 2026 23:17
XACKDEL expects to return -1 for messages not in the target group's
PEL. And in this case, we also expect the other groups' PEL's (for
DELREF) and the message itself (for ACKED) to remain
unmodified/undeleted.

To handle this, we need to loop over all messages checking them for
the target group first, then we can proceed with the existing code to
check the other groups and skip any messages as needed.

This addresses review comment
valkey-io#3466 (comment)

Signed-off-by: Nick Iaquinto <git+valkey@iaquinto.io>
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>
@nickiaq

nickiaq commented Jul 24, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for the review! There was lots here to tackle and it was fun digging into the source to figure out some of the more nuanced ones here.

I’ve added a note on each review comment above. I had one question (here) and 2 spots where I provided a bit more context (here and here) on what it's trying to do.

In addition to the fixes above, I’ve also implemented something to handle this comment (#3467 (comment)) from the XDELEX PR regarding deferring response enqueueing until after message deletion, server.dirty, and signaling. See ea8a26a.

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

I took a pass, trying to get this reviewed and merged before 9.2 RC1, next week. The logic looks correct. I have only minor comments. It should be close to merge.

If you can refactor to avoid the duplicated code with XDEL, it would be very good. We should not introduce technical dept and then plan to refactor it later, because that often means it never happens.

Comment thread src/t_stream.c Outdated
Comment thread src/t_stream.c Outdated
Comment thread src/t_stream.c Outdated
nickiaq pushed a commit to nickiaq/valkey that referenced this pull request Sep 8, 2026
Responding to [these review
comments](valkey-io#3466 (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 8, 2026
Responding to [these review
comments](valkey-io#3466 (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>
Responding to [these review
comments](valkey-io#3466 (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

nickiaq commented Sep 9, 2026

Copy link
Copy Markdown
Contributor Author

Thanks @zuiderkwast for the review. Also, changed the command JSON to "since": "9.2.0" if that plan is to get this whipped into shape for merge before that RC.

In addition to the commit here, I've taken your larger advice to create a new PR (#4629) that combines this with #3467 XDELEX and adds shared helpers to reduce duplicated code across XDEL, XDELEX , and this.

@nickiaq

nickiaq commented Sep 9, 2026

Copy link
Copy Markdown
Contributor Author

If #4629 looks like the right direction for your comment about deduplicating code, then I can close this and #3467.

@hpatro

hpatro commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

Thanks @zuiderkwast for the help with the reviews. I'm in inline with the feedback to merge these two together and avoid lot's of redundant boilerplate code.

@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-xackdel 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