Skip to content

feat: add preferred type to retry nodeAntiAffinity. Fixes #13969 - #16610

Open
somaz94 wants to merge 4 commits into
argoproj:mainfrom
somaz94:feat/retry-node-anti-affinity-preferred
Open

feat: add preferred type to retry nodeAntiAffinity. Fixes #13969#16610
somaz94 wants to merge 4 commits into
argoproj:mainfrom
somaz94:feat/retry-node-anti-affinity-preferred

Conversation

@somaz94

@somaz94 somaz94 commented Aug 5, 2026

Copy link
Copy Markdown

Fixes #13969

Motivation

retryStrategy.affinity.nodeAntiAffinity always excludes the hosts earlier attempts ran on with a requiredDuringSchedulingIgnoredDuringExecution term.
Once every eligible host has been tried, there is no host left that satisfies the term and the retry stays unschedulable, which is the case the issue describes.

This adds the soft variant so retries stay schedulable. It is the first of the two features requested in #13969; the cap on how many hosts are excluded is left for a separate PR, as suggested in the issue.

Modifications

RetryNodeAntiAffinity was an empty struct documented as "a placeholder for future expansion". It now carries a type field:

retryStrategy:
  limit: "10"
  affinity:
    nodeAntiAffinity:
      type: Preferred
  • Required keeps the current behaviour and is the default, so an existing nodeAntiAffinity: {} is unaffected.
  • Preferred emits a preferredDuringSchedulingIgnoredDuringExecution term instead, at weight 100.

AddHostnamesToAffinity is untouched. The preferred path is a sibling function, AddHostnamesToPreferredAffinity, which merges into an existing term for the same host selector (de-duplicating hosts) and otherwise appends, leaving any preference the user configured themselves in place.

I kept the weight fixed at 100 rather than exposing it, to keep this change small. Happy to make it configurable if you would prefer that.

Verification

gmake codegen -B
gmake lint
go test ./workflow/util/retry/... ./workflow/controller/

New unit tests:

  • TestAddHostnamesToPreferredAffinity covers the empty affinity, empty NodeAffinity, merge-into-existing-term (with de-duplication), append-alongside-an-unrelated-preference, and leave-the-required-term-alone cases.
  • TestRetryOnDifferentHostAntiAffinityType covers the controller dispatch: unset type, explicit Required, Preferred, and no nodeAntiAffinity at all.

Both Required cases assert that the preferred list stays empty and the Preferred case asserts that the required term stays nil, since a leftover required term would keep the retry unschedulable and defeat the point of the feature.

I checked the tests actually exercise the new branch by temporarily forcing the dispatch to the required path: only the Preferred subtest fails, the others still pass.

Note for anyone reproducing this locally on macOS: the generated files only refresh with gmake ... -B, and the default make is 3.81, which cannot parse the ifeq blocks in the Makefile.

Documentation

docs/walk-through/retrying-failed-or-errored-steps.md is where users meet this feature, and it currently states that only an empty nodeAntiAffinity is allowed. That sentence is replaced with a description of both types and a short example.

docs/fields.md and the swagger/JSON schema outputs are regenerated, so the new field also shows up in the Fields reference.

A feature note is included at .features/pending/retry-node-anti-affinity-preferred.md with a worked example.

AI

Claude Code was used as a coding assistant for the implementation, tests and this description.

Everything was reviewed and verified locally before opening (gmake codegen -B, gmake lint, gmake docs and the unit tests all pass, and the generated files are real codegen output). I take responsibility for the contents of this PR.

Summary by CodeRabbit

  • New Features
    • Added configurable retry node anti-affinity modes: Required avoids hosts used by earlier attempts, while Preferred deprioritizes them and allows reuse when necessary.
    • Added validation and schema support for the setting across workflow resources and templates.
  • Documentation
    • Updated retry guidance with host-selection behavior and a YAML configuration example.
  • Tests
    • Added coverage for default, required, preferred, and disabled anti-affinity behavior, including preferred host selection and affinity merging.

@somaz94
somaz94 marked this pull request as ready for review August 6, 2026 02:02
@somaz94
somaz94 requested review from a team, Joibel and terrytangyuan as code owners August 6, 2026 02:02
@coderabbitai

coderabbitai Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 3b6fea76-59e5-45a8-82f2-d728b224caa4

📥 Commits

Reviewing files that changed from the base of the PR and between e23fe15 and 2df54d8.

📒 Files selected for processing (1)
  • docs/walk-through/retrying-failed-or-errored-steps.md
🚧 Files skipped from review as they are similar to previous changes (1)
  • docs/walk-through/retrying-failed-or-errored-steps.md

📝 Walkthrough

Walkthrough

Retry node anti-affinity now supports Required and Preferred modes. The controller applies strict or preferred hostname scheduling. API schemas, generated manifests, tests, and documentation define the new type field.

Changes

Retry node anti-affinity

Layer / File(s) Summary
Retry anti-affinity contracts
pkg/apis/workflow/v1alpha1/*, pkg/plugins/executor/swagger.yml, api/jsonschema/schema.json
Adds the Type field and the Required and Preferred values to RetryNodeAntiAffinity.
Preferred retry scheduling
workflow/controller/retry_tweak.go, workflow/util/retry/retry.go, workflow/*/*_test.go
Uses required affinity by default and preferred affinity for Preferred. Preferred hostnames are merged into weighted Kubernetes node-affinity terms.
Workflow schema propagation
manifests/base/crds/*, manifests/quick-start-*.yaml
Adds the type field and validates "", Required, and Preferred across CRD and quick-start schemas.
Retry anti-affinity documentation
docs/*, .features/pending/*
Documents hostname-based retry exclusion, default required behavior, preferred behavior, and configuration examples.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant RetryOnDifferentHost
  participant RetryAffinityHelper
  participant KubernetesScheduler
  RetryOnDifferentHost->>RetryAffinityHelper: process failed hostnames
  alt Preferred mode
    RetryAffinityHelper->>KubernetesScheduler: add weighted preferred NotIn terms
  else Required or empty mode
    RetryAffinityHelper->>KubernetesScheduler: add required NotIn terms
  end
  KubernetesScheduler-->>RetryOnDifferentHost: return updated affinity
Loading

Suggested reviewers: joibel, terrytangyuan

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning The PR implements soft and hard anti-affinity but does not implement the linked issue's requested maximum excluded-host configuration. Implement the maximum excluded-host configuration or split and link that requirement to a separate issue before claiming full compliance.
Docstring Coverage ⚠️ Warning Docstring coverage is 62.50% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the new preferred retry node anti-affinity type and references the linked issue.
Description check ✅ Passed The description covers motivation, modifications, verification, documentation, and AI usage with sufficient implementation detail.
Out of Scope Changes check ✅ Passed The code, tests, generated schemas, documentation, and feature note directly support the preferred retry node anti-affinity feature.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/executor_swagger.md`:
- Around line 3392-3411: Update the RetryNodeAntiAffinityType documentation in
the Swagger section to state that an omitted or empty type defaults to Required,
and describe the behavioral difference between Required and Preferred modes. Add
this information to the generated field or type description while preserving the
existing allowed-values list.

In `@docs/walk-through/retrying-failed-or-errored-steps.md`:
- Around line 34-37: Qualify Preferred node anti-affinity behavior across all
three documented sites: in docs/walk-through/retrying-failed-or-errored-steps.md
lines 34-37, state that retries may use previously tried hosts when other
scheduling constraints permit; in docs/fields.md lines 4582-4587, replace “must
remain schedulable” with equivalent conditional wording; and in
.features/pending/retry-node-anti-affinity-preferred.md lines 6-9, replace
“every eligible host has failed” with “every eligible host has been tried” and
clarify that scheduling is only possible when other constraints permit it.

In `@workflow/util/retry/retry.go`:
- Around line 88-99: Update the existing-term merge in the preferred
node-affinity flow to set targetTerms[i].Weight to
PreferredHostAntiAffinityWeight before returning targetAffinity. Keep the
current Key/Operator matching and hostname deduplication behavior unchanged.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: ea3c9182-7a7e-40c8-9f24-877cde46e9e2

📥 Commits

Reviewing files that changed from the base of the PR and between e0b6328 and 2691d40.

⛔ Files ignored due to path filters (5)
  • api/openapi-spec/swagger.json is excluded by !**/api/openapi-spec/*.json
  • pkg/apis/workflow/v1alpha1/generated.pb.go is excluded by !**/*.pb.go, !**/*.pb.go
  • pkg/apis/workflow/v1alpha1/openapi_generated.go is excluded by !**/*_generated.go
  • sdks/java/client/docs/IoArgoprojWorkflowV1alpha1RetryAffinity.md is excluded by !**/sdks/java/client/**
  • sdks/java/client/docs/IoArgoprojWorkflowV1alpha1RetryNodeAntiAffinity.md is excluded by !**/sdks/java/client/**
📒 Files selected for processing (22)
  • .features/pending/retry-node-anti-affinity-preferred.md
  • api/jsonschema/schema.json
  • docs/executor_swagger.md
  • docs/fields.md
  • docs/walk-through/retrying-failed-or-errored-steps.md
  • manifests/base/crds/full/argoproj.io_clusterworkflowtemplates.yaml
  • manifests/base/crds/full/argoproj.io_cronworkflows.yaml
  • manifests/base/crds/full/argoproj.io_workflows.yaml
  • manifests/base/crds/full/argoproj.io_workflowtasksets.yaml
  • manifests/base/crds/full/argoproj.io_workflowtemplates.yaml
  • manifests/base/crds/minimal/argoproj.io_workflowtasksets.yaml
  • manifests/quick-start-minimal.yaml
  • manifests/quick-start-mysql.yaml
  • manifests/quick-start-postgres.yaml
  • manifests/quick-start-telemetry.yaml
  • pkg/apis/workflow/v1alpha1/generated.proto
  • pkg/apis/workflow/v1alpha1/workflow_types.go
  • pkg/plugins/executor/swagger.yml
  • workflow/controller/retry_tweak.go
  • workflow/controller/retry_tweak_test.go
  • workflow/util/retry/retry.go
  • workflow/util/retry/retry_test.go

Comment thread docs/executor_swagger.md Outdated
Comment thread docs/walk-through/retrying-failed-or-errored-steps.md Outdated
Comment thread workflow/util/retry/retry.go
…ng term

Signed-off-by: somaz <genius5711@gmail.com>

@tico24 tico24 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Docs only from me: docs/walk-through/retrying-failed-or-errored-steps.md and the feature note. The rest is for @argoproj/argo-workflows-approvers.

I checked the page against retry.go and retry_tweak.go and it matches what the code does, including nodeAntiAffinity: {} still behaving as it did before. Few small things inline.

* `backoff` is an exponential back-off
* `nodeAntiAffinity` prevents running steps on the same host. Current implementation allows only empty `nodeAntiAffinity` (i.e. `nodeAntiAffinity: {}`) and by default it uses label `kubernetes.io/hostname` as the selector.
* `nodeAntiAffinity` prevents running steps on the same host. By default it uses label `kubernetes.io/hostname` as the selector.
Its `type` controls how strictly earlier hosts are avoided:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The retryPolicy bullet above tags OnTransientError with the version it landed in. Worth doing the same for type? On an older version the field just gets pruned and there's nothing here to explain why.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point — done in 2df54d81c, tagged the same way as the retryPolicy bullet:

Its type controls how strictly earlier hosts are avoided (available after v4.2.0).

I picked v4.2.0 by inference rather than knowledge: main is at v4.1.0-rc2 with no release-4.1 branch cut yet, so this looks like it misses the 4.1 train. If you're targeting something else, say the word — it's a one-word change and I'd rather have it right than have the page lie the way you describe.

* `nodeAntiAffinity` prevents running steps on the same host. By default it uses label `kubernetes.io/hostname` as the selector.
Its `type` controls how strictly earlier hosts are avoided:
* `Required` (the default, and what an empty `nodeAntiAffinity: {}` gives you) excludes those hosts outright, so a retry stays unschedulable once every eligible host has been tried.
* `Preferred` only de-prioritises them, so a retry can be scheduled onto a previously tried host once every eligible host has been tried. This is a preference rather than a guarantee — other scheduling constraints can still leave the retry pending. Use this when the retry limit is higher than the number of eligible nodes.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Three sentences on this line, and two on line 34. Mind splitting?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Split in 2df54d81c — one sentence per line now, on both the line you flagged and line 34:

* `nodeAntiAffinity` prevents running steps on the same host.
  By default it uses label `kubernetes.io/hostname` as the selector.
  Its `type` controls how strictly earlier hosts are avoided (available after v4.2.0).
    * `Required` is the default, and is what an empty `nodeAntiAffinity: {}` gives you.
      It excludes previously tried hosts outright, so a retry stays unschedulable once every eligible host has been tried.
    * `Preferred` only de-prioritises them.
      A retry can then still be scheduled onto a previously tried host once every eligible host has been tried.
      This is a preference rather than a guarantee, so other scheduling constraints can still leave the retry pending.
      Use it when the retry limit is higher than the number of eligible nodes.

I also swapped the em-dash in the "preference rather than a guarantee" sentence for a plain clause while I was in there.

* `Preferred` only de-prioritises them, so a retry can be scheduled onto a previously tried host once every eligible host has been tried. This is a preference rather than a guarantee — other scheduling constraints can still leave the retry pending. Use this when the retry limit is higher than the number of eligible nodes.

```yaml
retryStrategy:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indented four spaces, where the block at the top of the page isn't, so the whitespace shows up in the output. Did you mean this to sit inside the bullet above?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, that was unintentional — I lifted the snippet out of the full manifest above and kept its original indentation, so it inherited four spaces it didn't need. Dedented to column 0 in 2df54d81c to match the block at the top of the page:

retryStrategy:
  limit: 10
  affinity:
    nodeAntiAffinity:
      type: Preferred

make docs is clean on all three changes — typos, cspell (0 issues across 135 files) and properdocs build --strict all pass.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support soft and hard affinity in nodeAntiAffinity

2 participants