zach-overflow opened a new issue, #76558:
URL: https://github.com/apache/airflow/issues/76558

   ### Under which category would you file this issue?
   
   Airflow Core
   
   ### Apache Airflow version
   
   3.3.2
   
   ### What happened and how to reproduce it?
   
   ## Issue Description
   
   We've run into an issue relating to runs triggered by a large number of 
asset events. `PATCH` requests to the 
`/execution/task-instances/{task_instance_id}/run` (`ti_run`) takes time 
proportional to the number of asset events the DagRun consumes, and it does 
that work while holding a `FOR UPDATE` lock on the `task_instance` row. 
   
   When an asset-triggered run consumes thousands of events, every task start 
in that run is slow enough to hit statement timeouts, while the Task SDK's 
retries queue up behind the lock held by the previous attempt.
   
   Code path on `main` ([`task_instances.py` @ 
bca1e14](https://github.com/apache/airflow/blob/bca1e144da1536eb168d925ac358a91e69ef3654/airflow-core/src/airflow/api_fastapi/execution_api/routes/task_instances.py#L157-L376)):
   
   1. 
[L182-L212](https://github.com/apache/airflow/blob/bca1e144da1536eb168d925ac358a91e69ef3654/airflow-core/src/airflow/api_fastapi/execution_api/routes/task_instances.py#L182-L212):
 `SELECT ... FROM task_instance JOIN dag_run JOIN dag WHERE task_instance.id = 
:id FOR UPDATE OF task_instance`.
   2. The TI is updated to `RUNNING`.
   3. 
[L322-L334](https://github.com/apache/airflow/blob/bca1e144da1536eb168d925ac358a91e69ef3654/airflow-core/src/airflow/api_fastapi/execution_api/routes/task_instances.py#L322-L334):
 the DagRun is loaded with `.options(joinedload(DR.consumed_asset_events), 
...)`. Only the collection itself is eager-loaded.
   4. 
[L366-L367](https://github.com/apache/airflow/blob/bca1e144da1536eb168d925ac358a91e69ef3654/airflow-core/src/airflow/api_fastapi/execution_api/routes/task_instances.py#L366-L367):
 `TIRunContext(dag_run=dr, ...)` is built. All of this runs in one transaction, 
so the row lock is held until commit.
   
   This endpoint runs `DagRun` datamodel validations with asset lazy-loading 
([`datamodels/taskinstance.py` 
L362-L396](https://github.com/apache/airflow/blob/bca1e144da1536eb168d925ac358a91e69ef3654/airflow-core/src/airflow/api_fastapi/execution_api/datamodels/taskinstance.py#L362-L396))
 which causes numerous reads for `asset` and `source_aliases` on every 
`AssetEventDagRunReference`. Because of this the validation issues about one 
`source_aliases` SELECT per consumed event, all while the TI row lock is still 
held.
   
   The scheduler does not run into the same issue because it avoids the pitfall 
(see 
[`_eager_load_dag_run_for_validation()`](https://github.com/apache/airflow/blob/bca1e144da1536eb168d925ac358a91e69ef3654/airflow-core/src/airflow/jobs/scheduler_job_runner.py#L192-L214)
 )
   
   Semi-related past issues and PRs: #65422, #59693, #66190.
   
   ## What We've Observed
   
   - An asset-scheduled DAG (`catchup=False`) stayed paused for ~27 days while 
its asset kept receiving ~280 events/day.
   - After it was unpaused, the first asset-triggered run consumed 7,512 
events. Scheduler log: `Created asset-triggered DagRun for '<dag>': run_id=..., 
consumed 7512 asset events`. Other runs of the same DAG consumed 1-11 events 
and started normally.
   - Each `ti_run` call for the first task of that run held the TI row lock for 
~25s on the server. Postgres `log_lock_waits` showed `process N still waiting 
for ShareLock on transaction ... after 1000 ms ... while locking tuple ... in 
relation "task_instance"`, and each waiter got the lock ~25s after the previous 
request started.
   - The client got `504 Gateway Timeout`. Each Task SDK retry queued behind 
the row lock of the previous, still-running request and was then cancelled by 
`statement_timeout` (SQLSTATE 57014).
   - All 4 tries of the task failed the same way, and the downstream tasks went 
to `upstream_failed`. Every task in such a run pays this cost because every 
task start goes through `ti_run`.
   
   ## How To Reproduce
   
   This section written by Claude Code Opus 5.5:
   
   1. Create a DAG scheduled on a single asset, then pause it.
   2. Emit several thousand events for that asset, e.g. with repeated `POST 
/api/v2/assets/events`.
   3. Unpause the DAG. The scheduler creates one asset-triggered run that 
consumes all of the queued events.
   4. Watch the `ti_run` latency for the first task of that run. With 
SQLAlchemy echo or Postgres statement logging on, you see one 
`asset_alias_asset_event` / `source_aliases` SELECT per consumed event inside 
the transaction that holds `FOR UPDATE OF task_instance`. Latency grows with 
the number of consumed events.
   
   ### What you think should happen instead?
   
   The `ti_run` execution API's endpoint latency, and particularly how long it 
holds the TI row lock, should not grow with the number of consumed asset events.
   
   Possible fixes:
   
   1. Eager-load the nested relations in `ti_run` in a similar way that the 
scheduler does via `_eager_load_dag_run_for_validation()` (see link in issue 
description above). That turns the N+1 queries into a fixed number of queries.
   2. Load and serialize the consumed events after the TI state update is 
committed, so that work does not run under `FOR UPDATE`. Alternatively, stop 
sending every consumed event in `TIRunContext` and have the Task SDK fetch them 
lazily when `triggering_asset_events` is accessed.
   
   Fix (1) is small and self-contained. Fix (2) also covers the cost of 
serializing large `extra` payloads.
   
   ### Operating System
   
   Linux (Debian-based container image)
   
   ### Deployment
   
   Other 3rd-party Helm chart
   
   ### Apache Airflow Provider(s)
   
   _No response_
   
   ### Versions of Apache Airflow Providers
   
   _No response_
   
   ### Official Helm Chart version
   
   Not Applicable
   
   ### Kubernetes Version
   
   _No response_
   
   ### Helm Chart configuration
   
   _No response_
   
   ### Docker Image customizations
   
   _No response_
   
   ### Anything else?
   
   This happens every time an asset-triggered run consumes a large number of 
events, which is common after a long pause of an asset-scheduled DAG whose 
upstream keeps producing events. Runs that consume only a few events are not 
noticeably affected.
   
   ### Are you willing to submit PR?
   
   - [x] Yes I am willing to submit a PR!
   
   ### Code of Conduct
   
   - [x] I agree to follow this project's [Code of 
Conduct](https://github.com/apache/airflow/blob/main/CODE_OF_CONDUCT.md)
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to