Skip to content

fix: re-introduce cluster members whose addresses changed after a full restart - #333

Merged
jdheyburn merged 5 commits into
valkey-io:mainfrom
matka12:fix-stale-address-remeet
Aug 7, 2026
Merged

jdheyburn merged 5 commits into
valkey-io:mainfrom
matka12:fix-stale-address-remeet

Conversation

@matka12

@matka12 matka12 commented Jul 26, 2026

Copy link
Copy Markdown
Contributor

Summary

Fixes #275 — a persistent ValkeyCluster never re-forms after a full restart that changes every pod IP; it stays in Reconciling until someone manually runs CLUSTER MEET.

Root cause: each node's persisted nodes.conf lists its peers' old pod IPs. A single restarted member is re-discovered through the survivors, but when all members restart at once there is no surviving gossip path — every node keeps dialing dead addresses and all peers stay fail?/fail forever. The operator's existing MEET phase only handles isolated nodes (cluster_known_nodes <= 1); these nodes know all their peers (at dead addresses), so nothing heals them.

Changes

  • ClusterState.FindStaleAddressPeers() (internal/valkey/clusterstate.go): detects peers flagged fail/fail?/noaddr in a node's table whose node ID belongs to a live, reachable member at a different address. fail? is included deliberately — pfail→fail promotion needs gossip between a majority of primaries, which is exactly what a cluster-wide address change breaks.
  • healStaleAddressPeers() (internal/controller/valkeycluster_controller.go): new reconcile phase, runs after promoteOrphanedReplicas and before forgetStaleNodes. For each stale pair it issues CLUSTER MEET <live-ip> <port> from the viewer; the handshake carries the member's node ID, so the viewer rebinds the existing entry to the new address and gossip propagates from there. Emits a StaleAddressesHealed event and requeues.
  • forgetStaleNodes guard: it matches failing entries to ValkeyNodes by pod IP, so a live member at a changed address looked like a dead node and could receive CLUSTER FORGET — a 60-second rejoin ban that actively fights the recovery. Entries whose ID matches a live scraped node are now skipped.

Verification

Unit tests for the detector (full-restart, dead-peer, transient-pfail, gossip-in-progress, pending-node cases) plus live verification on a 3-shard / 1-replica cluster with persistence (k3d, valkey/valkey:9.1.0):

unpatched patched
kubectl delete pod --all (all 6 pods, IPs change) cluster_state:fail, stuck Reconciling 13+ min, needs manual CLUSTER MEET cluster_state:ok + Ready in ~20s, no intervention
Data (AOF on PVC) intact after manual heal intact
Event trail StaleAddressesHealed: Re-introduced 30 peer link(s) whose addresses changed

The same failure is triggered by the 0.3.0 → 0.4.0 operator upgrade itself: the new auth env vars update all ValkeyNode StatefulSets in one reconcile pass, restarting every pod simultaneously — so this also makes operator upgrades on persistent clusters recover automatically.

make test passes (90 controller specs, valkey + api packages).

@greptile-apps

greptile-apps Bot commented Jul 26, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This change relocates cluster TLS configuration under spec.networking, stages cluster pod-template updates through workload revisions, and improves stale peer-address recovery. A focused controller reproduction found that restoring live workload-template drift can update a primary StatefulSet without the cluster controller first obtaining topology state or performing proactive failover. This availability issue should be fixed before merging.

Confidence Score: 4/5

Not safe to merge until primary workload-template drift is coordinated with the same failover protections used for planned rolling updates.

The production controller paths were exercised with a focused before-and-after reproducer. It confirmed that a real StatefulSet template update is applied while the cluster controller decides neither to scrape topology nor to run proactive failover.

Files Needing Attention: internal/controller/failover.go, internal/controller/valkeycluster_controller.go, internal/controller/valkeynode_controller.go

T-Rex T-Rex Logs

What T-Rex did

  • T-Rex produced a focused proof for a posted P1 finding and attached a Go reproducer and related outputs.
  • T-Rex produced a focused proof for a second posted P1 finding.
  • T-Rex documented the contract validation results, confirming reproduction outcomes and explaining the impact on StatefulSet updates relative to cluster failover.

View all artifacts

T-Rex Ran code and verified through T-Rex

Comments Outside Diff (2)

  1. internal/controller/failover.go, line 152-155 (link)

    P1 Live template drift bypasses primary failover coordination

    A cluster-owned ValkeyNode can restore a drifted StatefulSet or Deployment pod template even when its stored Spec.WorkloadRevision already authorizes the desired template. The node reconciler detects the live-template difference and applies the update, but this early nodeRequiresRoll return treats matching ValkeyNode specs as no roll. The cluster controller then skips topology scraping and proactive failover, allowing a template restoration to restart a live primary before leadership is moved to a healthy replica.

    Treat a live workload-template hash that differs from the desired authorized revision as a failover-aware roll even when the ValkeyNode spec is unchanged, or defer cluster-owned drift restoration until the cluster controller coordinates the update.

    Artifacts

    Focused Go reproducer for a cluster-owned StatefulSet template annotation drift

    • Temporary Go test source that creates a settled cluster-owned node, injects a live template annotation only in the drift scenario, and invokes the production node and cluster roll predicates; it demonstrates the mismatched decisions.

    Focused reproducer output with a settled StatefulSet template

    • Executed `TREX_DRIFT=0 go test ./internal/controller -run ^TestTrexClusterOwnedLiveTemplateDrift$ -count=1 -v` in `/home/user/repo`; it shows no template update and no failover/topology scrape in the baseline.

    Focused reproducer output with an externally injected StatefulSet template annotation

    • Executed `TREX_DRIFT=1 go test ./internal/controller -run ^TestTrexClusterOwnedLiveTemplateDrift$ -count=1 -v` in `/home/user/repo`; it shows the node applies the template restoration while the cluster still requests neither proactive failover nor topology scraping.

    View artifacts

    T-Rex Ran code and verified through T-Rex

  2. General comment

    P1 Live pod-template drift can roll a primary without proactive failover

    • Bug
      • When an external admission/controller adds a nonvolatile annotation to a cluster-owned StatefulSet template, the ValkeyNode reconciler sees a template difference and restores its desired template. Because the desired template hash equals the already stored Spec.WorkloadRevision, its gate permits the update. Meanwhile, the cluster controller compares only current versus desired ValkeyNode Specs; with no Spec difference it decides no failover-aware roll is needed and does not scrape topology. The reproduced drift run applied the template change while reporting no proactive failover or topology scrape.
    • Cause
      • ensureStatefulSet detects live StatefulSet template drift independently of ValkeyNode Spec changes (valkeynode_controller.go:326-354), whereas needsProactiveFailoverForRoll starts with nodeRequiresRoll, which only compares ValkeyNode Specs and returns false for this case (failover.go:152-155). The live template hash collected by liveWorkloadTemplateHashes is therefore ineffective once that early return is taken.
    • Fix
      • Treat a mismatch between the live workload template hash and the authorized desired WorkloadRevision as a failover-aware roll for a ready primary, even if ValkeyNode Spec is unchanged; alternatively, prevent the node reconciler from applying drift-restoration template changes for cluster-owned nodes until the cluster reconciler has coordinated the roll. Add an integration/unit regression that injects a live template annotation with unchanged ValkeyNode Spec and asserts topology scraping/failover coordination before the StatefulSet template update.

    T-Rex Ran code and verified through T-Rex

Reviews (7): Last reviewed commit: "Merge branch 'main' into fix-stale-addre..." | Re-trigger Greptile

@melancholictheory

Copy link
Copy Markdown
Contributor

this is the right fix for the all-restart case, and it gets the two things that are easy to get wrong: sourcing the MEET target from the live cluster view (live.Address) rather than the stale table (the persisted nodes.conf is exactly what can't be trusted here), and guarding CLUSTER FORGET when the failing entry's node ID still belongs to a reachable member, otherwise cleanup would ban the very node you just re-introduced. the address != peer.Address check also keeps this from firing on a normal transient fail?, so including fail? for the all-restart case doesn't cost spurious MEETs during ordinary gossip hiccups.

one optional thing: on a full N-node restart every node's table lists every other peer as stale, so this issues on the order of N² MEETs before it settles. CLUSTER MEET propagates transitively, once any reachable viewer meets a node, gossip carries that node's new address to the rest, so meeting each live node once from a single viewer is enough and is gentler on a large cluster. not a correctness issue, the N² just makes more noise before it converges.

and where this sits relative to #296: the phase exists because nodes announce pod IPs, so nodes.conf holds addresses that die on restart. if hostname announcement lands and is in use, the persisted entries are resolvable names that survive the IP change and this heal path stops having anything to do. so it's the reactive fix for IP-mode clusters and #296 is the preventive one, complementary, and the phase stays correct (just inert) under hostname mode.

@matka12

matka12 commented Jul 27, 2026

Copy link
Copy Markdown
Contributor Author

Thanks @melancholictheory — good call on the N² MEETs. Pushed 5c88340: the heal now dedupes by the moved member's node ID, issuing one MEET per member instead of one per (viewer, peer) pair. A failed MEET doesn't mark the member as met, so another viewer retries it on the same pass. The first viewer in iteration order lists all moved peers, so the resulting MEET graph is effectively a star from that viewer — connected, and gossip converges the rest.

Re-verified on the k3d repro (3 shards / 1 replica, persistence, delete all 6 pods): 6 MEETs (was 30), still ~20s to Ready with data intact.

Agreed on #296: this phase is the reactive fix for IP-announcement clusters, hostname announcement is the preventive one — and if hostname mode lands, the detector's address != peer.Address guard means this phase simply never fires.

@melancholictheory

Copy link
Copy Markdown
Contributor

star from the first viewer is the clean way to do it, and 30 -> 6 on the repro is the payoff. one edge to keep in mind: if that first viewer is itself unreachable on a given pass, no star forms that round, but since an absent viewer just means the next reconcile picks another one, it self-heals on the requeue rather than getting stuck. so the only cost is an extra pass in that case, which is fine. nice turnaround.

@matka12

matka12 commented Jul 27, 2026

Copy link
Copy Markdown
Contributor Author

Thanks! Small note on that edge: it's actually cheaper than an extra pass. An unreachable viewer never makes it into the scraped state at all (GetClusterState drops nodes it can't connect to), so its stale pairs simply don't exist that round — the dedupe walks the remaining viewers' pairs, and since every reachable viewer's table lists all moved members, the next viewer in iteration order becomes the hub in the same pass. The requeue-and-retry case only kicks in for MEETs that fail mid-command, which don't mark the member as met.

@jdheyburn

Copy link
Copy Markdown
Collaborator

Thanks for raising this @matka12. I want to validate it works properly with reproduction steps. You mention the upgrade from 0.3.0 -> 0.4.0, perhaps you could provide that? Would we then expect the issue to be resolved with an upgrade from 0.3.0 -> this PR?

@matka12

matka12 commented Jul 28, 2026

Copy link
Copy Markdown
Contributor Author

Sure — two repros, both on a scratch cluster (I used k3d, kind works the same). The minimal one first since it isolates the bug from the upgrade mechanics:

Repro A — minimal (any operator version, ~2 min)

helm repo add valkey https://valkey.io/valkey-helm/
helm install valkey-operator valkey/valkey-operator --version 0.4.0 \
  -n valkey-operator-system --create-namespace

kubectl apply -f - <<'YAML'
apiVersion: valkey.io/v1alpha1
kind: ValkeyCluster
metadata:
  name: test
spec:
  image: valkey/valkey:9.1.0
  shards: 3
  replicas: 1
  persistence:
    size: 1Gi
  config:
    appendonly: "yes"
YAML

# wait for state: Ready, then write a marker key
kubectl exec valkey-test-0-0-0 -c server -- valkey-cli -c set canary survives

# the trigger: every pod restarts at once, every pod IP changes
kubectl delete pod --all

Unpatched result: pods come back Ready, but every nodes.conf (persisted on the PVCs) lists the peers' old IPs. CLUSTER NODES on any pod shows all peers fail?/fail at dead addresses, cluster_state:fail, and the CR sits in Reconciling ("Waiting for replica sync") indefinitely — I gave it 13+ minutes. The existing MEET phase doesn't fire because no node is isolated (cluster_known_nodes = 6 on all of them; they know every peer, just at dead addresses). Manual CLUSTER MEET <new-ip> 6379 per peer from any one pod recovers it — that's the workaround folks in #275 landed on.

With this PR's image: same kill, the controller logs re-introducing peer whose address changed and emits StaleAddressesHealed: Re-introduced 6 peer link(s); cluster_state:ok and CR Ready ~20s after the pods are back, canary intact. No intervention.

Repro B — the 0.3.0 → 0.4.0 upgrade as trigger

Same as A but install --version 0.3.0 first (CR unchanged). Once Ready:

# per UPGRADE.md — CRDs first
kubectl apply --server-side -f https://raw.githubusercontent.com/valkey-io/valkey-operator/v0.4.0/config/crd/bases/valkey.io_valkeyclusters.yaml
kubectl apply --server-side -f https://raw.githubusercontent.com/valkey-io/valkey-operator/v0.4.0/config/crd/bases/valkey.io_valkeynodes.yaml

helm upgrade valkey-operator valkey/valkey-operator --version 0.4.0 -n valkey-operator-system

The v0.4.0 controller adds the system-users auth env (PRIMARY_AUTH, VALKEY_USER, ACL secret mounts) to every ValkeyNode's pod template in the same reconcile pass, so all six single-replica workloads roll simultaneously — which is repro A's trigger, no infrastructure event needed. Same deadlock follows.

Your question — 0.3.0 → this PR directly

Yes, resolved. The simultaneous roll itself still happens (the pod-template change is inherent to the version bump), but the controller doing the rolling is the patched one, so the moment the pods are back it detects the stale pairs and re-MEETs them — the cluster re-forms on its own in seconds instead of wedging. Data on the PVCs (AOF) is untouched throughout. That was exactly my original failure case: this bug turned a routine operator upgrade on a persistent production cluster into a manual-recovery incident.

@jdheyburn

Copy link
Copy Markdown
Collaborator

@greptile-apps

Comment thread internal/valkey/clusterstate.go Outdated
@jdheyburn

Copy link
Copy Markdown
Collaborator

I was able to repo it and get it to work locally, so thank you!

It would be great to have an e2e test for this, but I don't want to block the PR any further since I'd like to get it out for 0.5. Can you raise an issue for that and get one created once this is merged in please?

Could you also take a look at the AI reviewer comments to see if its applicable?

@matka12

matka12 commented Aug 2, 2026

Copy link
Copy Markdown
Contributor Author

Done on both:

AI reviewer comment — valid catch, fixed in d3ab0c7. CLUSTER NODES renders IPv6 endpoints bracketed ([fd00::2]:6379@16379), and the last-colon slice kept the brackets, so a failing IPv6 peer at its current address never compared equal to the bare pod IP — misclassified as moved, spurious MEET + requeue. Endpoint host extraction now goes through net.SplitHostPort (after trimming @cport and the optional ,hostname), which unbrackets IPv6 and leaves IPv4 untouched. Unit tests added for the endpoint parser and both IPv6 cases (current address → not stale; moved → stale).

e2e test — raised #352 with the scenario (create persistent cluster → kill all pods → assert StaleAddressesHealed + Ready + data intact, no manual MEET). I'll pick it up once this merges.

@coderabbitai

coderabbitai Bot commented Aug 2, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 59a9adb0-84bd-4018-aba8-0b406d8345f7

📥 Commits

Reviewing files that changed from the base of the PR and between f1574c2 and eef2743.

📒 Files selected for processing (1)
  • internal/controller/valkeycluster_controller.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • internal/controller/valkeycluster_controller.go

📝 Walkthrough

Walkthrough

Changes

Stale address healing

Layer / File(s) Summary
Cluster state detection
internal/valkey/clusterstate.go, internal/valkey/clusterstate_test.go
The cluster state aggregates shard and pending nodes, parses IPv4 and IPv6 endpoints, and detects failed or noaddr entries that reference live nodes at changed addresses.
Controller peer repair
internal/controller/valkeycluster_controller.go
Reconciliation issues deduplicated CLUSTER MEET commands before stale-node cleanup, records repair status, requeues healing attempts, and preserves moved node IDs from CLUSTER FORGET.

Sequence Diagram(s)

sequenceDiagram
  participant ClusterState
  participant Reconciler
  participant ValkeyNode
  ClusterState->>Reconciler: Return stale viewer/live node pairs
  Reconciler->>ValkeyNode: Send CLUSTER MEET to current address
  ValkeyNode-->>Reconciler: Return command result
  Reconciler->>Reconciler: Update status and requeue successful repairs
Loading

Possibly related issues

  • valkey-io/valkey-operator issue 352: Directly implements the stale-address healing behavior described by this change.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 16.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly describes the primary fix for restoring cluster members after pod addresses change during a full restart.
Description check ✅ Passed The description covers the issue, behavior changes, implementation, testing, and verification, but omits explicit limitations and checklist completion.
Linked Issues check ✅ Passed The changes satisfy issue #275 by detecting stale addresses, issuing CLUSTER MEET commands, and preventing premature CLUSTER FORGET operations.
Out of Scope Changes check ✅ Passed The code and tests are directly related to stale peer address recovery and contain no unrelated changes.

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 golangci-lint (2.12.2)

Error: build linters: plugin(logcheck): plugin "logcheck" not found
The command is terminated due to an error: build linters: plugin(logcheck): plugin "logcheck" not found


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai 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.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/valkey/clusterstate.go`:
- Around line 428-473: Update FindStaleAddressPeers to treat entries flagged
noaddr as stale when a live node with the same ID exists, bypassing the empty
hostFromClusterNodesEndpoint result for :0@0; retain the existing
address-difference check for non-noaddr entries. Add a test covering a :0@0
noaddr entry matched to a live node.
- Around line 400-419: Add coverage for real unbracketed IPv6 CLUSTER NODES
endpoints: update TestHostFromClusterNodesEndpoint in
internal/valkey/clusterstate_test.go to include unbracketed host:port@cport
cases, and mirror the stale-peer IPv6 scenarios with unbracketed endpoint
strings in internal/valkey/clusterstate_test.go:644-659 and 661-701; no direct
change is required in internal/valkey/clusterstate.go:400-419 unless needed to
support these tests.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 313fd6ae-5fd7-417c-80cf-58fd55e27d08

📥 Commits

Reviewing files that changed from the base of the PR and between 1c64d35 and d3ab0c7.

📒 Files selected for processing (3)
  • internal/controller/valkeycluster_controller.go
  • internal/valkey/clusterstate.go
  • internal/valkey/clusterstate_test.go

Comment thread internal/valkey/clusterstate.go
Comment thread internal/valkey/clusterstate.go
@matka12

matka12 commented Aug 2, 2026

Copy link
Copy Markdown
Contributor Author

Addressed both CodeRabbit findings in 19f641a:

  • noaddr entries skipped (major) — correct catch: the :0@0 placeholder parses to an empty host, so the address != "" guard I added for parse failures was also skipping noaddr — the one flag that by definition never has an address to compare. noaddr entries are now treated as stale whenever a live node with the same ID exists (no address comparison), with tests for both the live-match and genuinely-dead cases.
  • unbracketed IPv6 coverage (minor)CLUSTER NODES indeed reports IPv6 unbracketed (fd00::2:6379@16379); those already parse correctly via the last-colon fallback in hostFromClusterNodesEndpoint (net.SplitHostPort rejects them, fallback strips the port). Added parser cases and mirrored both stale-peer IPv6 scenarios with unbracketed endpoints, keeping the bracketed cases for defense in depth.

@matka12

matka12 commented Aug 2, 2026

Copy link
Copy Markdown
Contributor Author

The e2e failure here is inherited from main, not this branch: #341 granted _operator +config|get but the denied-commands e2e still expects CONFIG GET * to be NOPERM'd — main's own run for that merge commit fails identically (Expected <int>: 5 to equal <int>: 6, with the CONFIG GET output visible in the log).

Opened #355 with the test fix (move CONFIG GET to the allowed check, replace the denied slot with FLUSHALL). Once that merges I'll rebase/rerun here.

matka12 added 4 commits August 2, 2026 14:22
…l restart

When every pod restarts at the same time (full Kubernetes cluster
restart, node pool replacement) with persistence enabled, each node
comes back with a new pod IP while its persisted nodes.conf still lists
the peers' old IPs. With no surviving member to gossip the new
addresses, every node keeps dialing dead addresses, all peers stay
fail?/fail, and the cluster never re-forms without a manual
CLUSTER MEET (valkey-io#275).

The operator already knows both sides: the live address of every member
(ValkeyNode pod IPs) and each node's stale view (CLUSTER NODES). Add a
heal phase that detects known-but-failing peers whose node ID belongs to
a live member at a different address and re-introduces them with
CLUSTER MEET. The handshake carries the member's node ID, so the viewer
rebinds the existing entry to the new address and gossip propagates it.

Also guard forgetStaleNodes against this state: it matches failing
entries to ValkeyNodes by pod IP, so a live member at a changed address
looked like a dead node and could be issued CLUSTER FORGET - banning it
from rejoining for a minute and fighting the recovery.

Verified on a 3-shard/1-replica cluster with persistence: deleting all
six pods simultaneously previously left the cluster in cluster_state:fail
indefinitely; with this change the operator re-forms it in ~20s
(StaleAddressesHealed event, 30 peer links) with data intact.

Fixes valkey-io#275

Signed-off-by: Matan David <matan.david@eon.io>
On a full N-node restart every node's table lists every peer as stale,
so the heal issued ~N^2 MEETs before settling. One successful MEET per
moved member is enough: the handshake also registers the viewer's
current address on the target, and gossip carries both new addresses to
the remaining nodes.

Re-verified on the 3-shard/1-replica k3d repro: deleting all six pods
now heals with 6 MEETs (was 30), still ~20s to Ready with data intact.

Signed-off-by: Matan David <matan.david@eon.io>
CLUSTER NODES renders IPv6 endpoints bracketed ([fd00::2]:6379@16379).
Slicing the endpoint at its last colon kept the brackets, so a failing
IPv6 peer at its current address never compared equal to the bare pod
IP Kubernetes reports - misclassifying it as moved and issuing spurious
CLUSTER MEETs plus requeues on IPv6 clusters.

Extract the host with net.SplitHostPort (after trimming the @cport and
optional ,hostname suffix), which unbrackets IPv6 and leaves IPv4
untouched.

Signed-off-by: Matan David <matan.david@eon.io>
noaddr entries carry the :0@0 placeholder, so endpoint parsing yields no
host and the address-difference guard skipped them - the one flag that
by definition never has an address to compare. Treat a noaddr entry as
stale whenever a live node with the same ID exists.

CLUSTER NODES reports IPv6 endpoints unbracketed (fd00::2:6379@16379);
those take the last-colon fallback in hostFromClusterNodesEndpoint and
already parse correctly - add parser and stale-peer coverage for them
alongside the bracketed form.

Signed-off-by: Matan David <matan.david@eon.io>
@matka12
matka12 force-pushed the fix-stale-address-remeet branch from 19f641a to f1574c2 Compare August 2, 2026 11:22
@jdheyburn

Copy link
Copy Markdown
Collaborator

Will look to get this merged in after #338

@jdheyburn jdheyburn left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I did a test locally after #338 and it resolved from all the pods being deleted at once. Thank you for picking this up!

@jdheyburn
jdheyburn merged commit 1520e02 into valkey-io:main Aug 7, 2026
10 checks passed

@bjosv bjosv left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

LGTM, just a finding about the brackets.
I now read in the comments that it was added as a defense

}

// hostFromClusterNodesEndpoint extracts the bare host from a CLUSTER NODES
// endpoint field (<ip:port@cport[,hostname]>). IPv6 hosts appear bracketed

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I don't think CLUSTER NODES uses brackets by default, unlike valkeys logger?
Maybe it's when user sets in in cluster-announce-ip? (which seems to be blocked soon via valkey-io/valkey#4055)

Maybe I'm wrong, and in that case we should reuse this function in GetFailingNodes

jdheyburn added a commit that referenced this pull request Aug 27, 2026
## Summary

The e2e test promised in #352, covering the stale-address heal merged in
#333 (#275).

## Scenario

1. Create a 3-shard / 1-replica **persistent** ValkeyCluster (new sample
manifest) and wait for Ready.
2. Write a canary key.
3. `kubectl delete pod -l valkey.io/cluster=...` — all six pods restart
at once, every pod IP changes while each persisted `nodes.conf` still
holds the old peer addresses. No node is isolated, so only the heal
phase can re-form the cluster.
4. Assert:
   - a `StaleAddressesHealed` event is emitted for the cluster,
- the CR returns to `Ready` with 3 ready shards, without any manual
`CLUSTER MEET`,
   - `cluster_state:ok` and the canary key survived (AOF on the PVCs),
- **no** `StaleNodeForgotten` event for the cluster during recovery —
the forget guard from #333 (a forget here would 60s-ban the rejoining
member).

## Verification

Ran the exact scenario against an image built from current main
(`5676fb9`) on a local cluster: heal event fired (`Re-introduced 6 peer
link(s) whose addresses changed`), Ready ~40s after the kill, canary
intact, zero forget events.

Closes #352

---------

Signed-off-by: Matan David <matan.david@eon.io>
Co-authored-by: Joe Heyburn <34041368+jdheyburn@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.

Persistent ValkeyCluster can fail after full cluster restart due to stale pod IPs in nodes.conf

4 participants