Skip to content

feat(executor): fall back to non-interruptible capacity on the last attempt - #7911

Open
chris-thomas-dev wants to merge 2 commits into
flyteorg:mainfrom
chris-thomas-dev:feat/executor-interruptible-failure-threshold
Open

feat(executor): fall back to non-interruptible capacity on the last attempt#7911
chris-thomas-dev wants to merge 2 commits into
flyteorg:mainfrom
chris-thomas-dev:feat/executor-interruptible-failure-threshold

Conversation

@chris-thomas-dev

Copy link
Copy Markdown

Why are the changes needed?

The Interruptible tasks docs describe a spot-to-on-demand fallback: the final attempt of an interruptible task runs on an on-demand instance, so a task whose nodes keep being reclaimed cannot burn its whole retry budget without ever landing on capacity that survives. The v2 executor schedules interruptible tasks on interruptible capacity for every attempt, so that fallback never happens. Requested in the review of #7885.

Note

Stacked on #7885 — the first commit is that PR (the fallback needs it: without it, env/task-level interruptible never reaches TaskExecutionMetadata). Only the last commit is new here. I'll rebase once #7885 merges.

What changes were proposed in this pull request?

A new executor config key, interruptibleFailureThreshold, ports flytepropeller's interruptible-failure-threshold semantics to the v2 executor:

  • Positive values are an absolute count of burnt attempts; negative values are complementary to the maximum, so the default -1 moves only the final attempt to non-interruptible capacity. A value above the maximum disables the fallback; zero is rejected at startup.
  • Either budget trips it: the task's own retries (against the template's retry count) or the system-failure budget (against maxSystemFailures), since a reclaimed node may be charged to either. This covers both of v1's IgnoreRetryCause branches without needing that flag.
  • An interruptible task always gets at least one attempt on interruptible capacity, even with no retries.
  • GetInterruptibleFailureThreshold() reports the configured value instead of a hardcoded 0, so connector-backed tasks receive the real threshold in TaskExecutionMetadata.
  • DefaultMaxSystemFailures moved from executor/pkg/controller to executor/pkg/config (next to the field it defaults) so the plugin package can use it without an import cycle.

Notes for reviewers:

  • With the -1 default, existing interruptible tasks change behaviour on upgrade: their final attempt moves to on-demand. This matches the documented behaviour and v1's default.
  • Clusters need non-interruptible capacity (an on-demand node group/pool matching the non-interruptible-node-selector-requirement) or the fallback pod is unschedulable.
  • config_flags.go was updated by hand in the generator's style; pflags isn't installed locally, so re-running go generate ./executor/pkg/config/ to confirm identical output would be a good check.

How was this patch tested?

Table tests over the threshold arithmetic (aboveInterruptibleFailureThreshold, interruptibleThresholdReached, maxSystemAttempts) and end-to-end NewTaskExecutionMetadata tests driving attempts/system-failures against a swapped-in config: last user attempt falls back, last system attempt falls back, single-attempt tasks keep their interruptible attempt, a threshold above both budgets disables the fallback, non-interruptible tasks are unaffected, and the threshold is reported to plugins. go test ./executor/pkg/plugin/ ./executor/pkg/config/ ./executor/ and go vet ./executor/... pass (the envtest suites were not run locally — no kubebuilder test binaries on this machine).

Labels

  • added: For any new feature added.

Check all the applicable boxes

  • I updated the documentation accordingly. (No docs change needed — this implements the documented spot-to-on-demand fallback.)
  • All new and existing tests passed.
  • All commits are signed-off.

🤖 Generated with Claude Code

chris-thomas-dev and others added 2 commits August 20, 2026 20:03
… is unset

The executor read interruptibility only from TaskAction.Spec.Interruptible,
which is populated from the run-level RunSpec. Interruptible set at the
TaskEnvironment, @env.task, or task.override() level serializes into the
embedded TaskTemplate metadata but never reached pod scheduling, so the
plugins.k8s interruptible node affinity and tolerations were not applied.

The task template's metadata now decides interruptibility whenever the
run-level flag is unset; an explicit run-level value still wins.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Chris Thomas <chris.john.thomas@gmail.com>
…ttempt

Interruptible tasks ran on interruptible capacity for every attempt, so a
task whose nodes kept being reclaimed could burn its whole retry budget
without ever landing on capacity that survives.

The new executor `interruptibleFailureThreshold` sets the attempt count at
which a task falls back to non-interruptible capacity. It defaults to -1,
which moves only the final attempt; a value above the maximum disables the
fallback, and zero is rejected at startup. Either the task's own retries or
the system-failure budget can trip it, and at least one attempt always runs
on interruptible capacity.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: Chris Thomas <chris.john.thomas@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant