Skip to content

Conversation

@kostasrim
Copy link
Contributor

@kostasrim kostasrim commented Jan 28, 2026


Summary: This PR treats XREADGROUP as a write/journaled command and adds explicit journaling to replicate its side effects.

Changes:

  • Extends stream read records with delivery_time to support journaling PEL delivery metadata.
  • Updates OpRange/OpRead paths to capture delivery time when creating PEL entries for consumer groups.
  • Adds JournalXReadGroupIfNeeded to journal XCLAIM (when not NOACK) and XGROUP SETID with ENTRIESREAD when reading new messages (ID >).
  • Extends XGROUP SETID implementation to optionally update entries_read, and adds command parsing for ENTRIESREAD.
  • Adjusts XREADGROUP command registration flags to be JOURNALED with NO_AUTOJOURNAL (manual journaling).
  • Adds replication tests validating journal output and replica XINFO GROUPS state across NOACK/PEL and blocking/non-blocking paths.

Notes: The replication strategy emulates Redis/Valkey behavior by translating XREADGROUP effects into explicit journal entries (XCLAIM + XGROUP SETID) to keep consumer-group state consistent on replicas.

resolves #6493

Copilot AI review requested due to automatic review settings January 28, 2026 20:06
@kostasrim kostasrim self-assigned this Jan 28, 2026
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adjusts Dragonfly’s handling of XREADGROUP to behave as a write/journaled command and adds tests to validate correct replication/journaling behavior (including ENTRIESREAD propagation).

Changes:

  • Marks XREADGROUP as CO::JOURNALED | CO::NO_AUTOJOURNAL and adds manual journaling logic for XREADGROUP side effects.
  • Adds support for parsing/setting XGROUP SETID ... ENTRIESREAD <n> to keep group counters consistent across replicas.
  • Adds replication and monitor-based tests covering XREADGROUP journaling/replication (NOACK vs PEL, blocking vs non-blocking).

Reviewed changes

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

File Description
tests/dragonfly/replication_test.py Adds monitor expectations for XREADGROUP journaling and a new replication test ensuring group state matches between master/replica.
src/server/stream_family.cc Implements manual journaling for XREADGROUP, tracks delivery time, and propagates ENTRIESREAD via XGROUP SETID.
Comments suppressed due to low confidence (1)

src/server/stream_family.cc:2554

  • In XReadBlock(), the key variable is no longer assigned when a wake key is found (the previous key = *wake_key; was removed). The reply path uses SendStreamRecords(key, *result), so blocking XREAD/XREADGROUP responses will be sent with an empty/incorrect stream key. Restore setting key (e.g., to *wake_key) alongside computing result.
      range_opts.noack = opts->noack;

      result = OpRange(t->GetOpArgs(shard), *wake_key, range_opts);
      if (result) {
        JournalXReadGroupIfNeeded(t->GetOpArgs(shard), *opts, *result, *wake_key);
      }
    }
    return OpStatus::OK;
  };
  tx->Execute(std::move(range_cb), true);

  if (result) {
    SinkReplyBuilder::ReplyAggregator agg(rb);
    if (opts->read_group && rb->IsResp3()) {
      rb->StartCollection(1, CollectionType::MAP);
    } else {
      rb->StartArray(1);
      rb->StartArray(2);
    }
    return StreamReplies{rb}.SendStreamRecords(key, *result);
  } else if (result.status() == OpStatus::INVALID_VALUE) {

@augmentcode
Copy link

augmentcode bot commented Jan 28, 2026

🤖 Augment PR Summary

Summary: This PR treats XREADGROUP as a write/journaled command and adds explicit journaling to replicate its side effects.

Changes:

  • Extends stream read records with delivery_time to support journaling PEL delivery metadata.
  • Updates OpRange/OpRead paths to capture delivery time when creating PEL entries for consumer groups.
  • Adds JournalXReadGroupIfNeeded to journal XCLAIM (when not NOACK) and XGROUP SETID with ENTRIESREAD when reading new messages (ID >).
  • Extends XGROUP SETID implementation to optionally update entries_read, and adds command parsing for ENTRIESREAD.
  • Adjusts XREADGROUP command registration flags to be JOURNALED with NO_AUTOJOURNAL (manual journaling).
  • Adds replication tests validating journal output and replica XINFO GROUPS state across NOACK/PEL and blocking/non-blocking paths.

Technical Notes: The replication strategy emulates Redis/Valkey behavior by translating XREADGROUP effects into explicit journal entries (XCLAIM + XGROUP SETID) to keep consumer-group state consistent on replicas.

🤖 Was this summary useful? React with 👍 or 👎

Copy link

@augmentcode augmentcode bot left a comment

Choose a reason for hiding this comment

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

Review completed. 5 suggestions posted.

Fix All in Augment

Comment augment review to trigger a new review at any time.


OpResult<RecordVec> range_res;

// TODO server history IS PER stream. Fix this incorrect behaviour
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Copilot AI review requested due to automatic review settings January 29, 2026 10:58
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

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

Comments suppressed due to low confidence (1)

src/server/stream_family.cc:806

  • OpRange no longer short-circuits when opts.count == 0. ParseReadArgsOrReply allows COUNT 0, so this path can now scan and return entries instead of returning an empty result, changing command semantics and potentially causing unexpected work.

Restore the count == 0 early-return (and ensure it also avoids any XREADGROUP side-effects like PEL/NACK creation).

  RecordVec result;

  streamIterator si;
  int64_t numfields;
  streamID id;
  stream* s = (stream*)cobj->RObjPtr();
  streamID sstart = opts.start.val, send = opts.end.val;

  streamIteratorStart(&si, s, &sstart, &send, opts.is_rev);
  while (streamIteratorGetID(&si, &id, &numfields)) {

Copilot AI review requested due to automatic review settings January 29, 2026 11:39
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

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

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

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

Signed-off-by: Kostas Kyrimis <[email protected]>
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

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

Comments suppressed due to low confidence (1)

src/server/stream_family.cc:934

  • In OpRangeFromConsumerPEL, the code updates nack->delivery_time/delivery_count, but the returned Record doesn’t carry the updated delivery metadata (Record::delivery_time remains 0, and there’s no delivery-count field). If XREADGROUP history reads need to be journaled (or otherwise observed outside the shard), this loses the information required to reproduce PEL delivery metadata on replicas. Consider propagating delivery time/count into the returned record (or journaling directly from the nack).
      Record rec;
      rec.id = id;
      result.push_back(rec);
    } else {
      streamNACK* nack = static_cast<streamNACK*>(ri.data);
      nack->delivery_time = op_args.db_cntx.time_now_ms;
      nack->delivery_count++;
      result.push_back(std::move(op_result.value()[0]));

Comment on lines +2441 to +2465
if (!opts.serve_history) {
// Reading NEW messages (ID = ">")
auto journal_xgroup = [&opts, op_args](const auto& records, std::string_view key) {
if (!records.empty()) {
const auto& sitem = opts.stream_ids.at(key);
auto id = absl::StrCat(records.back().id.ms, "-", records.back().id.seq);
auto entries_read = absl::StrCat(sitem.group->entries_read);
CmdArgVec journal_args = {"SETID", key, opts.group_name, id, "ENTRIESREAD", entries_read};
RecordJournal(op_args, "XGROUP"sv, journal_args);
}
};
for (auto& record : records) {
if (!opts.noack) {
auto id = absl::StrCat(record.id.ms, "-", record.id.seq);
auto deliv_time = absl::StrCat(record.delivery_time);
CmdArgVec journal_args = {key, opts.group_name, opts.consumer_name, "0",
id, "TIME", deliv_time, "RETRYCOUNT",
"1", "FORCE", "JUSTID", "LASTID",
id};

RecordJournal(op_args, "XCLAIM"sv, journal_args);
}
}
journal_xgroup(records, key);
}
Copy link

Copilot AI Jan 29, 2026

Choose a reason for hiding this comment

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

JournalXReadGroupIfNeeded skips journaling entirely when opts.serve_history is true, but OpRangeFromConsumerPEL still mutates PEL metadata (delivery_time/delivery_count). That means XREADGROUP reads from the PEL (non->) will not replicate these side effects, and replicas can diverge from the master’s pending-entry delivery metadata. Consider journaling an XCLAIM (with correct TIME/RETRYCOUNT) for history reads as well, while still only emitting XGROUP SETID ... ENTRIESREAD when reading new messages (>).

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Interesting! I thought we did not propagate the consumer but I was wrong. Also there are some small corner cases! Will adress the shortly

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ok, so this is what I was talking about consumers. Valkey and Redis handles this slightly differently. Either way I will follow up on a different PR - it's getting too large and the case is not very interesting

@kostasrim kostasrim requested a review from romange January 29, 2026 16:28
Copy link
Collaborator

@romange romange left a comment

Choose a reason for hiding this comment

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

Looks good. there are some small comments that you still can fix.
also - seems it can benefit from unit tests as well, not only the replication.

@kostasrim
Copy link
Contributor Author

Looks good. there are some small comments that you still can fix. also - seems it can benefit from unit tests as well, not only the replication.

Can you plz clarify the following:

  1. What did I miss from small comments ?
    The only comments I did not address is (a) batching of journal changes (and I did that on purpose) (b) the consumer not being replicated via XGROUPCREATE under one flow which I can I will follow up on a separate PR (It's 260 lines already -- I don't want to push it)

  2. "benefit from unit tests as well ?"
    We have unit tests for xreadgroup in stream_family_test.cc so I am not really sure what you want to achieve with a unit here ? Do you mean rewritting the pytests as units or ? Furthermore, I am planning to improve our unit tests around xreadgroup but not on this PR: see XREADGROUP sticks if reading from history #6494

Copilot AI review requested due to automatic review settings January 30, 2026 07:50
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

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

Comment on lines +3373 to 3376
if (is_write_command) {
group = streamLookupCG(s, WrapSds(opts->group_name));
if (!group)
return facade::ErrorReply{
Copy link

Copilot AI Jan 30, 2026

Choose a reason for hiding this comment

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

In the XREADGROUP path, FindOrAddConsumer() (a few lines below) can return nullptr on OOM. That pointer is stored into requested_sitem.consumer and later OpRange dereferences opts.consumer when NOACK is not set, which can crash. Add an explicit null check after FindOrAddConsumer() and return an OUT_OF_MEMORY error reply instead of proceeding.

Copilot uses AI. Check for mistakes.
Copy link
Collaborator

@romange romange left a comment

Choose a reason for hiding this comment

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

You changed the semantics of the commands. Is it possible to capture these changes via unit tests? i.e. if you create a consumer - check that a consumer is created. if you updated the metadata - check that the metadata is updated.

@kostasrim
Copy link
Contributor Author

You changed the semantics of the commands. Is it possible to capture these changes via unit tests? i.e. if you create a consumer - check that a consumer is created. if you updated the metadata - check that the metadata is updated.

Yes this is done implicitly by the replication test + the XINFO command which checks if the side effect propagates. I did not change or affect consumer creation at all.

The main change for master, is that we properly call FindMutable on write paths which updates the dbtable via the post updater but I don't see why we would need a unit for this 🤷

I am happy to add anything -- just trying not to be redundant with tests at least

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.

XREADGROUP command should be a write command

3 participants