From 8daec1f83e8be47c0d7d6d863c34b441cedd0f25 Mon Sep 17 00:00:00 2001 From: dor-bernstein <160585775+dor-bernstein@users.noreply.github.com> Date: Sun, 26 Jul 2026 20:24:38 +0300 Subject: [PATCH] Skip per-DAG FAB permission sync when access_control is unset _serialize_dag_capturing_errors() calls _sync_dag_perms() for every DAG on every parse when the FAB auth manager is active. sync_perm_for_dag() creates a per-DAG `DAG:` resource (ab_view_menu / ab_permission_view) inside the same transaction that holds SELECT ... FOR UPDATE locks on the dag/dag_run rows (DagModelOperation.find_orm_dags -> update_dag_parsing_results_in_db). With a large number of dynamically-generated DAGs this per-DAG sync dominates the parse transaction's lock-hold time (observed: a single dag-processor transaction holding dag-row locks for ~3-4 minutes). Since 3.2.0 the scheduler writes `exceeds_max_non_backfill` to `dag` rows inside its critical section and blocks behind that lock, so the scheduler stalls (critical_section duration up, CPU down) whenever a parse is in flight. When a DAG defines no explicit access_control, the per-DAG resources are unused because DAG-level access is granted through the global DAG resource on the role. Gate the sync on `dag.access_control` so it only runs when there is per-DAG authz to sync. No authorization behaviour changes for DAGs without access_control. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../src/airflow/dag_processing/collection.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/airflow-core/src/airflow/dag_processing/collection.py b/airflow-core/src/airflow/dag_processing/collection.py index 06e4900d816e5..6b95bc1670d72 100644 --- a/airflow-core/src/airflow/dag_processing/collection.py +++ b/airflow-core/src/airflow/dag_processing/collection.py @@ -289,7 +289,21 @@ def _serialize_dag_capturing_errors( if not dag_was_updated: # Check and update DagCode DagCode.update_source_code(dag.dag_id, dag.fileloc, session=session) - if "FabAuthManager" in conf.get("core", "auth_manager"): + # Only sync per-DAG FAB resources when the DAG defines explicit access_control. + # + # sync_perm_for_dag() creates/refreshes a `DAG:` resource (ab_view_menu + + # ab_permission_view rows) for *every* DAG on *every* parse, inside the same + # transaction that holds SELECT ... FOR UPDATE row locks on the `dag`/`dag_run` + # tables (see DagModelOperation.find_orm_dags / update_dag_parsing_results_in_db). + # With a large number of dynamically-generated DAGs this per-DAG sync dominates the + # parse transaction's lock-hold time and starves the scheduler, which since 3.2.0 + # writes `exceeds_max_non_backfill` to `dag` rows inside its critical section and + # blocks behind that lock. + # + # When a DAG defines no per-DAG access_control, those per-DAG resources are unused: + # DAG-level access is already granted through the global DAG resource on the role. + # Skipping the sync in that case removes the contention without changing authz. + if "FabAuthManager" in conf.get("core", "auth_manager") and dag.access_control: _sync_dag_perms(dag, session=session) return []