Resolve dag versions once per scheduler loop#69281
Conversation
Each scheduling loop resolved the dag for every dag run individually, issuing one latest-version lookup per run per tick — and twice per run in the main scheduling loop, where the integrity check repeated the same lookup. The two loops that attempted to amortize this keyed an lru_cache on the DagRun instance, which appears exactly once per loop, so that cache never produced a hit. At the default per-loop caps this wastes dozens of point queries per scheduler tick in every deployment, adding database load and scheduling latency that grow with the number of runs examined. Resolve the latest versions for the whole batch in one query and share the result between dag resolution and the integrity check.
|
Nice scheduler cleanup. One thing I think we should align before merge: Drafted-by: Codex (GPT-5); reviewed by @Vamsi-klu before posting |
|
|
The scheduler resolves each dag run's dag through
DBDagBag.get_dag_for_run, which issues one uncachedDagVersion.get_latest_versionSELECT per call. Since #49097 every scheduling loop pays that cost once per dag run per tick — and the main scheduling loop pays it twice per run, because_verify_integrity_if_dag_changedrepeats the same lookup.#30704 ("Save scheduler execution time by caching dags") deliberately added a per-loop
lru_cachefor exactly this, keyed bydag_id. #49097 changed the cached call fromcached_get_dag(dag_run.dag_id)tocached_get_dag(dag_run)to make resolution version-aware — which silently disabled the cache: the key became theDagRunORM instance (identity hashing, no custom__eq__/__hash__), and each instance appears exactly once per loop. Rebuilding the old construct and passing 20 runs of one dag,cache_info()reportshits=0, misses=20. The "cache saves time during scheduling of many dag_runs for same dag" comment has not been true since.Measured with
CountQueries(which counts realafter_cursor_executeevents), 20 runs of one dag, verified on both SQLite and PostgreSQL:_start_queued_dagruns_schedule_all_dag_runsAt default per-loop caps this removes tens of point queries per scheduler tick per scheduler in every deployment; the waste scales with
max_dagruns_per_loop_to_schedule.What changed
DagVersion.get_latest_versions(dag_ids): batch latest-version lookup (oneGROUP BY max(version_number)query, backed by the(dag_id, version_number)unique constraint), semantics identical toget_latest_versionper dag.DBDagBag.get_dags_for_runs(dag_runs, ...): batch counterpart ofget_dag_for_run, mirroring_version_from_dag_runper run (bundle-pinned runs still resolve viacreated_dag_version_id); results keyed by(dag_id, run_id)._schedule_all_dag_runsresolves versions and dags once for the batch, threads them into_schedule_dag_run->_verify_integrity_if_dag_changedas optional parameters (per-call behavior unchanged for all other callers), and returns the resolved dag so the callback-dispatch loop no longer re-resolves;_start_queued_dagrunsuses the batch resolver; both deadlru_caches are removed.Notes
Tests
get_dags_for_runsresolution tests.test_scheduler_job.pysuite passes.related: #69275 (remaining per-item resolution paths in the critical section and dag-run listeners)
Was generative AI tooling used to co-author this PR?
Generated-by: Claude Code (Opus 4.8) following the guidelines