Skip to content

fix(trafficrouting): do not block abort/progressDeadline when Istio delays DestinationRule switch. Fixes #4626#4823

Open
ChanghwanK wants to merge 1 commit into
argoproj:masterfrom
ChanghwanK:fix/4626-delayed-dr-update-blocks-abort
Open

fix(trafficrouting): do not block abort/progressDeadline when Istio delays DestinationRule switch. Fixes #4626#4823
ChanghwanK wants to merge 1 commit into
argoproj:masterfrom
ChanghwanK:fix/4626-delayed-dr-update-blocks-abort

Conversation

@ChanghwanK

Copy link
Copy Markdown

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 reached syncRolloutStatusCanary(). As a result, a rollout whose canary pods never become available could not time out, turn Degraded, or auto-abort: the controller retried the same reconcile forever (rate-limited requeue) and the rollout stayed Progressing indefinitely, 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):

  1. rollout/trafficrouting/istio/istio.go UpdateHash() intentionally skips the DestinationRule switch and returns a plain error when shouldDelayDestinationRuleUpdate() is true. This is an expected, transient condition (logged at Info level), but it is indistinguishable from a fatal error to callers.
  2. rollout/trafficrouting.go reconcileTrafficRouting() propagates it unchanged.
  3. rollout/canary.go:67-69 rolloutCanary() returns early, skipping everything after it: experiments, analysis runs, ReplicaSet scaling/cleanup, and critically syncRolloutStatusCanary()persistRolloutStatus(), which is where evaluateProgressDeadlineAbort(), Progressing-condition timeout evaluation, and requeueStuckRollout() 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:

  1. rollout/trafficrouting/istio/istio.go: wrap the delay error with a sentinel (ErrDestinationRuleUpdateDelayed, %w) so callers can distinguish this expected condition via errors.Is. The error message is unchanged. All other errors (API failures etc.) are untouched and keep the existing propagate-and-requeue behavior.
  2. rollout/trafficrouting.go reconcileTrafficRouting(): when the sentinel is detected after UpdateHash(), skip SetWeight() 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 return nil so the rest of the reconcile still runs — abort/progressDeadline evaluation, conditions/phase updates, ReplicaSet cleanup, and the deadline-based requeue.
  3. rollout/canary.go completedCurrentCanaryStep() + a trafficRoutingDelayed flag on rolloutContext: while the switch is delayed, the current step is not treated as completed, so currentStepIndex cannot 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:

before after
SetWeight() called no no (unchanged, #4612 guarantee kept)
step index advances no (reconcile aborted) no (explicit guard)
progressDeadline / abort evaluated never every reconcile
ProgressingTimedOut, phase Degraded never on deadline expiry
ReplicaSet cleanup, analysis reconcile never every reconcile
fatal traffic-routing errors (API failures etc.) propagate + requeue propagate + requeue (unchanged)

Testing

  • New: TestCanaryProgressDeadlineAbortNotBlockedByDelayedDestinationRuleSwitch reproduces the reported symptom end-to-end at the controller level: a canary rollout with progressDeadlineAbort: true, stuck past its deadline with a partially-available canary ReplicaSet, must still auto-abort while UpdateHash reports the delay. It fails on master (no abort patch is produced) and passes with this fix.
  • New: TestReconcileTrafficRoutingUpdateHashDelayedErr verifies the delayed error is absorbed, SetWeight is not called (unstubbed on the mock, so a regression panics), the step is not completed (currentStepIndex does not advance, no RolloutStepCompleted event).
  • Updated: TestRollbackDestinationRuleBeforeSetWeight (the fix(trafficrouting): ensure DestinationRule is updated before SetWeight on rollback #4612 scenario test) still asserts SetWeight is 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).
  • Updated: TestUpdateHashNoReadyReplicaSets additionally pins the errors.Is contract on the sentinel.
  • Each new/updated assertion was verified to fail with the fix reverted (and with the step-completion guard disabled), so they genuinely pin the behavior.
  • go build ./..., full go test ./... (0 failures), and golangci-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 Progressing with no abort, no Degraded phase, and no status updates until manual intervention.


Checklist:

  • Either (a) I've created an enhancement proposal and discussed it with the community, (b) this is a bug fix, or (c) this is a chore.
  • The title of the PR is (a) conventional with a list of types and scopes found here, (b) states what changed, and (c) suffixes the related issues number. E.g. "fix(controller): Updates such and such. Fixes #1234".
  • I've signed my commits with DCO
  • My builds are green. Try syncing with master if they are not.
  • I have written unit and/or e2e tests for my change. PRs without these are unlikely to be merged.
  • I have run all tests locally (including the flaky ones) and they pass on my workstation — full unit suite passes locally; the Istio E2E suite (TestIstioSuite) was not run locally and relies on CI.
  • I have used LLM/AI/Agent tools for this PR but I am responsible for all code of this PR
  • I understand what the code does and WHY/HOW it works in several scenarios
  • I know if my code is just adding new functionality or changing old functionality for existing users — this changes error-handling behavior for the Istio subset-level delayed-switch path only; all other error paths are unchanged.
  • My organization is added to USERS.md.

🤖 Generated with Claude Code

…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>
@ChanghwanK ChanghwanK requested a review from a team as a code owner July 3, 2026 01:08
@sonarqubecloud

sonarqubecloud Bot commented Jul 3, 2026

Copy link
Copy Markdown

@codecov

codecov Bot commented Jul 3, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 85.03%. Comparing base (d1c2967) to head (d96c8fb).

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     
Flag Coverage Δ
e2e 52.84% <100.00%> (-0.13%) ⬇️
unit-tests 81.39% <100.00%> (+<0.01%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@github-actions

github-actions Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Published E2E Test Results

  4 files    4 suites   3h 46m 6s ⏱️
122 tests 112 ✅  7 💤 3 ❌
496 runs  461 ✅ 28 💤 7 ❌

For more details on these failures, see this check.

Results for commit d96c8fb.

@github-actions

github-actions Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Published Unit Test Results

2 518 tests   2 518 ✅  3m 23s ⏱️
  130 suites      0 💤
    1 files        0 ❌

Results for commit d96c8fb.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant