Skip to content

[Cluster] Fix MergeSlotMap discarding stale-ownership resets - #2068

Open
jiajunpeng-msft wants to merge 3 commits into
microsoft:mainfrom
jiajunpeng-msft:users/jiajunpeng/fix-mergeslotmap-reset
Open

[Cluster] Fix MergeSlotMap discarding stale-ownership resets#2068
jiajunpeng-msft wants to merge 3 commits into
microsoft:mainfrom
jiajunpeng-msft:users/jiajunpeng/fix-mergeslotmap-reset

Conversation

@jiajunpeng-msft

Copy link
Copy Markdown

Root Cause

ClusterConfig.MergeSlotMap builds a candidate slot map and returns it only if the updated flag is set (return updated ? new(newSlotMap, workers) : this;). The stale-ownership reset branch mutates newSlotMap — setting the slot to RESERVED_WORKER_ID / SlotState.OFFLINE — but never sets updated, then continues. A gossip merge whose only effect is clearing stale attributions is therefore discarded wholesale, and the receiver keeps crediting a node that no longer claims the slot.

This does not self-heal, because the claim path is epoch-gated on the recorded owner (workers[currentOwnerId].ConfigEpoch >= senderConfig.LocalNodeConfigEpoch). The stale owner's epoch keeps advancing for unrelated reasons, so the true owner — whose epoch was frozen when it claimed the slot — can never win the comparison. The existing comment on the reset branch describes exactly this trap: "the sender will falsely remain the owner and its epoch will be greater than that of the new owner and the new owner will not be able to claim the slot without outside intervention." The reset is the mechanism meant to prevent that, and it is being thrown away.

The reset branch (ed46945fd71, 2024-11-26) predates the updated flag (e9d79068491, 2025-01-09, #905), which instrumented only the ownership-assignment path and left the pre-existing reset branch unwired.

Description of Change

libs/cluster/Server/ClusterConfig.cs — in MergeSlotMap, set updated = true in the stale-ownership reset branch, and change the ownership-assignment line from updated = to updated |=. Both are required: the first makes a reset-only merge survive, the second stops a later no-change slot from clobbering it.

test/cluster/Garnet.test.cluster/ClusterConfigTests.cs — added ClusterConfigMergeSlotMapRetainsStaleOwnershipResetTest. The receiver believes the sender owns two slots; the sender claims only one and attributes the other to a third node, so the reset is the merge's only effect. Fails without the fix, passes with it.

No public API, configuration, or wire-format change, and no behavior change for merges that already had another effect.

What NOT to Do (for future agents)

  • Don't reproduce this with the receiver stale on a slot the sender claims. Such a test passes with and without the fix: the epoch gate continues before reaching the updated assignment, and since SlotState.OFFLINE is 0, unclaimed slots are skipped by the != STABLE guard. The sender must not claim the stale slot.

Validation

Garnet.test.cluster passes on net8.0 Debug (155/155) and net10.0 Release (138 passed, 7 skipped); Release build is warning-clean and dotnet format Garnet.slnx --verify-no-changes is clean.

Also verified end-to-end on a 100-node cluster by migrating slots off half the primaries under a live write workload, then collecting the slot map from every node individually. Without the fix the nodes settled into 27 distinct views of slot ownership — stable rather than transient, with no self-recovery — and migrations into any node holding a stale view failed permanently with ERR Slot <n> is not owned by <node> from TryPrepareImport, since the target validates the source against its own map. With the fix, all 100 nodes converged to a single view.

Issues Fixed

No existing issue — filed directly as a PR. Happy to open a tracking issue if preferred.

The stale-ownership reset branch in ClusterConfig.MergeSlotMap mutates the
working slot map but never sets `updated`, so a gossip merge whose only effect
is clearing stale slot attributions is thrown away by the
`return updated ? new(newSlotMap, workers) : this` at the end of the method.

The receiver therefore keeps crediting a node that no longer claims the slot.
Because the merge is epoch-gated on the *recorded* owner
(`workers[currentOwnerId].ConfigEpoch >= senderConfig.LocalNodeConfigEpoch`),
and that stale owner's epoch keeps advancing for unrelated reasons, the real
owner can never win the comparison and reclaim the slot. The divergence is
permanent without outside intervention.

Also changes the assignment below to accumulate (`|=`) rather than assign, so
a later slot needing no change cannot clobber a `true` set earlier in the loop.

Adds ClusterConfigMergeSlotMapRetainsStaleOwnershipResetTest, which fails
without the fix and passes with it.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI balanced review requested due to automatic review settings August 13, 2026 21:47

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

Fixes cluster gossip merges so stale slot-ownership resets are retained and cannot be overwritten by later no-op processing.

Changes:

  • Marks stale ownership resets as updates.
  • Accumulates updates across all slots.
  • Adds a regression test for reset-only merges.

Reviewed changes

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

File Description
libs/cluster/Server/ClusterConfig.cs Preserves stale slot-map resets during merges.
test/cluster/Garnet.test.cluster/ClusterConfigTests.cs Tests reset-only merge behavior.

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

// NOTE: this avoids message flooding when sender epoch equals zero
updated = newSlotMap[i]._workerId != assignToWorkerId || newSlotMap[i]._state != SlotState.STABLE;
// Accumulate across all slots, including resets handled above.
updated |= newSlotMap[i]._workerId != assignToWorkerId || newSlotMap[i]._state != SlotState.STABLE;
jackiepmsft and others added 2 commits August 18, 2026 12:53
The existing ClusterConfigMergeSlotMapRetainsStaleOwnershipResetTest does not
exercise the `updated |=` change: its sender config epoch is 20, so the slot the
sender genuinely owns is short-circuited by the epoch guard and never reaches
the ownership-assignment path. Reverting `|=` to `=` still passed that test.

Add ClusterConfigMergeSlotMapAccumulatesUpdatedAcrossSlotsTest, which gives the
sender a config epoch of zero so its owned slot bypasses the epoch guard and
reaches the assignment with nothing to change. That slot sits at a higher index
than the stale-ownership reset, so a plain assignment clears the flag set by the
reset and the merge is discarded. Verified the new test fails with `=` and
passes with `|=`, while the original test passes either way.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
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.

4 participants