fix: shardExistsInTopology only matches shards with a primary - #215
Conversation
When a Deployment rollout replaces a primary pod, the old and new pods briefly coexist (maxSurge). If the old primary dies before the replica can failover, the shard has no primary and no slots — just an orphaned replica. The old code matched these orphaned replicas, blocking slot assignment to the replacement primary indefinitely. Now shardExistsInTopology only matches shards that have a primary with slots, allowing the replacement primary to proceed with slot assignment. Signed-off-by: Björn Svensson <bjorn.a.svensson@est.tech>
|
| Filename | Overview |
|---|---|
| internal/controller/utils.go | Adds an early-continue guard in shardExistsInTopology to skip shards where PrimaryId is empty or Slots is empty, preventing orphaned replicas from blocking slot assignment to a replacement primary; the ` |
Sequence Diagram
sequenceDiagram
participant K8s as Kubernetes
participant Op as Operator Reconciler
participant VC as Valkey Cluster
Note over K8s: Deployment rollout begins
K8s->>K8s: Old primary pod dies before failover
Note over VC: Shard state: PrimaryId="", Slots=[]
K8s->>K8s: New replacement primary pod starts
Op->>VC: Fetch ClusterState
Op->>Op: assignSlotsToPendingPrimaries()
Op->>Op: "shardExistsInTopology(shard=0)"
alt OLD behavior (bug)
Op->>Op: Orphaned replica IP matches shard node
Op->>Op: Returns true → skip slot assignment
Note over Op: Replacement primary blocked indefinitely
else NEW behavior (fix)
Op->>Op: "Shard skipped: PrimaryId=="" or Slots==[]"
Op->>Op: Returns false → proceed with slot assignment
Op->>VC: CLUSTER ADDSLOTSRANGE → replacement primary gets slots
Op->>VC: CLUSTER REPLICATE → orphaned replica attaches
end
Comments Outside Diff (1)
-
internal/controller/utils.go, line 177-188 (link)Stale function docstring after behavior change
The docstring still says the function covers exactly two cases ("post-failover completed" and "mid-failover in progress"), but the implementation now has a third, excluded case: shards whose
PrimaryIdis empty or whoseSlotslist is empty are silently skipped. A reader relying on the comment won't know about this exclusion and may be surprised when a shard-match is not returned for an orphaned replica. The comment should mention the new "orphaned shard" guard alongside the existing cases.Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
Reviews (1): Last reviewed commit: "fix: shardExistsInTopology only matches ..." | Re-trigger Greptile
| if shard.PrimaryId == "" || len(shard.Slots) == 0 { | ||
| continue |
There was a problem hiding this comment.
|| guard could mask a transient mid-failover state
The condition skips a shard when shard.PrimaryId == "" regardless of whether shard.Slots is non-empty. During a Valkey failover election, there is a brief window where the old primary has been removed from the cluster view (clearing PrimaryId) but the slot ranges still appear in shard.Slots. In that transient state, the new condition would skip the shard, causing shardExistsInTopology to return false, and the replacement primary could attempt CLUSTER ADDSLOTSRANGE for slots that are mid-reassignment.
In practice this race appears narrow and the Valkey cluster itself would reject a conflicting ADDSLOTSRANGE, triggering a retry, so it's unlikely to cause lasting harm. But tightening the guard to shard.PrimaryId == "" && len(shard.Slots) == 0 would more precisely target the described "completely orphaned" scenario (no primary and no slots) while keeping the mid-failover shard visible.
There was a problem hiding this comment.
@greptileai Tell me when this case is possible.
Also see clusterstate.go and function GetClusterState where shard.Slots is set.
if node.IsPrimary() {
ranges, _ := parseSlotsRanges(node.GetSlots())
shard.Slots = ranges
shard.PrimaryId = node.Id
}
There was a problem hiding this comment.
You're right. Looking at GetClusterState, both fields are set in the exact same if node.IsPrimary() block:
if node.IsPrimary() {
ranges, _ := parseSlotsRanges(node.GetSlots())
shard.Slots = ranges
shard.PrimaryId = node.Id
}shard.PrimaryId and shard.Slots are always set together. There is no code path that sets one without the other. My hypothesized state — PrimaryId == "" with non-empty Slots — is structurally impossible. The || condition is fine, and the len(shard.Slots) == 0 check is actually redundant given the PrimaryId == "" check already covers the case completely.
My original concern was wrong. The PR's logic is correct as written.
Tip: You can customize Greptile's behavior for this repo with .greptile/rules.md and .greptile/config.json.
|
Good find!
We should set Deployment update strategy to be Recreate instead of the default RollingUpdate @bjosv |
|
Closing this since we now use the Recreate deployment strategy and |
Summary
When a Deployment rollout replaces a primary pod, the old and new pods briefly coexist (
maxSurge). If the old primary dies before the replica can failover, the shard has no primary and no slots, just an orphaned replica. The old code matched these orphaned replicas, blocking slot assignment to the replacement primary indefinitely.Now
shardExistsInTopologyonly matches shards that have a primary with slots, allowing the replacement primary to proceed with slot assignment.This fixes CI issues like this.
Testing
Found using #203 running the delete-recreate-cluster scenario. This scenario deletes a ValkeyCluster, using Deployment, and wait for all resource to be gone, and then recreates it again.
CHAOS_SCENARIOS=delete-recreate-cluster CHAOS_WORKLOAD_TYPE=Deployment make test-chaosIt needed 3 to 357 iterations before hitting the issue.
The reason for restarting ValkeyNode pods during cluster creation, according to added debug logs in testruns, are annotation changes:
The
config-hashannotation was missing when the Deployment was created (because serverConfigHash was empty due to the informer cache race?). When the ValkeyNode controller reconciles again and sees the hash, it adds the annotation to the pod template.Any change to the pod template spec (including annotations) causes Kubernetes to create a new ReplicaSet and roll the pods.
Checklist
Before submitting the PR make sure the following are checked:
pre-commit run --all-filesor hooks on commit)