Skip to content

Fix scheduler memory spike when a Dag run has a large conf - #73907

Merged
shahar1 merged 2 commits into
apache:mainfrom
luc-pimentel:fix/71267-defer-run-conf
Nov 1, 2026
Merged

shahar1 merged 2 commits into
apache:mainfrom
luc-pimentel:fix/71267-defer-run-conf

Conversation

@luc-pimentel

Copy link
Copy Markdown
Contributor

A Dag run with a large conf made the scheduler's memory grow with the number of task instances in the run. TaskInstance.dag_run is a joined eager load, so every row of a task-instance query also carries the run's conf. On every loop the scheduler reads all task instances of each running Dag run through DagRun.fetch_task_instances in airflow-core/src/airflow/models/dagrun.py, and re-reads the ones still waiting on their upstreams in _get_ready_tis, so it decoded the conf once per task instance.

fetch_task_instances now defers the conf, which still loads if something reads it, and the re-check, which only compares states, no longer joins the run. The other task-instance queries in the scheduling loop return a bounded number of rows (the critical section, executor events) or a few rows per task instance (the mapped upstream dependency check), so they are left as they are. #73022 changed these two queries and four others; it was closed under the open PR limit before it was reviewed.

Reproduced with airflow standalone (SQLite, LocalExecutor): one run of a Dag with a single mapped task, triggered through the REST API. The 1 MB conf is about 8,400 small JSON records. Scheduler RSS, idle → peak:

Mapped task instances Run conf 3.3.2 main With this change
500 empty 150 → 160 MB 206 → 216 MB 206 → 216 MB
50 1 MB 150 → 452 MB 206 → 553 MB 206 → 317 MB
500 1 MB 150 → 3,234 MB 206 → 3,422 MB 206 → 321 MB

A task mapped over an upstream task's output behaves the same: 206 → 3,355 MB on main, 206 → 328 MB with this change. The remaining ~110 MB is the same for 50 and 500 task instances, and the 500-instance run with the 1 MB conf takes 196 s instead of 376 s.

The two new tests in airflow-core/tests/unit/models/test_dagrun.py fail without this change; reverting either query's change fails at least one of them. Locally, the airflow-core models, jobs, ti_deps and execution API unit tests and the other tests that call these functions pass, apart from four that also fail without this change (two need graphviz, one moto, and test_still_in_retry_period). The prek pre-commit hooks, including mypy-airflow-core, and the manual stage pass.

closes: #71267


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

Generated-by: Claude Code (Opus 5.5) following the guidelines

@luc-pimentel

Copy link
Copy Markdown
Contributor Author

Could someone please take a look at this PR?
Thanks!

@luc-pimentel

Copy link
Copy Markdown
Contributor Author

Rebased on main to resolve the conflicts from the dynamic-regions changes. The state re-check in _get_ready_tis now looks task instances up by id, so the lazy load moved onto that query; nothing else changed.


Drafted-by: Claude Code (Fable 5.1); reviewed by @luc-pimentel before posting

@luc-pimentel
luc-pimentel force-pushed the fix/71267-defer-run-conf branch from 923edde to 9d98eea Compare October 18, 2026 01:38
@pedro-cf

Copy link
Copy Markdown

This PR makes a huge difference in production. My Scheduler was struggling badly with 2-5 MB Confs, so I created a cluster policy based on this PR as a temporary backport, which helped address the issue completly. This is a very valuable improvement, and I highly recommend merging it.

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

I think this is okay, but I'd like a second pair of eyes from someone else familiar with the code base on it before we merge.

TaskInstance.dag_run is a joined eager load, so a task-instance query
that also selects dag_run.conf returns and decodes one copy of the conf
per row. A scheduling pass does that twice: fetch_task_instances reads
every task instance of the run, and _get_ready_tis re-reads the ones
still waiting on their upstreams. A large conf on a run with many
mapped task instances multiplies scheduler memory this way.
TaskInstance.dag_run is a joined eager load, so every row of a
task-instance query also carries the run's conf. The scheduler fetches
all task instances of each running Dag run on every loop, and re-reads
the ones still waiting on their upstreams, so a large conf was decoded
once per task instance: a 1 MB conf on a run with 500 mapped task
instances took the scheduler from 150 MB to over 3 GB.

Neither query needs the conf up front: the few callers that read it
can load it on access, and the re-check only compares states.
@luc-pimentel

Copy link
Copy Markdown
Contributor Author

Rebased to resolve the conflict with #71737; conf deferral stays on the ORM query for unfinished task instances, preserving the new lightweight finished-task query.


Drafted-by: OpenCode 1.19.0 (GPT-6 Astra); reviewed by @luc-pimentel before posting

@luc-pimentel
luc-pimentel force-pushed the fix/71267-defer-run-conf branch from 9d98eea to af38159 Compare October 28, 2026 22:57
@shahar1 shahar1 added the backport-to-v3-4-test Backport to v3-4-test label Nov 1, 2026

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

Nice optimization!

@shahar1
shahar1 merged commit ce01a73 into apache:main Nov 1, 2026
71 checks passed
@github-actions github-actions Bot added this to the Airflow 3.4.1 milestone Nov 1, 2026
@github-actions

github-actions Bot commented Nov 1, 2026

Copy link
Copy Markdown
Contributor

Hi maintainer, this PR was merged without a milestone set.
We've automatically set the milestone to Airflow 3.4.1 based on: backport label targeting v3-4-test
If this milestone is not correct, please update it to the appropriate milestone.

This comment was generated by Milestone Tag Assistant.

@github-actions

github-actions Bot commented Nov 1, 2026

Copy link
Copy Markdown
Contributor

Backport failed to create: v3-4-test. View the failure log Run details

Note: As of Merging PRs targeted for Airflow 3.X
the committer who merges the PR is responsible for backporting the PRs that are bug fixes (generally speaking) to the maintenance branches.

In matter of doubt please ask in #release-management Slack channel.

Status Branch Result
❌ v3-4-test Commit Link

You can attempt to backport this manually by running:

cherry_picker ce01a73 v3-4-test

This should apply the commit to the v3-4-test branch and leave the commit in conflict state marking
the files that need manual conflict resolution.

After you have resolved the conflicts, you can continue the backport process by running:

cherry_picker --continue

If you don't have cherry-picker installed, see the installation guide.

shahar1 pushed a commit to shahar1/airflow that referenced this pull request Nov 1, 2026
apache#73907)

* Add tests for the run conf loaded per task instance when scheduling

TaskInstance.dag_run is a joined eager load, so a task-instance query
that also selects dag_run.conf returns and decodes one copy of the conf
per row. A scheduling pass does that twice: fetch_task_instances reads
every task instance of the run, and _get_ready_tis re-reads the ones
still waiting on their upstreams. A large conf on a run with many
mapped task instances multiplies scheduler memory this way.

* Fix scheduler memory spike when a Dag run has a large conf

TaskInstance.dag_run is a joined eager load, so every row of a
task-instance query also carries the run's conf. The scheduler fetches
all task instances of each running Dag run on every loop, and re-reads
the ones still waiting on their upstreams, so a large conf was decoded
once per task instance: a 1 MB conf on a run with 500 mapped task
instances took the scheduler from 150 MB to over 3 GB.

Neither query needs the conf up front: the few callers that read it
can load it on access, and the re-check only compares states.

(cherry picked from commit ce01a73)
@pedro-cf

pedro-cf commented Nov 2, 2026

Copy link
Copy Markdown

Any chance this makes it into 3.4.0 ?

shahar1 pushed a commit to shahar1/airflow that referenced this pull request Nov 4, 2026
apache#73907)

* Add tests for the run conf loaded per task instance when scheduling

TaskInstance.dag_run is a joined eager load, so a task-instance query
that also selects dag_run.conf returns and decodes one copy of the conf
per row. A scheduling pass does that twice: fetch_task_instances reads
every task instance of the run, and _get_ready_tis re-reads the ones
still waiting on their upstreams. A large conf on a run with many
mapped task instances multiplies scheduler memory this way.

* Fix scheduler memory spike when a Dag run has a large conf

TaskInstance.dag_run is a joined eager load, so every row of a
task-instance query also carries the run's conf. The scheduler fetches
all task instances of each running Dag run on every loop, and re-reads
the ones still waiting on their upstreams, so a large conf was decoded
once per task instance: a 1 MB conf on a run with 500 mapped task
instances took the scheduler from 150 MB to over 3 GB.

Neither query needs the conf up front: the few callers that read it
can load it on access, and the re-check only compares states.

(cherry picked from commit ce01a73)
shahar1 added a commit that referenced this pull request Nov 6, 2026
#73907) (#76252)

* Add tests for the run conf loaded per task instance when scheduling

TaskInstance.dag_run is a joined eager load, so a task-instance query
that also selects dag_run.conf returns and decodes one copy of the conf
per row. A scheduling pass does that twice: fetch_task_instances reads
every task instance of the run, and _get_ready_tis re-reads the ones
still waiting on their upstreams. A large conf on a run with many
mapped task instances multiplies scheduler memory this way.

* Fix scheduler memory spike when a Dag run has a large conf

TaskInstance.dag_run is a joined eager load, so every row of a
task-instance query also carries the run's conf. The scheduler fetches
all task instances of each running Dag run on every loop, and re-reads
the ones still waiting on their upstreams, so a large conf was decoded
once per task instance: a 1 MB conf on a run with 500 mapped task
instances took the scheduler from 150 MB to over 3 GB.

Neither query needs the conf up front: the few callers that read it
can load it on access, and the re-check only compares states.

(cherry picked from commit ce01a73)

Co-authored-by: Lucas Pimentel <[email protected]>
vatsrahul1001 pushed a commit that referenced this pull request Nov 9, 2026
#73907) (#76252)

* Add tests for the run conf loaded per task instance when scheduling

TaskInstance.dag_run is a joined eager load, so a task-instance query
that also selects dag_run.conf returns and decodes one copy of the conf
per row. A scheduling pass does that twice: fetch_task_instances reads
every task instance of the run, and _get_ready_tis re-reads the ones
still waiting on their upstreams. A large conf on a run with many
mapped task instances multiplies scheduler memory this way.

* Fix scheduler memory spike when a Dag run has a large conf

TaskInstance.dag_run is a joined eager load, so every row of a
task-instance query also carries the run's conf. The scheduler fetches
all task instances of each running Dag run on every loop, and re-reads
the ones still waiting on their upstreams, so a large conf was decoded
once per task instance: a 1 MB conf on a run with 500 mapped task
instances took the scheduler from 150 MB to over 3 GB.

Neither query needs the conf up front: the few callers that read it
can load it on access, and the re-check only compares states.

(cherry picked from commit ce01a73)

Co-authored-by: Lucas Pimentel <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

backport-to-v3-4-test Backport to v3-4-test

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Airflow scheduler memory spikes when a dag is run with large run config

4 participants