feat: stage pod template rolls behind cluster permit - #338
Conversation
Operator image upgrades that change the ValkeyNode pod template were rewriting every 1-pod StatefulSet in one reconcile wave, taking the whole cluster down at once. Gate cluster-owned StatefulSet/Deployment template updates on valkey.io/allow-workload-revision (granted one node at a time by the ValkeyCluster controller, replicas first with proactive failover). Surface WorkloadDrift while waiting. Own API-server defaults so residual default noise does not re-trigger rolls. Spec and workload grants share a single in-flight permit. Standalone ValkeyNodes still apply immediately. Create path unchanged. Signed-off-by: daanvinken <daanvinken@tythus.com>
CI Check formatting failed on third-party vs local import grouping. Signed-off-by: daanvinken <daanvinken@tythus.com>
|
Drop unused anyNodeHasInFlightWorkloadRoll. Split reconcileValkeyNode helpers so gocyclo stays under the limit. When the pod template already matches, still sync labels, owner, and Spec so non-template controller fields do not go stale. Signed-off-by: daanvinken <daanvinken@tythus.com>
jdheyburn
left a comment
There was a problem hiding this comment.
I did a quick scan on the review, but I don't know if I'm sold on the design yet. I would rather understand what is causing the ValkeyCluster controller to push the change out to so many nodes at once. I spent a bit of time on the original PRs to implement safe sequential rolls, so I'm interested to see what's happening.
|
|
||
| // syncDeploymentWithoutRoll updates labels, owner, and Spec when the pod | ||
| // template would not roll. | ||
| func (r *ValkeyNodeReconciler) syncDeploymentWithoutRoll(ctx context.Context, node *valkeyiov1alpha1.ValkeyNode, dep *appsv1.Deployment, desired *appsv1.Deployment) error { |
There was a problem hiding this comment.
There's a lot of similarity with syncStatefulSetWithoutRoll. Are we able to DRY these? Something like syncWorkloadWithoutRoll, that might help cut the LOC down.
There was a problem hiding this comment.
Fair! We can for sure
|
this is the right extension. the sequencing the cluster does at the CR level was being undone the moment each node rewrote its own template, so gating the template apply behind the same permit closes a real gap. and it's good that it also composes well with #317: now that the operator owns the api-server defaults, the template only differs on genuine changes (image, real builder changes), so this stages the changes that should be staged rather than defaulting churn. the two together are the full fix for #337. one thing to make sure is observable: the workload permit is a single cluster-wide token, so a node whose new pod never becomes Ready (bad image, failing probe) holds it indefinitely and no other node's workload roll proceeds. that's the safe behaviour, you don't want to cascade a broken rollout, but it means one stuck node wedges every other node's template roll silently. the (the DRY point on |
|
Let's take the discussion on the bug itself to #337 (comment) |
Replace the allow-workload-revision annotation permit with a Spec field owned by ValkeyCluster. The cluster sets WorkloadRevision to the hash of the built pod template; the node applies rolling template updates only when that hash matches. Operator upgrades and other builder drift become normal one-at-a-time Spec rolls (replica-first, failover before primary). Standalone nodes still apply immediately. Signed-off-by: daanvinken <daanvinken@tythus.com>
anyNodeRequiresRoll must set desired Spec.WorkloadRevision the same way reconcileValkeyNode does, or every settled node looks like it needs a roll and the topology scrape runs every reconcile. Also default Probe.TimeoutSeconds and PeriodSeconds so user probe patches do not thrash the pod template. Signed-off-by: daanvinken <daanvinken@tythus.com>
There was a problem hiding this comment.
E2E as a follow up I'd say.
(Meta) comment: A big chunk of the node-controller diff isn't new behavior, it's restructuring ensureStatefulSet/ensureDeployment and the cluster-controller extractions (maybeProactiveFailoverBeforeRoll, handleUnchangedValkeyNode).
We could split that out if this is found hard to review, let me know
I'll squash commits once this is approved.
| // proactive failover still runs before killing a primary. | ||
| aclSecret, err := r.getClusterACLSecret(ctx, cluster) | ||
| if err != nil { | ||
| // Bootstrap: secret may not exist yet; hash without ACL annotations. |
There was a problem hiding this comment.
When the ACL secret isn't ready the cluster hashes without template annotations, but the gate only engages when podTemplateWouldRoll is treu. On create the node bypasses the gate. It converges once the secret exists.
| needsSpecRoll := currentExists && nodeRequiresRoll(current, desired) | ||
|
|
||
| if deferred := r.maybeProactiveFailoverBeforeRoll(ctx, cluster, clusterState, current, needsSpecRoll); deferred { |
There was a problem hiding this comment.
Revision-only updates trigger an unnecessary failover
nodeRequiresRoll treats an empty Spec.WorkloadRevision on an existing node as a pod roll even when the live workload template already matches the desired template. That result reaches maybeProactiveFailoverBeforeRoll before CreateOrUpdate persists the revision, so upgrading an existing cluster can deliberately fail over every primary solely to backfill this new bookkeeping field. Gate proactive failover on an actual workload-template change, or persist the revision without considering it a roll.
Artifacts
Revision-only failover executable harness source
- The exact temporary Go test source that uses the production reconciliation path and a local RESP server; it asserts an unchanged pod template, an empty-to-desired revision-only change, and receipt of CLUSTER FAILOVER.
Revision-only failover harness output
- Captured successful `go test` output showing `podTemplateWouldRoll=false`, `CLUSTER FAILOVER`, failover events before `ValkeyNodeUpdated`, persisted revision, command, working directory, and exit code 0.
jdheyburn
left a comment
There was a problem hiding this comment.
I did a local test and it worked great, so thank you! I had a couple of comments.
- Can you update the PR title and description with the new setup?
- Can you check up on some of the AI code review comments?
- Can you check on the failing e2e test?
- I agree that we can park an e2e test for this, but let's capture an issue for it so we don't lose track of it
| // Spec.WorkloadRevision has not yet authorized that template. Status True | ||
| // means a rolling update is deferred until the ValkeyCluster controller | ||
| // advances Spec.WorkloadRevision (one node at a time today). | ||
| ValkeyNodeConditionWorkloadDrift = "WorkloadDrift" |
There was a problem hiding this comment.
Can you document this in docs/status-conditions.md please?
| dep.Spec.Template.Annotations = buildPodTemplateAnnotations(node, aclSecret) | ||
| } | ||
| return dep.Spec.Template, nil | ||
| default: |
There was a problem hiding this comment.
Can we case on StatefulSet, then error on default?
This PR closes #337
Summary
ValkeyCluster already updates ValkeyNode specs carefully: one node at a time, replicas before primaries, with a failover attempt before rolling a primary.
That care stopped at the ValkeyNode object. Each node rewrites its single-pod StatefulSet (or Deployment) as soon as the computed pod template differs, with no coordination across nodes. Image changes and operator upgrades that rewrite the built template could restart every pod in the cluster at once.
This PR extends the same staged idea to the pod template apply path.
Features / Behaviour Changes
valkey.io/allow-workload-revision).WorkloadDriftcondition and records the desired template revision for the cluster controller to grant.Implementation
workload_roll.go: hash the desired pod template, read/write permit annotations, decide grant vs wait, shared in-flight check used by both Spec and workload paths.WorkloadDrift, leave the live workload alone, and requeue.ValkeyNodeConditionWorkloadDrifton the ValkeyNode API for visibility while waiting.Review focus: permit grant/clear transitions, the shared in-flight check when
allow=*is used for Spec rolls, and that waiters do not thrash events on every requeue.Limitations
*for Spec-driven rolls where the cluster does not know the hash yet.Testing
go test ./internal/controller/passes.Checklist
pre-commit run --all-filesor hooks on commit)