Skip to content

Enforce execution_timeout from the task supervisor side - #73806

Merged
ashb merged 14 commits into
apache:mainfrom
Andrushika:evaluate-issue-53337-takeover
Oct 7, 2026
Merged

ashb merged 14 commits into
apache:mainfrom
Andrushika:evaluate-issue-53337-takeover

Conversation

@Andrushika

@Andrushika Andrushika commented Sep 27, 2026 •

Copy link
Copy Markdown
Contributor

Exact same work in #72493. Created this because that one was closed by the PR number limit policy.

Why

execution_timeout is only enforced by a SIGALRM handler inside the task runner process. When that process is stuck in native code or hit by SIGSEGV, the handler never runs and the task stays running forever (#57174, #57712, #45930).

To tackle the problem at its root, the better solution is to handle the runner's timeout from the supervisor side.

What

The task process sends a new SetExecutionTimeout message to the supervisor right before it arms its own alarm, so both clocks start at the same point. If the task has not reported a terminal state within [core] killed_task_cleanup_time seconds (default 60) after the timeout, the supervisor reuses kill(SIGTERM, force=True) to send SIGTERM to the process group, and SIGKILL after the same period again. kill() blocks the monitor loop while it waits, so no heartbeat is sent during that time. By default that is at most 120 seconds, under the 300 second task_instance_heartbeat_timeout.

This keeps the task runner's SIGALRM handler instead of "moving it", which is a bit different from what #53337 asks for. The reason is that on_kill, the retry policy and the failure callbacks can only run inside the task process, and dropping the handler would skip them on every timeout.

closes: #53337, closes #57174, closes #57712, closes #45930


Was generative AI tooling used to co-author this PR?
  • Yes — Claude Code (Fable 5.1)

Generated-by: Claude Code (Fable 5.1) following the guidelines

The timeout is only enforced by a SIGALRM handler inside the task
process. When that process is stuck in native code, deadlocked on a
lock inherited across fork, or corrupted by SIGSEGV, the handler never
runs and the task stays running forever. The task process now tells the
supervisor its timeout before starting execute(), and the supervisor
sends SIGTERM and then SIGKILL if the task has not reported a terminal
state within a grace period. The in-process handler stays as the first
responder because on_kill, the retry policy and callbacks can only run
there.
…7-takeover

Resolve supervisor.py: port the SetExecutionTimeout handling onto main's
request-handler dispatch, replacing the isinstance branch with
_handle_set_execution_timeout plus a register_request_method entry.
…7-takeover

# Conflicts:
#	task-sdk/src/airflow/sdk/execution_time/schema/versions/__init__.py
#	task-sdk/src/airflow/sdk/execution_time/schema/versions/v2026_10_30.py
#	task-sdk/src/airflow/sdk/execution_time/supervisor.py

@SameerMesiah97 SameerMesiah97 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.

Just 2 questions.

Comment thread task-sdk/tests/task_sdk/execution_time/test_supervisor.py
Comment thread task-sdk/src/airflow/sdk/execution_time/supervisor.py Outdated
@Andrushika
Andrushika marked this pull request as draft September 28, 2026 19:23
@Andrushika
Andrushika marked this pull request as ready for review September 28, 2026 19:24

@SameerMesiah97 SameerMesiah97 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.

It looks good to me but since this is touching a fairly sensitive part of airflow core, I think this needs opinions from the tagged maintainers for review before merge.

Comment thread task-sdk/src/airflow/sdk/execution_time/supervisor.py
When a heartbeat gets 404, 409 or 410 back, the supervisor kills the
task with SIGTERM and escalates to SIGKILL after kill()'s default of
5 seconds. killed_task_cleanup_time documents that wait, and Airflow 2
used it for this exact case, a task whose state was changed from
outside. Pass it as the escalation delay, and rename the constant that
holds it now that the execution_timeout backstop is not its only user.
…7-takeover

Resolve schema/versions: take main's side and drop
AddExecutionTimeoutSupervisorMessage. Main now documents in
v2026_10_30.py that a brand-new message body needs no VersionChange
entry, and the empty entry was exactly that case.
The supervisor schema gained SetExecutionTimeout, so the generated Go
coordinator-protocol models were out of date and check-go-sdk-generated-drift
failed in CI.
@ashb

ashb commented Oct 6, 2026

Copy link
Copy Markdown
Member

I'm not sure this will fix #57174 though -- as dag parsing is needed before the child can know the desired timeout to report to the scheduler.

  • I wonder if we need two timeouts like this:
  • child gets sent the TaskWorkload
  • Sends a SetTimeout to the supervisor of the dabgag import timeout (existing config)
  • Then the child parses the dag
  • Once done, either we send a 0 timeout to clear it in the parent;
  • Or if the gap between this and the next is small enough (bear in mind things like listeners or other things could be involved here) just send a new SetTimeout with the desired timeout.

Given not all tasks have timesouts, I think the SetTimeout(0) approach is "it's always correct, and easier to reason about even if fractionaly slower"

Comment thread airflow-core/src/airflow/config_templates/config.yml
Comment thread task-sdk/tests/task_sdk/execution_time/test_supervisor.py Outdated
Comment thread task-sdk/tests/task_sdk/execution_time/test_supervisor.py Outdated
Comment thread task-sdk/tests/task_sdk/execution_time/test_supervisor.py Outdated
Comment thread task-sdk/tests/task_sdk/execution_time/test_supervisor.py Outdated
Comment thread task-sdk/src/airflow/sdk/execution_time/supervisor.py Outdated
Comment thread task-sdk/src/airflow/sdk/execution_time/supervisor.py Outdated
Comment thread task-sdk/src/airflow/sdk/execution_time/supervisor.py
The supervisor kept its own SIGTERM-then-SIGKILL state machine for a task
that overran execution_timeout. kill(force=True) already escalates along
the same path and handles a process that is already gone, so call it with
killed_task_cleanup_time as the escalation delay instead. This drops the
next-signal field and folds the timeout deadline into the monitor loop's
existing select timeout.

The tests now advance a fake monotonic clock and assert the kill call and
the log record instead of private attributes.
@Andrushika
Andrushika force-pushed the evaluate-issue-53337-takeover branch from 83ebc09 to 9dbe650 Compare October 7, 2026 07:56
@Andrushika

Andrushika commented Oct 7, 2026 •

Copy link
Copy Markdown
Contributor Author

I'm not sure this will fix #57174 though -- as dag parsing is needed before the child can know the desired timeout to report to the scheduler.

I think this PR will fix that issue, since the log there shows the SIGTERM comes after parsing.
(I changed those #57174 and #57712 into related since I have no way to validate)

  • I wonder if we need two timeouts like this:
  • child gets sent the TaskWorkload
  • Sends a SetTimeout to the supervisor of the dabgag import timeout (existing config)
  • Then the child parses the dag
  • Once done, either we send a 0 timeout to clear it in the parent;
  • Or if the gap between this and the next is small enough (bear in mind things like listeners or other things could be involved here) just send a new SetTimeout with the desired timeout.

Given not all tasks have timesouts, I think the SetTimeout(0) approach is "it's always correct, and easier to reason about even if fractionaly slower"

But yeah I agree we need this. Could we do it in a follow-up? (I would like to make this as small as possible)

@Andrushika
Andrushika requested a review from ashb October 7, 2026 08:15
Comment thread task-sdk/src/airflow/sdk/execution_time/supervisor.py

@ashb ashb 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.

Much, much easier to understand now.

@Andrushika

Copy link
Copy Markdown
Contributor Author

Thanks!
Waiting for CI green :)

@ashb

ashb commented Oct 7, 2026

Copy link
Copy Markdown
Member

This should also close and fix #57174, #57712, #45930, right?

@Andrushika

Copy link
Copy Markdown
Contributor Author

Yes. I moved it to related bc I couldn't really reproduce it.
But maintainers in all three threads already pointed to #53337 (the previous closed PR, similar solution) as the fix, so I'll move them back to closes:.

@ashb
ashb merged commit 634d6bd into apache:main Oct 7, 2026
109 checks passed
@Andrushika
Andrushika deleted the evaluate-issue-53337-takeover branch October 7, 2026 11:48
dheerajturaga pushed a commit that referenced this pull request Oct 7, 2026
#73806 added the SetExecutionTimeout message to the supervisor wire
schema and regenerated the Go SDK models from it, but the TS SDK's
generated `src/generated/supervisor.ts` was left behind. Its
supervisor-schema static check has failed on main ever since, because
the committed types no longer match what the schema produces.
pierrejeambrun added a commit to astronomer/airflow that referenced this pull request Oct 7, 2026
The TS SDK read the supervisor wire schema straight from the Task SDK
source and had no hook watching that source, so a schema change there —
a new comms message, say — regenerated the Go SDK (whose sync hook does
watch the source) but left `src/generated/supervisor.ts` stale, with
nothing to catch it until some unrelated ts-sdk change happened to run
the local check. That is how apache#73806 reddened main.

Vendor the supervisor schema into `ts-sdk/schema/` alongside the Dag
schema, and sync both from their Python sources in one
`sync-ts-sdk-schemas` hook — a `VENDORED_SCHEMAS` list the way
`sync-go-sdk-schemas` does it, replacing the single-schema
`sync-ts-sdk-dag-schema`. A schema change now refreshes the vendored
copy and fails loudly in the same commit instead of shipping a stale
generated file.
pierrejeambrun added a commit to astronomer/airflow that referenced this pull request Oct 7, 2026
The TS SDK read the supervisor wire schema straight from the Task SDK
source and had no hook watching that source, so a schema change there —
a new comms message, say — regenerated the Go SDK (whose sync hook does
watch the source) but left `src/generated/supervisor.ts` stale, with
nothing to catch it until some unrelated ts-sdk change happened to run
the local check. That is how apache#73806 reddened main.

Vendor the supervisor schema into `ts-sdk/schema/` alongside the Dag
schema, and sync both from their Python sources in one
`sync-ts-sdk-schemas` hook — a `VENDORED_SCHEMAS` list the way
`sync-go-sdk-schemas` does it, replacing the single-schema
`sync-ts-sdk-dag-schema`. A schema change now refreshes the vendored
copy and fails loudly in the same commit instead of shipping a stale
generated file.
pierrejeambrun added a commit that referenced this pull request Oct 8, 2026
* Sync the TS SDK vendored schemas from their Python sources

The TS SDK read the supervisor wire schema straight from the Task SDK
source and had no hook watching that source, so a schema change there —
a new comms message, say — regenerated the Go SDK (whose sync hook does
watch the source) but left `src/generated/supervisor.ts` stale, with
nothing to catch it until some unrelated ts-sdk change happened to run
the local check. That is how #73806 reddened main.

Vendor the supervisor schema into `ts-sdk/schema/` alongside the Dag
schema, and sync both from their Python sources in one
`sync-ts-sdk-schemas` hook — a `VENDORED_SCHEMAS` list the way
`sync-go-sdk-schemas` does it, replacing the single-schema
`sync-ts-sdk-dag-schema`. A schema change now refreshes the vendored
copy and fails loudly in the same commit instead of shipping a stale
generated file.

* Fix comment for schema sync hook in generate-supervisor.mjs

Update comments to reflect the correct hook name for syncing schemas.

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* Add tests for the TS schema sync and fix references left by the rename

Addresses the review on #74415:

- Move sync_ts_sdk_schemas.py to scripts/ci/prek/ beside sync_go_sdk_schemas.py
  and add test_sync_ts_sdk_schemas.py mirroring the Go sync's tests, so the
  refresh and report logic is covered (in-sync, stale, missing copy/source,
  and the per-schema and combined reports).
- List the script itself in the sync-ts-sdk-schemas files filter, so a change
  to its paths or report logic re-runs the hook, as sync-go-sdk-schemas does.
- Point generate-dag-schema.mjs at the renamed sync-ts-sdk-schemas hook.
- Update the selective-check comment and 04_selective_checks.md, which still
  described the dropped "defer ts-sdk regeneration to a follow-up PR" policy.

---------

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
jason810496 added a commit that referenced this pull request Oct 23, 2026
…uage SDK (#74869)" (#75376) (#75469)

This reverts commit 8326025.

3.4.0 closes today and #75376 needs #75387 alongside it, which needs more
bandwidth than is left. On v3-4-test alone, the Python task runner arms
SIGALRM from task.execution_timeout while the supervisor arms its deadline
from TIRunContext.execution_timeout. When the server value is smaller, the
supervisor sends SIGTERM before the runner raises AirflowTaskTimeout, so
on_kill, the retry policy, callbacks and listeners are skipped. Reverting
avoids shipping that regression in 3.4.0.

#74869 stays on main. SetExecutionTimeout (#73806) is untouched, so Python
tasks keep the supervisor backstop they had before #75376.

Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

4 participants