Skip to content

AIP-97 (draft): failure context propagation and transparent infra retry#66405

Draft
1fanwang wants to merge 3 commits into
apache:mainfrom
1fanwang:1fanwang/aip97-failure-details
Draft

AIP-97 (draft): failure context propagation and transparent infra retry#66405
1fanwang wants to merge 3 commits into
apache:mainfrom
1fanwang:1fanwang/aip97-failure-details

Conversation

@1fanwang

@1fanwang 1fanwang commented May 5, 2026

Copy link
Copy Markdown
Contributor

Concrete draft backing AIP-97 (in DRAFT on the cwiki), opened so the listener-side API shape can be discussed against working code. Marked draft for that reason. This is the consolidated view; the Merge plan below decomposes it into three reviewable PRs.

Why

on_task_instance_failed sees only the worker-side error. When a task is killed from outside the worker — a pod eviction, an OOM kill, a force-deleted pod, a lost heartbeat — the worker never raises, so the listener gets nothing that says why. A pod evicted mid-run and a real ValueError look identical to alerting and lineage. The same blind spot burns retries: is_eligible_to_retry() is try_number <= max_tries with max_tries = task.retries, so an eviction spends a retry the user meant for their own code.

What this adds

Pillar 1 — failure context. on_task_instance_failed gains an optional failure_kind argument, a TaskFailureKind string enum (infra / application / timeout / manual). pluggy dispatches by parameter name, so a listener that does not declare it keeps working. The executor's reason token (Evicted, PodDeleted, ...) persists on the task instance as a new nullable infra_reason column. The Kubernetes executor's classify_pod_failure() maps a node-level disruption (Evicted, Preempting, DisruptionTarget, node shutdown) or a pod removed while running (PodDeleted — drain, preemption, spot reclaim, force-delete) to infra; a container that ended on its own (an app crash, an OOM against its own limit) to application. An unclassified death stays None.

Pillar 2 — transparent infra retry (opt-in). When failure_kind == infra, [core] infra_failure_refund_retries refunds the attempt (max_tries += 1) instead of drawing on the user's retries, bounded by max_infra_refunds. Off by default; application, timeout, and manual failures never refund. This extends the executor's existing pre-execution behavior — a pod that dies before the task starts is already requeued without consuming a retry — to the mid-run case.

Merge plan

Lands as three incremental PRs, each reviewable on its own:

  1. failure_kind on the listener + infra_reason column (the type/hookspec foundation).
  2. The Kubernetes executor bridge — classify_pod_failure() surfaces infra to the scheduler.
  3. The opt-in transparent infra retry (refund max_tries, gated and capped).

Testing

Full-stack on a real KubernetesExecutor cluster (kind): a running task pod force-deleted, classify_pod_failure reads PodDeleted, the scheduler refunds while the TI is still RUNNING:

PRE : running try=1 max_tries=1 infra_reason=-
kubectl delete pod <task-pod> --force --grace-period=0
POST: running try=2 max_tries=2 infra_reason=PodDeleted   # refunded, retrying
infra failure (PodDeleted) refunded attempt ...; max_tries now 2 (refund 1/3)
on_task_instance_failed ... failure_kind=infra infra_reason='PodDeleted'

The non-infra paths spend the retry, verified live on LocalExecutor + Postgres: a task mark-failed is manual, a DAG-run mark-failed sets the terminal state first so it is never misclassified, an execution_timeout is timeout, a dagrun_timeout skips the task. None refunds. 34 unit tests plus scheduler-wiring, ruff clean.

Open questions (for the AIP discussion)

  • The infra budget's shape: a server-side default plus a per-DAG or per-task override, versus the single max_infra_refunds cap here.
  • The refund mechanism: max_tries += 1 (one write, no migration, but shares the counter with application retries) versus a dedicated infra_retry_count column.

Was generative AI tooling used to co-author this PR?
  • Yes (please specify the tool below)

Generated-by: GitHub Copilot CLI following the guidelines

@1fanwang
1fanwang force-pushed the 1fanwang/aip97-failure-details branch from 20d6bd9 to 8c53810 Compare May 5, 2026 10:21
@1fanwang
1fanwang force-pushed the 1fanwang/aip97-failure-details branch from 57f338e to fcc393a Compare July 16, 2026 05:33
@1fanwang 1fanwang closed this Jul 16, 2026
@1fanwang 1fanwang reopened this Jul 16, 2026
@1fanwang
1fanwang force-pushed the 1fanwang/aip97-failure-details branch from fcc393a to bd8e7ec Compare July 24, 2026 19:28
@1fanwang 1fanwang changed the title AIP-97 (draft): add FailureDetails primitive for infrastructure-side failure context AIP-97 (draft): failure context propagation and transparent infra retry Jul 24, 2026
@1fanwang
1fanwang force-pushed the 1fanwang/aip97-failure-details branch from bd8e7ec to e87f0ba Compare July 24, 2026 19:38
1fanwang added 3 commits July 24, 2026 15:36
…column

on_task_instance_failed gains an optional failure_kind argument, a
TaskFailureKind str enum (infra / application / timeout / manual) in
airflow_shared.state. pluggy dispatches by parameter name, so a listener that
does not declare it keeps working. The worker populates application or timeout;
a manual mark-failed populates manual. The executor's reason token persists on
the task instance as a new nullable infra_reason column.

Foundation for AIP-97; the Kubernetes classifier (infra) and the opt-in retry
refund follow as separate PRs.

Signed-off-by: 1fanwang <1fannnw@gmail.com>
…he scheduler

The Kubernetes executor's classify_pod_failure() maps a node-level disruption
(Evicted, Preempting, DisruptionTarget, node shutdown) or a pod removed while
its task is running (PodDeleted — drain, preemption, spot reclaim, force-delete)
to failure_kind=infra; a container that ended on its own (an app crash, an OOM
against its own limit) to application. The classification rides a side channel on
BaseExecutor that the scheduler reads once and passes to the listener with the
executor's reason token. An unclassified death stays None, so the scheduler stays
conservative and never guesses infra.

Builds on the failure_kind foundation; the opt-in retry refund follows.

Signed-off-by: 1fanwang <1fannnw@gmail.com>
…-in)

When failure_kind is infra, [core] infra_failure_refund_retries bumps max_tries
so the attempt is given back instead of drawing on the user's retries, bounded by
[core] max_infra_refunds. Off by default; application, timeout, and manual
failures never refund. This extends the Kubernetes executor's existing behavior —
a pod that dies before the task starts is already requeued without consuming a
retry — to the mid-execution case, now that the failure is classified.

Completes the AIP-97 foundation stack.

Signed-off-by: 1fanwang <1fannnw@gmail.com>
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.

1 participant