Skip to content

test(e2e): add shutdown-on-sigterm failover test - #295

Open
Sagar2366 wants to merge 8 commits into
valkey-io:mainfrom
Sagar2366:test/sigterm-failover-e2e
Open

test(e2e): add shutdown-on-sigterm failover test#295
Sagar2366 wants to merge 8 commits into
valkey-io:mainfrom
Sagar2366:test/sigterm-failover-e2e

Conversation

@Sagar2366

Copy link
Copy Markdown
Collaborator

This PR closes #270

Summary

Adds the E2E test deferred from #268: verify that when a primary pod is gracefully terminated, the shutdown-on-sigterm failover directive hands the shard off to a replica before the pod exits, so the shard keeps a writer through the disruption and no data is lost.

Features / Behaviour Changes

Test-only change; no operator behaviour is modified. The new spec runs under the failover Ginkgo label.

Implementation

The test follows the outline in #270:

  1. Creates a ValkeyCluster with shards: 3, replicas: 1 and waits for Ready.
  2. Identifies shard 0's primary and replica, and records the primary pod's UID so the StatefulSet-recreated pod (same name) can be distinguished from the old one.
  3. Writes 50 keys across the keyspace, then deletes the primary pod with the default grace period (SIGTERM path — the lighter proxy for a drain mentioned in the issue notes).
  4. Asserts the replica reports role:master within the 30s grace window (Eventually timeout = terminationGracePeriodSeconds), i.e. the handover beat SIGKILL.
  5. Asserts the shard keeps accepting writes, the replaced pod comes back (new UID) and rejoins as role:slave, cluster_state:ok, and all 50 keys read back intact.

Two things reviewers may want to pay attention to, both learned from runs of this test against Kind:

  • Roles are read live from INFO replication, not from ValkeyNode.status.role. Right after cluster formation the status can report two primaries for a shard (every node boots as a master before CLUSTER REPLICATE, and the status refresh lags) — this is the staleness described in [enhancement] Event-driven ValkeyNode reconcile for timely Status.Role after failover / cluster changes #261, and the first draft of this test flaked on exactly that. The replica is also only accepted once master_link_status:up, so the failover is not attempted against a still-syncing replica.
  • VALKEYCLI_AUTH is unset before running valkey-cli inside the server container (execValkeyPodShell helper). The operator injects that variable for the probe scripts, and valkey-cli auto-sends AUTH as the default user whenever it is set — which fails (ERR AUTH ... without any password configured for the default user) and pollutes command output. Commands run as the default nopass user, consistent with the rest of the e2e suite. The _operator user cannot be used instead because its ACL has no SET/GET.

Limitations

  • Uses kubectl delete pod (graceful, default grace period) rather than a node drain; the issue notes name this as the acceptable lighter proxy for CI. A drain-based variant can be layered on later.
  • "Promoted before the grace period ends" is asserted by bounding the promotion check at 30s after the delete, matching the default terminationGracePeriodSeconds; the observed promotion latency in practice is ~4s.

Testing

Run against a 3-node Kind cluster via:

KIND_CLUSTER=<cluster> go test -tags=e2e ./test/e2e/ -v -ginkgo.v -ginkgo.label-filter failover

Result: 1 Passed | 0 Failed in 159s. Timeline from the passing run: SIGTERM at 22:21:06.4, replica reported role:master by 22:21:10.1 (~4s, well inside the 30s grace period), replaced pod rejoined as replica ~4s later, cluster_state:ok, readable=50 keys plus the write made during the disruption.

go vet -tags=e2e and golangci-lint run --build-tags e2e are clean for the new file.

Checklist

Before submitting the PR make sure the following are checked:

  • This Pull Request is related to one issue.
  • Commit message explains what changed and why
  • Tests are added or updated.
  • Documentation files are updated.
  • I have run pre-commit locally (pre-commit run --all-files or hooks on commit)

@greptile-apps

greptile-apps Bot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds e2e coverage for graceful primary termination failover in a ValkeyCluster. The main changes are:

  • Adds an authenticated failover test cluster setup with a password-backed default user.
  • Detects live primary and replica roles from INFO replication before triggering failover.
  • Starts a continuous writer during primary StatefulSet deletion and verifies acknowledged writes remain readable.
  • Checks promotion within the termination grace period and verifies cluster recovery plus seeded key preservation.

Confidence Score: 5/5

Safe to merge with minimal risk.

The change is test-only and scoped to one e2e spec plus helper functions. The added assertions exercise the intended graceful failover behavior without changing operator runtime code. No verified correctness or security issues were found.

No files require special attention.

T-Rex T-Rex Logs

What T-Rex did

  • Checked the failover environment and confirmed Kubernetes tooling was unavailable and there was no usable Kubernetes context.
  • Attempted the failover e2e command with a 60-second timeout; the run exited with code 124 due to environmental unavailability rather than a test assertion.
  • Validated the e2e package with a compile-only check, which finished successfully with exit code 0.
  • Ran a static analysis pass using go vet on the e2e tests, which completed successfully with exit code 0.

View all artifacts

T-Rex Ran code and verified through T-Rex

Important Files Changed

Filename Overview
test/e2e/valkeycluster_test.go Extends the existing failover e2e spec with authenticated setup, live role detection, continuous write verification during graceful primary termination, and post-recovery data checks.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
participant Test as E2E test
participant K8s as Kubernetes API
participant Primary as Shard primary pod
participant Replica as Shard replica pod
participant Operator as Valkey Operator

Test->>K8s: Create Secret + ValkeyCluster
Test->>K8s: Wait for ValkeyCluster Ready
Test->>Primary: INFO replication / seed SET keys
Test->>Replica: INFO replication confirms in-sync replica
Test->>Replica: Start continuous cluster-mode writer
Test->>K8s: Delete primary StatefulSet
K8s->>Primary: Graceful termination (SIGTERM)
Primary->>Replica: shutdown-on-sigterm failover handoff
Test->>Replica: Assert role:master within grace period
Test->>Replica: Verify acknowledged writes are readable
Operator->>K8s: Recreate StatefulSet/pod
Test->>K8s: Wait for 6 ready pods and Ready cluster
Test->>Replica: Verify seeded keys remain readable
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
participant Test as E2E test
participant K8s as Kubernetes API
participant Primary as Shard primary pod
participant Replica as Shard replica pod
participant Operator as Valkey Operator

Test->>K8s: Create Secret + ValkeyCluster
Test->>K8s: Wait for ValkeyCluster Ready
Test->>Primary: INFO replication / seed SET keys
Test->>Replica: INFO replication confirms in-sync replica
Test->>Replica: Start continuous cluster-mode writer
Test->>K8s: Delete primary StatefulSet
K8s->>Primary: Graceful termination (SIGTERM)
Primary->>Replica: shutdown-on-sigterm failover handoff
Test->>Replica: Assert role:master within grace period
Test->>Replica: Verify acknowledged writes are readable
Operator->>K8s: Recreate StatefulSet/pod
Test->>K8s: Wait for 6 ready pods and Ready cluster
Test->>Replica: Verify seeded keys remain readable
Loading

Reviews (7): Last reviewed commit: "test(e2e): batch the seeded writes throu..." | Re-trigger Greptile

Comment thread test/e2e/failover_sigterm_test.go Outdated
Expect(output).To(ContainSubstring(fmt.Sprintf("written=%d", keyCount)),
fmt.Sprintf("Not all keys were written: %s", output))

By("gracefully terminating the primary pod (SIGTERM with default grace period)")

@sandeepkunusoth sandeepkunusoth Jul 6, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

i think there is some overlap between this test and https://github.com/sandeepkunusoth/valkey-k8s-operator/blob/5eb8a3380eb55a6151d6e9e7df7f838de654726e/test/e2e/valkeycluster_test.go#L951. can we merge both of them together?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

@sandeepkunusoth can you get your PR merged if not done already? I can rebase my PR on your changes.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This is already in main from very long time

By(fmt.Sprintf("deleting primary statefulset %s to trigger Valkey failover", primaryStatefulset))

@sandeepkunusoth sandeepkunusoth Jul 9, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

hi i think u didn't get my previous comment instead of having duplicate test at both places i was suggesting if we we can move this to existing e2e test where we are doing failover already. you may just need to verify key are persisted and some other changes.

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

Thanks for raising! It looks like this PR added supported for default user password for e2e cluster tests. Are you able to rebase and use that instead of unsetting the VALKEYCLI_AUTH env var?

@melancholictheory

Copy link
Copy Markdown
Contributor

nice, this closes the e2e i raised in #270. the structure is solid, and a couple of details are better than the outline i sketched: reading roles live from INFO replication with the master_link_status:up gate (sidesteps the status lag in #261), and keying replacement detection off the pod UID.

one thing worth thinking about, since i wrote the shutdown-on-sigterm change (#268): the current assertions would mostly pass with the feature turned off too, so the test doesn't fully isolate what it's meant to prove.

  • "replica reports role:master within 30s": without the directive, valkey's default SIGTERM behaviour is a plain shutdown of the primary, and the cluster then promotes the replica through normal failure detection, which can also land inside 30s. so a promotion in that window doesn't by itself mean the graceful handoff drove it.
  • "no keys lost": the 50 keys are written and replicated before the delete, so they already live on the replica. they'd survive an abrupt failover just as well, since nothing is in flight during the disruption.

what shutdown-on-sigterm actually buys is an orderly handoff: the primary fails over to a caught-up replica before it exits, so there's no window where the shard has no writer and no acknowledged write is dropped. to target that, i'd write continuously through the termination (a loop of SET with unique values while the pod is terminating) and then assert every acknowledged write is readable afterwards, plus that the write-error window is near zero. that's the behaviour the directive changes; the current version mostly checks that a failover eventually happened.

not blocking, it's a good baseline, just flagging that it would likely stay green even if #268 were reverted.

Sagar2366 added 3 commits July 8, 2026 19:22
Cover the graceful-termination handover deferred from valkey-io#268: delete a
shard primary with the default grace period and assert a replica is
promoted before the grace period ends, the shard keeps serving writes,
the replaced pod rejoins as a replica, and no keys are lost.

Roles are read live from INFO replication rather than ValkeyNode
status, which can report stale roles right after cluster formation
(see valkey-io#261). VALKEYCLI_AUTH is unset when running valkey-cli inside the
server container, since valkey-cli would otherwise auto-send AUTH as
the default user and fail.

Closes valkey-io#270

Signed-off-by: Sagar Utekar <sagarutekar2366@gmail.com>
Merge the primary-workload-deletion recovery scenario suggested in
review into the failover suite as a second spec: delete the shard
primary's StatefulSet, assert the replica is promoted, the operator
recreates the StatefulSet, the recreated pod rejoins as a replica, and
the ValkeyCluster returns to Ready with no keys lost. Cluster creation,
key seeding, and health/data assertions are shared between both specs.

Signed-off-by: Sagar Utekar <sagarutekar2366@gmail.com>
Give the failover test clusters a password-protected default user via a
passwordSecret, following the pattern from valkey-io#292, and set VALKEYCLI_AUTH
to that password for every valkey-cli invocation instead of unsetting
it. The tests no longer rely on the default user being passwordless.

Signed-off-by: Sagar Utekar <sagarutekar2366@gmail.com>
@Sagar2366
Sagar2366 force-pushed the test/sigterm-failover-e2e branch from e63d186 to 4dcc856 Compare July 8, 2026 14:13
Sagar2366 added 2 commits July 8, 2026 20:17
The StatefulSet-deletion recovery scenario is already covered in main
by 'should detect and recover when a primary deployment is deleted' in
valkeycluster_test.go. Keep this suite focused on what that test does
not exercise: the graceful shutdown-on-sigterm handoff of a terminating
primary pod.

Signed-off-by: Sagar Utekar <sagarutekar2366@gmail.com>
Strengthen the sigterm failover spec per review: a continuous writer
runs through the termination recording per-attempt acks, and the test
asserts every acknowledged write is readable afterwards and that the
longest gap between acknowledged writes stays bounded. The failover
path (coordinated handoff vs failure detection) is detected from the
promoted replica's log and reported.

The handoff path is reported rather than hard-asserted: valkey 9.0's
clusterAutoFailoverOnShutdown requires exact ack-offset equality when
selecting a replica and intermittently falls back to failure-detection
promotion (~1 in 3 under write load in local testing), so a hard
assertion would flake until that promotion is deterministic.

Observed on Kind with the handoff engaged: 7490 acknowledged writes
through the disruption, longest writer gap 0.05s, zero lost.

Signed-off-by: Sagar Utekar <sagarutekar2366@gmail.com>
Comment thread test/e2e/failover_sigterm_test.go Outdated
Per review, drop the separate failover spec and extend the existing
'should detect and recover when a primary deployment is deleted' test
instead, so failover coverage lives in one place. The existing test
gains, around its StatefulSet-deletion disruption:

- a password-protected default user (passwordSecret, as in valkey-io#292) so
  valkey-cli commands run authenticated
- 50 keys seeded before the disruption and verified after recovery
- a continuous writer through the disruption with per-attempt acks and
  a 2s connection timeout (a stale MOVED redirect to the terminated
  primary's IP otherwise hangs a connect for the ~130s TCP SYN timeout)
- promotion of the shard's replica asserted within the 30s termination
  grace period
- verification that every write acknowledged during the disruption is
  readable afterwards
- detection of whether the shutdown-on-sigterm handoff engaged (from
  the promoted replica's log), with the writer-gap bound asserted on
  the handoff path; the path itself is reported rather than
  hard-asserted because valkey 9.0's replica selection on shutdown
  requires exact ack-offset equality and intermittently falls back to
  failure detection

Full e2e suite on Kind: 41 of 41 specs passed. On the handoff path the
writer recorded 7904 acknowledged writes with a 0.23s longest gap and
zero lost.

Signed-off-by: Sagar Utekar <sagarutekar2366@gmail.com>
Comment thread test/e2e/valkeycluster_test.go Outdated

script := fmt.Sprintf(
"for i in $(seq 0 %d); do v=$(valkey-cli -t 2 -c get e2e:cw:$i 2>/dev/null | tail -n 1); echo \"$i $v\"; done", maxIdx)
output, err := execValkeyPodShell(pod, script)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Right now every key starts a new valkey-cli process

Instead is there anyway we can open valey-cli once and do these commands through a single instance.

Per review, stop spawning one valkey-cli process per key when verifying
data after the disruption. Both read-back paths now pipe their GETs
into one valkey-cli over stdin, with an ECHO KEY:<index> marker before
each GET so the raw-mode output is correlated per key without relying
on line ordering. Missing keys print an empty line and simply leave no
entry.

The continuous writer intentionally keeps one process per attempt: each
attempt samples fresh-connection availability (what a refilling client
pool experiences during the disruption) and records a per-attempt
timestamp between commands, which a single long-lived instance would
not measure.

Verified on Kind: spec passes with the handoff engaged, 7597
acknowledged writes, all read back through the batched path.

Signed-off-by: Sagar Utekar <sagarutekar2366@gmail.com>
Apply the same single-instance pattern to writeTestKeys: all seed SETs
are piped through one valkey-cli, counting the OK responses. Only the
continuous writer keeps one process per attempt, deliberately, to
sample fresh-connection availability during the disruption.

Signed-off-by: Sagar Utekar <sagarutekar2366@gmail.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.

E2E test for shutdown-on-sigterm failover (drain primary node, verify replica promotion)

4 participants