fix(trafficrouting): do not block abort/progressDeadline when Istio delays DestinationRule switch. Fixes #4626#4823
Open
ChanghwanK wants to merge 1 commit into
Conversation
…elays DestinationRule switch. Fixes argoproj#4626 When subset-level Istio traffic routing delays the DestinationRule switch because a traffic-receiving ReplicaSet is not fully available (behavior introduced by argoproj#4612), UpdateHash returns a plain error. rolloutCanary() treated it as fatal and returned before syncRolloutStatusCanary(), so evaluateProgressDeadlineAbort(), Progressing-condition timeout evaluation, and ReplicaSet cleanup never ran. A rollout whose canary pods never become available therefore could not time out, turn Degraded, or auto-abort: the controller retried the same reconcile forever and the rollout stayed Progressing indefinitely. The condition that should trigger the abort (unavailable ReplicaSet) was exactly the condition that blocked the abort code path. Fix it in three parts: 1. Wrap the delay with a sentinel error (istio.ErrDestinationRuleUpdateDelayed) so callers can distinguish this transient, expected condition from fatal errors via errors.Is. All other errors keep the existing propagate-and-requeue behavior. 2. In reconcileTrafficRouting(), when the sentinel is detected, skip SetWeight for the round (preserving the ordering guarantee from argoproj#4612) but return nil so the rest of the reconcile still runs: abort/progressDeadline evaluation, conditions/phase updates, and ReplicaSet cleanup. 3. Flag the delay on the rollout context so completedCurrentCanaryStep() does not advance the current step while the desired traffic weight has not actually been applied, keeping the progression backpressure the fatal error used to provide. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: changhwanK <devchanghwan@gmail.com>
|
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #4823 +/- ##
==========================================
- Coverage 85.09% 85.03% -0.06%
==========================================
Files 166 166
Lines 19136 19142 +6
==========================================
- Hits 16283 16278 -5
- Misses 2005 2011 +6
- Partials 848 853 +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:
|
Contributor
Published E2E Test Results 4 files 4 suites 3h 46m 6s ⏱️ For more details on these failures, see this check. Results for commit d96c8fb. |
Contributor
Published Unit Test Results2 518 tests 2 518 ✅ 3m 23s ⏱️ Results for commit d96c8fb. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.



Fixes #4626
Summary
When subset-level Istio traffic routing delays the DestinationRule switch because a traffic-receiving ReplicaSet is not fully available (behavior introduced by #4612), the resulting error propagated as fatal and short-circuited
rolloutCanary()before it ever reachedsyncRolloutStatusCanary(). As a result, a rollout whose canary pods never become available could not time out, turnDegraded, or auto-abort: the controller retried the same reconcile forever (rate-limited requeue) and the rollout stayedProgressingindefinitely, requiring manual intervention (and in the worst observed case, a controller restart) to recover.The core of the bug is a circular blocking condition: the condition that should trigger the progress-deadline abort (an unavailable ReplicaSet) is exactly the condition that blocked the code path that evaluates the abort.
Root cause
Call path (all line refs at current master):
rollout/trafficrouting/istio/istio.goUpdateHash()intentionally skips the DestinationRule switch and returns a plainerrorwhenshouldDelayDestinationRuleUpdate()is true. This is an expected, transient condition (logged atInfolevel), but it is indistinguishable from a fatal error to callers.rollout/trafficrouting.goreconcileTrafficRouting()propagates it unchanged.rollout/canary.go:67-69rolloutCanary()returns early, skipping everything after it: experiments, analysis runs, ReplicaSet scaling/cleanup, and criticallysyncRolloutStatusCanary()→persistRolloutStatus(), which is whereevaluateProgressDeadlineAbort(),Progressing-condition timeout evaluation, andrequeueStuckRollout()live.Since the delay error recurs on every reconcile as long as the ReplicaSet stays unavailable (e.g. crashlooping pods), the abort/timeout evaluation is blocked permanently, not transiently. Note that abort is also the natural exit for this loop:
shouldDelayDestinationRuleUpdate()already exempts the canary ReplicaSet during an abort, so an abort would make the delay condition disappear and let the system converge — it just could never fire.Fix
Three small pieces:
rollout/trafficrouting/istio/istio.go: wrap the delay error with a sentinel (ErrDestinationRuleUpdateDelayed,%w) so callers can distinguish this expected condition viaerrors.Is. The error message is unchanged. All other errors (API failures etc.) are untouched and keep the existing propagate-and-requeue behavior.rollout/trafficrouting.goreconcileTrafficRouting(): when the sentinel is detected afterUpdateHash(), skipSetWeight()for this round (preserving the ordering guarantee from fix(trafficrouting): ensure DestinationRule is updated before SetWeight on rollback #4612: never set weight while the DestinationRule switch is pending) but returnnilso the rest of the reconcile still runs — abort/progressDeadline evaluation, conditions/phase updates, ReplicaSet cleanup, and the deadline-based requeue.rollout/canary.gocompletedCurrentCanaryStep()+ atrafficRoutingDelayedflag onrolloutContext: while the switch is delayed, the current step is not treated as completed, socurrentStepIndexcannot advance while the desired traffic weight has not actually been applied. This keeps the progression backpressure that the fatal error used to provide.Behavior before/after while the DestinationRule switch is delayed:
SetWeight()calledProgressing→TimedOut, phaseDegradedTesting
TestCanaryProgressDeadlineAbortNotBlockedByDelayedDestinationRuleSwitchreproduces the reported symptom end-to-end at the controller level: a canary rollout withprogressDeadlineAbort: true, stuck past its deadline with a partially-available canary ReplicaSet, must still auto-abort whileUpdateHashreports the delay. It fails on master (no abort patch is produced) and passes with this fix.TestReconcileTrafficRoutingUpdateHashDelayedErrverifies the delayed error is absorbed,SetWeightis not called (unstubbed on the mock, so a regression panics), the step is not completed (currentStepIndexdoes not advance, noRolloutStepCompletedevent).TestRollbackDestinationRuleBeforeSetWeight(the fix(trafficrouting): ensure DestinationRule is updated before SetWeight on rollback #4612 scenario test) still assertsSetWeightis not called on delay, and now also asserts reconciliation continues (stale canary RS cleanup proceeds in the same sync instead of the sync erroring out).TestUpdateHashNoReadyReplicaSetsadditionally pins theerrors.Iscontract on the sentinel.go build ./..., fullgo test ./...(0 failures), andgolangci-lint run ./rollout/...(0 issues) pass locally.Production context
This was hit three times in production (2026-05-07, 2026-05-29, 2026-06-23) on v1.8.2; details posted in #4626. The third recurrence affected 8 rollouts simultaneously and required a controller restart to recover. In all cases the trigger was canary pods that could never become available, and the rollouts stayed
Progressingwith no abort, noDegradedphase, and no status updates until manual intervention.Checklist:
"fix(controller): Updates such and such. Fixes #1234".TestIstioSuite) was not run locally and relies on CI.🤖 Generated with Claude Code