Skip to content

feat: inject shutdown-on-sigterm failover by default - #268

Merged
bjosv merged 2 commits into
valkey-io:mainfrom
melancholictheory:feat/shutdown-on-sigterm-failover
Jun 23, 2026
Merged

feat: inject shutdown-on-sigterm failover by default#268
bjosv merged 2 commits into
valkey-io:mainfrom
melancholictheory:feat/shutdown-on-sigterm-failover

Conversation

@melancholictheory

Copy link
Copy Markdown
Contributor

Closes #248

Summary

Inject shutdown-on-sigterm failover into the managed Valkey config by default for every ValkeyCluster. On SIGTERM (node drain, eviction, preemption, or kubectl delete pod), a primary fails its slots over to a replica during graceful shutdown, which covers the unplanned termination that the operator's own proactive failover never sees.

Implementation

  • Added the directive to getBaseConfig, alongside the other operator-managed defaults.
  • It is a Valkey 9.0+ directive, which matches the operator's documented baseline, so it renders unconditionally with no version gate.
  • No double-failover risk: it only fires if the node is still primary at SIGTERM, and it is a no-op on replicas.

Acceptance criteria

  • shutdown-on-sigterm failover injected into valkey.conf by default
  • Works alongside the operator's proactive failover without double-firing (no-op on replicas)
  • E2E test (drain the primary's node, verify a replica is promoted before the pod exits)

I left the E2E test out for now since it needs node-drain orchestration in the e2e harness. Happy to add it here or as a follow-up, whichever you prefer. The unit test asserts the directive is rendered.

The terminationGracePeriodSeconds constraint is tracked separately in #260; the docs note the relationship.

Testing

  • Unit test asserts shutdown-on-sigterm failover is in the rendered config.
  • make test and make lint pass locally.

References

Checklist

  • 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 (ran make test and make lint instead)

Render `shutdown-on-sigterm failover` in the base cluster config so a
primary hands its slots off to a replica during graceful shutdown
(node drain, eviction, preemption). This covers out-of-band
descheduling that the operator's own rolling failover never observes.

Requires Valkey 9.0+.

Signed-off-by: melancholictheory <selimvhorst@gmail.com>
@greptile-apps

greptile-apps Bot commented Jun 19, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

Injects shutdown-on-sigterm failover into the operator-managed Valkey config so that a primary node receiving SIGTERM (from a node drain, eviction, or preemption) hands its slots to a replica before exiting, covering out-of-band descheduling that the operator's proactive failover never observes.

  • config.go: Adds "shutdown-on-sigterm": "failover" to getBaseConfig alongside the other non-overridable cluster directives; scoped only to ValkeyCluster (not standalone ValkeyNode), and is a no-op on replicas so there is no double-failover risk.
  • config_test.go: Extends the existing cluster config unit test to assert the directive appears in the rendered valkey.conf output.
  • docs/valkeycluster.md: Adds a new "Graceful shutdown" section explaining the Valkey 9.0+ requirement, the terminationGracePeriodSeconds / cluster-manual-failover-timeout relationship, and what to do if users raise the manual failover timeout.

Confidence Score: 5/5

Safe to merge — the change is a single base-config addition that fires only on SIGTERM, is a no-op on replicas, and does not interfere with the operator's own proactive failover path.

The code change is minimal: one map entry in getBaseConfig, a matching unit test, and accurate documentation. The directive is correctly scoped to ValkeyCluster only (standalone ValkeyNode uses buildManagedConfig, which is untouched). The default timeout values cited in the docs (5s cluster-manual-failover-timeout, 30s Kubernetes grace period) are verified correct against the Valkey documentation, giving roughly 25s of margin. No control-flow, reconciler logic, or data paths are modified.

No files require special attention.

Important Files Changed

Filename Overview
internal/controller/config.go Adds "shutdown-on-sigterm failover" to getBaseConfig; correctly scoped to ValkeyCluster only (standalone ValkeyNode uses buildManagedConfig which is unaffected), and last-value-wins placement makes it non-overridable by user config.
internal/controller/config_test.go Adds a ContainSubstring assertion for "shutdown-on-sigterm failover" in the rendered config; matches the exact "key value\n" format produced by writeConfigLine.
docs/valkeycluster.md Adds a Graceful shutdown section; default values cited (30s terminationGracePeriodSeconds, 5s cluster-manual-failover-timeout) are accurate per Valkey documentation and give correct margin guidance.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant K8s as Kubernetes
    participant Primary as Valkey Primary Pod
    participant Replica as Valkey Replica Pod

    K8s->>Primary: SIGTERM (node drain / eviction / preemption)
    Note over Primary: shutdown-on-sigterm failover triggers
    Primary->>Replica: CLUSTER FAILOVER (manual)
    Note over Replica: Pauses replication stream,<br/>catches up to primary offset
    Replica-->>Primary: Failover votes secured
    Note over Replica: Promoted to Primary<br/>(within cluster-manual-failover-timeout, default 5s)
    Primary->>Primary: Demoted to Replica
    Note over Primary: Normal shutdown sequence<br/>(within terminationGracePeriodSeconds, default 30s)
    Primary->>K8s: Pod exits cleanly
    Note over K8s: SIGKILL never sent<br/>(~25s margin with defaults)
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 K8s as Kubernetes
    participant Primary as Valkey Primary Pod
    participant Replica as Valkey Replica Pod

    K8s->>Primary: SIGTERM (node drain / eviction / preemption)
    Note over Primary: shutdown-on-sigterm failover triggers
    Primary->>Replica: CLUSTER FAILOVER (manual)
    Note over Replica: Pauses replication stream,<br/>catches up to primary offset
    Replica-->>Primary: Failover votes secured
    Note over Replica: Promoted to Primary<br/>(within cluster-manual-failover-timeout, default 5s)
    Primary->>Primary: Demoted to Replica
    Note over Primary: Normal shutdown sequence<br/>(within terminationGracePeriodSeconds, default 30s)
    Primary->>K8s: Pod exits cleanly
    Note over K8s: SIGKILL never sent<br/>(~25s margin with defaults)
Loading

Reviews (2): Last reviewed commit: "docs: clarify shutdown-on-sigterm grace-..." | Re-trigger Greptile

Comment thread docs/valkeycluster.md Outdated
Comment thread internal/controller/config.go
Spell out the actual defaults: the Kubernetes terminationGracePeriodSeconds
of 30s comfortably exceeds the Valkey cluster-manual-failover-timeout default
of 5s, so the failover completes out of the box. A grace-period bump is only
needed when the failover timeout is raised.

Signed-off-by: melancholictheory <selimvhorst@gmail.com>
@melancholictheory

Copy link
Copy Markdown
Contributor Author

on the grace-period note: the numbers are the other way round. the default cluster-manual-failover-timeout is 5s, not 30s (config.c registers it with a 5000ms default), so with the Kubernetes default terminationGracePeriodSeconds of 30s there is roughly 25s of margin and the failover completes out of the box. updated the docs to spell out both defaults and to say you only need to bump the grace period if you raise the timeout.

on whether shutdown-on-sigterm should be overridable: fair distinction, it is more of an operational preference than the correctness settings it sits next to in getBaseConfig. the operator doesn't have an "overridable defaults" layer today, it is either enforced base config or user config. #248 framed it as injected by default, so i kept it enforced, but happy to move it to an overridable layer if you'd rather users could set nosave / default. @jdheyburn that one is a design call for you.

@jdheyburn

Copy link
Copy Markdown
Collaborator

Let's keep it enforced for now, if there is a need to override the defaults then we'll consider that when we get there.

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

LGTM, thank you for updating the docs too!

@jdheyburn

Copy link
Copy Markdown
Collaborator

For the e2e test, I think a follow up issue is fine for now.

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

Nice!

@bjosv
bjosv merged commit 21dd262 into valkey-io:main Jun 23, 2026
8 checks passed
sandeepkunusoth pushed a commit to sandeepkunusoth/valkey-k8s-operator that referenced this pull request Jul 5, 2026
Closes valkey-io#248

### Summary

Inject `shutdown-on-sigterm failover` into the managed Valkey config by
default for every ValkeyCluster. On SIGTERM (node drain, eviction,
preemption, or `kubectl delete pod`), a primary fails its slots over to
a replica during graceful shutdown, which covers the unplanned
termination that the operator's own proactive failover never sees.

### Implementation

- Added the directive to `getBaseConfig`, alongside the other
operator-managed defaults.
- It is a Valkey 9.0+ directive, which matches the operator's documented
baseline, so it renders unconditionally with no version gate.
- No double-failover risk: it only fires if the node is still primary at
SIGTERM, and it is a no-op on replicas.

### Acceptance criteria

- [x] `shutdown-on-sigterm failover` injected into valkey.conf by
default
- [x] Works alongside the operator's proactive failover without
double-firing (no-op on replicas)
- [ ] E2E test (drain the primary's node, verify a replica is promoted
before the pod exits)

I left the E2E test out for now since it needs node-drain orchestration
in the e2e harness. Happy to add it here or as a follow-up, whichever
you prefer. The unit test asserts the directive is rendered.

The `terminationGracePeriodSeconds` constraint is tracked separately in
valkey-io#260; the docs note the relationship.

### Testing

- Unit test asserts `shutdown-on-sigterm failover` is in the rendered
config.
- `make test` and `make lint` pass locally.

### References

- Discussion valkey-io#231 (shutdown-on-sigterm section)
- valkey-io#120 (out-of-band termination discussion)
- valkey/valkey#1091

### Checklist

- [x] This Pull Request is related to one issue.
- [x] Commit message explains what changed and why
- [x] Tests are added or updated.
- [x] Documentation files are updated.
- [ ] I have run pre-commit locally (ran `make test` and `make lint`
instead)

---------

Signed-off-by: melancholictheory <selimvhorst@gmail.com>
Sagar2366 added a commit to Sagar2366/valkey-operator that referenced this pull request Jul 8, 2026
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>
jdheyburn pushed a commit that referenced this pull request Jul 10, 2026
#271)

Closes #260

### Summary

Add `spec.terminationGracePeriodSeconds` to `ValkeyCluster` (threaded
through to `ValkeyNode` and onto the pod) so the graceful `CLUSTER
FAILOVER` triggered on SIGTERM has time to hand the shard off to a
replica before SIGKILL.

### Design

Following the direction in #260:

- New `TerminationGracePeriodSeconds *int64` on both `ValkeyClusterSpec`
(user-facing) and `ValkeyNodeSpec` (the cluster controller threads it
through to the pod). Not grouped under a pod-template surface yet, per
your note that we can redesign that while still in alpha.
- When unset, the operator derives a safe value: `max(30s,
cluster-manual-failover-timeout / 1000 + 10s)`. With the defaults (5s
timeout) that stays at the Kubernetes default of 30s. Raising the
timeout pulls the grace period up with it.
- An explicit value is honoured as-is, so a user who wants a long grace
period (for example to allow a final RDB snapshot) gets exactly what
they asked for. If the value is below the recommended minimum, the
operator emits a `GracePeriodTooShort` warning event on the
`ValkeyCluster` instead of silently overriding it.

### On the "block the apply" idea

You floated blocking the manifest apply when the value is too short. i
went with respect-the-value-and-warn instead, because the recommended
minimum depends on `cluster-manual-failover-timeout`, which lives in
`spec.config` as a string map entry. a CEL admission rule can't cleanly
parse and compare that, so a hard block would need a validating webhook.
happy to add that as a follow-up if you'd rather it be a hard stop; the
warning event is the lighter "inform the user" mechanism for now.

### Acceptance criteria

- [x] Warns (event + log) when `terminationGracePeriodSeconds` is below
the recommended minimum
- [~] Reconcile-time check (CEL block deferred, see above)
- [x] Documented in the CRD field comments and `docs/valkeycluster.md`

### Testing

- Unit tests for the timeout / recommended / effective grace-period
helpers.
- `make test` and `make lint` pass locally.

Docs note: i added a `Termination grace period` section, which sits next
to the `Graceful shutdown` section from #268 once that merges.

### Checklist

- [x] This Pull Request is related to one issue.
- [x] Commit message explains what changed and why
- [x] Tests are added or updated.
- [x] Documentation files are updated.
- [ ] I have run pre-commit locally (ran `make test` and `make lint`
instead)

---------

Signed-off-by: melancholictheory <selimvhorst@gmail.com>
sandeepkunusoth added a commit that referenced this pull request Aug 9, 2026
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 #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:

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

---------

Signed-off-by: Sagar Utekar <sagarutekar2366@gmail.com>
Co-authored-by: sandeep kunusoth <31273507+sandeepkunusoth@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.

[feat] Support shutdown-on-sigterm failover config

3 participants