Requeue KubernetesExecutor tasks whose pod failed before execution started#69058
Conversation
|
Working on a test confirmation in Astro Hosted now |
|
We should add a release note for this. Especially since this changes the default behavior—all pods are now retried once by default instead of directly fail. We should probably also call out somewhere that if you set |
arkadiuszbach
left a comment
There was a problem hiding this comment.
Adding my findings, as I was working on task failures in KubernetesExecutor due to SIGTERM handling and this PR might address the failures before the Supervisor gets to register its own SIGTERM handler, see #69034
|
Pushed
Full Drafted-by: Claude Code (Opus 4.8); reviewed by @seanmuth before posting |
7bfd8f3 to
6bde46e
Compare
|
Rebased onto current What #68674 changed under this PR: it fixed the never-drained adoption set by converting How I reconciled it:
Why it composes correctly (not just a marker resolution):
Verified locally: Rebase performed by Claude (Opus 4.8), reviewed by me. |
|
Pushed 1. K8s system tests — Fix — and a genuine improvement: reorder the guard so the cheap in-memory # Only pods this executor launched and is still tracking can be requeued; checking the
# in-memory attempt first avoids a metadata-db lookup for adopted or already-finalized pods.
attempt = self.pod_launch_attempts.get(key)
if (
attempt is not None
and state == TaskInstanceState.FAILED
and self.pod_launch_failure_max_retries != 0
and self._is_pre_execution_failure(...)
):
...2. Docs spellcheck — Verified locally: Changes by Claude (Opus 4.8), reviewed by me. |
1a6b59c to
2af7e8a
Compare
cb32253 to
326a3a9
Compare
|
Friendly nudge that this one looks ready to land whenever a committer has a moment:
Happy to rebase or make any further tweaks if needed. Thanks! Drafted-by: Claude Code (Opus 4.8), reviewed by @seanmuth before posting |
…arted When a worker pod is destroyed before the task process starts (node drain, autoscaler scale-down, node boot race, transient image pull failure), the task instance is still queued and no task code has run. Reporting this to the scheduler as a normal failure consumes a user-configured retry and raises a misleading failure alert for work that never executed. The executor already has the signal to tell this apart from an execution failure, so it now transparently requeues the pod without consuming a task retry, bounded by pod_launch_failure_retries and excluding container reasons in pod_launch_failure_excluded_container_reasons (default Error).
The tests replaced executor.task_queue with a MagicMock, so the executor.end() teardown looped forever on get_nowait() instead of raising Empty, hitting the 60s CI timeout. Assert the requeue via observable executor state instead of mocking the queue, and pass a valid executor_config to the stash test so execute_async does not bail before recording the job.
Kubernetes can emit several Failed events for a single worker pod (for example two MODIFIED then DELETED on SIGTERM); each one previously triggered another requeue, creating duplicate pods and over-counting attempts. Track the job spec, requeue count, and the pod a requeue was last issued for together in one per-key object so duplicate events for the same pod are ignored and the requeue count no longer leaks when a pod is adopted. Skip the task-instance state lookup when requeues are disabled, and document the default-behavior change and the accumulation risk of setting the retry count to -1.
…oss scheduler restarts Clarify that the in-memory requeue state is intentionally not persisted and that the dead-scheduler adoption path is a safe no-op for it, so a future reader doesn't mistake the lack of recovery for a bug.
…equeues' The pre-execution-failure check ran a metadata-db query for every failed pod, breaking the test_pod_failure_logging_* K8s system tests (no task_instance table) and needlessly querying for adopted/finalized pods. Gate the lookup on an existing pod_launch_attempts entry so only pods this executor launched and still tracks are considered. Also add the plural 'requeues' to the docs spelling wordlist (singular forms were already present).
self.task_queue is typed Queue | None; guard the requeue put() with a TYPE_CHECKING assert (matching the other call sites in this file) so mypy narrows it without tripping ruff's S101 on a runtime assert.
326a3a9 to
57e2ca4
Compare
…arted (apache#69058) * Requeue KubernetesExecutor tasks whose pod failed before execution started When a worker pod is destroyed before the task process starts (node drain, autoscaler scale-down, node boot race, transient image pull failure), the task instance is still queued and no task code has run. Reporting this to the scheduler as a normal failure consumes a user-configured retry and raises a misleading failure alert for work that never executed. The executor already has the signal to tell this apart from an execution failure, so it now transparently requeues the pod without consuming a task retry, bounded by pod_launch_failure_retries and excluding container reasons in pod_launch_failure_excluded_container_reasons (default Error). * Fix KubernetesExecutor pre-execution requeue tests hanging in CI The tests replaced executor.task_queue with a MagicMock, so the executor.end() teardown looped forever on get_nowait() instead of raising Empty, hitting the 60s CI timeout. Assert the requeue via observable executor state instead of mocking the queue, and pass a valid executor_config to the stash test so execute_async does not bail before recording the job. * Address review feedback on KubernetesExecutor pre-execution requeue Kubernetes can emit several Failed events for a single worker pod (for example two MODIFIED then DELETED on SIGTERM); each one previously triggered another requeue, creating duplicate pods and over-counting attempts. Track the job spec, requeue count, and the pod a requeue was last issued for together in one per-key object so duplicate events for the same pod are ignored and the requeue count no longer leaks when a pod is adopted. Skip the task-instance state lookup when requeues are disabled, and document the default-behavior change and the accumulation risk of setting the retry count to -1. * Document that KubernetesExecutor pod_launch_attempts is ephemeral across scheduler restarts Clarify that the in-memory requeue state is intentionally not persisted and that the dead-scheduler adoption path is a safe no-op for it, so a future reader doesn't mistake the lack of recovery for a bug. * Fix CI: skip pre-execution DB lookup for untracked pods; whitelist 'requeues' The pre-execution-failure check ran a metadata-db query for every failed pod, breaking the test_pod_failure_logging_* K8s system tests (no task_instance table) and needlessly querying for adopted/finalized pods. Gate the lookup on an existing pod_launch_attempts entry so only pods this executor launched and still tracks are considered. Also add the plural 'requeues' to the docs spelling wordlist (singular forms were already present). * Fix mypy: narrow task_queue before requeuing pod launch job self.task_queue is typed Queue | None; guard the requeue put() with a TYPE_CHECKING assert (matching the other call sites in this file) so mypy narrows it without tripping ruff's S101 on a runtime assert. * Set version_added to 10.20.0 for new KubernetesExecutor pod-launch configs
When a worker pod is destroyed before the task process starts — a node drain, autoscaler scale-down, node boot race, or transient image pull failure — the task instance is still in
queuedstate and no task code has run. Today the KubernetesExecutor reports this to the scheduler as a normalFAILED, which consumes a user-configured task retry and raises a misleading failure alert for work that never executed.This adds a transparent, executor-level requeue for that case. In
_change_state, a pod that reportsFAILEDwhile its task instance is stillQUEUEDis requeued onto the existingtask_queue(the same mechanismtask_publish_max_retriesalready uses for pod creation failures) without writing to the event buffer, so the scheduler never observes the failure and no task-level retry is consumed.Behavior is bounded and configurable:
pod_launch_failure_retries(default1,-1unlimited,0disables) — how many times a task is transparently requeued before failing normally.pod_launch_failure_excluded_container_reasons(defaultError) — container reasons that opt out of the requeue path and consume a normal retry instead. The default excludesError, which covers a container that started executing but whose worker process exited before writingrunningto the DB (most likely an Airflow-specific startup error rather than a transient infrastructure event).The
ti_state == QUEUEDcheck is the authoritative signal: a task that was actually executing would already have transitioned torunning, so OOM-kills and other mid-execution failures are unaffected. Deferrable-operator resume pods are covered for free — when the triggerer fires, the TI returns toqueued, so a resume pod killed beforeexecute_completestarts is requeued rather than discarding already-completed external work.closes: #69052
Was generative AI tooling used to co-author this PR?
Generated-by: Claude Code (Opus 4.8) following the guidelines
Important
🛠️ Maintainer triage note for @seanmuth · by
@potiuk· 2026-07-02 17:46 UTCSome review feedback from
@uranusjris waiting on you:@uranusjrneed a reply or a fix.The ball is in your court — you've been assigned to this PR. Reply or push a fix in each thread, then mark them resolved.
Automated triage — may be imperfect; a maintainer takes the next look.