Skip to content

Remove redundant full-Dag TaskInstance query from the execution API task-states endpoint - #72270

Closed
ColtenOuO wants to merge 1 commit into
apache:mainfrom
ColtenOuO:fix-execution-api-task-states-redundant-query
Closed

ColtenOuO wants to merge 1 commit into
apache:mainfrom
ColtenOuO:fix-execution-api-task-states-redundant-query

Conversation

@ColtenOuO

Copy link
Copy Markdown
Contributor

Sumarry

GET /execution/task-instances/states (get_task_instance_states in airflow-core/src/airflow/api_fastapi/execution_api/routes/task_instances.py) is called by the worker/Task SDK whenever a task needs the states of sibling task instances (e.g. trigger-rule evaluation, wait_for_downstream, cross-task state checks). When the request is scoped with task_group_id, the handler was doing unnecessary DB work on every call:

  1. It first ran a query selecting every TaskInstance row for the Dag (optionally narrowed by task_ids, logical_dates, run_ids, map_index), loading full ORM entities.
  2. It then ran a second query to resolve the task group's members and fetch their TaskInstance rows.
  3. If the caller had not also passed task_ids, the result of the first query was thrown away entirely and only the second query's rows were used — meaning step 1 was pure waste on every task-group-scoped call.

For Dags with many tasks and/or many Dag runs, that first discarded query could return and hydrate a large number of rows for nothing, adding an avoidable round trip and CPU/memory cost to a request that sits on the task execution hot path (workers call this while running tasks, so the extra latency is paid per task, not once).

This PR removes the wasted round trip:

  • The task group's task IDs are now resolved from the already-parsed Dag definition (task_group.iter_tasks()), which requires no extra TaskInstance query — only the existing Dag-version lookup that was already being performed.
  • Those IDs are unioned with any explicit task_ids from the request before building the query, so there is exactly one TaskInstance query per request, filtered with task_id.in_(...) — preserving the exact original semantics (task_ids alone, task_group_id alone, or the union of both).
  • That single query now selects only the four columns actually used (run_id, task_id, map_index, state) instead of loading full TaskInstance ORM objects, cutting the amount of data hydrated per call further.
  • The pre-existing _get_group_tasks helper (still used by the sibling /count endpoint, which needs actual map_index values from persisted rows for a different query shape) is left untouched aside from factoring out the shared "resolve the task group or 404" logic into _get_task_group.

Change

  • get_task_instance_states: replaced the "query everything, then maybe discard it" flow with a single TaskInstance query. The task group's task IDs are resolved up front (from task_group.iter_tasks()) and unioned with any request task_ids into one task_id.in_(...) filter before the query runs, instead of running one query filtered by task_ids and a second, separate query for the group, then conditionally throwing the first result set away.
  • That single query now does select(TI.run_id, TI.task_id, TI.map_index, TI.state) instead of select(TI), so it loads four scalar columns per row instead of hydrating full TaskInstance ORM objects.
  • Added _get_task_group (Dag/task-group lookup + 404 handling) factored out of the pre-existing _get_group_tasks, and a new _get_group_task_ids that returns just the group's task IDs without touching the TaskInstance table. _get_group_tasks itself is unchanged in behavior — it's still used as-is by the /count endpoint, which needs real persisted (task_id, map_index) pairs for a different query shape.
  • Added test_get_task_states_with_task_group_id_query_count (query-count regression test).

Impact

For any task_group_id-scoped call to this endpoint, DB round trips drop from 2 (one of which was always discarded) down to 1, and that query is narrower (4 scalar columns instead of full-row ORM hydration). This directly reduces the RTT the worker/Task SDK pays on this call, and removes the redundant row hydration/network cost on the API server and DB — proportional to Dag size and Dag-run count, so the win scales with the DAGs that would have hit this path hardest.

Testing

  • Added test_get_task_states_with_task_group_id_query_count, a query-count regression test using assert_queries_count, which fails against the old code (3 queries) and passes against the fix (2 queries: one Dag-version lookup, one TaskInstance query).
  • All existing tests in TestGetTaskStates and TestGetCount (the /count endpoint that shares the untouched helper) continue to pass, confirming the task_ids/task_group_id union semantics are unchanged, including the mixed task_ids + task_group_id case.

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

@boring-cyborg boring-cyborg Bot added area:API Airflow's REST/HTTP API area:task-sdk labels Aug 29, 2026
@ColtenOuO
ColtenOuO marked this pull request as ready for review August 29, 2026 19:26
…on API

get_task_instance_states always ran a full, unfiltered TaskInstance query
for the Dag even when scoped to a single task_group_id, then discarded
those rows in favor of a second, separate query for the group's members.
Workers call this endpoint on the task execution hot path, so the wasted
round trip added avoidable RTT and DB load on every task-group-scoped
call, proportional to Dag size and Dag-run count.
@ColtenOuO
ColtenOuO force-pushed the fix-execution-api-task-states-redundant-query branch from 7daf7c0 to 686424e Compare September 24, 2026 03:37
@ColtenOuO

Copy link
Copy Markdown
Contributor Author

Resolved the conflict :D

@potiuk

potiuk commented Sep 25, 2026

Copy link
Copy Markdown
Member

Hello @ColtenOuO - thank you for your contributions to Apache Airflow!

The Airflow community has introduced a limit of 5 open pull requests at a time for contributors without write access to the repository. You currently have 19 open pull requests, so - as a one-time step of introducing the limit - we closed the ones where maintainers have not engaged yet:

These pull requests stay open because maintainers are already engaged in them - they count towards your limit:

This is not a judgement of you or of your changes. We never told contributors before that opening many pull requests at once was a problem, so there is nothing to feel bad about - and nothing is lost: your branches, commits and the review history stay where they are.

What we ask you to do is to make your first prioritization decision: choose which of the pull requests above matter most to you, and reopen them (up to 5 open at a time, including the ones still open) with the "Reopen pull request" button or gh pr reopen <PR_NUMBER> --repo apache/airflow. Reopen the ones you are ready to follow through - keep them rebased, respond to review comments and fix failing checks.

While your pull requests are waiting for review, the most valuable thing you can do is help in other ways - reviewing other contributors' pull requests, helping with issues, and taking part in the discussions on the devlist and Slack.

Why we introduced the limit, what it means for you and how to reopen or restore a pull request is explained in https://github.com/apache/airflow/blob/main/contributing-docs/32_open_pull_request_limit.rst.


Drafted-by: Claude Code (Opus 5); reviewed by @potiuk before posting

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:task-sdk closed because of open PR limit Closed as a one-time step of introducing the open pull request limit

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants