Skip to content

fix(cluster): derive shard membership from topology, not pod ordinal - #37

Merged
immanuel-peter merged 5 commits into
mainfrom
fix/cluster-replicas-per-shard-scale
Sep 11, 2026
Merged

fix(cluster): derive shard membership from topology, not pod ordinal#37
immanuel-peter merged 5 commits into
mainfrom
fix/cluster-replicas-per-shard-scale

Conversation

@immanuel-peter

@immanuel-peter immanuel-peter commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Fixes #36.

Shard membership used to be index / (1 + replicasPerShard). Raise replicas from 0 to 1 on a 3-shard cluster and pod 1, primary of s1, becomes the expected replica of s0. The operator sends CLUSTER REPLICATE, Redis refuses because the node owns slots, and the loop sits on HTTP 500. The same formula made a Redis failover look like a misplacement, so reshard tried to move slots back to the old primary.

Membership now comes from live topology in cluster_layout.go. Slot owners are primaries of the range they serve. Replicas follow primaryNodeID from /v1/status. Only empty pods get placed. Labels and status.shards use the same plan.

Scale-down still deletes the highest ordinals, which can be slot owners after a failover. Those owners are marked doomed. A surviving heir replicates from them, then takes over with CLUSTER FAILOVER. The old primary is fenced unready first; Redis stays up. Emergency fencing still stops Redis.

Also: the instance manager no longer wipes operator-owned cluster fields each tick. CLUSTER REPLICATE against an unknown primary is 409 and retries.

Kind check with the issue manifest: 0→1→2→1 replicas stayed healthy, no slot moves, failover and label repair followed topology. test/smoke/cluster_replica_scaling.py covers doomed-primary scale-down and both fence types.

Leftovers: MIGRATE still has no auth password. Scaled-down nodes are never CLUSTER FORGET-ed. New clusters put primaries on pods 0..shards-1.

Changing spec.replicasPerShard on a running cluster-mode RedisCluster
reinterpreted existing primaries as replicas, because shard membership
was computed as index / (1 + replicasPerShard). The operator then sent
CLUSTER REPLICATE to nodes that own slots and looped on HTTP 500 (#36).

Shard membership now follows the live topology: a slot owner is the
primary of the shard whose range it serves, replicas follow the primary
they replicate from (new primaryNodeID in /v1/status), and only empty
pods are placed. Pod labels and status.shards are derived the same way.

Also fixed along the way:
- the instance manager replaced its whole instancesStatus entry and so
  wiped the operator's nodeID/slotsServed/clusterState fields each tick;
  it now updates only the fields it observes
- CLUSTER REPLICATE against a pod that has not yet learned about its
  primary returns 409 and is retried instead of logged as an error
- instance-manager HTTP errors include the response body

Fixes #36

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 3557494a1c

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +55 to +60
for shardIndex := 0; shardIndex < shardCount; shardIndex++ {
podName := layout.primaryOf[shardIndex]
if podName == "" {
return false, nil
}
desiredPrimaryPods = append(desiredPrimaryPods, podName)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Drain primaries before selecting them for downscale

When replicasPerShard is reduced after a Redis failover or on a legacy contiguous layout, layout.primaryOf can select a high-ordinal pod that falls outside the new desired pod set. Resharding then treats that pod as the desired target and leaves its slots in place, but reconcilePods subsequently deletes the highest ordinals; cluster mode also clears status.currentPrimary, so its primary safeguard cannot help. For example, scaling a six-pod cluster to three after pod 3 takes over shard 0 deletes pod 3 with its slots, leaving the shard unavailable. Limit targets to pods that will survive the downscale, or migrate slots off doomed primaries before deletion.

Useful? React with 👍 / 👎.

Scale-down deletes the highest ordinals. After a Redis failover, or on
the old contiguous layout, a slot owner can live there, and lowering
spec.replicasPerShard would delete it with its slots.

The layout planner now marks owners beyond the desired pod count as
doomed and picks a surviving pod as the shard primary: a surviving
replica of the owner, else a free survivor, else a replica borrowed from
the largest shard. Reshard hands the shard over before deletion: the
heir replicates from the doomed owner, then takes over with CLUSTER
FAILOVER once its link is up. Pod deletion waits on reshard as before.
The unreleased notes and the cluster handover section of the service
contract were one long clause after another. Split them into the
actual steps. Drop comments that restated the next line.
The planner comments restated the next line. The service contract
handover section is one table row. Changelog is the user-facing
summary, not a second design doc.
@immanuel-peter
immanuel-peter merged commit d8c2630 into main Sep 11, 2026
9 checks passed
immanuel-peter added a commit that referenced this pull request Sep 11, 2026
Bump the chart and install docs to 0.2.7 and record the cluster-mode
topology membership fix (#37) in the changelog ahead of tagging v0.2.7.
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.

[BUG] Patching spec.replicasPerShard post-deployment causes status/label desync and HTTP 500 errors

1 participant