Skip to content

fix: self-heal partial-slot outage (slots stuck on failed master)#300

Merged
jongko54 merged 2 commits into
mainfrom
fix/selfheal-partial-slot-outage
Jun 18, 2026
Merged

fix: self-heal partial-slot outage (slots stuck on failed master)#300
jongko54 merged 2 commits into
mainfrom
fix/selfheal-partial-slot-outage

Conversation

@jongko54

Copy link
Copy Markdown
Contributor

Found in a production incident: a ValkeyCluster reported cluster_state:ok and cluster_slots_assigned:16384 but cluster_slots_ok:10922 — ~5462 slots (one shard) were owned by a master,fail node, so 1/3 of the keyspace was actually DOWN. The operator's INC-0001 self-heal stopped because its health gate only checked cluster_state, not whether all slots were ok or whether a failed node still owned slots. The partial outage was left unhealed (recovered manually with CLUSTER FAILOVER TAKEOVER).

Fix

  • Strengthened health gate (internal/valkey/stuckslots.go): IsClusterDegraded now also trips when cluster_slots_ok < 16384 OR a master,fail node still owns slots — not only on cluster_state != ok.
  • Detection DetectStuckSlotHeals: for each stuck failed master that still owns slots, pick a healthy replica (no fail/pfail flags, link connected; deterministic by node id) as the takeover target. Masters with no healthy replica are excluded (data-preservation) and surfaced as a PartialSlotOutage warning event rather than risking an unsafe promotion.
  • Action (internal/controller/cluster_stuckslots.go, Reconcile step 10.6): issue CLUSTER FAILOVER TAKEOVER on the chosen replica (new ClusterFailoverTakeover primitive — go-redis's ClusterFailover can't do the consensus-free TAKEOVER needed when the master is already fail). Emits events + new valkey_cluster_stuck_slot_takeover_total metric.
  • Reporting: decidePhase/ClusterReady now factor in slots_ok, so a partial outage is reported as PartialSlotOutage instead of being mislabeled Running/Ready.

Conservative & idempotent: only acts on the fail flag (set only after node-timeout + gossip agreement), never transient pfail/fail?, so it respects node-timeout and won't thrash a converging cluster.

Why not reuse failover.go

That path (REPLICAOF NO ONE, offset-based, Pod-Ready-driven) is for Replication mode and does not transfer cluster slot ownership/epoch, so it cannot heal a Cluster-mode stuck-slot outage. Added the minimal cluster-specific primitive instead; reused existing dial/TLS/query abstractions.

Tests

go build ./... clean; go test ./internal/valkey/... ./internal/controller/... pass, incl. new unit tests (incident snapshot slots_ok=10922 + fail-master → degraded + correct takeover target; no-candidate/unhealthy-skip/determinism; decidePhase partial-outage). Pre-existing unrelated TestChartIconURLUsesCurrentValkeyAsset (fails on main) ignored. 8 files, +497/-8.

Needs live e2e (not coverable by envtest — no real gossip): that TAKEOVER against a genuinely fail master returns slots_ok to 16384 and convergence timing holds. Follow-up: pick takeover replica by slave_repl_offset (most up-to-date) instead of lowest id.

🤖 Generated with Claude Code

jongko54 and others added 2 commits June 18, 2026 17:16
A ValkeyCluster could report cluster_state:ok and
cluster_slots_assigned:16384 while cluster_slots_ok was below 16384 —
one shard's slots were owned by a master flagged `fail`, leaving ~1/3
of the keyspace DOWN. The self-heal health gate only checked
cluster_state / slots_assigned, so this partial outage was never healed.

Detection (pure, unit-tested in internal/valkey/stuckslots.go):
- IsClusterDegraded(): degraded when state != ok, OR slots_ok < 16384,
  OR any `master,fail` node still owns slots.
- DetectStuckSlotHeals(): per stuck fail-master, picks a healthy replica
  (no fail/pfail flags, link connected; deterministic by node ID) as the
  takeover target; fail-masters with no healthy replica are excluded
  (data-preservation) and surfaced as a warning event.

Action (internal/controller/cluster_stuckslots.go, wired into Reconcile
step 10.6): issues CLUSTER FAILOVER TAKEOVER on the chosen replica
(vk.ClusterFailoverTakeover) so the replica succeeds the slots and they
return to ok. Idempotent (no-op once recovered) and conservative — the
`fail` flag is only set by Valkey after node-timeout + gossip agreement,
so converging clusters are not thrashed; pfail/fail? are not acted on.

Also: ClusterReady condition and decidePhase now factor in slots_ok, so a
partial outage is no longer mis-reported as Running/Ready (Reason
PartialSlotOutage, Resharding cadence to retry the heal). New metric
valkey_cluster_stuck_slot_takeover_total.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
# Conflicts:
#	internal/controller/valkeycluster_controller.go
@jongko54 jongko54 merged commit a862a7a into main Jun 18, 2026
9 of 13 checks passed
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.

1 participant