fix(trafficrouting): do not block abort/progressDeadline when Istio delays DestinationRule switch. Fixes #4626 - #4823
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.17% 85.15% -0.03%
==========================================
Files 166 166
Lines 19453 19459 +6
==========================================
+ Hits 16570 16571 +1
- Misses 2030 2033 +3
- Partials 853 855 +2
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:
|
Published E2E Test Results 4 files 4 suites 4h 8m 28s ⏱️ For more details on these failures, see this check. Results for commit d7026be. ♻️ This comment has been updated with latest results. |
Published Unit Test Results2 627 tests 2 627 ✅ 3m 29s ⏱️ Results for commit d7026be. ♻️ This comment has been updated with latest results. |
|
Hi @zachaller / @kostis-codefresh, would appreciate a look at this when you have bandwidth. This fixes a bug we've hit three times in production (2026-05-07, 2026-05-29, 2026-06-23 on v1.8.2) where canary rollouts get permanently stuck The fix is small and scoped (sentinel error to distinguish the transient Istio DestinationRule delay from fatal errors), all CI is green, and it includes a new test that reproduces the exact stuck-forever scenario end-to-end and fails on master without the fix. Happy to address any review feedback quickly. |
There was a problem hiding this comment.
While reading the PR description and associated issue I had the feeling that we had fixed this same issue in the past.
I took a look at GitHub history and found the following
- #2507 (issue, 2023) first issue in this area
- #3602 (PR) first change in UpdateHash
- #4128 (issue, 2025-02) same issue as #4626 (progressDeadline doesn't take effect)
- #4287 (PR, closed unmerged) first PR that addresses the issue. but it was never merged
- #4299 (PR) first PR that was actually merged
- #4390 (issue) issue in the same area
- #4560 PR fixing #4390
- #4612 yet another Istio related PR
There I also added a comment where I said I feel we let Istio semantics "bleed" to the rest of the code.
I see that this PR is doing the same. We introduce a new flag trafficRoutingDelayed which in theory is universal but in practice is only used by Istio and not any of the other traffic providers.
@ChanghwanK First of all many thanks for the time and effort you put into this. Did your research turn up any of the issues/PRs above? I am asking because this area of the code has been changes a few times already, and I want to make sure any proposed solution is also taking into account all the previous issues.
@zachaller I think we need to make a hard choice. This is the second PR where we change generic traffic routing code because of Istio semantics. We can either continue this route (doing small Istio related fixes) or accept the fact that maybe the existing API is not enough for what Istio needs and we need a proper redesign. But right now I feel there is a contant stream of issues/PRs where the main theme is "Istio is not ready for X, Argo Rollouts things that everything is fine".
It seems to me that several Argo Rollouts functions have very simple semantics (either X happened, or there was an error) and we need to change the API to also include (X did not happen yet, because Istio/traffic provider was not ready, not an error yet). But instead of doing in a case-by-case basis we need to bake this into the API itself. Thoughts?
|
@kostis-codefresh Thank you for the thorough archaeology, this context is exactly what I needed. Did my research turn up these issues/PRs? Partially, to be honest. I root-caused the bug to #4612 (which made the delay error fatal on the generic path) and traced the delay mechanism itself back to #3602. I had not found #4128 / #4287 / #4299 before your comment, so I went through them now. Here is how I see the history fitting together:
So I believe #4299 and this PR are complementary rather than competing: #4299 makes the delay condition disappear once an abort is in progress, while this PR restores the path that declares the abort in the first place. #4612 unintentionally cut that path, which is why the symptom from #4128 resurfaced as #4626 (we hit it three times in production on v1.8.2). In other words, this PR does not replace any of the previous fixes; it makes the exit installed by #4299 reachable again. On the "Istio semantics bleed" point: agreed, and the current shape of this PR is guilty of it. On the API redesign: I agree that "X did not happen yet, provider not ready" deserves a first-class representation in the |
…his PR) Signed-off-by: changhwanK <devchanghwan@gmail.com>
f97f695 to
b6976ba
Compare
|
|
Following up here, it's been about 2.5 weeks since my last reply and I know review bandwidth is limited, so no pressure on timeline. Just wanted to keep this visible since it's still an active production issue for us (rollouts stuck un-abortable, requiring manual intervention to recover). @zachaller whenever you get a chance, I'd appreciate your call on the two paths from my July 22 comment: merge this as a scoped fix now (I can go ahead and move the sentinel out of the istio package to address @kostis-codefresh's "semantics bleed" concern), or hold for the broader interface redesign first. Happy to start on either. |
Root cause
If that's right, any provider that errors persistently gets stuck forever. Not just this case:
This PR fixes one instance of a class. The alternative: #4962To make this concrete, I put up #4962. It absorbs all traffic routing errors at the Two guards preserve what the fatal error used to protect:
No sentinel. No @ChanghwanK — would value your eyes on #4962 too. It builds directly on your analysis and reuses your test approach. |
|
#4963 is maybe a more complete refactor as well |



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