Skip to content

Skip per-DAG FAB permission sync when access_control is unset (3.2.2) - #4

Open
dor-bernstein wants to merge 1 commit into
release-3.2.2from
clut/dag-processor-lock-contention-3.2.2
Open

Skip per-DAG FAB permission sync when access_control is unset (3.2.2)#4
dor-bernstein wants to merge 1 commit into
release-3.2.2from
clut/dag-processor-lock-contention-3.2.2

Conversation

@dor-bernstein

Copy link
Copy Markdown
Collaborator

Problem

After upgrading Airflow 3.1.7 → 3.2.2, scheduling became severely slow (mainly visible as Celery tasks not getting queued). Metrics showed the classic signature of the scheduler being blocked, not busy: scheduler.critical_section_duration up, scheduler CPU down.

Live investigation on the metadata DB (Postgres) found dag-table row-lock contention:

  • The dag-processor holds a single long transaction (observed ~3m40s, idle in transaction / active on SELECT dag_code…) that has taken SELECT … FOR UPDATE locks on the dag/dag_run rows (DagModelOperation.find_orm_dags).
  • Both scheduler replicas block for ~2.5 min on UPDATE dag SET exceeds_max_non_backfill=… WHERE dag.dag_id = …, waiting on transactionid locks held by that dag-processor transaction.

Two things make this new in 3.2:

  1. The scheduler now writes exceeds_max_non_backfill to dag rows inside its critical section (added in Separate "next dag run" from "max active runs" apache/airflow#60006, shipped 3.2.0). In 3.1 it computed this in memory and never took a dag-row write lock there, so it never collided with the dag-processor's locks.
  2. _serialize_dag_capturing_errors() calls _sync_dag_perms() for every DAG on every parse (FAB auth manager), and sync_perm_for_dag() creates/refreshes a per-DAG DAG:<dag_id> resource (ab_view_menu / ab_permission_view) inside that same transaction — we caught the blocker running exactly these SELECT ab_view_menu … statements.

This is amplified enormously by our workload: thousands of dynamically-generated per-tenant DAGs, so one parse transaction locks a huge number of dag rows for minutes.

Change

Gate the per-DAG permission sync on the DAG actually defining access_control:

if "FabAuthManager" in conf.get("core", "auth_manager") and dag.access_control:
    _sync_dag_perms(dag, session=session)

When a DAG has no explicit access_control, its per-DAG FAB resources are unused — DAG-level access is granted via the global DAG resource on the role. Skipping the sync in that case removes a major component of the parse transaction's lock-hold work with no change to authorization.

Why this is safe for us

  • None of our DAGs set access_control (verified).
  • Okta SSO is unaffected — it runs through FAB OAuth + our OktaTeamAuthorizer group→role mapping (authentication + role assignment). This change only touches per-DAG resource creation (authorization granularity we don't use).
  • Our Okta groups map to the standard global Admin/Viewer roles, which grant DAG access via the global DAG resource, not per-DAG grants.
  • Existing per-DAG resources already in the DB are left untouched (just no longer refreshed).

Scope / what this deliberately does NOT do

This does not chunk the update_dags / update_dag_parsing_results_in_db transaction. That transaction's scope is per parsed file, so the deeper fix for the lock-hold duration is to reduce DAGs-per-file in our dynamic DAG factory (nitro side) and/or a properly test-validated core change to batch-commit bulk_write_to_db. Both are follow-ups; this PR is the low-risk, high-confidence first step that removes the FAB perm-sync component we directly observed blocking the scheduler.

Testing

  • Run airflow-core dag-processing / permission tests in CI.
  • Verify on a repro tenant that dag-processor transactions no longer hold dag locks for minutes and that UPDATE dag SET exceeds_max_non_backfill no longer blocks.

References

🤖 Generated with Claude Code

_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:<dag_id>` 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) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant