Skip to content

Avoid unbounded IN lists for queued asset events - #75943

Merged
uranusjr merged 1 commit into
apache:mainfrom
astronomer:fix-adrq-unbounded-in
Oct 29, 2026
Merged

uranusjr merged 1 commit into
apache:mainfrom
astronomer:fix-adrq-unbounded-in

Conversation

@uranusjr

@uranusjr uranusjr commented Oct 29, 2026 •

Copy link
Copy Markdown
Member

Fix issue mentioned in #75865 (comment)

Each asset_dag_run_queue row now references a single asset event, so a Dag can have any number of queued rows. The scheduler sent them in a literal IN, and the statement would exceed database limits on bind parameters or expression depth if there are too many rows.

Those queries now use subqueries bounded by the largest event id so the query’s bind parameter count is constant.

As a result, a run may consume queued events whose rows were skipped by SKIP LOCKED if their ids are within the bound. Since only one scheduler can hold an event row (the Dag row itself is locked), this should only affect rows being dequeued concurrently elsewhere (e.g. with the API). Even in this scenario, the end result should still be correct, and the scheduler's existing retry mechanism should recover potential deadlock issues.

@uranusjr uranusjr added this to the Airflow 3.4.0 milestone Oct 29, 2026
@uranusjr uranusjr added the backport-to-v3-4-test Backport to v3-4-test label Oct 29, 2026
@uranusjr
uranusjr requested review from Lee-W and ashb as code owners October 29, 2026 07:55
@uranusjr
uranusjr requested a review from XD-DENG as a code owner October 29, 2026 07:55
Each asset_dag_run_queue row now references a single asset event, so
a Dag can have any number of queued rows. The scheduler sent them in
a literal IN, and the statement would exceed database limits on bind
parameters or expression depth if there are too many rows.

Those queries now use subqueries bounded by the largest event id so the
query's bind parameter count is constant.

As a result, a run may consume queued events whose rows were skipped
by SKIP LOCKED if their ids are within the bound. Since only one
scheduler can hold an event row (the Dag row itself is locked), this
should only affect rows being dequeued concurrently elsewhere (e.g.
with the API). Even in this scenario, the end result should still be
correct, and the scheduler's existing retry mechanism should recover
potential deadlock issues.
@uranusjr
uranusjr force-pushed the fix-adrq-unbounded-in branch from 6ad02ca to d69dccb Compare October 29, 2026 08:27
@uranusjr
uranusjr merged commit 20e4e57 into apache:main Oct 29, 2026
70 checks passed
@uranusjr
uranusjr deleted the fix-adrq-unbounded-in branch October 29, 2026 09:25
@github-actions

Copy link
Copy Markdown
Contributor

Backport successfully created: v3-4-test

Note: As of Merging PRs targeted for Airflow 3.X
the committer who merges the PR is responsible for backporting the PRs that are bug fixes (generally speaking) to the maintenance branches.

In matter of doubt please ask in #release-management Slack channel.

Status Branch Result
✅ v3-4-test PR Link

Lee-W pushed a commit that referenced this pull request Oct 29, 2026
"""
stmt = delete(AssetDagRunQueue).where(
AssetDagRunQueue.target_dag_id == dag_id,
AssetDagRunQueue.asset_event_id <= max_event_id,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This range now also matches rows SKIP LOCKED skipped, and a DELETE waits on a locked row instead of skipping it. If one of the queuedEvents DELETE endpoints has locked row A and is waiting on a row this scheduler holds, this statement reaching A closes the cycle. A Postgres repro with the timing forced by a sleep gets DeadlockDetected at this head, while on the base commit the API call just waits. The description says the scheduler's retry recovers, but Postgres aborts whichever backend runs the deadlock check first, which can be the API request, and that request gets a 500 with no retry. Could the DELETE be limited to rows this transaction locked, e.g. asset_event_id IN (SELECT ... WHERE target_dag_id = :d AND asset_event_id <= :max FOR UPDATE SKIP LOCKED)? The parameter count stays constant, though MySQL needs a derived table for the self-reference.

Select unconsumed events referenced by a Dag's locked ADRQ rows.

*max_event_id* is the largest event ID among the rows locked by this
scheduler, so events queued after the lock was taken are left for the

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think the bound guarantees this. With catchup=True the two OR'd arms below have no id bound, so they still pick up events queued after the lock. And event ids aren't assigned in commit order, so on Postgres a lower id can commit after the lock and still fall inside the bound. Both behave the same as the old code, so it's only the docstring (and the max_event_id param doc on _delete_consumed_asset_records) that overpromises.

pytest.param(1, {0, 1}, set(), id="skipped-row-older"),
],
)
def test_create_dag_runs_asset_triggered_deletes_only_selected_adrq_rows(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the skipped-row-older case the skipped row is consumed and dequeued too, so "deletes only selected" no longer describes what this asserts. Maybe rename it to something like ..._consumes_skipped_rows_within_bound?

def test_create_dagruns_asset_triggered_many_queued_events_no_literal_in(
session: Session, dag_maker, batch_asset_events: bool
):
"""Queued events must not be sent to the database as a literal list (#75865)."""

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we drop the (#75865) here and in the docstring on line 13134? The test name and docstring already describe the behaviour, and the PR links the issue.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants