This is an automated email from the ASF dual-hosted git repository.

jason810496 pushed a commit to branch v3-3-test
in repository https://gitbox.apache.org/repos/asf/airflow.git


The following commit(s) were added to refs/heads/v3-3-test by this push:
     new b32636456ee [v3-3-test] Bound the scheduler's deserialized Dag cache 
(#71704) (#71821)
b32636456ee is described below

commit b32636456ee11dd79eb79ebe1430b1419247c05d
Author: github-actions[bot] 
<41898282+github-actions[bot]@users.noreply.github.com>
AuthorDate: Wed Aug 19 16:50:58 2026 +0800

    [v3-3-test] Bound the scheduler's deserialized Dag cache (#71704) (#71821)
    
    The scheduler kept every Dag version it deserialized in a mapping that never
    evicted, so a long-running scheduler grew with the number of versions it had
    ever seen until it was restarted or OOM killed. Deployments that redeploy 
Dags
    frequently accumulate versions fastest and hit this soonest.
    
    A least-recently-used cap is the only thing that bounds this outright. An 
idle
    timeout would not: the scheduler re-checks an entry on each lookup, which
    re-arms its expiry, so a timeout reclaims a version only once its runs 
finish
    and it stops being requested, leaving memory a function of the concurrently
    active set rather than a fixed ceiling.
    
    Deliberately not configurable here, so the fix stays small enough to
    cherry-pick. Cache activity currently reports under the existing
    api_server.dag_bag.* metrics; a scheduler-specific namespace, along with
    configuration, follows separately.
    (cherry picked from commit 29dd99d0034c3cff0fe37882f6b210ffca902d29)
    
    
    closes: #69001
    
    Co-authored-by: Jason(Zhe-You) Liu 
<[email protected]>
---
 airflow-core/newsfragments/71704.bugfix.rst           |  1 +
 airflow-core/src/airflow/jobs/scheduler_job_runner.py | 14 +++++++++++++-
 airflow-core/tests/unit/jobs/test_scheduler_job.py    | 11 ++++++++++-
 3 files changed, 24 insertions(+), 2 deletions(-)

diff --git a/airflow-core/newsfragments/71704.bugfix.rst 
b/airflow-core/newsfragments/71704.bugfix.rst
new file mode 100644
index 00000000000..5cd2aed7ec0
--- /dev/null
+++ b/airflow-core/newsfragments/71704.bugfix.rst
@@ -0,0 +1 @@
+The scheduler's Dag cache is now a bounded LRU of 512 versions, so scheduler 
memory no longer grows with every Dag version the process has ever seen.
diff --git a/airflow-core/src/airflow/jobs/scheduler_job_runner.py 
b/airflow-core/src/airflow/jobs/scheduler_job_runner.py
index a6343c4ae6c..d03b1d456f4 100644
--- a/airflow-core/src/airflow/jobs/scheduler_job_runner.py
+++ b/airflow-core/src/airflow/jobs/scheduler_job_runner.py
@@ -154,6 +154,18 @@ DM = DagModel
 TASK_STUCK_IN_QUEUED_RESCHEDULE_EVENT = "stuck in queued reschedule"
 """:meta private:"""
 
+SCHEDULER_DAG_CACHE_SIZE = 512
+"""
+Max deserialized Dag versions the scheduler keeps in memory.
+
+The scheduler reaches its DagBag through the Dag version of each active Dag 
run, so an
+unbounded cache retains every version the process has ever seen and grows for 
the life of
+the process. Sized to sit above the versions-with-runs-in-flight working set 
of a typical
+deployment, so eviction costs a re-fetch only where that working set is 
genuinely larger.
+
+:meta private:
+"""
+
 # Per-tick cap on pending AssetPartitionDagRun rows the scheduler evaluates.
 # Bounds the per-tick transaction so executor heartbeats and regular scheduling
 # aren't starved; remaining APDRs drain across subsequent ticks.
@@ -348,7 +360,7 @@ class SchedulerJobRunner(BaseJobRunner, LoggingMixin):
         if log:
             self._log = log
 
-        self.scheduler_dag_bag = DBDagBag(load_op_links=False)
+        self.scheduler_dag_bag = DBDagBag(load_op_links=False, 
cache_size=SCHEDULER_DAG_CACHE_SIZE)
 
         # Set of (dag_id, asset_name, asset_uri) tuples for trigger policies 
that
         # are permanently unreachable for the rollup window's cardinality — the
diff --git a/airflow-core/tests/unit/jobs/test_scheduler_job.py 
b/airflow-core/tests/unit/jobs/test_scheduler_job.py
index 72ed2065bdb..419b459bc36 100644
--- a/airflow-core/tests/unit/jobs/test_scheduler_job.py
+++ b/airflow-core/tests/unit/jobs/test_scheduler_job.py
@@ -57,7 +57,7 @@ from airflow.executors.executor_loader import ExecutorLoader
 from airflow.executors.executor_utils import ExecutorName
 from airflow.executors.local_executor import LocalExecutor
 from airflow.jobs.job import Job, run_job
-from airflow.jobs.scheduler_job_runner import SchedulerJobRunner
+from airflow.jobs.scheduler_job_runner import SCHEDULER_DAG_CACHE_SIZE, 
SchedulerJobRunner
 from airflow.models.asset import (
     AssetActive,
     AssetAliasModel,
@@ -410,6 +410,15 @@ class TestSchedulerJob:
         assert scheduler_job.executor == mock_local_executor
         assert scheduler_job.executors == [mock_local_executor]
 
+    def test_scheduler_dag_bag_is_bounded(self):
+        """The scheduler's Dag cache must evict, or it retains every version 
it has ever seen."""
+        from cachetools import LRUCache
+
+        job_runner = SchedulerJobRunner(Job())
+
+        assert isinstance(job_runner.scheduler_dag_bag._dags, LRUCache)
+        assert job_runner.scheduler_dag_bag._dags.maxsize == 
SCHEDULER_DAG_CACHE_SIZE
+
     @pytest.mark.parametrize(
         "heartrate",
         [10, 5],

Reply via email to