Skip to content

Correlate executor task events by attempt UUID - #73916

Merged
ashb merged 5 commits into
mainfrom
executor-ti-identity
Oct 1, 2026
Merged

ashb merged 5 commits into
mainfrom
executor-ti-identity

Conversation

@ashb

@ashb ashb commented Sep 29, 2026

Copy link
Copy Markdown
Member

The executor event sub-system in Airflow is a little bit confusing. This hopefully tidies things up a little bit. Previously events were keyed/stored by their "coordinates", the 4-tuple (dag_id, run_id, task_id, map_index) (and in some places a 5-tuple also including try_number). That is unwieldy, and since 3.0.0 a better option is available: a single UUID that uniquely identifies the TI try.

This changes the event system to use that, with the net effect being that a deleyed event from a previous try is no longer able to to update the state for a newly scheduled replacement try. I have no evidence this has ever happened, but it is conceptually cleaner, and lets us make some important assumptions going forward with the introduction of Task Loops.

Since apache-airflow-core can be upgrade by itself and executors from not-yet-updated providers might be in use, we preserve their coordinate-keyed interfaces by capturing UUIDs before submission or adoption and translate legacy events from those snapshots without looking up the current row by coordinates.

LocalExecutor opts into UUID keys end to end in this change. Provider-based executors will be their own PRs.

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

  • Read the Pull Request Guidelines for more information. Note: commit author/co-author name and email in commits become permanently public when merged.
  • For fundamental code changes, an Airflow Improvement Proposal (AIP) is needed.
  • When adding dependency, check compliance with the ASF 3rd Party License Policy.
  • For significant user-facing changes create newsfragment: {pr_number}.significant.rst, in airflow-core/newsfragments. You can add this file in a follow-up commit after the PR is created so you know the PR number.

Stack created with GitHub Stacks CLI • Give Feedback 💬

A delayed executor event must identify the attempt that produced it after
clearing or retrying replaces the live task instance. Reconcile scheduler
events by UUID and retain the retiring identity through restart completion
and heartbeat cleanup.

Core upgrades can retain older executor providers, so preserve their
coordinate-keyed interfaces. Capture UUIDs before submission or adoption
and translate legacy events from those snapshots without looking up the
current row by coordinates. Retain accepted adoptions across empty event
drains, including providers that track adopted work only in private maps.

LocalExecutor opts into UUID keys end to end. Legacy providers cannot
distinguish attempts that reuse the entire coordinate key; features that
require that isolation must require the UUID capability until providers
migrate their submission, adoption, result decoding and cleanup paths.
@ashb
ashb force-pushed the executor-ti-identity branch from 9f29ed6 to 7ea8db2 Compare September 30, 2026 21:05
@ashb
ashb merged commit bb463bf into main Oct 1, 2026
154 of 155 checks passed
@ashb
ashb deleted the executor-ti-identity branch October 1, 2026 21:00
@ashb

ashb commented Oct 1, 2026

Copy link
Copy Markdown
Member Author

Argh damn GH UI. It hadn't loaded the test results, so I thought it was green when I hit merge :(

@ashb

ashb commented Oct 1, 2026

Copy link
Copy Markdown
Member Author

Huh, thought it seems fine in main. I'll keep an eye on it.

ashb added a commit that referenced this pull request Oct 3, 2026
…g` list

Since #73916 landed, BaseExecutor now keeps a UUID-to-coordinates map for each
attempt and drops an entry once the key is no longer queued, running or in the
event buffer. The issue was that LocalExecutor never added dispatched work to
`running`, so the entry was dropped while the task was still executing. The
scheduler then received the final event with no coordinates ("Received executor
event with state success for task instance <uuid> (coordinates=None)").

This PR fixes that, and addresses a few gotchas that could, in edge cases
around callbacks or post-task finalization, lead to a forever-dead/locked
worker slot.

Holding the key in `running` until the worker finishes needs LocalExecutor
to know when that is, and workers share one activity queue so the parent
cannot tell which worker took which workload. Results now carry the
worker's pid and START tells the parent which pid owns the key. That lets
it:

- release the slot only on a terminal result from the owning worker,
ignoring results from unknown pids or pids that don't own the key
- fail the workload when its worker dies, instead of leaving it in
`running` until the scheduler's heartbeat timeout

A resumed task reuses its key, so a per-key dispatch count stops the old
run's terminal result from releasing the new run's slot.
ashb added a commit that referenced this pull request Oct 3, 2026
…g` list

Since #73916 landed, BaseExecutor now keeps a UUID-to-coordinates map for each
attempt and drops an entry once the key is no longer queued, running or in the
event buffer. The issue was that LocalExecutor never added dispatched work to
`running`, so the entry was dropped while the task was still executing. The
scheduler then received the final event with no coordinates ("Received executor
event with state success for task instance <uuid> (coordinates=None)").

This PR fixes that, and addresses a few gotchas that could, in edge cases
around callbacks or post-task finalization, lead to a forever-dead/locked
worker slot.

Holding the key in `running` until the worker finishes needs LocalExecutor
to know when that is, and workers share one activity queue so the parent
cannot tell which worker took which workload. Results now carry the
worker's pid and START tells the parent which pid owns the key. That lets
it:

- release the slot only on a terminal result from the owning worker,
ignoring results from unknown pids or pids that don't own the key
- fail the workload when its worker dies, instead of leaving it in
`running` until the scheduler's heartbeat timeout

A resumed task reuses its key, so a per-key dispatch count stops the old
run's terminal result from releasing the new run's slot.
aws-airflow-bot pushed a commit to aws-mwaa/upstream-to-airflow that referenced this pull request Oct 3, 2026
…g` list

Since apache#73916 landed, BaseExecutor now keeps a UUID-to-coordinates map for each
attempt and drops an entry once the key is no longer queued, running or in the
event buffer. The issue was that LocalExecutor never added dispatched work to
`running`, so the entry was dropped while the task was still executing. The
scheduler then received the final event with no coordinates ("Received executor
event with state success for task instance <uuid> (coordinates=None)").

This PR fixes that, and addresses a few gotchas that could, in edge cases
around callbacks or post-task finalization, lead to a forever-dead/locked
worker slot.

Holding the key in `running` until the worker finishes needs LocalExecutor
to know when that is, and workers share one activity queue so the parent
cannot tell which worker took which workload. Results now carry the
worker's pid and START tells the parent which pid owns the key. That lets
it:

- release the slot only on a terminal result from the owning worker,
ignoring results from unknown pids or pids that don't own the key
- fail the workload when its worker dies, instead of leaving it in
`running` until the scheduler's heartbeat timeout

A resumed task reuses its key, so a per-key dispatch count stops the old
run's terminal result from releasing the new run's slot.
eladkal pushed a commit that referenced this pull request Oct 5, 2026
…g` list (#74142)

* Improve LocalExecutor bookkeeping: correctly add tasks to the `running` list

Since #73916 landed, BaseExecutor now keeps a UUID-to-coordinates map for each
attempt and drops an entry once the key is no longer queued, running or in the
event buffer. The issue was that LocalExecutor never added dispatched work to
`running`, so the entry was dropped while the task was still executing. The
scheduler then received the final event with no coordinates ("Received executor
event with state success for task instance <uuid> (coordinates=None)").

This PR fixes that, and addresses a few gotchas that could, in edge cases
around callbacks or post-task finalization, lead to a forever-dead/locked
worker slot.

Holding the key in `running` until the worker finishes needs LocalExecutor
to know when that is, and workers share one activity queue so the parent
cannot tell which worker took which workload. Results now carry the
worker's pid and START tells the parent which pid owns the key. That lets
it:

- release the slot only on a terminal result from the owning worker,
ignoring results from unknown pids or pids that don't own the key
- fail the workload when its worker dies, instead of leaving it in
`running` until the scheduler's heartbeat timeout

A resumed task reuses its key, so a per-key dispatch count stops the old
run's terminal result from releasing the new run's slot.

* fixup! Improve LocalExecutor bookkeeping: correctly add tasks to the `running` list

* fixup! Improve LocalExecutor bookkeeping: correctly add tasks to the `running` list
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:API Airflow's REST/HTTP API area:Executors-core LocalExecutor & SequentialExecutor area:scheduler kind:documentation

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants