Apache Airflow version
3.2.0+ (observed on 3.2.2)
What happened
The scheduler intermittently stalls: scheduler.critical_section_duration climbs while scheduler CPU drops and task scheduling starves. Inspecting the metadata DB during a stall shows scheduler statements of the form UPDATE dag SET exceeds_max_non_backfill = … WHERE dag.dag_id = … blocked with wait_event_type = Lock / wait_event = transactionid, waiting on the DAG processor's parse transaction.
Root cause
Before 3.2, the scheduler evaluated max_active_runs in memory and did not write the dag row during dagrun scheduling. #60006 ("Separate 'next dag run' from 'max active runs'", merge commit 32bc0119b408c9a37841edd021b646324a02745e, shipped in 3.2.0) added the dag.exceeds_max_non_backfill column (migration 0097_3_2_0_add_exceeds_max_runs_flag_to_dag_model) and made the scheduler write it to the dag row during dagrun scheduling:
- Write:
airflow-core/src/airflow/jobs/scheduler_job_runner.py::_set_exceeds_max_active_runs (called from _create_dag_runs and _start_queued_dagruns).
That write now competes with a pre-existing lock held by the DAG processor for the entire per-file parse transaction:
- Lock:
airflow-core/src/airflow/dag_processing/collection.py::DagModelOperation.find_orm_dags → with_row_locks(select(DagModel) …) i.e. SELECT … FOR UPDATE on every dag row for a parsed file, held through update_dags + serialization + permission sync until the transaction commits.
So the scheduler's UPDATE dag SET exceeds_max_non_backfill = … blocks on the DAG processor's FOR UPDATE. This is amplified when a single parsed file defines many DAGs (e.g. dynamically generated DAGs), because that parse transaction can run for minutes — holding the dag-row locks the whole time and stalling the scheduler loop.
How to reproduce it (conceptually)
- Use the FAB auth manager and generate many DAGs from a single DAG file.
- While the DAG processor is parsing that file (holding
SELECT … FOR UPDATE on its dag rows), have the scheduler attempt to update exceeds_max_non_backfill for one of those DAGs.
- Observe the scheduler statement blocked on
Lock/transactionid, and critical_section_duration rising while scheduler CPU falls.
Suggested direction
Reduce the parse transaction's dag-row lock hold and/or the scheduler's contention on it. As a partial step, per-DAG permission sync (another writer in the same parse transaction) can be made to honor [fab] update_fab_perms — proposed in #70589. A more complete fix would shorten/scope the find_orm_dags lock or the scheduler's dag-row write.
Anything else
Related: #23232 (pluggable appless security manager).
Apache Airflow version
3.2.0+ (observed on 3.2.2)
What happened
The scheduler intermittently stalls:
scheduler.critical_section_durationclimbs while scheduler CPU drops and task scheduling starves. Inspecting the metadata DB during a stall shows scheduler statements of the formUPDATE dag SET exceeds_max_non_backfill = … WHERE dag.dag_id = …blocked withwait_event_type = Lock/wait_event = transactionid, waiting on the DAG processor's parse transaction.Root cause
Before 3.2, the scheduler evaluated
max_active_runsin memory and did not write thedagrow during dagrun scheduling. #60006 ("Separate 'next dag run' from 'max active runs'", merge commit32bc0119b408c9a37841edd021b646324a02745e, shipped in 3.2.0) added thedag.exceeds_max_non_backfillcolumn (migration0097_3_2_0_add_exceeds_max_runs_flag_to_dag_model) and made the scheduler write it to thedagrow during dagrun scheduling:airflow-core/src/airflow/jobs/scheduler_job_runner.py::_set_exceeds_max_active_runs(called from_create_dag_runsand_start_queued_dagruns).That write now competes with a pre-existing lock held by the DAG processor for the entire per-file parse transaction:
airflow-core/src/airflow/dag_processing/collection.py::DagModelOperation.find_orm_dags→with_row_locks(select(DagModel) …)i.e.SELECT … FOR UPDATEon everydagrow for a parsed file, held throughupdate_dags+ serialization + permission sync until the transaction commits.So the scheduler's
UPDATE dag SET exceeds_max_non_backfill = …blocks on the DAG processor'sFOR UPDATE. This is amplified when a single parsed file defines many DAGs (e.g. dynamically generated DAGs), because that parse transaction can run for minutes — holding thedag-row locks the whole time and stalling the scheduler loop.How to reproduce it (conceptually)
SELECT … FOR UPDATEon itsdagrows), have the scheduler attempt to updateexceeds_max_non_backfillfor one of those DAGs.Lock/transactionid, andcritical_section_durationrising while scheduler CPU falls.Suggested direction
Reduce the parse transaction's
dag-row lock hold and/or the scheduler's contention on it. As a partial step, per-DAG permission sync (another writer in the same parse transaction) can be made to honor[fab] update_fab_perms— proposed in #70589. A more complete fix would shorten/scope thefind_orm_dagslock or the scheduler'sdag-row write.Anything else
Related: #23232 (pluggable appless security manager).