feat: configurable terminationGracePeriodSeconds for graceful failover - #271
Conversation
|
| Filename | Overview |
|---|---|
| internal/controller/valkeycluster_controller.go | Adds grace-period helpers and idempotent ConfigurationWarning condition; event guard correctly fires only on first transition into warning state; all early-return paths call updateStatus so the condition is always persisted. |
| internal/controller/grace_period_test.go | New unit tests cover all helper functions including edge cases (nil map, unparseable values, round-up, nil-on-default for upgrade safety). |
| internal/controller/valkeynode_resources.go | Passes TerminationGracePeriodSeconds from ValkeyNodeSpec directly to the PodSpec; straightforward field addition. |
| api/v1alpha1/valkeycluster_types.go | Adds TerminationGracePeriodSeconds field with kubebuilder minimum:1 validation, plus ConditionConfigurationWarning and ReasonGracePeriodTooShort constants. |
| api/v1alpha1/valkeynode_types.go | Adds TerminationGracePeriodSeconds to ValkeyNodeSpec with matching minimum:1 kubebuilder marker. |
| api/v1alpha1/zz_generated.deepcopy.go | Auto-generated deep-copy for the new *int64 fields; correct nil-guard pattern. |
| config/crd/bases/valkey.io_valkeyclusters.yaml | CRD schema for terminationGracePeriodSeconds with minimum:1 added; matches kubebuilder marker in the types file. |
| config/crd/bases/valkey.io_valkeynodes.yaml | CRD schema for ValkeyNode terminationGracePeriodSeconds with minimum:1. |
| docs/valkeycluster.md | Adds Termination grace period section accurately describing auto-derive logic, warning behavior, and CRD constraint. |
| docs/status-conditions.md | Documents the new ConfigurationWarning condition and GracePeriodTooShort reason; accurate and consistent with implementation. |
Flowchart
%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[ValkeyCluster spec
terminationGracePeriodSeconds] --> B{Explicit value set?}
B -- Yes --> C{value < recommended?
failoverTimeout + 10s}
C -- Yes --> D[Set ConfigurationWarning
condition = True]
D --> E{Was condition
already True?}
E -- No --> F[Emit one-shot
GracePeriodTooShort event]
E -- Yes --> G[Skip event
already emitted]
C -- No --> H[Remove ConfigurationWarning
condition if present]
B -- No --> I[Derive effective value
max 30s, failoverTimeout + 10s]
I --> H
B -- Yes --> J[Store value verbatim
on ValkeyNode]
I --> K{effective == 30s
Kubernetes default?}
K -- Yes --> L[Leave ValkeyNode field nil
avoid upgrade churn]
K -- No --> M[Set ValkeyNode field
to derived value]
J --> N[ValkeyNodeSpec
TerminationGracePeriodSeconds]
L --> N
M --> N
N --> O[PodSpec
TerminationGracePeriodSeconds]
%%{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"}}}%%
flowchart TD
A[ValkeyCluster spec
terminationGracePeriodSeconds] --> B{Explicit value set?}
B -- Yes --> C{value < recommended?
failoverTimeout + 10s}
C -- Yes --> D[Set ConfigurationWarning
condition = True]
D --> E{Was condition
already True?}
E -- No --> F[Emit one-shot
GracePeriodTooShort event]
E -- Yes --> G[Skip event
already emitted]
C -- No --> H[Remove ConfigurationWarning
condition if present]
B -- No --> I[Derive effective value
max 30s, failoverTimeout + 10s]
I --> H
B -- Yes --> J[Store value verbatim
on ValkeyNode]
I --> K{effective == 30s
Kubernetes default?}
K -- Yes --> L[Leave ValkeyNode field nil
avoid upgrade churn]
K -- No --> M[Set ValkeyNode field
to derived value]
J --> N[ValkeyNodeSpec
TerminationGracePeriodSeconds]
L --> N
M --> N
N --> O[PodSpec
TerminationGracePeriodSeconds]
Reviews (6): Last reviewed commit: "fix: address review feedback on terminat..." | Re-trigger Greptile
c390120 to
ea1ab76
Compare
ea1ab76 to
c5cd791
Compare
|
T-Rex pricing update — T-Rex was free through June 2026. Effective July 1, 2026, T-Rex adds 2 credits on top of the standard 1-credit review (3 total). T-Rex settings |
| // not silently override it. The condition is idempotent, so the event fires | ||
| // only when the cluster first enters the warning state. | ||
| if g := cluster.Spec.TerminationGracePeriodSeconds; g != nil && *g < recommendedGracePeriodSeconds(cluster) { | ||
| rec := recommendedGracePeriodSeconds(cluster) |
There was a problem hiding this comment.
nit: can we call recommendedGracePeriodSeconds once here? we can create constant and reuse that? if recalculation is not required?
There was a problem hiding this comment.
good call, computed it once before the branch now. 4c16269.
| // operator upgrade. | ||
| var gracePeriod *int64 | ||
| if g := effectiveGracePeriodSeconds(cluster); g != defaultGracePeriodSeconds { | ||
| gracePeriod = &g |
There was a problem hiding this comment.
what happens when previous graceperiod is 60 sec and if user wants to change graceperiod to default value 30 sec will this check cause updates to ignore silently?
There was a problem hiding this comment.
it doesn't drop the update silently: going from &60 to nil is still a spec change, so the pod does update to 30 (via the kubernetes default). but you're right that storing nil for an explicitly requested value is confusing, so i changed it to store explicit values verbatim, including 30. the field only stays nil for an unset value that resolves to the default, which is the case that keeps existing clusters from rolling on upgrade. 4c16269.
| } | ||
| ms, err := strconv.ParseInt(strings.TrimSpace(v), 10, 64) | ||
| if err != nil || ms <= 0 { | ||
| return defaultFailoverTimeoutSeconds |
There was a problem hiding this comment.
(nit) i think its better to add logs in error cases in 709 and 712 when it falls back to defaultFailoverTimeoutSeconds
There was a problem hiding this comment.
the missing-key path (709) is the common case, so a log there would fire on every reconcile for clusters that never set the timeout. a malformed value (712) also fails at valkey boot, and validating config values is tracked separately in #141, so i left the helper silent for now. happy to add a warning specifically for the unparseable case if you'd prefer it here.
| ReasonSlotsUnassigned = "SlotsUnassigned" | ||
| ReasonGracePeriodTooShort = "GracePeriodTooShort" | ||
| ReasonConfigurationValid = "ConfigurationValid" | ||
| ReasonPrimaryLost = "PrimaryLost" |
There was a problem hiding this comment.
ReasonConfigurationValid is not used anywhere
There was a problem hiding this comment.
removed, thanks. 4c16269.
| if !meta.IsStatusConditionTrue(cluster.Status.Conditions, valkeyiov1alpha1.ConditionConfigurationWarning) { | ||
| log.Info("terminationGracePeriodSeconds is below the recommended minimum for graceful failover", | ||
| "requested", *g, "recommended", rec) | ||
| r.Recorder.Eventf(cluster, nil, corev1.EventTypeWarning, valkeyiov1alpha1.ReasonGracePeriodTooShort, "ReconcileValkeyCluster", "%s", msg) |
There was a problem hiding this comment.
(nit) can u update docs/status-conditions.md with this new status.
There was a problem hiding this comment.
added a ConfigurationWarning section to docs/status-conditions.md in 4c16269.
Add spec.terminationGracePeriodSeconds to ValkeyCluster (threaded through to the ValkeyNode and the pod) so the graceful CLUSTER FAILOVER on SIGTERM can finish before SIGKILL. When unset, the operator derives a safe value: the larger of the Kubernetes default (30s) and cluster-manual-failover-timeout (default 5s) plus a 10s buffer. An explicit value is honoured as-is; if it is below the recommended minimum the operator emits a GracePeriodTooShort warning event rather than silently overriding it. Signed-off-by: melancholictheory <selimvhorst@gmail.com>
- CRD: add minimum=1 so a negative value is rejected at admission rather than looping the reconciler on every pod create. - Report a too-short grace period via an idempotent ConfigurationWarning condition; emit the event only on the transition into the warning state instead of on every reconcile. - Only set the node field when it differs from the Kubernetes default, so upgrading the operator does not roll existing clusters that resolve to the default. Signed-off-by: melancholictheory <selimvhorst@gmail.com>
- Compute recommendedGracePeriodSeconds once in the warn path. - Store an explicit terminationGracePeriodSeconds verbatim (including a value equal to the Kubernetes default) so the change always propagates; only leave the field nil for an unset value that resolves to the default. - Remove the unused ReasonConfigurationValid constant. - Document the ConfigurationWarning condition in docs/status-conditions.md. Signed-off-by: melancholictheory <selimvhorst@gmail.com>
4c16269 to
6282053
Compare
jdheyburn
left a comment
There was a problem hiding this comment.
LGTM - thanks for raising! Will let @sandeepkunusoth have another look too.
| // ConditionConfigurationWarning flags a spec value the operator accepted but | ||
| // considers risky, for example a terminationGracePeriodSeconds too short for | ||
| // graceful failover. | ||
| ConditionConfigurationWarning = "ConfigurationWarning" |
There was a problem hiding this comment.
This will be useful elsewhere, thanks!
There was a problem hiding this comment.
thanks for the review. agreed, the ConfigurationWarning condition and the recommended-minimum helper should carry over to any config-versus-resource guard down the line. happy to iterate if anything comes up downstream.
Closes #260
Summary
Add
spec.terminationGracePeriodSecondstoValkeyCluster(threaded through toValkeyNodeand onto the pod) so the gracefulCLUSTER FAILOVERtriggered on SIGTERM has time to hand the shard off to a replica before SIGKILL.Design
Following the direction in #260:
TerminationGracePeriodSeconds *int64on bothValkeyClusterSpec(user-facing) andValkeyNodeSpec(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.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.GracePeriodTooShortwarning event on theValkeyClusterinstead 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 inspec.configas 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
terminationGracePeriodSecondsis below the recommended minimumdocs/valkeycluster.mdTesting
make testandmake lintpass locally.Docs note: i added a
Termination grace periodsection, which sits next to theGraceful shutdownsection from #268 once that merges.Checklist
make testandmake lintinstead)