fix(controller): delay background analysis until the canary receives traffic - #4864
fix(controller): delay background analysis until the canary receives traffic#4864n1koo wants to merge 2 commits into
Conversation
Published E2E Test Results 4 files 4 suites 3h 57m 27s ⏱️ For more details on these failures, see this check. Results for commit c30e93a. ♻️ This comment has been updated with latest results. |
Published Unit Test Results2 580 tests 2 580 ✅ 3m 28s ⏱️ Results for commit c30e93a. ♻️ This comment has been updated with latest results. |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #4864 +/- ##
==========================================
- Coverage 85.12% 85.08% -0.05%
==========================================
Files 166 166
Lines 19238 19251 +13
==========================================
+ Hits 16376 16379 +3
- Misses 2019 2024 +5
- Partials 843 848 +5
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
…traffic For a traffic-routed canary the background AnalysisRun is created as soon as the new ReplicaSet exists, with no check that the controller has programmed the traffic router for it. A canary-scoped metric is then evaluated against a ReplicaSet receiving no traffic: it passes on no evidence, or -- more often -- fails on no data and aborts a healthy rollout. Gate creation on canaryWeightReached(): the recorded weight is for the current canary ReplicaSet, has reached the current setWeight, and, for routers that verify weights, has been verified. Weights are read from newStatus, which reconcileTrafficRouting populates earlier in the same reconcile. Only applies when traffic routing is configured; a basic canary shifts traffic by moving service selectors and is unchanged. Step-based analysis is deliberately not gated. completedCurrentCanaryStep already refuses to advance past a setWeight step until the replica counts are right and the weight is verified, so an analysis step that follows a setWeight is covered. Gating it would also mean an unbounded stall: an Analysis step is an indefinite step, so progressDeadlineSeconds does not apply to it. Signed-off-by: Niko Kurtti <niko@kurtti.eu> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
be99674 to
522ab43
Compare
|
|
||
| For a canary with traffic routing, background analysis additionally waits until the canary is | ||
| receiving the weight the current step calls for, so it is not evaluated against a ReplicaSet that | ||
| has no traffic yet. This needs no configuration; `startingStep` remains available to delay it |
There was a problem hiding this comment.
The startingStep is a field from the above custom resource. It is not a variable name from code?
Not sure if I followed you though, mind explaining bit more?
|
@kostis-codefresh @zachaller The reason behind this change looks valid and I can't think of a scenario where this change can impact negatively. Although if some users have come to expect the analysis to start from the beginning of the rollout irrespective of the traffic being routed to the canary pods or not, then it would be a noticeable change in behaviour. But still, this change seems valid. |
…raffic Signed-off-by: Niko Kurtti <niko@kurtti.eu> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
10eea6f to
c30e93a
Compare
|



For a traffic-routed canary, the background AnalysisRun is created as soon as the new ReplicaSet exists —
reconcileBackgroundAnalysisRunchecks only that the rollout is not fully promoted, has a stable RS, and is not beforestartingStep. Nothing checks that the controller has actually programmed the traffic router for that ReplicaSet.A canary-scoped metric is then evaluated against a ReplicaSet receiving no traffic. Either outcome is wrong: it passes on no evidence, or — the common case, since most providers return no data for a pod with no requests — it fails or errors and aborts a healthy rollout.
The window is real and not small. The canary has to scale up before
reconcileTrafficRoutingwill give it any weight (newRS.Status.AvailableReplicas == 0forcesdesiredWeightto 0), and on ALB the target group has to converge on top of that.The fix
Gate creation on
canaryWeightReached():Status.Canary.Weights != nil— the router has been programmed for some canary.Weights.Canary.PodTemplateHashmatches the current canary ReplicaSet — not a previous revision that still holds the traffic split.Weights.Canary.Weight >= GetCurrentSetWeight(rollout).Weights.Verified, it must betrue.Weights are read from
newStatus, whichreconcileTrafficRoutingpopulates earlier in the same reconcile (rolloutCanaryruns it at canary.go:67, analysis at :76), so the gate does not lag a reconcile behind the router.Only applies when
TrafficRouting != nil; a basic canary shifts traffic by moving service selectors, so there is nothing to wait for.Where a router does verify weights, the timing works out. Background creation is retried every reconcile while the rollout sits at its
setWeightstep, and at asetWeightstepShouldVerifyWeightreturns true, soWeights.Verifiedis meaningful exactly when the gate consults it. Note this only applies to ALB with--aws-verify-target-groupenabled (defaults.defaultVerifyTargetGroupisfalse) or a plugin that populatesVerified; every other in-tree router returnsnil, and for those the gate reduces to "the controller has programmed the step's weight".Why not
startingStepRolloutAnalysisBackground.StartingStepis the closest existing knob, butBeforeStartingStepcompares step indices only. Reaching step 1 does not mean step 0's weight landed: with a slow-scaling canary, or an ALB target group that has not converged, it demonstrably has not.startingStepalso cannot express "step 0's weight actually took effect", which is the condition that matters here.Behaviour change
For a traffic-routed canary with background analysis, the AnalysisRun now starts once traffic has shifted rather than immediately. For analysis scoped to the canary — the documented purpose — that is the intended fix. For a template that measures overall service health as a kill switch, it means the analysis no longer covers the canary scale-up window; nothing is routed to the canary during that window, so a regression there is not attributable to it, but it is a change and worth calling out.
If you would rather this were opt-in behind a field rather than the default, I am happy to rework it that way. I have proposed it as the default because the current behaviour aborts healthy rollouts.
Step-based analysis is deliberately not gated
An earlier version of this PR gated
reconcileStepBasedAnalysisRuntoo. I dropped it:completedCurrentCanaryStep(canary.go:319-327) already refuses to advance past asetWeightstep untilAtDesiredReplicaCountsForCanaryandWeights.Verified != false. An analysis step that follows asetWeightstep is therefore already reached only after the weight has landed and been verified.currentStep.SetWeight == nil, soShouldVerifyWeightis false, ALB'sVerifyWeightreturnsnil, nil, andVerifiedis overwritten to nil — the clause is inert precisely where a step gate would rely on it.isIndefiniteStep(sync.go:563) covers Analysis steps, so theprogressDeadlineExceededbranch at sync.go:708 is skipped for them. A step-based AnalysisRun that is never created would hang the rollout with no timeout and no explanation.The background gate has none of these problems: when the canary never becomes ready the rollout stays parked on the
setWeightstep, which is not an indefinite step, soprogressDeadlineExceededstill fires and the rollout goes Degraded with an accurate reason.The one case dropping the step gate leaves uncovered:
[setWeight 50, pause 10m, analysis]where canary pods churn during the pause and the weight regresses before the step's AnalysisRun is created. That is worth a separate change with its own evidence, not a hang risk carried here.Related work
#4867 gates the abort scale-down delay on the canary traffic having shifted away. Same underlying idea — don't act until the router has converged — but a different predicate: that one asks "has the weight reached zero", this one asks "has it reached the step's target". They share only the one-line
Weights.Verifiedcheck, so I have kept them as separate helpers in separate files rather than coupling the two PRs to share it. The PRs are independent and can merge in either order.Testing
TestDelayBackgroundAnalysisUntilCanaryWeightReached— no recorded weights, weight 0, weights belonging to a previous ReplicaSet, unverified router, verified router, non-verifying router, and a basic canary (which must be unaffected). Each of the "wait" cases fails if the gate is removed, and the basic-canary case fails if the traffic-routing guard is removed.TestIstioSuite/TestIstioCanaryBackgroundAnalysisWaitsForCanaryWeight— against a real Istio VirtualService, holds the canary pod un-ready so the canary stays at weight 0 and asserts no AnalysisRun is created; marking the pod ready takes the weight to 50 and the AnalysisRun appears.docs/features/analysis.md, alongsideinitialDelayandstartingStep.go build,go vet,golangci-lint run,gofmt, and the full./rollout/...suite: clean. (gofmtflagstest/e2e/functional_test.go, which is pre-existing on master and untouched here.)No filed upstream issue matches this behaviour; the closest related report is #2519 (analysis running multiple times), which is a different failure.
Checklist:
Fixes #suffix.