Fix asset event scheduling race condition and consistency - #62501

Merged
Lee-W merged 8 commits into
apache:mainfrom
dingo4dev:fix/asset-triggered-dag-run
Jul 29, 2026
Merged

Fix asset event scheduling race condition and consistency#62501
Lee-W merged 8 commits into
apache:mainfrom
dingo4dev:fix/asset-triggered-dag-run

Conversation

@dingo4dev

@dingo4devdingo4dev commented Feb 26, 2026

Copy link
Copy Markdown
Contributor

This PR addresses a race condition in the Asset-based scheduling logic where AssetEvent records created by concurrent tasks can be "missed" or "orphaned" by the Scheduler.

The issue stems from the Visibility Gap between a database flush() (which generates the created_at timestamp) and the final commit() (which makes the record visible to other sessions).

This change ensures that the synchronization between the AssetEvent and the AssetDagRunQueue is atomic and that the timestamps are aligned to prevent the drift caused by performance issue.

  • Manually assigns the same timestamp to both the AssetEvent and the ADRQ before the flush.
  • Commit and populate the asset event record in database immediately, so that the event can be accessible by job scheduler
  • Prevent creating dag by empty asset event as the asset event is trigger in prev_dag_run

related: #54659, #56750, #56749

---
config:
theme: redux-dark-color
look: neo
---
sequenceDiagram
participant J1 as Task Instance 1
participant J2 as Task Instance 2
participant JS as Job Scheduler
participant DB as Database
Note over J1,DB: T0
Note left of J1: T1
J1-->>DB: Asset Event 1 (AE1) & commit
activate J1
activate DB
Note left of J1: T2
J2-->>DB: Asset Event 2 (AE2) & commit
activate J2
activate DB
Note left of J1: T3
J2-->>-DB: Asset Dag Run Queue (ADRQ)<br/>created_at = AE2.timestamp<br/>(flush & commit)
deactivate DB
Note left of J1: T4
activate JS
activate DB
JS-->>DB: Fetch created_at from ADRQ
JS-->>DB: Fetch events (AE1, AE2)
JS->>JS: Create DAG with run_after = ADRQ created_at (AE2.timestamp)
deactivate DB
deactivate JS
Note left of J1: T5
J1-->>DB: Asset Dag Run Queue (ADRQ)<br/>created_at = AE1.timestamp<br/>(flush & commit)
deactivate J1
deactivate DB
Note left of J1: T6
activate JS
activate DB
JS-->>DB: Fetch created_at from ADRQ
JS->>JS: No event found<br/>(AE1.timestamp < prev DAG run_after)
deactivate DB
deactivate JS
Loading

Durability tradeoff (new failure mode)

On Postgres/MySQL the AssetEvent is committed in a short-lived independent session so it is visible to the scheduler before the producing task's transaction commits the alias association, AssetDagRunQueue rows, and TI state change. This is intentional and required to fix the scheduling race. The tradeoff is that a failure after the event is committed but before the caller's transaction commits can leave:

  • an orphaned asset event — persisted with no AssetDagRunQueue row for the scheduler to consume, or
  • a duplicate asset event — if the producing task retries register_asset_change.

Operators should be aware of this new failure mode when diagnosing missing or duplicate asset-triggered Dag runs. This is also documented in the newsfragment for this change.

Was generative AI tooling used to co-author this PR?
  • Yes (please specify the tool below)
    Copilot

Open with GitKraken

Important

🛠️ Maintainer triage note for @dingo4dev · by @potiuk · 2026-06-17 14:51 UTC

Helpful heads-up from the maintainers — please address before this PR can be reviewed:

  • Static / docs checks failing (CI image checks / Static checks). Run them locally with prek run --all-files (or pre-commit run --all-files) and push the fixes.
  • Failing test jobs: MySQL tests: core / DB-core:MySQL:8.0:3.10:Core...Serialization. Reproduce and fix locally, then push.
  • See the Pull Request quality criteria.

The ball is in your court — you've been assigned to this PR. Fix the above, then mark it Ready for review.

Automated triage — may be imperfect; a maintainer takes the next look.

CopilotAI review requested due to automatic review settings February 26, 2026 04:41
@boring-cyborgboring-cyborgBot added the area:Scheduler including HA (high availability) scheduler label Feb 26, 2026
@dingo4dev
dingo4devforce-pushed the fix/asset-triggered-dag-run branch from 11ed83d to 624e1eaCompareFebruary 26, 2026 04:41
@dingo4devdingo4dev changed the title [WIP] Fix asset event scheduling race conditionFix asset event scheduling race conditionFeb 26, 2026

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR attempts to fix a race condition in asset-based scheduling where AssetEvent records created by concurrent tasks can be missed by the Scheduler due to a visibility gap between database flush() and commit() operations. The fix synchronizes timestamps between AssetEvent and AssetDagRunQueue records by manually assigning timestamps and committing the asset event immediately.

Changes:

  • Modified asset event registration to commit immediately after creating AssetEvent, ensuring early visibility to the scheduler
  • Updated AssetDagRunQueue creation to use the asset event's timestamp for synchronization across both PostgreSQL and slow-path implementations
  • Changed scheduler logic to conditionally create DAG runs only when asset events exist, and to delete ADRQ entries more selectively based on timestamps

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 7 comments.

FileDescription
airflow-core/src/airflow/jobs/scheduler_job_runner.pyModified DAG run creation to be conditional on asset_events existence and updated ADRQ deletion to filter by timestamp
airflow-core/src/airflow/assets/manager.pyChanged from session.flush() to session.commit() for early AssetEvent persistence and updated ADRQ timestamp synchronization logic

Comment threadairflow-core/src/airflow/assets/manager.py Outdated
Comment threadairflow-core/src/airflow/assets/manager.py
Comment threadairflow-core/src/airflow/assets/manager.py Outdated
Comment threadairflow-core/src/airflow/assets/manager.py Outdated
Comment threadairflow-core/src/airflow/jobs/scheduler_job_runner.py Outdated
Comment threadairflow-core/src/airflow/assets/manager.py Outdated
Comment threadairflow-core/src/airflow/assets/manager.py
@dingo4dev
dingo4devforce-pushed the fix/asset-triggered-dag-run branch 4 times, most recently from 2308c0b to 0019359CompareFebruary 27, 2026 02:42
@dingo4devdingo4dev changed the title Fix asset event scheduling race conditionFix asset event scheduling race condition and consistencyFeb 27, 2026
@dingo4dev
dingo4devforce-pushed the fix/asset-triggered-dag-run branch from 0019359 to e796ff5CompareMarch 5, 2026 14:17
@dingo4dev
dingo4devforce-pushed the fix/asset-triggered-dag-run branch from e796ff5 to bcdd0c5CompareMarch 6, 2026 02:03
@dingo4dev

Copy link
Copy Markdown
ContributorAuthor

@Lee-W@uranusjr Please take a time to review it for me. Thanks a lot ;p

joseotaviorf added a commit to joseotaviorf/airflow that referenced this pull request Mar 12, 2026
…rt of apache#62501)
Backports three targeted fixes to 2.10.4 to address the race condition reported
in GH#56541 and GH#41101, where a dataset-dependent DAG fires with one or more
upstream dataset events missing from consumed_dataset_events.
Root cause: stale `created_at` timestamps in DatasetDagRunQueue caused the
scheduler's event-window query to exclude valid events, yet the presence-based
readiness check still considered the DAG ready to run.
Changes:
- manager.py: replace ON CONFLICT DO NOTHING with ON CONFLICT DO UPDATE
(Postgres) and add a WHERE guard to prevent backwards timestamp drift;
pass explicit created_at=utcnow() on the non-Postgres merge path so
existing rows are always refreshed.
- scheduler_job_runner.py: wrap create_dagrun in `if dataset_events:` to
prevent phantom runs when the event window is empty; scope the DDRQ
DELETE to `created_at <= exec_date` instead of deleting all rows, so
events that arrived during processing are preserved for the next cycle.
- tests/datasets/test_manager.py: add WHERE-guard regression test for the
Postgres upsert path.
- dev/BUG_REPORT_DATASET_SCHEDULING.md: detailed bug report with timeline
diagrams, related issues (apache#56541, apache#41101, apache#35870), root cause analysis,
and explanation of each fix.
- reproduce_bug_56541.py: end-to-end reproduction and verification script.
Made-with: Cursor
@dingo4dev
dingo4devforce-pushed the fix/asset-triggered-dag-run branch from bcdd0c5 to c7dae2dCompareMarch 17, 2026 06:36
@Lee-W

Copy link
Copy Markdown
Member

will try to take a look this week

joseotaviorf added a commit to joseotaviorf/airflow that referenced this pull request Mar 19, 2026
…rt of apache#62501)
Backports three targeted fixes to 2.10.4 to address the race condition reported
in GH#56541 and GH#41101, where a dataset-dependent DAG fires with one or more
upstream dataset events missing from consumed_dataset_events.
Root cause: stale `created_at` timestamps in DatasetDagRunQueue caused the
scheduler's event-window query to exclude valid events, yet the presence-based
readiness check still considered the DAG ready to run.
Changes:
- manager.py: replace ON CONFLICT DO NOTHING with ON CONFLICT DO UPDATE
(Postgres) and add a WHERE guard to prevent backwards timestamp drift;
pass explicit created_at=utcnow() on the non-Postgres merge path so
existing rows are always refreshed.
- scheduler_job_runner.py: wrap create_dagrun in `if dataset_events:` to
prevent phantom runs when the event window is empty; scope the DDRQ
DELETE to `created_at <= exec_date` instead of deleting all rows, so
events that arrived during processing are preserved for the next cycle.
- tests/datasets/test_manager.py: add WHERE-guard regression test for the
Postgres upsert path.
- dev/BUG_REPORT_DATASET_SCHEDULING.md: detailed bug report with timeline
diagrams, related issues (apache#56541, apache#41101, apache#35870), root cause analysis,
and explanation of each fix.
- reproduce_bug_56541.py: end-to-end reproduction and verification script.

@kaxilkaxil left a comment

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.

Left a few comments

Comment threadairflow-core/src/airflow/assets/manager.py Outdated
Comment threadairflow-core/src/airflow/assets/manager.py
Comment threadairflow-core/src/airflow/jobs/scheduler_job_runner.py Outdated
Comment threadairflow-core/tests/unit/jobs/test_scheduler_job.py Outdated
Comment threadairflow-core/tests/unit/jobs/test_scheduler_job.py
@dingo4dev
dingo4devforce-pushed the fix/asset-triggered-dag-run branch 2 times, most recently from e674f0f to 1a08e6fCompareMarch 22, 2026 19:58
@dingo4dev
dingo4devforce-pushed the fix/asset-triggered-dag-run branch 2 times, most recently from 3a647bf to f0d5a65CompareMarch 29, 2026 16:48
@kaxil
kaxil requested a review from CopilotApril 2, 2026 01:03
Comment threadairflow-core/src/airflow/assets/manager.py Outdated
Comment threadairflow-core/src/airflow/assets/manager.py Outdated
Comment threadairflow-core/src/airflow/assets/manager.py
@molcay

Copy link
Copy Markdown
Contributor

Hi @dingo4dev, I wanted to ping you here if you missed my message on Slack.

I am repeating my message and offer for help in here as well:

Hi @Stanley Law, hope you're having a good week!

I have been following your PR, it seems great stuff and thanks for tackling this. I know bouncing between reviews and CI pipelines can take up a lot of time. If you're feeling swamped or just want someone to bounce ideas off of (since I am having trouble reproducing the issue), I'd be glad to step in and help in way that I can. If you've got it fully under control though, totally feel free to ignore this message 🙂
Just wanted to offer some support.

@dingo4dev

Copy link
Copy Markdown
ContributorAuthor

Hi @molcay, Thank you so much for checking in and offering help!
Some extra eyes would be amazing. Right now I am investing the failure ci test on Mysql, the concurrent tests sometimes failure and orphan few asset event, and I can't reproduce it in my local tho.
I had logged the asset event and dag run with id and timestamp.

_______________________________________________________________TestSchedulerJob.test_create_dag_runs_when_concurrent_asset_events_created_______________________________________________________________airflow-core/tests/unit/jobs/test_scheduler_job.py:5792: intest_create_dag_runs_when_concurrent_asset_events_createdasserttotal_consumed_asset_events==ASSET_EVENT_COUNTEassert29==30--------------------------------------------Capturedstdoutsetup-----------------------------------------
] FillinguptheDagBagfrom/dev/null [airflow.dag_processing.dagbag.DagBag]
--------------------------------------------Capturedstdoutcall------------------------------------------MissingAssetEventMetadata: {(16, '2026-06-23T05:02:05.983645+00:00')}
{'event_': (8, '2026-06-23T05:02:05.967063+00:00'), 'dagrun': (434, '2026-06-23T05:02:05.989838+00:00')}
{'event_': (9, '2026-06-23T05:02:05.972418+00:00'), 'dagrun': (434, '2026-06-23T05:02:05.989838+00:00')}
{'event_': (13, '2026-06-23T05:02:05.982971+00:00'), 'dagrun': (436, '2026-06-23T05:02:05.982971+00:00')}
{'event_': (16, '2026-06-23T05:02:05.983645+00:00'), 'dagrun': ()}
{'event_': (10, '2026-06-23T05:02:05.989838+00:00'), 'dagrun': (434, '2026-06-23T05:02:05.989838+00:00')}
{'event_': (15, '2026-06-23T05:02:05.994289+00:00'), 'dagrun': (435, '2026-06-23T05:02:06.034978+00:00')}
{'event_': (14, '2026-06-23T05:02:05.995763+00:00'), 'dagrun': (435, '2026-06-23T05:02:06.034978+00:00')}
{'event_': (11, '2026-06-23T05:02:06.025005+00:00'), 'dagrun': (435, '2026-06-23T05:02:06.034978+00:00')}
{'event_': (12, '2026-06-23T05:02:06.034978+00:00'), 'dagrun': (435, '2026-06-23T05:02:06.034978+00:00')}
{'event_': (18, '2026-06-23T05:02:06.059185+00:00'), 'dagrun': (437, '2026-06-23T05:02:07.001801+00:00')}
{'event_': (17, '2026-06-23T05:02:06.060623+00:00'), 'dagrun': (437, '2026-06-23T05:02:07.001801+00:00')}
{'event_': (19, '2026-06-23T05:02:06.099119+00:00'), 'dagrun': (437, '2026-06-23T05:02:07.001801+00:00')}
{'event_': (20, '2026-06-23T05:02:07.001801+00:00'), 'dagrun': (437, '2026-06-23T05:02:07.001801+00:00')}
{'event_': (21, '2026-06-23T05:02:07.022033+00:00'), 'dagrun': (438, '2026-06-23T05:02:07.090158+00:00')}
{'event_': (22, '2026-06-23T05:02:07.083870+00:00'), 'dagrun': (438, '2026-06-23T05:02:07.090158+00:00')}
{'event_': (23, '2026-06-23T05:02:07.090158+00:00'), 'dagrun': (438, '2026-06-23T05:02:07.090158+00:00')}
{'event_': (24, '2026-06-23T05:02:07.097476+00:00'), 'dagrun': (439, '2026-06-23T05:02:08.046920+00:00')}
{'event_': (25, '2026-06-23T05:02:07.116000+00:00'), 'dagrun': (439, '2026-06-23T05:02:08.046920+00:00')}
{'event_': (26, '2026-06-23T05:02:08.046920+00:00'), 'dagrun': (439, '2026-06-23T05:02:08.046920+00:00')}
{'event_': (27, '2026-06-23T05:02:08.054838+00:00'), 'dagrun': (440, '2026-06-23T05:02:08.092453+00:00')}
{'event_': (28, '2026-06-23T05:02:08.067990+00:00'), 'dagrun': (440, '2026-06-23T05:02:08.092453+00:00')}
{'event_': (29, '2026-06-23T05:02:08.092453+00:00'), 'dagrun': (440, '2026-06-23T05:02:08.092453+00:00')}
{'event_': (30, '2026-06-23T05:02:08.094004+00:00'), 'dagrun': (441, '2026-06-23T05:02:08.125311+00:00')}
{'event_': (31, '2026-06-23T05:02:08.118823+00:00'), 'dagrun': (441, '2026-06-23T05:02:08.125311+00:00')}
{'event_': (32, '2026-06-23T05:02:08.125311+00:00'), 'dagrun': (441, '2026-06-23T05:02:08.125311+00:00')}
{'event_': (33, '2026-06-23T05:02:08.127450+00:00'), 'dagrun': (442, '2026-06-23T05:02:09.073822+00:00')}
{'event_': (34, '2026-06-23T05:02:08.154315+00:00'), 'dagrun': (442, '2026-06-23T05:02:09.073822+00:00')}
{'event_': (35, '2026-06-23T05:02:09.073822+00:00'), 'dagrun': (442, '2026-06-23T05:02:09.073822+00:00')}
{'event_': (36, '2026-06-23T05:02:09.091265+00:00'), 'dagrun': (443, '2026-06-23T05:02:09.091265+00:00')}
{'event_': (37, '2026-06-23T05:02:09.118804+00:00'), 'dagrun': (444, '2026-06-23T05:02:09.118804+00:00')}

Comment threadairflow-core/tests/unit/jobs/test_scheduler_job.py Outdated
Comment threadairflow-core/tests/unit/jobs/test_scheduler_job.py Outdated
@molcay

molcay commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Hi @molcay, Thank you so much for checking in and offering help! Some extra eyes would be amazing. Right now I am investing the failure ci test on Mysql, the concurrent tests sometimes failure and orphan few asset event, and I can't reproduce it in my local tho. I had logged the asset event and dag run with id and timestamp.

_______________________________________________________________TestSchedulerJob.test_create_dag_runs_when_concurrent_asset_events_created_______________________________________________________________airflow-core/tests/unit/jobs/test_scheduler_job.py:5792: intest_create_dag_runs_when_concurrent_asset_events_createdasserttotal_consumed_asset_events==ASSET_EVENT_COUNTEassert29==30

Hi @dingo4dev,
As I see that you already mark the PR as "ready for review" and you've already got an approval, I need to ask if you still need help on the matter?

@Lee-WLee-W left a comment

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.

a few nits, but overall looks good

Comment threadairflow-core/newsfragments/62501.significant.rst Outdated
Comment threadairflow-core/tests/unit/models/test_taskinstance.py Outdated
Comment threadairflow-core/tests/unit/models/test_taskinstance.py Outdated
Comment threadairflow-core/tests/unit/models/test_taskinstance.py Outdated
Comment threadairflow-core/tests/unit/jobs/test_scheduler_job.py Outdated
Comment threadairflow-core/src/airflow/assets/manager.py Outdated
@dingo4dev

dingo4dev commented Jul 7, 2026

Copy link
Copy Markdown
ContributorAuthor

Hi @dingo4dev, As I see that you already mark the PR as "ready for review" and you've already got an approval, I need to ask if you still need help on the matter?

Hi @molcay ! Thanks for following up.

Even though we got an approval, I have a feeling this might not completely catch every edge case of the underlying issue. we should definitely keep a close eye on it and monitor how it behaves. Thanks again for the support. 🙌

@VladaZakharova

Copy link
Copy Markdown
Contributor

Hi there!
Thank you for the work you are doing, really waiting for the changes to be merged and available <3

Comment threadairflow-core/src/airflow/assets/manager.py

@Lee-WLee-W left a comment

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.

a few nits. mostly good

Comment threadairflow-core/src/airflow/assets/manager.py
Comment threadairflow-core/src/airflow/assets/manager.py Outdated
Comment threadairflow-core/src/airflow/assets/manager.py
Comment threadairflow-core/tests/unit/assets/test_manager.py Outdated
Comment threadairflow-core/tests/unit/models/test_taskinstance.py
Comment threadairflow-core/tests/unit/models/test_taskinstance.py Outdated
@VladaZakharova

Copy link
Copy Markdown
Contributor

Looks like green and beautiful :)
Are we close to merge this change?

dingo4devand others added 8 commits July 29, 2026 14:17
Fixes an issue where the Scheduler misses AssetEvents due to a
visibility gap between flush() and commit().
When multiple tasks create events for the same asset, the AssetEvent
timestamp is generated during flush, but the record remains invisible
to the Scheduler until the final commit. If the AssetDagRunQueue is
processed in the interim, the 'late-committing' event is orphaned.
The job scheduler will read the AssetDagRunQueue and fetch the created_at column as the triggered_date which will use for fetching the asset event and assign dag `run_after`.
This change ensures timestamps are synchronized and the transaction
boundary is tightened to prevent data-aware scheduling misses.
Add check for asset events before creating asset-triggered DAG runs
This is to prevent double triggering when the late-commit asset_dag_run_queue is exist in entry but the asset event is already process in prev dag
Fix update statement condition in AssetDagRunQueue to use correct column reference
Add tests for concurrent asset events and no asset event scenarios in DAG run creation
linting tests
make mypy happy
Add check for existing asset DAG run queue entry before merge
Co-authored-by: Kaxil Naik <kaxilnaik@gmail.com>
add log when no dag run without asset event
Support concurrent asset event queuing on MySQL
Implement a MySQL-specific optimization using `ON DUPLICATE KEY UPDATE` when queuing asset-triggered DAG runs. This prevents unique constraint violations during concurrent asset events, mirroring the existing PostgreSQL implementation. The concurrency tests are also refactored to verify correct behavior across both database dialects.
Refactor asset event handling and improve session management in AssetManager
make lint happy
Refactor asset event handling to streamline session management in AssetManager
Re-attach asset_event to caller's session to prevent DetachedInstanceError
fix: prevent race condition causing lost events in asset-triggered DAG run creation
- Remove CTE lower-bound filter from asset event query: the bound excluded
late-committed events that arrived after a concurrent DagRun was created,
causing those events to be permanently lost and no follow-up DagRun created.
- Add 'not consumed' filter using association_table to prevent duplicate
consumption of asset events across concurrent DagRuns.
- Always delete ADRQ rows after processing, even when no new DagRun is
created, to prevent stale entries accumulating and causing infinite
scheduler loops.
- Fix concurrent test to use seen_dr_ids set instead of prev_dr reference,
ensuring all DagRuns are counted regardless of creation order.
fix: Persist AssetEvent in independent session for immediate visibility
Persist AssetEvent using a truly independent session (`scoped=False`)
to ensure it's committed and visible to processes like the Scheduler
before other transaction operations proceed. This closes a critical
visibility gap for Asset-Triggered DagRuns (ADRQ).
The event is then explicitly reloaded into the caller's session
to prevent DetachedInstanceError and allow subsequent relationship
operations to function correctly.
make static check happy
refactor: Consolidate `AssetEvent` persistence logic
Extract the complex logic for persisting `AssetEvent` instances into a new
dedicated class method, `create_asset_event`.
This centralizes the handling of distinct session management strategies
(independent session for non-SQLite, direct flush for SQLite) and ensures
immediate visibility of asset events to the scheduler. It also re-attaches
the event to the caller's session to prevent `DetachedInstanceError`.
fix: Ensure DagRun updates and AssetEvents are visible across transaction boundaries
Add `session.flush()` after `DagRun` partition key updates to ensure these changes are persisted within the caller's transaction before an independent session for `AssetEvent` creation begins. This prevents stale `DagRun` data from being read.
Change `AssetEvent` creation to use `create_session(scoped=False)`, guaranteeing a truly independent session and immediate commit of the asset event. This ensures the event is visible to processes like the Scheduler for Asset-Triggered DagRuns (ADRQ) without transactional delays.
fix: Change log level from warning to info for DagRun creation status
apply copilot reviewer
test: Add backend markers for concurrent asset event tests
docs: Address asset event scheduling race condition by using independent session commits
Ensures the asset-triggered consumer DAG is registered in the session prior to creating the AssetEvent. This prevents potential test failures or incorrect behavior due to timing or visibility issues with DAG metadata, aligning the test setup with real-world scheduler requirements.
Per review feedback the change is not significant-tier; downgrade to a
bugfix newsfragment while keeping the operator-facing note about the
new duplicate/orphaned asset-event failure mode introduced by the
early-commit fix.
Reflects changes in asset manager's internal queueing helpers to standardize
conflict handling across dialects and to pass the AssetEvent context.
Co-authored-by: Copilot <copilot@github.com>
@Lee-W

Copy link
Copy Markdown
Member

Hey @dingo4dev , thanks for being so patient! We can merge this after the CI is green

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

Labels

area:Schedulerincluding HA (high availability) schedulerready for maintainer reviewSet after triaging when all criteria pass.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

8 participants

@dingo4dev@Lee-W@potiuk@molcay@VladaZakharova@uranusjr@kaxil
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Fix asset event scheduling race condition and consistency - #62501

Merged
Lee-W merged 8 commits into
apache:mainfrom
dingo4dev:fix/asset-triggered-dag-run
Jul 29, 2026
Merged

Fix asset event scheduling race condition and consistency#62501
Lee-W merged 8 commits into
apache:mainfrom
dingo4dev:fix/asset-triggered-dag-run

Conversation

@dingo4dev

@dingo4devdingo4dev commented Feb 26, 2026

Copy link
Copy Markdown
Contributor

This PR addresses a race condition in the Asset-based scheduling logic where AssetEvent records created by concurrent tasks can be "missed" or "orphaned" by the Scheduler.

The issue stems from the Visibility Gap between a database flush() (which generates the created_at timestamp) and the final commit() (which makes the record visible to other sessions).

This change ensures that the synchronization between the AssetEvent and the AssetDagRunQueue is atomic and that the timestamps are aligned to prevent the drift caused by performance issue.

  • Manually assigns the same timestamp to both the AssetEvent and the ADRQ before the flush.
  • Commit and populate the asset event record in database immediately, so that the event can be accessible by job scheduler
  • Prevent creating dag by empty asset event as the asset event is trigger in prev_dag_run

related: #54659, #56750, #56749

---
config:
theme: redux-dark-color
look: neo
---
sequenceDiagram
participant J1 as Task Instance 1
participant J2 as Task Instance 2
participant JS as Job Scheduler
participant DB as Database
Note over J1,DB: T0
Note left of J1: T1
J1-->>DB: Asset Event 1 (AE1) & commit
activate J1
activate DB
Note left of J1: T2
J2-->>DB: Asset Event 2 (AE2) & commit
activate J2
activate DB
Note left of J1: T3
J2-->>-DB: Asset Dag Run Queue (ADRQ)<br/>created_at = AE2.timestamp<br/>(flush & commit)
deactivate DB
Note left of J1: T4
activate JS
activate DB
JS-->>DB: Fetch created_at from ADRQ
JS-->>DB: Fetch events (AE1, AE2)
JS->>JS: Create DAG with run_after = ADRQ created_at (AE2.timestamp)
deactivate DB
deactivate JS
Note left of J1: T5
J1-->>DB: Asset Dag Run Queue (ADRQ)<br/>created_at = AE1.timestamp<br/>(flush & commit)
deactivate J1
deactivate DB
Note left of J1: T6
activate JS
activate DB
JS-->>DB: Fetch created_at from ADRQ
JS->>JS: No event found<br/>(AE1.timestamp < prev DAG run_after)
deactivate DB
deactivate JS
Loading

Durability tradeoff (new failure mode)

On Postgres/MySQL the AssetEvent is committed in a short-lived independent session so it is visible to the scheduler before the producing task's transaction commits the alias association, AssetDagRunQueue rows, and TI state change. This is intentional and required to fix the scheduling race. The tradeoff is that a failure after the event is committed but before the caller's transaction commits can leave:

  • an orphaned asset event — persisted with no AssetDagRunQueue row for the scheduler to consume, or
  • a duplicate asset event — if the producing task retries register_asset_change.

Operators should be aware of this new failure mode when diagnosing missing or duplicate asset-triggered Dag runs. This is also documented in the newsfragment for this change.

Was generative AI tooling used to co-author this PR?
  • Yes (please specify the tool below)
    Copilot

Open with GitKraken

Important

🛠️ Maintainer triage note for @dingo4dev · by @potiuk · 2026-06-17 14:51 UTC

Helpful heads-up from the maintainers — please address before this PR can be reviewed:

  • Static / docs checks failing (CI image checks / Static checks). Run them locally with prek run --all-files (or pre-commit run --all-files) and push the fixes.
  • Failing test jobs: MySQL tests: core / DB-core:MySQL:8.0:3.10:Core...Serialization. Reproduce and fix locally, then push.
  • See the Pull Request quality criteria.

The ball is in your court — you've been assigned to this PR. Fix the above, then mark it Ready for review.

Automated triage — may be imperfect; a maintainer takes the next look.

CopilotAI review requested due to automatic review settings February 26, 2026 04:41
@boring-cyborgboring-cyborgBot added the area:Scheduler including HA (high availability) scheduler label Feb 26, 2026
@dingo4dev
dingo4devforce-pushed the fix/asset-triggered-dag-run branch from 11ed83d to 624e1eaCompareFebruary 26, 2026 04:41
@dingo4devdingo4dev changed the title [WIP] Fix asset event scheduling race conditionFix asset event scheduling race conditionFeb 26, 2026

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR attempts to fix a race condition in asset-based scheduling where AssetEvent records created by concurrent tasks can be missed by the Scheduler due to a visibility gap between database flush() and commit() operations. The fix synchronizes timestamps between AssetEvent and AssetDagRunQueue records by manually assigning timestamps and committing the asset event immediately.

Changes:

  • Modified asset event registration to commit immediately after creating AssetEvent, ensuring early visibility to the scheduler
  • Updated AssetDagRunQueue creation to use the asset event's timestamp for synchronization across both PostgreSQL and slow-path implementations
  • Changed scheduler logic to conditionally create DAG runs only when asset events exist, and to delete ADRQ entries more selectively based on timestamps

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 7 comments.

FileDescription
airflow-core/src/airflow/jobs/scheduler_job_runner.pyModified DAG run creation to be conditional on asset_events existence and updated ADRQ deletion to filter by timestamp
airflow-core/src/airflow/assets/manager.pyChanged from session.flush() to session.commit() for early AssetEvent persistence and updated ADRQ timestamp synchronization logic

Comment threadairflow-core/src/airflow/assets/manager.py Outdated
Comment threadairflow-core/src/airflow/assets/manager.py
Comment threadairflow-core/src/airflow/assets/manager.py Outdated
Comment threadairflow-core/src/airflow/assets/manager.py Outdated
Comment threadairflow-core/src/airflow/jobs/scheduler_job_runner.py Outdated
Comment threadairflow-core/src/airflow/assets/manager.py Outdated
Comment threadairflow-core/src/airflow/assets/manager.py
@dingo4dev
dingo4devforce-pushed the fix/asset-triggered-dag-run branch 4 times, most recently from 2308c0b to 0019359CompareFebruary 27, 2026 02:42
@dingo4devdingo4dev changed the title Fix asset event scheduling race conditionFix asset event scheduling race condition and consistencyFeb 27, 2026
@dingo4dev
dingo4devforce-pushed the fix/asset-triggered-dag-run branch from 0019359 to e796ff5CompareMarch 5, 2026 14:17
@dingo4dev
dingo4devforce-pushed the fix/asset-triggered-dag-run branch from e796ff5 to bcdd0c5CompareMarch 6, 2026 02:03
@dingo4dev

Copy link
Copy Markdown
ContributorAuthor

@Lee-W@uranusjr Please take a time to review it for me. Thanks a lot ;p

joseotaviorf added a commit to joseotaviorf/airflow that referenced this pull request Mar 12, 2026
…rt of apache#62501)
Backports three targeted fixes to 2.10.4 to address the race condition reported
in GH#56541 and GH#41101, where a dataset-dependent DAG fires with one or more
upstream dataset events missing from consumed_dataset_events.
Root cause: stale `created_at` timestamps in DatasetDagRunQueue caused the
scheduler's event-window query to exclude valid events, yet the presence-based
readiness check still considered the DAG ready to run.
Changes:
- manager.py: replace ON CONFLICT DO NOTHING with ON CONFLICT DO UPDATE
(Postgres) and add a WHERE guard to prevent backwards timestamp drift;
pass explicit created_at=utcnow() on the non-Postgres merge path so
existing rows are always refreshed.
- scheduler_job_runner.py: wrap create_dagrun in `if dataset_events:` to
prevent phantom runs when the event window is empty; scope the DDRQ
DELETE to `created_at <= exec_date` instead of deleting all rows, so
events that arrived during processing are preserved for the next cycle.
- tests/datasets/test_manager.py: add WHERE-guard regression test for the
Postgres upsert path.
- dev/BUG_REPORT_DATASET_SCHEDULING.md: detailed bug report with timeline
diagrams, related issues (apache#56541, apache#41101, apache#35870), root cause analysis,
and explanation of each fix.
- reproduce_bug_56541.py: end-to-end reproduction and verification script.
Made-with: Cursor
@dingo4dev
dingo4devforce-pushed the fix/asset-triggered-dag-run branch from bcdd0c5 to c7dae2dCompareMarch 17, 2026 06:36
@Lee-W

Copy link
Copy Markdown
Member

will try to take a look this week

joseotaviorf added a commit to joseotaviorf/airflow that referenced this pull request Mar 19, 2026
…rt of apache#62501)
Backports three targeted fixes to 2.10.4 to address the race condition reported
in GH#56541 and GH#41101, where a dataset-dependent DAG fires with one or more
upstream dataset events missing from consumed_dataset_events.
Root cause: stale `created_at` timestamps in DatasetDagRunQueue caused the
scheduler's event-window query to exclude valid events, yet the presence-based
readiness check still considered the DAG ready to run.
Changes:
- manager.py: replace ON CONFLICT DO NOTHING with ON CONFLICT DO UPDATE
(Postgres) and add a WHERE guard to prevent backwards timestamp drift;
pass explicit created_at=utcnow() on the non-Postgres merge path so
existing rows are always refreshed.
- scheduler_job_runner.py: wrap create_dagrun in `if dataset_events:` to
prevent phantom runs when the event window is empty; scope the DDRQ
DELETE to `created_at <= exec_date` instead of deleting all rows, so
events that arrived during processing are preserved for the next cycle.
- tests/datasets/test_manager.py: add WHERE-guard regression test for the
Postgres upsert path.
- dev/BUG_REPORT_DATASET_SCHEDULING.md: detailed bug report with timeline
diagrams, related issues (apache#56541, apache#41101, apache#35870), root cause analysis,
and explanation of each fix.
- reproduce_bug_56541.py: end-to-end reproduction and verification script.

@kaxilkaxil left a comment

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.

Left a few comments

Comment threadairflow-core/src/airflow/assets/manager.py Outdated
Comment threadairflow-core/src/airflow/assets/manager.py
Comment threadairflow-core/src/airflow/jobs/scheduler_job_runner.py Outdated
Comment threadairflow-core/tests/unit/jobs/test_scheduler_job.py Outdated
Comment threadairflow-core/tests/unit/jobs/test_scheduler_job.py
@dingo4dev
dingo4devforce-pushed the fix/asset-triggered-dag-run branch 2 times, most recently from e674f0f to 1a08e6fCompareMarch 22, 2026 19:58
@dingo4dev
dingo4devforce-pushed the fix/asset-triggered-dag-run branch 2 times, most recently from 3a647bf to f0d5a65CompareMarch 29, 2026 16:48
@kaxil
kaxil requested a review from CopilotApril 2, 2026 01:03
Comment threadairflow-core/src/airflow/assets/manager.py Outdated
Comment threadairflow-core/src/airflow/assets/manager.py Outdated
Comment threadairflow-core/src/airflow/assets/manager.py
@molcay

Copy link
Copy Markdown
Contributor

Hi @dingo4dev, I wanted to ping you here if you missed my message on Slack.

I am repeating my message and offer for help in here as well:

Hi @Stanley Law, hope you're having a good week!

I have been following your PR, it seems great stuff and thanks for tackling this. I know bouncing between reviews and CI pipelines can take up a lot of time. If you're feeling swamped or just want someone to bounce ideas off of (since I am having trouble reproducing the issue), I'd be glad to step in and help in way that I can. If you've got it fully under control though, totally feel free to ignore this message 🙂
Just wanted to offer some support.

@dingo4dev

Copy link
Copy Markdown
ContributorAuthor

Hi @molcay, Thank you so much for checking in and offering help!
Some extra eyes would be amazing. Right now I am investing the failure ci test on Mysql, the concurrent tests sometimes failure and orphan few asset event, and I can't reproduce it in my local tho.
I had logged the asset event and dag run with id and timestamp.

_______________________________________________________________TestSchedulerJob.test_create_dag_runs_when_concurrent_asset_events_created_______________________________________________________________airflow-core/tests/unit/jobs/test_scheduler_job.py:5792: intest_create_dag_runs_when_concurrent_asset_events_createdasserttotal_consumed_asset_events==ASSET_EVENT_COUNTEassert29==30--------------------------------------------Capturedstdoutsetup-----------------------------------------
] FillinguptheDagBagfrom/dev/null [airflow.dag_processing.dagbag.DagBag]
--------------------------------------------Capturedstdoutcall------------------------------------------MissingAssetEventMetadata: {(16, '2026-06-23T05:02:05.983645+00:00')}
{'event_': (8, '2026-06-23T05:02:05.967063+00:00'), 'dagrun': (434, '2026-06-23T05:02:05.989838+00:00')}
{'event_': (9, '2026-06-23T05:02:05.972418+00:00'), 'dagrun': (434, '2026-06-23T05:02:05.989838+00:00')}
{'event_': (13, '2026-06-23T05:02:05.982971+00:00'), 'dagrun': (436, '2026-06-23T05:02:05.982971+00:00')}
{'event_': (16, '2026-06-23T05:02:05.983645+00:00'), 'dagrun': ()}
{'event_': (10, '2026-06-23T05:02:05.989838+00:00'), 'dagrun': (434, '2026-06-23T05:02:05.989838+00:00')}
{'event_': (15, '2026-06-23T05:02:05.994289+00:00'), 'dagrun': (435, '2026-06-23T05:02:06.034978+00:00')}
{'event_': (14, '2026-06-23T05:02:05.995763+00:00'), 'dagrun': (435, '2026-06-23T05:02:06.034978+00:00')}
{'event_': (11, '2026-06-23T05:02:06.025005+00:00'), 'dagrun': (435, '2026-06-23T05:02:06.034978+00:00')}
{'event_': (12, '2026-06-23T05:02:06.034978+00:00'), 'dagrun': (435, '2026-06-23T05:02:06.034978+00:00')}
{'event_': (18, '2026-06-23T05:02:06.059185+00:00'), 'dagrun': (437, '2026-06-23T05:02:07.001801+00:00')}
{'event_': (17, '2026-06-23T05:02:06.060623+00:00'), 'dagrun': (437, '2026-06-23T05:02:07.001801+00:00')}
{'event_': (19, '2026-06-23T05:02:06.099119+00:00'), 'dagrun': (437, '2026-06-23T05:02:07.001801+00:00')}
{'event_': (20, '2026-06-23T05:02:07.001801+00:00'), 'dagrun': (437, '2026-06-23T05:02:07.001801+00:00')}
{'event_': (21, '2026-06-23T05:02:07.022033+00:00'), 'dagrun': (438, '2026-06-23T05:02:07.090158+00:00')}
{'event_': (22, '2026-06-23T05:02:07.083870+00:00'), 'dagrun': (438, '2026-06-23T05:02:07.090158+00:00')}
{'event_': (23, '2026-06-23T05:02:07.090158+00:00'), 'dagrun': (438, '2026-06-23T05:02:07.090158+00:00')}
{'event_': (24, '2026-06-23T05:02:07.097476+00:00'), 'dagrun': (439, '2026-06-23T05:02:08.046920+00:00')}
{'event_': (25, '2026-06-23T05:02:07.116000+00:00'), 'dagrun': (439, '2026-06-23T05:02:08.046920+00:00')}
{'event_': (26, '2026-06-23T05:02:08.046920+00:00'), 'dagrun': (439, '2026-06-23T05:02:08.046920+00:00')}
{'event_': (27, '2026-06-23T05:02:08.054838+00:00'), 'dagrun': (440, '2026-06-23T05:02:08.092453+00:00')}
{'event_': (28, '2026-06-23T05:02:08.067990+00:00'), 'dagrun': (440, '2026-06-23T05:02:08.092453+00:00')}
{'event_': (29, '2026-06-23T05:02:08.092453+00:00'), 'dagrun': (440, '2026-06-23T05:02:08.092453+00:00')}
{'event_': (30, '2026-06-23T05:02:08.094004+00:00'), 'dagrun': (441, '2026-06-23T05:02:08.125311+00:00')}
{'event_': (31, '2026-06-23T05:02:08.118823+00:00'), 'dagrun': (441, '2026-06-23T05:02:08.125311+00:00')}
{'event_': (32, '2026-06-23T05:02:08.125311+00:00'), 'dagrun': (441, '2026-06-23T05:02:08.125311+00:00')}
{'event_': (33, '2026-06-23T05:02:08.127450+00:00'), 'dagrun': (442, '2026-06-23T05:02:09.073822+00:00')}
{'event_': (34, '2026-06-23T05:02:08.154315+00:00'), 'dagrun': (442, '2026-06-23T05:02:09.073822+00:00')}
{'event_': (35, '2026-06-23T05:02:09.073822+00:00'), 'dagrun': (442, '2026-06-23T05:02:09.073822+00:00')}
{'event_': (36, '2026-06-23T05:02:09.091265+00:00'), 'dagrun': (443, '2026-06-23T05:02:09.091265+00:00')}
{'event_': (37, '2026-06-23T05:02:09.118804+00:00'), 'dagrun': (444, '2026-06-23T05:02:09.118804+00:00')}

Comment threadairflow-core/tests/unit/jobs/test_scheduler_job.py Outdated
Comment threadairflow-core/tests/unit/jobs/test_scheduler_job.py Outdated
@molcay

molcay commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Hi @molcay, Thank you so much for checking in and offering help! Some extra eyes would be amazing. Right now I am investing the failure ci test on Mysql, the concurrent tests sometimes failure and orphan few asset event, and I can't reproduce it in my local tho. I had logged the asset event and dag run with id and timestamp.

_______________________________________________________________TestSchedulerJob.test_create_dag_runs_when_concurrent_asset_events_created_______________________________________________________________airflow-core/tests/unit/jobs/test_scheduler_job.py:5792: intest_create_dag_runs_when_concurrent_asset_events_createdasserttotal_consumed_asset_events==ASSET_EVENT_COUNTEassert29==30

Hi @dingo4dev,
As I see that you already mark the PR as "ready for review" and you've already got an approval, I need to ask if you still need help on the matter?

@Lee-WLee-W left a comment

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.

a few nits, but overall looks good

Comment threadairflow-core/newsfragments/62501.significant.rst Outdated
Comment threadairflow-core/tests/unit/models/test_taskinstance.py Outdated
Comment threadairflow-core/tests/unit/models/test_taskinstance.py Outdated
Comment threadairflow-core/tests/unit/models/test_taskinstance.py Outdated
Comment threadairflow-core/tests/unit/jobs/test_scheduler_job.py Outdated
Comment threadairflow-core/src/airflow/assets/manager.py Outdated
@dingo4dev

dingo4dev commented Jul 7, 2026

Copy link
Copy Markdown
ContributorAuthor

Hi @dingo4dev, As I see that you already mark the PR as "ready for review" and you've already got an approval, I need to ask if you still need help on the matter?

Hi @molcay ! Thanks for following up.

Even though we got an approval, I have a feeling this might not completely catch every edge case of the underlying issue. we should definitely keep a close eye on it and monitor how it behaves. Thanks again for the support. 🙌

@VladaZakharova

Copy link
Copy Markdown
Contributor

Hi there!
Thank you for the work you are doing, really waiting for the changes to be merged and available <3

Comment threadairflow-core/src/airflow/assets/manager.py

@Lee-WLee-W left a comment

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.

a few nits. mostly good

Comment threadairflow-core/src/airflow/assets/manager.py
Comment threadairflow-core/src/airflow/assets/manager.py Outdated
Comment threadairflow-core/src/airflow/assets/manager.py
Comment threadairflow-core/tests/unit/assets/test_manager.py Outdated
Comment threadairflow-core/tests/unit/models/test_taskinstance.py
Comment threadairflow-core/tests/unit/models/test_taskinstance.py Outdated
@VladaZakharova

Copy link
Copy Markdown
Contributor

Looks like green and beautiful :)
Are we close to merge this change?

dingo4devand others added 8 commits July 29, 2026 14:17
Fixes an issue where the Scheduler misses AssetEvents due to a
visibility gap between flush() and commit().
When multiple tasks create events for the same asset, the AssetEvent
timestamp is generated during flush, but the record remains invisible
to the Scheduler until the final commit. If the AssetDagRunQueue is
processed in the interim, the 'late-committing' event is orphaned.
The job scheduler will read the AssetDagRunQueue and fetch the created_at column as the triggered_date which will use for fetching the asset event and assign dag `run_after`.
This change ensures timestamps are synchronized and the transaction
boundary is tightened to prevent data-aware scheduling misses.
Add check for asset events before creating asset-triggered DAG runs
This is to prevent double triggering when the late-commit asset_dag_run_queue is exist in entry but the asset event is already process in prev dag
Fix update statement condition in AssetDagRunQueue to use correct column reference
Add tests for concurrent asset events and no asset event scenarios in DAG run creation
linting tests
make mypy happy
Add check for existing asset DAG run queue entry before merge
Co-authored-by: Kaxil Naik <kaxilnaik@gmail.com>
add log when no dag run without asset event
Support concurrent asset event queuing on MySQL
Implement a MySQL-specific optimization using `ON DUPLICATE KEY UPDATE` when queuing asset-triggered DAG runs. This prevents unique constraint violations during concurrent asset events, mirroring the existing PostgreSQL implementation. The concurrency tests are also refactored to verify correct behavior across both database dialects.
Refactor asset event handling and improve session management in AssetManager
make lint happy
Refactor asset event handling to streamline session management in AssetManager
Re-attach asset_event to caller's session to prevent DetachedInstanceError
fix: prevent race condition causing lost events in asset-triggered DAG run creation
- Remove CTE lower-bound filter from asset event query: the bound excluded
late-committed events that arrived after a concurrent DagRun was created,
causing those events to be permanently lost and no follow-up DagRun created.
- Add 'not consumed' filter using association_table to prevent duplicate
consumption of asset events across concurrent DagRuns.
- Always delete ADRQ rows after processing, even when no new DagRun is
created, to prevent stale entries accumulating and causing infinite
scheduler loops.
- Fix concurrent test to use seen_dr_ids set instead of prev_dr reference,
ensuring all DagRuns are counted regardless of creation order.
fix: Persist AssetEvent in independent session for immediate visibility
Persist AssetEvent using a truly independent session (`scoped=False`)
to ensure it's committed and visible to processes like the Scheduler
before other transaction operations proceed. This closes a critical
visibility gap for Asset-Triggered DagRuns (ADRQ).
The event is then explicitly reloaded into the caller's session
to prevent DetachedInstanceError and allow subsequent relationship
operations to function correctly.
make static check happy
refactor: Consolidate `AssetEvent` persistence logic
Extract the complex logic for persisting `AssetEvent` instances into a new
dedicated class method, `create_asset_event`.
This centralizes the handling of distinct session management strategies
(independent session for non-SQLite, direct flush for SQLite) and ensures
immediate visibility of asset events to the scheduler. It also re-attaches
the event to the caller's session to prevent `DetachedInstanceError`.
fix: Ensure DagRun updates and AssetEvents are visible across transaction boundaries
Add `session.flush()` after `DagRun` partition key updates to ensure these changes are persisted within the caller's transaction before an independent session for `AssetEvent` creation begins. This prevents stale `DagRun` data from being read.
Change `AssetEvent` creation to use `create_session(scoped=False)`, guaranteeing a truly independent session and immediate commit of the asset event. This ensures the event is visible to processes like the Scheduler for Asset-Triggered DagRuns (ADRQ) without transactional delays.
fix: Change log level from warning to info for DagRun creation status
apply copilot reviewer
test: Add backend markers for concurrent asset event tests
docs: Address asset event scheduling race condition by using independent session commits
Ensures the asset-triggered consumer DAG is registered in the session prior to creating the AssetEvent. This prevents potential test failures or incorrect behavior due to timing or visibility issues with DAG metadata, aligning the test setup with real-world scheduler requirements.
Per review feedback the change is not significant-tier; downgrade to a
bugfix newsfragment while keeping the operator-facing note about the
new duplicate/orphaned asset-event failure mode introduced by the
early-commit fix.
Reflects changes in asset manager's internal queueing helpers to standardize
conflict handling across dialects and to pass the AssetEvent context.
Co-authored-by: Copilot <copilot@github.com>
@Lee-W

Copy link
Copy Markdown
Member

Hey @dingo4dev , thanks for being so patient! We can merge this after the CI is green

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

Labels

area:Schedulerincluding HA (high availability) schedulerready for maintainer reviewSet after triaging when all criteria pass.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

8 participants

@dingo4dev@Lee-W@potiuk@molcay@VladaZakharova@uranusjr@kaxil
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Fix asset event scheduling race condition and consistency - #62501

Merged
Lee-W merged 8 commits into
apache:mainfrom
dingo4dev:fix/asset-triggered-dag-run
Jul 29, 2026
Merged

Fix asset event scheduling race condition and consistency#62501
Lee-W merged 8 commits into
apache:mainfrom
dingo4dev:fix/asset-triggered-dag-run

Conversation

@dingo4dev

@dingo4devdingo4dev commented Feb 26, 2026

Copy link
Copy Markdown
Contributor

This PR addresses a race condition in the Asset-based scheduling logic where AssetEvent records created by concurrent tasks can be "missed" or "orphaned" by the Scheduler.

The issue stems from the Visibility Gap between a database flush() (which generates the created_at timestamp) and the final commit() (which makes the record visible to other sessions).

This change ensures that the synchronization between the AssetEvent and the AssetDagRunQueue is atomic and that the timestamps are aligned to prevent the drift caused by performance issue.

  • Manually assigns the same timestamp to both the AssetEvent and the ADRQ before the flush.
  • Commit and populate the asset event record in database immediately, so that the event can be accessible by job scheduler
  • Prevent creating dag by empty asset event as the asset event is trigger in prev_dag_run

related: #54659, #56750, #56749

---
config:
theme: redux-dark-color
look: neo
---
sequenceDiagram
participant J1 as Task Instance 1
participant J2 as Task Instance 2
participant JS as Job Scheduler
participant DB as Database
Note over J1,DB: T0
Note left of J1: T1
J1-->>DB: Asset Event 1 (AE1) & commit
activate J1
activate DB
Note left of J1: T2
J2-->>DB: Asset Event 2 (AE2) & commit
activate J2
activate DB
Note left of J1: T3
J2-->>-DB: Asset Dag Run Queue (ADRQ)<br/>created_at = AE2.timestamp<br/>(flush & commit)
deactivate DB
Note left of J1: T4
activate JS
activate DB
JS-->>DB: Fetch created_at from ADRQ
JS-->>DB: Fetch events (AE1, AE2)
JS->>JS: Create DAG with run_after = ADRQ created_at (AE2.timestamp)
deactivate DB
deactivate JS
Note left of J1: T5
J1-->>DB: Asset Dag Run Queue (ADRQ)<br/>created_at = AE1.timestamp<br/>(flush & commit)
deactivate J1
deactivate DB
Note left of J1: T6
activate JS
activate DB
JS-->>DB: Fetch created_at from ADRQ
JS->>JS: No event found<br/>(AE1.timestamp < prev DAG run_after)
deactivate DB
deactivate JS
Loading

Durability tradeoff (new failure mode)

On Postgres/MySQL the AssetEvent is committed in a short-lived independent session so it is visible to the scheduler before the producing task's transaction commits the alias association, AssetDagRunQueue rows, and TI state change. This is intentional and required to fix the scheduling race. The tradeoff is that a failure after the event is committed but before the caller's transaction commits can leave:

  • an orphaned asset event — persisted with no AssetDagRunQueue row for the scheduler to consume, or
  • a duplicate asset event — if the producing task retries register_asset_change.

Operators should be aware of this new failure mode when diagnosing missing or duplicate asset-triggered Dag runs. This is also documented in the newsfragment for this change.

Was generative AI tooling used to co-author this PR?
  • Yes (please specify the tool below)
    Copilot

Open with GitKraken

Important

🛠️ Maintainer triage note for @dingo4dev · by @potiuk · 2026-06-17 14:51 UTC

Helpful heads-up from the maintainers — please address before this PR can be reviewed:

  • Static / docs checks failing (CI image checks / Static checks). Run them locally with prek run --all-files (or pre-commit run --all-files) and push the fixes.
  • Failing test jobs: MySQL tests: core / DB-core:MySQL:8.0:3.10:Core...Serialization. Reproduce and fix locally, then push.
  • See the Pull Request quality criteria.

The ball is in your court — you've been assigned to this PR. Fix the above, then mark it Ready for review.

Automated triage — may be imperfect; a maintainer takes the next look.

CopilotAI review requested due to automatic review settings February 26, 2026 04:41
@boring-cyborgboring-cyborgBot added the area:Scheduler including HA (high availability) scheduler label Feb 26, 2026
@dingo4dev
dingo4devforce-pushed the fix/asset-triggered-dag-run branch from 11ed83d to 624e1eaCompareFebruary 26, 2026 04:41
@dingo4devdingo4dev changed the title [WIP] Fix asset event scheduling race conditionFix asset event scheduling race conditionFeb 26, 2026

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR attempts to fix a race condition in asset-based scheduling where AssetEvent records created by concurrent tasks can be missed by the Scheduler due to a visibility gap between database flush() and commit() operations. The fix synchronizes timestamps between AssetEvent and AssetDagRunQueue records by manually assigning timestamps and committing the asset event immediately.

Changes:

  • Modified asset event registration to commit immediately after creating AssetEvent, ensuring early visibility to the scheduler
  • Updated AssetDagRunQueue creation to use the asset event's timestamp for synchronization across both PostgreSQL and slow-path implementations
  • Changed scheduler logic to conditionally create DAG runs only when asset events exist, and to delete ADRQ entries more selectively based on timestamps

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 7 comments.

FileDescription
airflow-core/src/airflow/jobs/scheduler_job_runner.pyModified DAG run creation to be conditional on asset_events existence and updated ADRQ deletion to filter by timestamp
airflow-core/src/airflow/assets/manager.pyChanged from session.flush() to session.commit() for early AssetEvent persistence and updated ADRQ timestamp synchronization logic

Comment threadairflow-core/src/airflow/assets/manager.py Outdated
Comment threadairflow-core/src/airflow/assets/manager.py
Comment threadairflow-core/src/airflow/assets/manager.py Outdated
Comment threadairflow-core/src/airflow/assets/manager.py Outdated
Comment threadairflow-core/src/airflow/jobs/scheduler_job_runner.py Outdated
Comment threadairflow-core/src/airflow/assets/manager.py Outdated
Comment threadairflow-core/src/airflow/assets/manager.py
@dingo4dev
dingo4devforce-pushed the fix/asset-triggered-dag-run branch 4 times, most recently from 2308c0b to 0019359CompareFebruary 27, 2026 02:42
@dingo4devdingo4dev changed the title Fix asset event scheduling race conditionFix asset event scheduling race condition and consistencyFeb 27, 2026
@dingo4dev
dingo4devforce-pushed the fix/asset-triggered-dag-run branch from 0019359 to e796ff5CompareMarch 5, 2026 14:17
@dingo4dev
dingo4devforce-pushed the fix/asset-triggered-dag-run branch from e796ff5 to bcdd0c5CompareMarch 6, 2026 02:03
@dingo4dev

Copy link
Copy Markdown
ContributorAuthor

@Lee-W@uranusjr Please take a time to review it for me. Thanks a lot ;p

joseotaviorf added a commit to joseotaviorf/airflow that referenced this pull request Mar 12, 2026
…rt of apache#62501)
Backports three targeted fixes to 2.10.4 to address the race condition reported
in GH#56541 and GH#41101, where a dataset-dependent DAG fires with one or more
upstream dataset events missing from consumed_dataset_events.
Root cause: stale `created_at` timestamps in DatasetDagRunQueue caused the
scheduler's event-window query to exclude valid events, yet the presence-based
readiness check still considered the DAG ready to run.
Changes:
- manager.py: replace ON CONFLICT DO NOTHING with ON CONFLICT DO UPDATE
(Postgres) and add a WHERE guard to prevent backwards timestamp drift;
pass explicit created_at=utcnow() on the non-Postgres merge path so
existing rows are always refreshed.
- scheduler_job_runner.py: wrap create_dagrun in `if dataset_events:` to
prevent phantom runs when the event window is empty; scope the DDRQ
DELETE to `created_at <= exec_date` instead of deleting all rows, so
events that arrived during processing are preserved for the next cycle.
- tests/datasets/test_manager.py: add WHERE-guard regression test for the
Postgres upsert path.
- dev/BUG_REPORT_DATASET_SCHEDULING.md: detailed bug report with timeline
diagrams, related issues (apache#56541, apache#41101, apache#35870), root cause analysis,
and explanation of each fix.
- reproduce_bug_56541.py: end-to-end reproduction and verification script.
Made-with: Cursor
@dingo4dev
dingo4devforce-pushed the fix/asset-triggered-dag-run branch from bcdd0c5 to c7dae2dCompareMarch 17, 2026 06:36
@Lee-W

Copy link
Copy Markdown
Member

will try to take a look this week

joseotaviorf added a commit to joseotaviorf/airflow that referenced this pull request Mar 19, 2026
…rt of apache#62501)
Backports three targeted fixes to 2.10.4 to address the race condition reported
in GH#56541 and GH#41101, where a dataset-dependent DAG fires with one or more
upstream dataset events missing from consumed_dataset_events.
Root cause: stale `created_at` timestamps in DatasetDagRunQueue caused the
scheduler's event-window query to exclude valid events, yet the presence-based
readiness check still considered the DAG ready to run.
Changes:
- manager.py: replace ON CONFLICT DO NOTHING with ON CONFLICT DO UPDATE
(Postgres) and add a WHERE guard to prevent backwards timestamp drift;
pass explicit created_at=utcnow() on the non-Postgres merge path so
existing rows are always refreshed.
- scheduler_job_runner.py: wrap create_dagrun in `if dataset_events:` to
prevent phantom runs when the event window is empty; scope the DDRQ
DELETE to `created_at <= exec_date` instead of deleting all rows, so
events that arrived during processing are preserved for the next cycle.
- tests/datasets/test_manager.py: add WHERE-guard regression test for the
Postgres upsert path.
- dev/BUG_REPORT_DATASET_SCHEDULING.md: detailed bug report with timeline
diagrams, related issues (apache#56541, apache#41101, apache#35870), root cause analysis,
and explanation of each fix.
- reproduce_bug_56541.py: end-to-end reproduction and verification script.

@kaxilkaxil left a comment

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.

Left a few comments

Comment threadairflow-core/src/airflow/assets/manager.py Outdated
Comment threadairflow-core/src/airflow/assets/manager.py
Comment threadairflow-core/src/airflow/jobs/scheduler_job_runner.py Outdated
Comment threadairflow-core/tests/unit/jobs/test_scheduler_job.py Outdated
Comment threadairflow-core/tests/unit/jobs/test_scheduler_job.py
@dingo4dev
dingo4devforce-pushed the fix/asset-triggered-dag-run branch 2 times, most recently from e674f0f to 1a08e6fCompareMarch 22, 2026 19:58
@dingo4dev
dingo4devforce-pushed the fix/asset-triggered-dag-run branch 2 times, most recently from 3a647bf to f0d5a65CompareMarch 29, 2026 16:48
@kaxil
kaxil requested a review from CopilotApril 2, 2026 01:03
Comment threadairflow-core/src/airflow/assets/manager.py Outdated
Comment threadairflow-core/src/airflow/assets/manager.py Outdated
Comment threadairflow-core/src/airflow/assets/manager.py
@molcay

Copy link
Copy Markdown
Contributor

Hi @dingo4dev, I wanted to ping you here if you missed my message on Slack.

I am repeating my message and offer for help in here as well:

Hi @Stanley Law, hope you're having a good week!

I have been following your PR, it seems great stuff and thanks for tackling this. I know bouncing between reviews and CI pipelines can take up a lot of time. If you're feeling swamped or just want someone to bounce ideas off of (since I am having trouble reproducing the issue), I'd be glad to step in and help in way that I can. If you've got it fully under control though, totally feel free to ignore this message 🙂
Just wanted to offer some support.

@dingo4dev

Copy link
Copy Markdown
ContributorAuthor

Hi @molcay, Thank you so much for checking in and offering help!
Some extra eyes would be amazing. Right now I am investing the failure ci test on Mysql, the concurrent tests sometimes failure and orphan few asset event, and I can't reproduce it in my local tho.
I had logged the asset event and dag run with id and timestamp.

_______________________________________________________________TestSchedulerJob.test_create_dag_runs_when_concurrent_asset_events_created_______________________________________________________________airflow-core/tests/unit/jobs/test_scheduler_job.py:5792: intest_create_dag_runs_when_concurrent_asset_events_createdasserttotal_consumed_asset_events==ASSET_EVENT_COUNTEassert29==30--------------------------------------------Capturedstdoutsetup-----------------------------------------
] FillinguptheDagBagfrom/dev/null [airflow.dag_processing.dagbag.DagBag]
--------------------------------------------Capturedstdoutcall------------------------------------------MissingAssetEventMetadata: {(16, '2026-06-23T05:02:05.983645+00:00')}
{'event_': (8, '2026-06-23T05:02:05.967063+00:00'), 'dagrun': (434, '2026-06-23T05:02:05.989838+00:00')}
{'event_': (9, '2026-06-23T05:02:05.972418+00:00'), 'dagrun': (434, '2026-06-23T05:02:05.989838+00:00')}
{'event_': (13, '2026-06-23T05:02:05.982971+00:00'), 'dagrun': (436, '2026-06-23T05:02:05.982971+00:00')}
{'event_': (16, '2026-06-23T05:02:05.983645+00:00'), 'dagrun': ()}
{'event_': (10, '2026-06-23T05:02:05.989838+00:00'), 'dagrun': (434, '2026-06-23T05:02:05.989838+00:00')}
{'event_': (15, '2026-06-23T05:02:05.994289+00:00'), 'dagrun': (435, '2026-06-23T05:02:06.034978+00:00')}
{'event_': (14, '2026-06-23T05:02:05.995763+00:00'), 'dagrun': (435, '2026-06-23T05:02:06.034978+00:00')}
{'event_': (11, '2026-06-23T05:02:06.025005+00:00'), 'dagrun': (435, '2026-06-23T05:02:06.034978+00:00')}
{'event_': (12, '2026-06-23T05:02:06.034978+00:00'), 'dagrun': (435, '2026-06-23T05:02:06.034978+00:00')}
{'event_': (18, '2026-06-23T05:02:06.059185+00:00'), 'dagrun': (437, '2026-06-23T05:02:07.001801+00:00')}
{'event_': (17, '2026-06-23T05:02:06.060623+00:00'), 'dagrun': (437, '2026-06-23T05:02:07.001801+00:00')}
{'event_': (19, '2026-06-23T05:02:06.099119+00:00'), 'dagrun': (437, '2026-06-23T05:02:07.001801+00:00')}
{'event_': (20, '2026-06-23T05:02:07.001801+00:00'), 'dagrun': (437, '2026-06-23T05:02:07.001801+00:00')}
{'event_': (21, '2026-06-23T05:02:07.022033+00:00'), 'dagrun': (438, '2026-06-23T05:02:07.090158+00:00')}
{'event_': (22, '2026-06-23T05:02:07.083870+00:00'), 'dagrun': (438, '2026-06-23T05:02:07.090158+00:00')}
{'event_': (23, '2026-06-23T05:02:07.090158+00:00'), 'dagrun': (438, '2026-06-23T05:02:07.090158+00:00')}
{'event_': (24, '2026-06-23T05:02:07.097476+00:00'), 'dagrun': (439, '2026-06-23T05:02:08.046920+00:00')}
{'event_': (25, '2026-06-23T05:02:07.116000+00:00'), 'dagrun': (439, '2026-06-23T05:02:08.046920+00:00')}
{'event_': (26, '2026-06-23T05:02:08.046920+00:00'), 'dagrun': (439, '2026-06-23T05:02:08.046920+00:00')}
{'event_': (27, '2026-06-23T05:02:08.054838+00:00'), 'dagrun': (440, '2026-06-23T05:02:08.092453+00:00')}
{'event_': (28, '2026-06-23T05:02:08.067990+00:00'), 'dagrun': (440, '2026-06-23T05:02:08.092453+00:00')}
{'event_': (29, '2026-06-23T05:02:08.092453+00:00'), 'dagrun': (440, '2026-06-23T05:02:08.092453+00:00')}
{'event_': (30, '2026-06-23T05:02:08.094004+00:00'), 'dagrun': (441, '2026-06-23T05:02:08.125311+00:00')}
{'event_': (31, '2026-06-23T05:02:08.118823+00:00'), 'dagrun': (441, '2026-06-23T05:02:08.125311+00:00')}
{'event_': (32, '2026-06-23T05:02:08.125311+00:00'), 'dagrun': (441, '2026-06-23T05:02:08.125311+00:00')}
{'event_': (33, '2026-06-23T05:02:08.127450+00:00'), 'dagrun': (442, '2026-06-23T05:02:09.073822+00:00')}
{'event_': (34, '2026-06-23T05:02:08.154315+00:00'), 'dagrun': (442, '2026-06-23T05:02:09.073822+00:00')}
{'event_': (35, '2026-06-23T05:02:09.073822+00:00'), 'dagrun': (442, '2026-06-23T05:02:09.073822+00:00')}
{'event_': (36, '2026-06-23T05:02:09.091265+00:00'), 'dagrun': (443, '2026-06-23T05:02:09.091265+00:00')}
{'event_': (37, '2026-06-23T05:02:09.118804+00:00'), 'dagrun': (444, '2026-06-23T05:02:09.118804+00:00')}

Comment threadairflow-core/tests/unit/jobs/test_scheduler_job.py Outdated
Comment threadairflow-core/tests/unit/jobs/test_scheduler_job.py Outdated
@molcay

molcay commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Hi @molcay, Thank you so much for checking in and offering help! Some extra eyes would be amazing. Right now I am investing the failure ci test on Mysql, the concurrent tests sometimes failure and orphan few asset event, and I can't reproduce it in my local tho. I had logged the asset event and dag run with id and timestamp.

_______________________________________________________________TestSchedulerJob.test_create_dag_runs_when_concurrent_asset_events_created_______________________________________________________________airflow-core/tests/unit/jobs/test_scheduler_job.py:5792: intest_create_dag_runs_when_concurrent_asset_events_createdasserttotal_consumed_asset_events==ASSET_EVENT_COUNTEassert29==30

Hi @dingo4dev,
As I see that you already mark the PR as "ready for review" and you've already got an approval, I need to ask if you still need help on the matter?

@Lee-WLee-W left a comment

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.

a few nits, but overall looks good

Comment threadairflow-core/newsfragments/62501.significant.rst Outdated
Comment threadairflow-core/tests/unit/models/test_taskinstance.py Outdated
Comment threadairflow-core/tests/unit/models/test_taskinstance.py Outdated
Comment threadairflow-core/tests/unit/models/test_taskinstance.py Outdated
Comment threadairflow-core/tests/unit/jobs/test_scheduler_job.py Outdated
Comment threadairflow-core/src/airflow/assets/manager.py Outdated
@dingo4dev

dingo4dev commented Jul 7, 2026

Copy link
Copy Markdown
ContributorAuthor

Hi @dingo4dev, As I see that you already mark the PR as "ready for review" and you've already got an approval, I need to ask if you still need help on the matter?

Hi @molcay ! Thanks for following up.

Even though we got an approval, I have a feeling this might not completely catch every edge case of the underlying issue. we should definitely keep a close eye on it and monitor how it behaves. Thanks again for the support. 🙌

@VladaZakharova

Copy link
Copy Markdown
Contributor

Hi there!
Thank you for the work you are doing, really waiting for the changes to be merged and available <3

Comment threadairflow-core/src/airflow/assets/manager.py

@Lee-WLee-W left a comment

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.

a few nits. mostly good

Comment threadairflow-core/src/airflow/assets/manager.py
Comment threadairflow-core/src/airflow/assets/manager.py Outdated
Comment threadairflow-core/src/airflow/assets/manager.py
Comment threadairflow-core/tests/unit/assets/test_manager.py Outdated
Comment threadairflow-core/tests/unit/models/test_taskinstance.py
Comment threadairflow-core/tests/unit/models/test_taskinstance.py Outdated
@VladaZakharova

Copy link
Copy Markdown
Contributor

Looks like green and beautiful :)
Are we close to merge this change?

dingo4devand others added 8 commits July 29, 2026 14:17
Fixes an issue where the Scheduler misses AssetEvents due to a
visibility gap between flush() and commit().
When multiple tasks create events for the same asset, the AssetEvent
timestamp is generated during flush, but the record remains invisible
to the Scheduler until the final commit. If the AssetDagRunQueue is
processed in the interim, the 'late-committing' event is orphaned.
The job scheduler will read the AssetDagRunQueue and fetch the created_at column as the triggered_date which will use for fetching the asset event and assign dag `run_after`.
This change ensures timestamps are synchronized and the transaction
boundary is tightened to prevent data-aware scheduling misses.
Add check for asset events before creating asset-triggered DAG runs
This is to prevent double triggering when the late-commit asset_dag_run_queue is exist in entry but the asset event is already process in prev dag
Fix update statement condition in AssetDagRunQueue to use correct column reference
Add tests for concurrent asset events and no asset event scenarios in DAG run creation
linting tests
make mypy happy
Add check for existing asset DAG run queue entry before merge
Co-authored-by: Kaxil Naik <kaxilnaik@gmail.com>
add log when no dag run without asset event
Support concurrent asset event queuing on MySQL
Implement a MySQL-specific optimization using `ON DUPLICATE KEY UPDATE` when queuing asset-triggered DAG runs. This prevents unique constraint violations during concurrent asset events, mirroring the existing PostgreSQL implementation. The concurrency tests are also refactored to verify correct behavior across both database dialects.
Refactor asset event handling and improve session management in AssetManager
make lint happy
Refactor asset event handling to streamline session management in AssetManager
Re-attach asset_event to caller's session to prevent DetachedInstanceError
fix: prevent race condition causing lost events in asset-triggered DAG run creation
- Remove CTE lower-bound filter from asset event query: the bound excluded
late-committed events that arrived after a concurrent DagRun was created,
causing those events to be permanently lost and no follow-up DagRun created.
- Add 'not consumed' filter using association_table to prevent duplicate
consumption of asset events across concurrent DagRuns.
- Always delete ADRQ rows after processing, even when no new DagRun is
created, to prevent stale entries accumulating and causing infinite
scheduler loops.
- Fix concurrent test to use seen_dr_ids set instead of prev_dr reference,
ensuring all DagRuns are counted regardless of creation order.
fix: Persist AssetEvent in independent session for immediate visibility
Persist AssetEvent using a truly independent session (`scoped=False`)
to ensure it's committed and visible to processes like the Scheduler
before other transaction operations proceed. This closes a critical
visibility gap for Asset-Triggered DagRuns (ADRQ).
The event is then explicitly reloaded into the caller's session
to prevent DetachedInstanceError and allow subsequent relationship
operations to function correctly.
make static check happy
refactor: Consolidate `AssetEvent` persistence logic
Extract the complex logic for persisting `AssetEvent` instances into a new
dedicated class method, `create_asset_event`.
This centralizes the handling of distinct session management strategies
(independent session for non-SQLite, direct flush for SQLite) and ensures
immediate visibility of asset events to the scheduler. It also re-attaches
the event to the caller's session to prevent `DetachedInstanceError`.
fix: Ensure DagRun updates and AssetEvents are visible across transaction boundaries
Add `session.flush()` after `DagRun` partition key updates to ensure these changes are persisted within the caller's transaction before an independent session for `AssetEvent` creation begins. This prevents stale `DagRun` data from being read.
Change `AssetEvent` creation to use `create_session(scoped=False)`, guaranteeing a truly independent session and immediate commit of the asset event. This ensures the event is visible to processes like the Scheduler for Asset-Triggered DagRuns (ADRQ) without transactional delays.
fix: Change log level from warning to info for DagRun creation status
apply copilot reviewer
test: Add backend markers for concurrent asset event tests
docs: Address asset event scheduling race condition by using independent session commits
Ensures the asset-triggered consumer DAG is registered in the session prior to creating the AssetEvent. This prevents potential test failures or incorrect behavior due to timing or visibility issues with DAG metadata, aligning the test setup with real-world scheduler requirements.
Per review feedback the change is not significant-tier; downgrade to a
bugfix newsfragment while keeping the operator-facing note about the
new duplicate/orphaned asset-event failure mode introduced by the
early-commit fix.
Reflects changes in asset manager's internal queueing helpers to standardize
conflict handling across dialects and to pass the AssetEvent context.
Co-authored-by: Copilot <copilot@github.com>
@Lee-W

Copy link
Copy Markdown
Member

Hey @dingo4dev , thanks for being so patient! We can merge this after the CI is green

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

Labels

area:Schedulerincluding HA (high availability) schedulerready for maintainer reviewSet after triaging when all criteria pass.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

8 participants

@dingo4dev@Lee-W@potiuk@molcay@VladaZakharova@uranusjr@kaxil
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Fix asset event scheduling race condition and consistency - #62501

Merged
Lee-W merged 8 commits into
apache:mainfrom
dingo4dev:fix/asset-triggered-dag-run
Jul 29, 2026
Merged

Fix asset event scheduling race condition and consistency#62501
Lee-W merged 8 commits into
apache:mainfrom
dingo4dev:fix/asset-triggered-dag-run

Conversation

@dingo4dev

@dingo4devdingo4dev commented Feb 26, 2026

Copy link
Copy Markdown
Contributor

This PR addresses a race condition in the Asset-based scheduling logic where AssetEvent records created by concurrent tasks can be "missed" or "orphaned" by the Scheduler.

The issue stems from the Visibility Gap between a database flush() (which generates the created_at timestamp) and the final commit() (which makes the record visible to other sessions).

This change ensures that the synchronization between the AssetEvent and the AssetDagRunQueue is atomic and that the timestamps are aligned to prevent the drift caused by performance issue.

  • Manually assigns the same timestamp to both the AssetEvent and the ADRQ before the flush.
  • Commit and populate the asset event record in database immediately, so that the event can be accessible by job scheduler
  • Prevent creating dag by empty asset event as the asset event is trigger in prev_dag_run

related: #54659, #56750, #56749

---
config:
theme: redux-dark-color
look: neo
---
sequenceDiagram
participant J1 as Task Instance 1
participant J2 as Task Instance 2
participant JS as Job Scheduler
participant DB as Database
Note over J1,DB: T0
Note left of J1: T1
J1-->>DB: Asset Event 1 (AE1) & commit
activate J1
activate DB
Note left of J1: T2
J2-->>DB: Asset Event 2 (AE2) & commit
activate J2
activate DB
Note left of J1: T3
J2-->>-DB: Asset Dag Run Queue (ADRQ)<br/>created_at = AE2.timestamp<br/>(flush & commit)
deactivate DB
Note left of J1: T4
activate JS
activate DB
JS-->>DB: Fetch created_at from ADRQ
JS-->>DB: Fetch events (AE1, AE2)
JS->>JS: Create DAG with run_after = ADRQ created_at (AE2.timestamp)
deactivate DB
deactivate JS
Note left of J1: T5
J1-->>DB: Asset Dag Run Queue (ADRQ)<br/>created_at = AE1.timestamp<br/>(flush & commit)
deactivate J1
deactivate DB
Note left of J1: T6
activate JS
activate DB
JS-->>DB: Fetch created_at from ADRQ
JS->>JS: No event found<br/>(AE1.timestamp < prev DAG run_after)
deactivate DB
deactivate JS
Loading

Durability tradeoff (new failure mode)

On Postgres/MySQL the AssetEvent is committed in a short-lived independent session so it is visible to the scheduler before the producing task's transaction commits the alias association, AssetDagRunQueue rows, and TI state change. This is intentional and required to fix the scheduling race. The tradeoff is that a failure after the event is committed but before the caller's transaction commits can leave:

  • an orphaned asset event — persisted with no AssetDagRunQueue row for the scheduler to consume, or
  • a duplicate asset event — if the producing task retries register_asset_change.

Operators should be aware of this new failure mode when diagnosing missing or duplicate asset-triggered Dag runs. This is also documented in the newsfragment for this change.

Was generative AI tooling used to co-author this PR?
  • Yes (please specify the tool below)
    Copilot

Open with GitKraken

Important

🛠️ Maintainer triage note for @dingo4dev · by @potiuk · 2026-06-17 14:51 UTC

Helpful heads-up from the maintainers — please address before this PR can be reviewed:

  • Static / docs checks failing (CI image checks / Static checks). Run them locally with prek run --all-files (or pre-commit run --all-files) and push the fixes.
  • Failing test jobs: MySQL tests: core / DB-core:MySQL:8.0:3.10:Core...Serialization. Reproduce and fix locally, then push.
  • See the Pull Request quality criteria.

The ball is in your court — you've been assigned to this PR. Fix the above, then mark it Ready for review.

Automated triage — may be imperfect; a maintainer takes the next look.

CopilotAI review requested due to automatic review settings February 26, 2026 04:41
@boring-cyborgboring-cyborgBot added the area:Scheduler including HA (high availability) scheduler label Feb 26, 2026
@dingo4dev
dingo4devforce-pushed the fix/asset-triggered-dag-run branch from 11ed83d to 624e1eaCompareFebruary 26, 2026 04:41
@dingo4devdingo4dev changed the title [WIP] Fix asset event scheduling race conditionFix asset event scheduling race conditionFeb 26, 2026

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR attempts to fix a race condition in asset-based scheduling where AssetEvent records created by concurrent tasks can be missed by the Scheduler due to a visibility gap between database flush() and commit() operations. The fix synchronizes timestamps between AssetEvent and AssetDagRunQueue records by manually assigning timestamps and committing the asset event immediately.

Changes:

  • Modified asset event registration to commit immediately after creating AssetEvent, ensuring early visibility to the scheduler
  • Updated AssetDagRunQueue creation to use the asset event's timestamp for synchronization across both PostgreSQL and slow-path implementations
  • Changed scheduler logic to conditionally create DAG runs only when asset events exist, and to delete ADRQ entries more selectively based on timestamps

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 7 comments.

FileDescription
airflow-core/src/airflow/jobs/scheduler_job_runner.pyModified DAG run creation to be conditional on asset_events existence and updated ADRQ deletion to filter by timestamp
airflow-core/src/airflow/assets/manager.pyChanged from session.flush() to session.commit() for early AssetEvent persistence and updated ADRQ timestamp synchronization logic

Comment threadairflow-core/src/airflow/assets/manager.py Outdated
Comment threadairflow-core/src/airflow/assets/manager.py
Comment threadairflow-core/src/airflow/assets/manager.py Outdated
Comment threadairflow-core/src/airflow/assets/manager.py Outdated
Comment threadairflow-core/src/airflow/jobs/scheduler_job_runner.py Outdated
Comment threadairflow-core/src/airflow/assets/manager.py Outdated
Comment threadairflow-core/src/airflow/assets/manager.py
@dingo4dev
dingo4devforce-pushed the fix/asset-triggered-dag-run branch 4 times, most recently from 2308c0b to 0019359CompareFebruary 27, 2026 02:42
@dingo4devdingo4dev changed the title Fix asset event scheduling race conditionFix asset event scheduling race condition and consistencyFeb 27, 2026
@dingo4dev
dingo4devforce-pushed the fix/asset-triggered-dag-run branch from 0019359 to e796ff5CompareMarch 5, 2026 14:17
@dingo4dev
dingo4devforce-pushed the fix/asset-triggered-dag-run branch from e796ff5 to bcdd0c5CompareMarch 6, 2026 02:03
@dingo4dev

Copy link
Copy Markdown
ContributorAuthor

@Lee-W@uranusjr Please take a time to review it for me. Thanks a lot ;p

joseotaviorf added a commit to joseotaviorf/airflow that referenced this pull request Mar 12, 2026
…rt of apache#62501)
Backports three targeted fixes to 2.10.4 to address the race condition reported
in GH#56541 and GH#41101, where a dataset-dependent DAG fires with one or more
upstream dataset events missing from consumed_dataset_events.
Root cause: stale `created_at` timestamps in DatasetDagRunQueue caused the
scheduler's event-window query to exclude valid events, yet the presence-based
readiness check still considered the DAG ready to run.
Changes:
- manager.py: replace ON CONFLICT DO NOTHING with ON CONFLICT DO UPDATE
(Postgres) and add a WHERE guard to prevent backwards timestamp drift;
pass explicit created_at=utcnow() on the non-Postgres merge path so
existing rows are always refreshed.
- scheduler_job_runner.py: wrap create_dagrun in `if dataset_events:` to
prevent phantom runs when the event window is empty; scope the DDRQ
DELETE to `created_at <= exec_date` instead of deleting all rows, so
events that arrived during processing are preserved for the next cycle.
- tests/datasets/test_manager.py: add WHERE-guard regression test for the
Postgres upsert path.
- dev/BUG_REPORT_DATASET_SCHEDULING.md: detailed bug report with timeline
diagrams, related issues (apache#56541, apache#41101, apache#35870), root cause analysis,
and explanation of each fix.
- reproduce_bug_56541.py: end-to-end reproduction and verification script.
Made-with: Cursor
@dingo4dev
dingo4devforce-pushed the fix/asset-triggered-dag-run branch from bcdd0c5 to c7dae2dCompareMarch 17, 2026 06:36
@Lee-W

Copy link
Copy Markdown
Member

will try to take a look this week

joseotaviorf added a commit to joseotaviorf/airflow that referenced this pull request Mar 19, 2026
…rt of apache#62501)
Backports three targeted fixes to 2.10.4 to address the race condition reported
in GH#56541 and GH#41101, where a dataset-dependent DAG fires with one or more
upstream dataset events missing from consumed_dataset_events.
Root cause: stale `created_at` timestamps in DatasetDagRunQueue caused the
scheduler's event-window query to exclude valid events, yet the presence-based
readiness check still considered the DAG ready to run.
Changes:
- manager.py: replace ON CONFLICT DO NOTHING with ON CONFLICT DO UPDATE
(Postgres) and add a WHERE guard to prevent backwards timestamp drift;
pass explicit created_at=utcnow() on the non-Postgres merge path so
existing rows are always refreshed.
- scheduler_job_runner.py: wrap create_dagrun in `if dataset_events:` to
prevent phantom runs when the event window is empty; scope the DDRQ
DELETE to `created_at <= exec_date` instead of deleting all rows, so
events that arrived during processing are preserved for the next cycle.
- tests/datasets/test_manager.py: add WHERE-guard regression test for the
Postgres upsert path.
- dev/BUG_REPORT_DATASET_SCHEDULING.md: detailed bug report with timeline
diagrams, related issues (apache#56541, apache#41101, apache#35870), root cause analysis,
and explanation of each fix.
- reproduce_bug_56541.py: end-to-end reproduction and verification script.

@kaxilkaxil left a comment

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.

Left a few comments

Comment threadairflow-core/src/airflow/assets/manager.py Outdated
Comment threadairflow-core/src/airflow/assets/manager.py
Comment threadairflow-core/src/airflow/jobs/scheduler_job_runner.py Outdated
Comment threadairflow-core/tests/unit/jobs/test_scheduler_job.py Outdated
Comment threadairflow-core/tests/unit/jobs/test_scheduler_job.py
@dingo4dev
dingo4devforce-pushed the fix/asset-triggered-dag-run branch 2 times, most recently from e674f0f to 1a08e6fCompareMarch 22, 2026 19:58
@dingo4dev
dingo4devforce-pushed the fix/asset-triggered-dag-run branch 2 times, most recently from 3a647bf to f0d5a65CompareMarch 29, 2026 16:48
@kaxil
kaxil requested a review from CopilotApril 2, 2026 01:03
Comment threadairflow-core/src/airflow/assets/manager.py Outdated
Comment threadairflow-core/src/airflow/assets/manager.py Outdated
Comment threadairflow-core/src/airflow/assets/manager.py
@molcay

Copy link
Copy Markdown
Contributor

Hi @dingo4dev, I wanted to ping you here if you missed my message on Slack.

I am repeating my message and offer for help in here as well:

Hi @Stanley Law, hope you're having a good week!

I have been following your PR, it seems great stuff and thanks for tackling this. I know bouncing between reviews and CI pipelines can take up a lot of time. If you're feeling swamped or just want someone to bounce ideas off of (since I am having trouble reproducing the issue), I'd be glad to step in and help in way that I can. If you've got it fully under control though, totally feel free to ignore this message 🙂
Just wanted to offer some support.

@dingo4dev

Copy link
Copy Markdown
ContributorAuthor

Hi @molcay, Thank you so much for checking in and offering help!
Some extra eyes would be amazing. Right now I am investing the failure ci test on Mysql, the concurrent tests sometimes failure and orphan few asset event, and I can't reproduce it in my local tho.
I had logged the asset event and dag run with id and timestamp.

_______________________________________________________________TestSchedulerJob.test_create_dag_runs_when_concurrent_asset_events_created_______________________________________________________________airflow-core/tests/unit/jobs/test_scheduler_job.py:5792: intest_create_dag_runs_when_concurrent_asset_events_createdasserttotal_consumed_asset_events==ASSET_EVENT_COUNTEassert29==30--------------------------------------------Capturedstdoutsetup-----------------------------------------
] FillinguptheDagBagfrom/dev/null [airflow.dag_processing.dagbag.DagBag]
--------------------------------------------Capturedstdoutcall------------------------------------------MissingAssetEventMetadata: {(16, '2026-06-23T05:02:05.983645+00:00')}
{'event_': (8, '2026-06-23T05:02:05.967063+00:00'), 'dagrun': (434, '2026-06-23T05:02:05.989838+00:00')}
{'event_': (9, '2026-06-23T05:02:05.972418+00:00'), 'dagrun': (434, '2026-06-23T05:02:05.989838+00:00')}
{'event_': (13, '2026-06-23T05:02:05.982971+00:00'), 'dagrun': (436, '2026-06-23T05:02:05.982971+00:00')}
{'event_': (16, '2026-06-23T05:02:05.983645+00:00'), 'dagrun': ()}
{'event_': (10, '2026-06-23T05:02:05.989838+00:00'), 'dagrun': (434, '2026-06-23T05:02:05.989838+00:00')}
{'event_': (15, '2026-06-23T05:02:05.994289+00:00'), 'dagrun': (435, '2026-06-23T05:02:06.034978+00:00')}
{'event_': (14, '2026-06-23T05:02:05.995763+00:00'), 'dagrun': (435, '2026-06-23T05:02:06.034978+00:00')}
{'event_': (11, '2026-06-23T05:02:06.025005+00:00'), 'dagrun': (435, '2026-06-23T05:02:06.034978+00:00')}
{'event_': (12, '2026-06-23T05:02:06.034978+00:00'), 'dagrun': (435, '2026-06-23T05:02:06.034978+00:00')}
{'event_': (18, '2026-06-23T05:02:06.059185+00:00'), 'dagrun': (437, '2026-06-23T05:02:07.001801+00:00')}
{'event_': (17, '2026-06-23T05:02:06.060623+00:00'), 'dagrun': (437, '2026-06-23T05:02:07.001801+00:00')}
{'event_': (19, '2026-06-23T05:02:06.099119+00:00'), 'dagrun': (437, '2026-06-23T05:02:07.001801+00:00')}
{'event_': (20, '2026-06-23T05:02:07.001801+00:00'), 'dagrun': (437, '2026-06-23T05:02:07.001801+00:00')}
{'event_': (21, '2026-06-23T05:02:07.022033+00:00'), 'dagrun': (438, '2026-06-23T05:02:07.090158+00:00')}
{'event_': (22, '2026-06-23T05:02:07.083870+00:00'), 'dagrun': (438, '2026-06-23T05:02:07.090158+00:00')}
{'event_': (23, '2026-06-23T05:02:07.090158+00:00'), 'dagrun': (438, '2026-06-23T05:02:07.090158+00:00')}
{'event_': (24, '2026-06-23T05:02:07.097476+00:00'), 'dagrun': (439, '2026-06-23T05:02:08.046920+00:00')}
{'event_': (25, '2026-06-23T05:02:07.116000+00:00'), 'dagrun': (439, '2026-06-23T05:02:08.046920+00:00')}
{'event_': (26, '2026-06-23T05:02:08.046920+00:00'), 'dagrun': (439, '2026-06-23T05:02:08.046920+00:00')}
{'event_': (27, '2026-06-23T05:02:08.054838+00:00'), 'dagrun': (440, '2026-06-23T05:02:08.092453+00:00')}
{'event_': (28, '2026-06-23T05:02:08.067990+00:00'), 'dagrun': (440, '2026-06-23T05:02:08.092453+00:00')}
{'event_': (29, '2026-06-23T05:02:08.092453+00:00'), 'dagrun': (440, '2026-06-23T05:02:08.092453+00:00')}
{'event_': (30, '2026-06-23T05:02:08.094004+00:00'), 'dagrun': (441, '2026-06-23T05:02:08.125311+00:00')}
{'event_': (31, '2026-06-23T05:02:08.118823+00:00'), 'dagrun': (441, '2026-06-23T05:02:08.125311+00:00')}
{'event_': (32, '2026-06-23T05:02:08.125311+00:00'), 'dagrun': (441, '2026-06-23T05:02:08.125311+00:00')}
{'event_': (33, '2026-06-23T05:02:08.127450+00:00'), 'dagrun': (442, '2026-06-23T05:02:09.073822+00:00')}
{'event_': (34, '2026-06-23T05:02:08.154315+00:00'), 'dagrun': (442, '2026-06-23T05:02:09.073822+00:00')}
{'event_': (35, '2026-06-23T05:02:09.073822+00:00'), 'dagrun': (442, '2026-06-23T05:02:09.073822+00:00')}
{'event_': (36, '2026-06-23T05:02:09.091265+00:00'), 'dagrun': (443, '2026-06-23T05:02:09.091265+00:00')}
{'event_': (37, '2026-06-23T05:02:09.118804+00:00'), 'dagrun': (444, '2026-06-23T05:02:09.118804+00:00')}

Comment threadairflow-core/tests/unit/jobs/test_scheduler_job.py Outdated
Comment threadairflow-core/tests/unit/jobs/test_scheduler_job.py Outdated
@molcay

molcay commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Hi @molcay, Thank you so much for checking in and offering help! Some extra eyes would be amazing. Right now I am investing the failure ci test on Mysql, the concurrent tests sometimes failure and orphan few asset event, and I can't reproduce it in my local tho. I had logged the asset event and dag run with id and timestamp.

_______________________________________________________________TestSchedulerJob.test_create_dag_runs_when_concurrent_asset_events_created_______________________________________________________________airflow-core/tests/unit/jobs/test_scheduler_job.py:5792: intest_create_dag_runs_when_concurrent_asset_events_createdasserttotal_consumed_asset_events==ASSET_EVENT_COUNTEassert29==30

Hi @dingo4dev,
As I see that you already mark the PR as "ready for review" and you've already got an approval, I need to ask if you still need help on the matter?

@Lee-WLee-W left a comment

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.

a few nits, but overall looks good

Comment threadairflow-core/newsfragments/62501.significant.rst Outdated
Comment threadairflow-core/tests/unit/models/test_taskinstance.py Outdated
Comment threadairflow-core/tests/unit/models/test_taskinstance.py Outdated
Comment threadairflow-core/tests/unit/models/test_taskinstance.py Outdated
Comment threadairflow-core/tests/unit/jobs/test_scheduler_job.py Outdated
Comment threadairflow-core/src/airflow/assets/manager.py Outdated
@dingo4dev

dingo4dev commented Jul 7, 2026

Copy link
Copy Markdown
ContributorAuthor

Hi @dingo4dev, As I see that you already mark the PR as "ready for review" and you've already got an approval, I need to ask if you still need help on the matter?

Hi @molcay ! Thanks for following up.

Even though we got an approval, I have a feeling this might not completely catch every edge case of the underlying issue. we should definitely keep a close eye on it and monitor how it behaves. Thanks again for the support. 🙌

@VladaZakharova

Copy link
Copy Markdown
Contributor

Hi there!
Thank you for the work you are doing, really waiting for the changes to be merged and available <3

Comment threadairflow-core/src/airflow/assets/manager.py

@Lee-WLee-W left a comment

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.

a few nits. mostly good

Comment threadairflow-core/src/airflow/assets/manager.py
Comment threadairflow-core/src/airflow/assets/manager.py Outdated
Comment threadairflow-core/src/airflow/assets/manager.py
Comment threadairflow-core/tests/unit/assets/test_manager.py Outdated
Comment threadairflow-core/tests/unit/models/test_taskinstance.py
Comment threadairflow-core/tests/unit/models/test_taskinstance.py Outdated
@VladaZakharova

Copy link
Copy Markdown
Contributor

Looks like green and beautiful :)
Are we close to merge this change?

dingo4devand others added 8 commits July 29, 2026 14:17
Fixes an issue where the Scheduler misses AssetEvents due to a
visibility gap between flush() and commit().
When multiple tasks create events for the same asset, the AssetEvent
timestamp is generated during flush, but the record remains invisible
to the Scheduler until the final commit. If the AssetDagRunQueue is
processed in the interim, the 'late-committing' event is orphaned.
The job scheduler will read the AssetDagRunQueue and fetch the created_at column as the triggered_date which will use for fetching the asset event and assign dag `run_after`.
This change ensures timestamps are synchronized and the transaction
boundary is tightened to prevent data-aware scheduling misses.
Add check for asset events before creating asset-triggered DAG runs
This is to prevent double triggering when the late-commit asset_dag_run_queue is exist in entry but the asset event is already process in prev dag
Fix update statement condition in AssetDagRunQueue to use correct column reference
Add tests for concurrent asset events and no asset event scenarios in DAG run creation
linting tests
make mypy happy
Add check for existing asset DAG run queue entry before merge
Co-authored-by: Kaxil Naik <kaxilnaik@gmail.com>
add log when no dag run without asset event
Support concurrent asset event queuing on MySQL
Implement a MySQL-specific optimization using `ON DUPLICATE KEY UPDATE` when queuing asset-triggered DAG runs. This prevents unique constraint violations during concurrent asset events, mirroring the existing PostgreSQL implementation. The concurrency tests are also refactored to verify correct behavior across both database dialects.
Refactor asset event handling and improve session management in AssetManager
make lint happy
Refactor asset event handling to streamline session management in AssetManager
Re-attach asset_event to caller's session to prevent DetachedInstanceError
fix: prevent race condition causing lost events in asset-triggered DAG run creation
- Remove CTE lower-bound filter from asset event query: the bound excluded
late-committed events that arrived after a concurrent DagRun was created,
causing those events to be permanently lost and no follow-up DagRun created.
- Add 'not consumed' filter using association_table to prevent duplicate
consumption of asset events across concurrent DagRuns.
- Always delete ADRQ rows after processing, even when no new DagRun is
created, to prevent stale entries accumulating and causing infinite
scheduler loops.
- Fix concurrent test to use seen_dr_ids set instead of prev_dr reference,
ensuring all DagRuns are counted regardless of creation order.
fix: Persist AssetEvent in independent session for immediate visibility
Persist AssetEvent using a truly independent session (`scoped=False`)
to ensure it's committed and visible to processes like the Scheduler
before other transaction operations proceed. This closes a critical
visibility gap for Asset-Triggered DagRuns (ADRQ).
The event is then explicitly reloaded into the caller's session
to prevent DetachedInstanceError and allow subsequent relationship
operations to function correctly.
make static check happy
refactor: Consolidate `AssetEvent` persistence logic
Extract the complex logic for persisting `AssetEvent` instances into a new
dedicated class method, `create_asset_event`.
This centralizes the handling of distinct session management strategies
(independent session for non-SQLite, direct flush for SQLite) and ensures
immediate visibility of asset events to the scheduler. It also re-attaches
the event to the caller's session to prevent `DetachedInstanceError`.
fix: Ensure DagRun updates and AssetEvents are visible across transaction boundaries
Add `session.flush()` after `DagRun` partition key updates to ensure these changes are persisted within the caller's transaction before an independent session for `AssetEvent` creation begins. This prevents stale `DagRun` data from being read.
Change `AssetEvent` creation to use `create_session(scoped=False)`, guaranteeing a truly independent session and immediate commit of the asset event. This ensures the event is visible to processes like the Scheduler for Asset-Triggered DagRuns (ADRQ) without transactional delays.
fix: Change log level from warning to info for DagRun creation status
apply copilot reviewer
test: Add backend markers for concurrent asset event tests
docs: Address asset event scheduling race condition by using independent session commits
Ensures the asset-triggered consumer DAG is registered in the session prior to creating the AssetEvent. This prevents potential test failures or incorrect behavior due to timing or visibility issues with DAG metadata, aligning the test setup with real-world scheduler requirements.
Per review feedback the change is not significant-tier; downgrade to a
bugfix newsfragment while keeping the operator-facing note about the
new duplicate/orphaned asset-event failure mode introduced by the
early-commit fix.
Reflects changes in asset manager's internal queueing helpers to standardize
conflict handling across dialects and to pass the AssetEvent context.
Co-authored-by: Copilot <copilot@github.com>
@Lee-W

Copy link
Copy Markdown
Member

Hey @dingo4dev , thanks for being so patient! We can merge this after the CI is green

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

Labels

area:Schedulerincluding HA (high availability) schedulerready for maintainer reviewSet after triaging when all criteria pass.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

8 participants

@dingo4dev@Lee-W@potiuk@molcay@VladaZakharova@uranusjr@kaxil
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Fix asset event scheduling race condition and consistency - #62501

Merged
Lee-W merged 8 commits into
apache:mainfrom
dingo4dev:fix/asset-triggered-dag-run
Jul 29, 2026
Merged

Fix asset event scheduling race condition and consistency#62501
Lee-W merged 8 commits into
apache:mainfrom
dingo4dev:fix/asset-triggered-dag-run

Conversation

@dingo4dev

@dingo4devdingo4dev commented Feb 26, 2026

Copy link
Copy Markdown
Contributor

This PR addresses a race condition in the Asset-based scheduling logic where AssetEvent records created by concurrent tasks can be "missed" or "orphaned" by the Scheduler.

The issue stems from the Visibility Gap between a database flush() (which generates the created_at timestamp) and the final commit() (which makes the record visible to other sessions).

This change ensures that the synchronization between the AssetEvent and the AssetDagRunQueue is atomic and that the timestamps are aligned to prevent the drift caused by performance issue.

  • Manually assigns the same timestamp to both the AssetEvent and the ADRQ before the flush.
  • Commit and populate the asset event record in database immediately, so that the event can be accessible by job scheduler
  • Prevent creating dag by empty asset event as the asset event is trigger in prev_dag_run

related: #54659, #56750, #56749

---
config:
theme: redux-dark-color
look: neo
---
sequenceDiagram
participant J1 as Task Instance 1
participant J2 as Task Instance 2
participant JS as Job Scheduler
participant DB as Database
Note over J1,DB: T0
Note left of J1: T1
J1-->>DB: Asset Event 1 (AE1) & commit
activate J1
activate DB
Note left of J1: T2
J2-->>DB: Asset Event 2 (AE2) & commit
activate J2
activate DB
Note left of J1: T3
J2-->>-DB: Asset Dag Run Queue (ADRQ)<br/>created_at = AE2.timestamp<br/>(flush & commit)
deactivate DB
Note left of J1: T4
activate JS
activate DB
JS-->>DB: Fetch created_at from ADRQ
JS-->>DB: Fetch events (AE1, AE2)
JS->>JS: Create DAG with run_after = ADRQ created_at (AE2.timestamp)
deactivate DB
deactivate JS
Note left of J1: T5
J1-->>DB: Asset Dag Run Queue (ADRQ)<br/>created_at = AE1.timestamp<br/>(flush & commit)
deactivate J1
deactivate DB
Note left of J1: T6
activate JS
activate DB
JS-->>DB: Fetch created_at from ADRQ
JS->>JS: No event found<br/>(AE1.timestamp < prev DAG run_after)
deactivate DB
deactivate JS
Loading

Durability tradeoff (new failure mode)

On Postgres/MySQL the AssetEvent is committed in a short-lived independent session so it is visible to the scheduler before the producing task's transaction commits the alias association, AssetDagRunQueue rows, and TI state change. This is intentional and required to fix the scheduling race. The tradeoff is that a failure after the event is committed but before the caller's transaction commits can leave:

  • an orphaned asset event — persisted with no AssetDagRunQueue row for the scheduler to consume, or
  • a duplicate asset event — if the producing task retries register_asset_change.

Operators should be aware of this new failure mode when diagnosing missing or duplicate asset-triggered Dag runs. This is also documented in the newsfragment for this change.

Was generative AI tooling used to co-author this PR?
  • Yes (please specify the tool below)
    Copilot

Open with GitKraken

Important

🛠️ Maintainer triage note for @dingo4dev · by @potiuk · 2026-06-17 14:51 UTC

Helpful heads-up from the maintainers — please address before this PR can be reviewed:

  • Static / docs checks failing (CI image checks / Static checks). Run them locally with prek run --all-files (or pre-commit run --all-files) and push the fixes.
  • Failing test jobs: MySQL tests: core / DB-core:MySQL:8.0:3.10:Core...Serialization. Reproduce and fix locally, then push.
  • See the Pull Request quality criteria.

The ball is in your court — you've been assigned to this PR. Fix the above, then mark it Ready for review.

Automated triage — may be imperfect; a maintainer takes the next look.

CopilotAI review requested due to automatic review settings February 26, 2026 04:41
@boring-cyborgboring-cyborgBot added the area:Scheduler including HA (high availability) scheduler label Feb 26, 2026
@dingo4dev
dingo4devforce-pushed the fix/asset-triggered-dag-run branch from 11ed83d to 624e1eaCompareFebruary 26, 2026 04:41
@dingo4devdingo4dev changed the title [WIP] Fix asset event scheduling race conditionFix asset event scheduling race conditionFeb 26, 2026

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR attempts to fix a race condition in asset-based scheduling where AssetEvent records created by concurrent tasks can be missed by the Scheduler due to a visibility gap between database flush() and commit() operations. The fix synchronizes timestamps between AssetEvent and AssetDagRunQueue records by manually assigning timestamps and committing the asset event immediately.

Changes:

  • Modified asset event registration to commit immediately after creating AssetEvent, ensuring early visibility to the scheduler
  • Updated AssetDagRunQueue creation to use the asset event's timestamp for synchronization across both PostgreSQL and slow-path implementations
  • Changed scheduler logic to conditionally create DAG runs only when asset events exist, and to delete ADRQ entries more selectively based on timestamps

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 7 comments.

FileDescription
airflow-core/src/airflow/jobs/scheduler_job_runner.pyModified DAG run creation to be conditional on asset_events existence and updated ADRQ deletion to filter by timestamp
airflow-core/src/airflow/assets/manager.pyChanged from session.flush() to session.commit() for early AssetEvent persistence and updated ADRQ timestamp synchronization logic

Comment threadairflow-core/src/airflow/assets/manager.py Outdated
Comment threadairflow-core/src/airflow/assets/manager.py
Comment threadairflow-core/src/airflow/assets/manager.py Outdated
Comment threadairflow-core/src/airflow/assets/manager.py Outdated
Comment threadairflow-core/src/airflow/jobs/scheduler_job_runner.py Outdated
Comment threadairflow-core/src/airflow/assets/manager.py Outdated
Comment threadairflow-core/src/airflow/assets/manager.py
@dingo4dev
dingo4devforce-pushed the fix/asset-triggered-dag-run branch 4 times, most recently from 2308c0b to 0019359CompareFebruary 27, 2026 02:42
@dingo4devdingo4dev changed the title Fix asset event scheduling race conditionFix asset event scheduling race condition and consistencyFeb 27, 2026
@dingo4dev
dingo4devforce-pushed the fix/asset-triggered-dag-run branch from 0019359 to e796ff5CompareMarch 5, 2026 14:17
@dingo4dev
dingo4devforce-pushed the fix/asset-triggered-dag-run branch from e796ff5 to bcdd0c5CompareMarch 6, 2026 02:03
@dingo4dev

Copy link
Copy Markdown
ContributorAuthor

@Lee-W@uranusjr Please take a time to review it for me. Thanks a lot ;p

joseotaviorf added a commit to joseotaviorf/airflow that referenced this pull request Mar 12, 2026
…rt of apache#62501)
Backports three targeted fixes to 2.10.4 to address the race condition reported
in GH#56541 and GH#41101, where a dataset-dependent DAG fires with one or more
upstream dataset events missing from consumed_dataset_events.
Root cause: stale `created_at` timestamps in DatasetDagRunQueue caused the
scheduler's event-window query to exclude valid events, yet the presence-based
readiness check still considered the DAG ready to run.
Changes:
- manager.py: replace ON CONFLICT DO NOTHING with ON CONFLICT DO UPDATE
(Postgres) and add a WHERE guard to prevent backwards timestamp drift;
pass explicit created_at=utcnow() on the non-Postgres merge path so
existing rows are always refreshed.
- scheduler_job_runner.py: wrap create_dagrun in `if dataset_events:` to
prevent phantom runs when the event window is empty; scope the DDRQ
DELETE to `created_at <= exec_date` instead of deleting all rows, so
events that arrived during processing are preserved for the next cycle.
- tests/datasets/test_manager.py: add WHERE-guard regression test for the
Postgres upsert path.
- dev/BUG_REPORT_DATASET_SCHEDULING.md: detailed bug report with timeline
diagrams, related issues (apache#56541, apache#41101, apache#35870), root cause analysis,
and explanation of each fix.
- reproduce_bug_56541.py: end-to-end reproduction and verification script.
Made-with: Cursor
@dingo4dev
dingo4devforce-pushed the fix/asset-triggered-dag-run branch from bcdd0c5 to c7dae2dCompareMarch 17, 2026 06:36
@Lee-W

Copy link
Copy Markdown
Member

will try to take a look this week

joseotaviorf added a commit to joseotaviorf/airflow that referenced this pull request Mar 19, 2026
…rt of apache#62501)
Backports three targeted fixes to 2.10.4 to address the race condition reported
in GH#56541 and GH#41101, where a dataset-dependent DAG fires with one or more
upstream dataset events missing from consumed_dataset_events.
Root cause: stale `created_at` timestamps in DatasetDagRunQueue caused the
scheduler's event-window query to exclude valid events, yet the presence-based
readiness check still considered the DAG ready to run.
Changes:
- manager.py: replace ON CONFLICT DO NOTHING with ON CONFLICT DO UPDATE
(Postgres) and add a WHERE guard to prevent backwards timestamp drift;
pass explicit created_at=utcnow() on the non-Postgres merge path so
existing rows are always refreshed.
- scheduler_job_runner.py: wrap create_dagrun in `if dataset_events:` to
prevent phantom runs when the event window is empty; scope the DDRQ
DELETE to `created_at <= exec_date` instead of deleting all rows, so
events that arrived during processing are preserved for the next cycle.
- tests/datasets/test_manager.py: add WHERE-guard regression test for the
Postgres upsert path.
- dev/BUG_REPORT_DATASET_SCHEDULING.md: detailed bug report with timeline
diagrams, related issues (apache#56541, apache#41101, apache#35870), root cause analysis,
and explanation of each fix.
- reproduce_bug_56541.py: end-to-end reproduction and verification script.

@kaxilkaxil left a comment

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.

Left a few comments

Comment threadairflow-core/src/airflow/assets/manager.py Outdated
Comment threadairflow-core/src/airflow/assets/manager.py
Comment threadairflow-core/src/airflow/jobs/scheduler_job_runner.py Outdated
Comment threadairflow-core/tests/unit/jobs/test_scheduler_job.py Outdated
Comment threadairflow-core/tests/unit/jobs/test_scheduler_job.py
@dingo4dev
dingo4devforce-pushed the fix/asset-triggered-dag-run branch 2 times, most recently from e674f0f to 1a08e6fCompareMarch 22, 2026 19:58
@dingo4dev
dingo4devforce-pushed the fix/asset-triggered-dag-run branch 2 times, most recently from 3a647bf to f0d5a65CompareMarch 29, 2026 16:48
@kaxil
kaxil requested a review from CopilotApril 2, 2026 01:03
Comment threadairflow-core/src/airflow/assets/manager.py Outdated
Comment threadairflow-core/src/airflow/assets/manager.py Outdated
Comment threadairflow-core/src/airflow/assets/manager.py
@molcay

Copy link
Copy Markdown
Contributor

Hi @dingo4dev, I wanted to ping you here if you missed my message on Slack.

I am repeating my message and offer for help in here as well:

Hi @Stanley Law, hope you're having a good week!

I have been following your PR, it seems great stuff and thanks for tackling this. I know bouncing between reviews and CI pipelines can take up a lot of time. If you're feeling swamped or just want someone to bounce ideas off of (since I am having trouble reproducing the issue), I'd be glad to step in and help in way that I can. If you've got it fully under control though, totally feel free to ignore this message 🙂
Just wanted to offer some support.

@dingo4dev

Copy link
Copy Markdown
ContributorAuthor

Hi @molcay, Thank you so much for checking in and offering help!
Some extra eyes would be amazing. Right now I am investing the failure ci test on Mysql, the concurrent tests sometimes failure and orphan few asset event, and I can't reproduce it in my local tho.
I had logged the asset event and dag run with id and timestamp.

_______________________________________________________________TestSchedulerJob.test_create_dag_runs_when_concurrent_asset_events_created_______________________________________________________________airflow-core/tests/unit/jobs/test_scheduler_job.py:5792: intest_create_dag_runs_when_concurrent_asset_events_createdasserttotal_consumed_asset_events==ASSET_EVENT_COUNTEassert29==30--------------------------------------------Capturedstdoutsetup-----------------------------------------
] FillinguptheDagBagfrom/dev/null [airflow.dag_processing.dagbag.DagBag]
--------------------------------------------Capturedstdoutcall------------------------------------------MissingAssetEventMetadata: {(16, '2026-06-23T05:02:05.983645+00:00')}
{'event_': (8, '2026-06-23T05:02:05.967063+00:00'), 'dagrun': (434, '2026-06-23T05:02:05.989838+00:00')}
{'event_': (9, '2026-06-23T05:02:05.972418+00:00'), 'dagrun': (434, '2026-06-23T05:02:05.989838+00:00')}
{'event_': (13, '2026-06-23T05:02:05.982971+00:00'), 'dagrun': (436, '2026-06-23T05:02:05.982971+00:00')}
{'event_': (16, '2026-06-23T05:02:05.983645+00:00'), 'dagrun': ()}
{'event_': (10, '2026-06-23T05:02:05.989838+00:00'), 'dagrun': (434, '2026-06-23T05:02:05.989838+00:00')}
{'event_': (15, '2026-06-23T05:02:05.994289+00:00'), 'dagrun': (435, '2026-06-23T05:02:06.034978+00:00')}
{'event_': (14, '2026-06-23T05:02:05.995763+00:00'), 'dagrun': (435, '2026-06-23T05:02:06.034978+00:00')}
{'event_': (11, '2026-06-23T05:02:06.025005+00:00'), 'dagrun': (435, '2026-06-23T05:02:06.034978+00:00')}
{'event_': (12, '2026-06-23T05:02:06.034978+00:00'), 'dagrun': (435, '2026-06-23T05:02:06.034978+00:00')}
{'event_': (18, '2026-06-23T05:02:06.059185+00:00'), 'dagrun': (437, '2026-06-23T05:02:07.001801+00:00')}
{'event_': (17, '2026-06-23T05:02:06.060623+00:00'), 'dagrun': (437, '2026-06-23T05:02:07.001801+00:00')}
{'event_': (19, '2026-06-23T05:02:06.099119+00:00'), 'dagrun': (437, '2026-06-23T05:02:07.001801+00:00')}
{'event_': (20, '2026-06-23T05:02:07.001801+00:00'), 'dagrun': (437, '2026-06-23T05:02:07.001801+00:00')}
{'event_': (21, '2026-06-23T05:02:07.022033+00:00'), 'dagrun': (438, '2026-06-23T05:02:07.090158+00:00')}
{'event_': (22, '2026-06-23T05:02:07.083870+00:00'), 'dagrun': (438, '2026-06-23T05:02:07.090158+00:00')}
{'event_': (23, '2026-06-23T05:02:07.090158+00:00'), 'dagrun': (438, '2026-06-23T05:02:07.090158+00:00')}
{'event_': (24, '2026-06-23T05:02:07.097476+00:00'), 'dagrun': (439, '2026-06-23T05:02:08.046920+00:00')}
{'event_': (25, '2026-06-23T05:02:07.116000+00:00'), 'dagrun': (439, '2026-06-23T05:02:08.046920+00:00')}
{'event_': (26, '2026-06-23T05:02:08.046920+00:00'), 'dagrun': (439, '2026-06-23T05:02:08.046920+00:00')}
{'event_': (27, '2026-06-23T05:02:08.054838+00:00'), 'dagrun': (440, '2026-06-23T05:02:08.092453+00:00')}
{'event_': (28, '2026-06-23T05:02:08.067990+00:00'), 'dagrun': (440, '2026-06-23T05:02:08.092453+00:00')}
{'event_': (29, '2026-06-23T05:02:08.092453+00:00'), 'dagrun': (440, '2026-06-23T05:02:08.092453+00:00')}
{'event_': (30, '2026-06-23T05:02:08.094004+00:00'), 'dagrun': (441, '2026-06-23T05:02:08.125311+00:00')}
{'event_': (31, '2026-06-23T05:02:08.118823+00:00'), 'dagrun': (441, '2026-06-23T05:02:08.125311+00:00')}
{'event_': (32, '2026-06-23T05:02:08.125311+00:00'), 'dagrun': (441, '2026-06-23T05:02:08.125311+00:00')}
{'event_': (33, '2026-06-23T05:02:08.127450+00:00'), 'dagrun': (442, '2026-06-23T05:02:09.073822+00:00')}
{'event_': (34, '2026-06-23T05:02:08.154315+00:00'), 'dagrun': (442, '2026-06-23T05:02:09.073822+00:00')}
{'event_': (35, '2026-06-23T05:02:09.073822+00:00'), 'dagrun': (442, '2026-06-23T05:02:09.073822+00:00')}
{'event_': (36, '2026-06-23T05:02:09.091265+00:00'), 'dagrun': (443, '2026-06-23T05:02:09.091265+00:00')}
{'event_': (37, '2026-06-23T05:02:09.118804+00:00'), 'dagrun': (444, '2026-06-23T05:02:09.118804+00:00')}

Comment threadairflow-core/tests/unit/jobs/test_scheduler_job.py Outdated
Comment threadairflow-core/tests/unit/jobs/test_scheduler_job.py Outdated
@molcay

molcay commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Hi @molcay, Thank you so much for checking in and offering help! Some extra eyes would be amazing. Right now I am investing the failure ci test on Mysql, the concurrent tests sometimes failure and orphan few asset event, and I can't reproduce it in my local tho. I had logged the asset event and dag run with id and timestamp.

_______________________________________________________________TestSchedulerJob.test_create_dag_runs_when_concurrent_asset_events_created_______________________________________________________________airflow-core/tests/unit/jobs/test_scheduler_job.py:5792: intest_create_dag_runs_when_concurrent_asset_events_createdasserttotal_consumed_asset_events==ASSET_EVENT_COUNTEassert29==30

Hi @dingo4dev,
As I see that you already mark the PR as "ready for review" and you've already got an approval, I need to ask if you still need help on the matter?

@Lee-WLee-W left a comment

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.

a few nits, but overall looks good

Comment threadairflow-core/newsfragments/62501.significant.rst Outdated
Comment threadairflow-core/tests/unit/models/test_taskinstance.py Outdated
Comment threadairflow-core/tests/unit/models/test_taskinstance.py Outdated
Comment threadairflow-core/tests/unit/models/test_taskinstance.py Outdated
Comment threadairflow-core/tests/unit/jobs/test_scheduler_job.py Outdated
Comment threadairflow-core/src/airflow/assets/manager.py Outdated
@dingo4dev

dingo4dev commented Jul 7, 2026

Copy link
Copy Markdown
ContributorAuthor

Hi @dingo4dev, As I see that you already mark the PR as "ready for review" and you've already got an approval, I need to ask if you still need help on the matter?

Hi @molcay ! Thanks for following up.

Even though we got an approval, I have a feeling this might not completely catch every edge case of the underlying issue. we should definitely keep a close eye on it and monitor how it behaves. Thanks again for the support. 🙌

@VladaZakharova

Copy link
Copy Markdown
Contributor

Hi there!
Thank you for the work you are doing, really waiting for the changes to be merged and available <3

Comment threadairflow-core/src/airflow/assets/manager.py

@Lee-WLee-W left a comment

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.

a few nits. mostly good

Comment threadairflow-core/src/airflow/assets/manager.py
Comment threadairflow-core/src/airflow/assets/manager.py Outdated
Comment threadairflow-core/src/airflow/assets/manager.py
Comment threadairflow-core/tests/unit/assets/test_manager.py Outdated
Comment threadairflow-core/tests/unit/models/test_taskinstance.py
Comment threadairflow-core/tests/unit/models/test_taskinstance.py Outdated
@VladaZakharova

Copy link
Copy Markdown
Contributor

Looks like green and beautiful :)
Are we close to merge this change?

dingo4devand others added 8 commits July 29, 2026 14:17
Fixes an issue where the Scheduler misses AssetEvents due to a
visibility gap between flush() and commit().
When multiple tasks create events for the same asset, the AssetEvent
timestamp is generated during flush, but the record remains invisible
to the Scheduler until the final commit. If the AssetDagRunQueue is
processed in the interim, the 'late-committing' event is orphaned.
The job scheduler will read the AssetDagRunQueue and fetch the created_at column as the triggered_date which will use for fetching the asset event and assign dag `run_after`.
This change ensures timestamps are synchronized and the transaction
boundary is tightened to prevent data-aware scheduling misses.
Add check for asset events before creating asset-triggered DAG runs
This is to prevent double triggering when the late-commit asset_dag_run_queue is exist in entry but the asset event is already process in prev dag
Fix update statement condition in AssetDagRunQueue to use correct column reference
Add tests for concurrent asset events and no asset event scenarios in DAG run creation
linting tests
make mypy happy
Add check for existing asset DAG run queue entry before merge
Co-authored-by: Kaxil Naik <kaxilnaik@gmail.com>
add log when no dag run without asset event
Support concurrent asset event queuing on MySQL
Implement a MySQL-specific optimization using `ON DUPLICATE KEY UPDATE` when queuing asset-triggered DAG runs. This prevents unique constraint violations during concurrent asset events, mirroring the existing PostgreSQL implementation. The concurrency tests are also refactored to verify correct behavior across both database dialects.
Refactor asset event handling and improve session management in AssetManager
make lint happy
Refactor asset event handling to streamline session management in AssetManager
Re-attach asset_event to caller's session to prevent DetachedInstanceError
fix: prevent race condition causing lost events in asset-triggered DAG run creation
- Remove CTE lower-bound filter from asset event query: the bound excluded
late-committed events that arrived after a concurrent DagRun was created,
causing those events to be permanently lost and no follow-up DagRun created.
- Add 'not consumed' filter using association_table to prevent duplicate
consumption of asset events across concurrent DagRuns.
- Always delete ADRQ rows after processing, even when no new DagRun is
created, to prevent stale entries accumulating and causing infinite
scheduler loops.
- Fix concurrent test to use seen_dr_ids set instead of prev_dr reference,
ensuring all DagRuns are counted regardless of creation order.
fix: Persist AssetEvent in independent session for immediate visibility
Persist AssetEvent using a truly independent session (`scoped=False`)
to ensure it's committed and visible to processes like the Scheduler
before other transaction operations proceed. This closes a critical
visibility gap for Asset-Triggered DagRuns (ADRQ).
The event is then explicitly reloaded into the caller's session
to prevent DetachedInstanceError and allow subsequent relationship
operations to function correctly.
make static check happy
refactor: Consolidate `AssetEvent` persistence logic
Extract the complex logic for persisting `AssetEvent` instances into a new
dedicated class method, `create_asset_event`.
This centralizes the handling of distinct session management strategies
(independent session for non-SQLite, direct flush for SQLite) and ensures
immediate visibility of asset events to the scheduler. It also re-attaches
the event to the caller's session to prevent `DetachedInstanceError`.
fix: Ensure DagRun updates and AssetEvents are visible across transaction boundaries
Add `session.flush()` after `DagRun` partition key updates to ensure these changes are persisted within the caller's transaction before an independent session for `AssetEvent` creation begins. This prevents stale `DagRun` data from being read.
Change `AssetEvent` creation to use `create_session(scoped=False)`, guaranteeing a truly independent session and immediate commit of the asset event. This ensures the event is visible to processes like the Scheduler for Asset-Triggered DagRuns (ADRQ) without transactional delays.
fix: Change log level from warning to info for DagRun creation status
apply copilot reviewer
test: Add backend markers for concurrent asset event tests
docs: Address asset event scheduling race condition by using independent session commits
Ensures the asset-triggered consumer DAG is registered in the session prior to creating the AssetEvent. This prevents potential test failures or incorrect behavior due to timing or visibility issues with DAG metadata, aligning the test setup with real-world scheduler requirements.
Per review feedback the change is not significant-tier; downgrade to a
bugfix newsfragment while keeping the operator-facing note about the
new duplicate/orphaned asset-event failure mode introduced by the
early-commit fix.
Reflects changes in asset manager's internal queueing helpers to standardize
conflict handling across dialects and to pass the AssetEvent context.
Co-authored-by: Copilot <copilot@github.com>
@Lee-W

Copy link
Copy Markdown
Member

Hey @dingo4dev , thanks for being so patient! We can merge this after the CI is green

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

Labels

area:Schedulerincluding HA (high availability) schedulerready for maintainer reviewSet after triaging when all criteria pass.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

8 participants

@dingo4dev@Lee-W@potiuk@molcay@VladaZakharova@uranusjr@kaxil
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Fix asset event scheduling race condition and consistency - #62501

Merged
Lee-W merged 8 commits into
apache:mainfrom
dingo4dev:fix/asset-triggered-dag-run
Jul 29, 2026
Merged

Fix asset event scheduling race condition and consistency#62501
Lee-W merged 8 commits into
apache:mainfrom
dingo4dev:fix/asset-triggered-dag-run

Conversation

@dingo4dev

@dingo4devdingo4dev commented Feb 26, 2026

Copy link
Copy Markdown
Contributor

This PR addresses a race condition in the Asset-based scheduling logic where AssetEvent records created by concurrent tasks can be "missed" or "orphaned" by the Scheduler.

The issue stems from the Visibility Gap between a database flush() (which generates the created_at timestamp) and the final commit() (which makes the record visible to other sessions).

This change ensures that the synchronization between the AssetEvent and the AssetDagRunQueue is atomic and that the timestamps are aligned to prevent the drift caused by performance issue.

  • Manually assigns the same timestamp to both the AssetEvent and the ADRQ before the flush.
  • Commit and populate the asset event record in database immediately, so that the event can be accessible by job scheduler
  • Prevent creating dag by empty asset event as the asset event is trigger in prev_dag_run

related: #54659, #56750, #56749

---
config:
theme: redux-dark-color
look: neo
---
sequenceDiagram
participant J1 as Task Instance 1
participant J2 as Task Instance 2
participant JS as Job Scheduler
participant DB as Database
Note over J1,DB: T0
Note left of J1: T1
J1-->>DB: Asset Event 1 (AE1) & commit
activate J1
activate DB
Note left of J1: T2
J2-->>DB: Asset Event 2 (AE2) & commit
activate J2
activate DB
Note left of J1: T3
J2-->>-DB: Asset Dag Run Queue (ADRQ)<br/>created_at = AE2.timestamp<br/>(flush & commit)
deactivate DB
Note left of J1: T4
activate JS
activate DB
JS-->>DB: Fetch created_at from ADRQ
JS-->>DB: Fetch events (AE1, AE2)
JS->>JS: Create DAG with run_after = ADRQ created_at (AE2.timestamp)
deactivate DB
deactivate JS
Note left of J1: T5
J1-->>DB: Asset Dag Run Queue (ADRQ)<br/>created_at = AE1.timestamp<br/>(flush & commit)
deactivate J1
deactivate DB
Note left of J1: T6
activate JS
activate DB
JS-->>DB: Fetch created_at from ADRQ
JS->>JS: No event found<br/>(AE1.timestamp < prev DAG run_after)
deactivate DB
deactivate JS
Loading

Durability tradeoff (new failure mode)

On Postgres/MySQL the AssetEvent is committed in a short-lived independent session so it is visible to the scheduler before the producing task's transaction commits the alias association, AssetDagRunQueue rows, and TI state change. This is intentional and required to fix the scheduling race. The tradeoff is that a failure after the event is committed but before the caller's transaction commits can leave:

  • an orphaned asset event — persisted with no AssetDagRunQueue row for the scheduler to consume, or
  • a duplicate asset event — if the producing task retries register_asset_change.

Operators should be aware of this new failure mode when diagnosing missing or duplicate asset-triggered Dag runs. This is also documented in the newsfragment for this change.

Was generative AI tooling used to co-author this PR?
  • Yes (please specify the tool below)
    Copilot

Open with GitKraken

Important

🛠️ Maintainer triage note for @dingo4dev · by @potiuk · 2026-06-17 14:51 UTC

Helpful heads-up from the maintainers — please address before this PR can be reviewed:

  • Static / docs checks failing (CI image checks / Static checks). Run them locally with prek run --all-files (or pre-commit run --all-files) and push the fixes.
  • Failing test jobs: MySQL tests: core / DB-core:MySQL:8.0:3.10:Core...Serialization. Reproduce and fix locally, then push.
  • See the Pull Request quality criteria.

The ball is in your court — you've been assigned to this PR. Fix the above, then mark it Ready for review.

Automated triage — may be imperfect; a maintainer takes the next look.

CopilotAI review requested due to automatic review settings February 26, 2026 04:41
@boring-cyborgboring-cyborgBot added the area:Scheduler including HA (high availability) scheduler label Feb 26, 2026
@dingo4dev
dingo4devforce-pushed the fix/asset-triggered-dag-run branch from 11ed83d to 624e1eaCompareFebruary 26, 2026 04:41
@dingo4devdingo4dev changed the title [WIP] Fix asset event scheduling race conditionFix asset event scheduling race conditionFeb 26, 2026

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR attempts to fix a race condition in asset-based scheduling where AssetEvent records created by concurrent tasks can be missed by the Scheduler due to a visibility gap between database flush() and commit() operations. The fix synchronizes timestamps between AssetEvent and AssetDagRunQueue records by manually assigning timestamps and committing the asset event immediately.

Changes:

  • Modified asset event registration to commit immediately after creating AssetEvent, ensuring early visibility to the scheduler
  • Updated AssetDagRunQueue creation to use the asset event's timestamp for synchronization across both PostgreSQL and slow-path implementations
  • Changed scheduler logic to conditionally create DAG runs only when asset events exist, and to delete ADRQ entries more selectively based on timestamps

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 7 comments.

FileDescription
airflow-core/src/airflow/jobs/scheduler_job_runner.pyModified DAG run creation to be conditional on asset_events existence and updated ADRQ deletion to filter by timestamp
airflow-core/src/airflow/assets/manager.pyChanged from session.flush() to session.commit() for early AssetEvent persistence and updated ADRQ timestamp synchronization logic

Comment threadairflow-core/src/airflow/assets/manager.py Outdated
Comment threadairflow-core/src/airflow/assets/manager.py
Comment threadairflow-core/src/airflow/assets/manager.py Outdated
Comment threadairflow-core/src/airflow/assets/manager.py Outdated
Comment threadairflow-core/src/airflow/jobs/scheduler_job_runner.py Outdated
Comment threadairflow-core/src/airflow/assets/manager.py Outdated
Comment threadairflow-core/src/airflow/assets/manager.py
@dingo4dev
dingo4devforce-pushed the fix/asset-triggered-dag-run branch 4 times, most recently from 2308c0b to 0019359CompareFebruary 27, 2026 02:42
@dingo4devdingo4dev changed the title Fix asset event scheduling race conditionFix asset event scheduling race condition and consistencyFeb 27, 2026
@dingo4dev
dingo4devforce-pushed the fix/asset-triggered-dag-run branch from 0019359 to e796ff5CompareMarch 5, 2026 14:17
@dingo4dev
dingo4devforce-pushed the fix/asset-triggered-dag-run branch from e796ff5 to bcdd0c5CompareMarch 6, 2026 02:03
@dingo4dev

Copy link
Copy Markdown
ContributorAuthor

@Lee-W@uranusjr Please take a time to review it for me. Thanks a lot ;p

joseotaviorf added a commit to joseotaviorf/airflow that referenced this pull request Mar 12, 2026
…rt of apache#62501)
Backports three targeted fixes to 2.10.4 to address the race condition reported
in GH#56541 and GH#41101, where a dataset-dependent DAG fires with one or more
upstream dataset events missing from consumed_dataset_events.
Root cause: stale `created_at` timestamps in DatasetDagRunQueue caused the
scheduler's event-window query to exclude valid events, yet the presence-based
readiness check still considered the DAG ready to run.
Changes:
- manager.py: replace ON CONFLICT DO NOTHING with ON CONFLICT DO UPDATE
(Postgres) and add a WHERE guard to prevent backwards timestamp drift;
pass explicit created_at=utcnow() on the non-Postgres merge path so
existing rows are always refreshed.
- scheduler_job_runner.py: wrap create_dagrun in `if dataset_events:` to
prevent phantom runs when the event window is empty; scope the DDRQ
DELETE to `created_at <= exec_date` instead of deleting all rows, so
events that arrived during processing are preserved for the next cycle.
- tests/datasets/test_manager.py: add WHERE-guard regression test for the
Postgres upsert path.
- dev/BUG_REPORT_DATASET_SCHEDULING.md: detailed bug report with timeline
diagrams, related issues (apache#56541, apache#41101, apache#35870), root cause analysis,
and explanation of each fix.
- reproduce_bug_56541.py: end-to-end reproduction and verification script.
Made-with: Cursor
@dingo4dev
dingo4devforce-pushed the fix/asset-triggered-dag-run branch from bcdd0c5 to c7dae2dCompareMarch 17, 2026 06:36
@Lee-W

Copy link
Copy Markdown
Member

will try to take a look this week

joseotaviorf added a commit to joseotaviorf/airflow that referenced this pull request Mar 19, 2026
…rt of apache#62501)
Backports three targeted fixes to 2.10.4 to address the race condition reported
in GH#56541 and GH#41101, where a dataset-dependent DAG fires with one or more
upstream dataset events missing from consumed_dataset_events.
Root cause: stale `created_at` timestamps in DatasetDagRunQueue caused the
scheduler's event-window query to exclude valid events, yet the presence-based
readiness check still considered the DAG ready to run.
Changes:
- manager.py: replace ON CONFLICT DO NOTHING with ON CONFLICT DO UPDATE
(Postgres) and add a WHERE guard to prevent backwards timestamp drift;
pass explicit created_at=utcnow() on the non-Postgres merge path so
existing rows are always refreshed.
- scheduler_job_runner.py: wrap create_dagrun in `if dataset_events:` to
prevent phantom runs when the event window is empty; scope the DDRQ
DELETE to `created_at <= exec_date` instead of deleting all rows, so
events that arrived during processing are preserved for the next cycle.
- tests/datasets/test_manager.py: add WHERE-guard regression test for the
Postgres upsert path.
- dev/BUG_REPORT_DATASET_SCHEDULING.md: detailed bug report with timeline
diagrams, related issues (apache#56541, apache#41101, apache#35870), root cause analysis,
and explanation of each fix.
- reproduce_bug_56541.py: end-to-end reproduction and verification script.

@kaxilkaxil left a comment

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.

Left a few comments

Comment threadairflow-core/src/airflow/assets/manager.py Outdated
Comment threadairflow-core/src/airflow/assets/manager.py
Comment threadairflow-core/src/airflow/jobs/scheduler_job_runner.py Outdated
Comment threadairflow-core/tests/unit/jobs/test_scheduler_job.py Outdated
Comment threadairflow-core/tests/unit/jobs/test_scheduler_job.py
@dingo4dev
dingo4devforce-pushed the fix/asset-triggered-dag-run branch 2 times, most recently from e674f0f to 1a08e6fCompareMarch 22, 2026 19:58
@dingo4dev
dingo4devforce-pushed the fix/asset-triggered-dag-run branch 2 times, most recently from 3a647bf to f0d5a65CompareMarch 29, 2026 16:48
@kaxil
kaxil requested a review from CopilotApril 2, 2026 01:03
Comment threadairflow-core/src/airflow/assets/manager.py Outdated
Comment threadairflow-core/src/airflow/assets/manager.py Outdated
Comment threadairflow-core/src/airflow/assets/manager.py
@molcay

Copy link
Copy Markdown
Contributor

Hi @dingo4dev, I wanted to ping you here if you missed my message on Slack.

I am repeating my message and offer for help in here as well:

Hi @Stanley Law, hope you're having a good week!

I have been following your PR, it seems great stuff and thanks for tackling this. I know bouncing between reviews and CI pipelines can take up a lot of time. If you're feeling swamped or just want someone to bounce ideas off of (since I am having trouble reproducing the issue), I'd be glad to step in and help in way that I can. If you've got it fully under control though, totally feel free to ignore this message 🙂
Just wanted to offer some support.

@dingo4dev

Copy link
Copy Markdown
ContributorAuthor

Hi @molcay, Thank you so much for checking in and offering help!
Some extra eyes would be amazing. Right now I am investing the failure ci test on Mysql, the concurrent tests sometimes failure and orphan few asset event, and I can't reproduce it in my local tho.
I had logged the asset event and dag run with id and timestamp.

_______________________________________________________________TestSchedulerJob.test_create_dag_runs_when_concurrent_asset_events_created_______________________________________________________________airflow-core/tests/unit/jobs/test_scheduler_job.py:5792: intest_create_dag_runs_when_concurrent_asset_events_createdasserttotal_consumed_asset_events==ASSET_EVENT_COUNTEassert29==30--------------------------------------------Capturedstdoutsetup-----------------------------------------
] FillinguptheDagBagfrom/dev/null [airflow.dag_processing.dagbag.DagBag]
--------------------------------------------Capturedstdoutcall------------------------------------------MissingAssetEventMetadata: {(16, '2026-06-23T05:02:05.983645+00:00')}
{'event_': (8, '2026-06-23T05:02:05.967063+00:00'), 'dagrun': (434, '2026-06-23T05:02:05.989838+00:00')}
{'event_': (9, '2026-06-23T05:02:05.972418+00:00'), 'dagrun': (434, '2026-06-23T05:02:05.989838+00:00')}
{'event_': (13, '2026-06-23T05:02:05.982971+00:00'), 'dagrun': (436, '2026-06-23T05:02:05.982971+00:00')}
{'event_': (16, '2026-06-23T05:02:05.983645+00:00'), 'dagrun': ()}
{'event_': (10, '2026-06-23T05:02:05.989838+00:00'), 'dagrun': (434, '2026-06-23T05:02:05.989838+00:00')}
{'event_': (15, '2026-06-23T05:02:05.994289+00:00'), 'dagrun': (435, '2026-06-23T05:02:06.034978+00:00')}
{'event_': (14, '2026-06-23T05:02:05.995763+00:00'), 'dagrun': (435, '2026-06-23T05:02:06.034978+00:00')}
{'event_': (11, '2026-06-23T05:02:06.025005+00:00'), 'dagrun': (435, '2026-06-23T05:02:06.034978+00:00')}
{'event_': (12, '2026-06-23T05:02:06.034978+00:00'), 'dagrun': (435, '2026-06-23T05:02:06.034978+00:00')}
{'event_': (18, '2026-06-23T05:02:06.059185+00:00'), 'dagrun': (437, '2026-06-23T05:02:07.001801+00:00')}
{'event_': (17, '2026-06-23T05:02:06.060623+00:00'), 'dagrun': (437, '2026-06-23T05:02:07.001801+00:00')}
{'event_': (19, '2026-06-23T05:02:06.099119+00:00'), 'dagrun': (437, '2026-06-23T05:02:07.001801+00:00')}
{'event_': (20, '2026-06-23T05:02:07.001801+00:00'), 'dagrun': (437, '2026-06-23T05:02:07.001801+00:00')}
{'event_': (21, '2026-06-23T05:02:07.022033+00:00'), 'dagrun': (438, '2026-06-23T05:02:07.090158+00:00')}
{'event_': (22, '2026-06-23T05:02:07.083870+00:00'), 'dagrun': (438, '2026-06-23T05:02:07.090158+00:00')}
{'event_': (23, '2026-06-23T05:02:07.090158+00:00'), 'dagrun': (438, '2026-06-23T05:02:07.090158+00:00')}
{'event_': (24, '2026-06-23T05:02:07.097476+00:00'), 'dagrun': (439, '2026-06-23T05:02:08.046920+00:00')}
{'event_': (25, '2026-06-23T05:02:07.116000+00:00'), 'dagrun': (439, '2026-06-23T05:02:08.046920+00:00')}
{'event_': (26, '2026-06-23T05:02:08.046920+00:00'), 'dagrun': (439, '2026-06-23T05:02:08.046920+00:00')}
{'event_': (27, '2026-06-23T05:02:08.054838+00:00'), 'dagrun': (440, '2026-06-23T05:02:08.092453+00:00')}
{'event_': (28, '2026-06-23T05:02:08.067990+00:00'), 'dagrun': (440, '2026-06-23T05:02:08.092453+00:00')}
{'event_': (29, '2026-06-23T05:02:08.092453+00:00'), 'dagrun': (440, '2026-06-23T05:02:08.092453+00:00')}
{'event_': (30, '2026-06-23T05:02:08.094004+00:00'), 'dagrun': (441, '2026-06-23T05:02:08.125311+00:00')}
{'event_': (31, '2026-06-23T05:02:08.118823+00:00'), 'dagrun': (441, '2026-06-23T05:02:08.125311+00:00')}
{'event_': (32, '2026-06-23T05:02:08.125311+00:00'), 'dagrun': (441, '2026-06-23T05:02:08.125311+00:00')}
{'event_': (33, '2026-06-23T05:02:08.127450+00:00'), 'dagrun': (442, '2026-06-23T05:02:09.073822+00:00')}
{'event_': (34, '2026-06-23T05:02:08.154315+00:00'), 'dagrun': (442, '2026-06-23T05:02:09.073822+00:00')}
{'event_': (35, '2026-06-23T05:02:09.073822+00:00'), 'dagrun': (442, '2026-06-23T05:02:09.073822+00:00')}
{'event_': (36, '2026-06-23T05:02:09.091265+00:00'), 'dagrun': (443, '2026-06-23T05:02:09.091265+00:00')}
{'event_': (37, '2026-06-23T05:02:09.118804+00:00'), 'dagrun': (444, '2026-06-23T05:02:09.118804+00:00')}

Comment threadairflow-core/tests/unit/jobs/test_scheduler_job.py Outdated
Comment threadairflow-core/tests/unit/jobs/test_scheduler_job.py Outdated
@molcay

molcay commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Hi @molcay, Thank you so much for checking in and offering help! Some extra eyes would be amazing. Right now I am investing the failure ci test on Mysql, the concurrent tests sometimes failure and orphan few asset event, and I can't reproduce it in my local tho. I had logged the asset event and dag run with id and timestamp.

_______________________________________________________________TestSchedulerJob.test_create_dag_runs_when_concurrent_asset_events_created_______________________________________________________________airflow-core/tests/unit/jobs/test_scheduler_job.py:5792: intest_create_dag_runs_when_concurrent_asset_events_createdasserttotal_consumed_asset_events==ASSET_EVENT_COUNTEassert29==30

Hi @dingo4dev,
As I see that you already mark the PR as "ready for review" and you've already got an approval, I need to ask if you still need help on the matter?

@Lee-WLee-W left a comment

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.

a few nits, but overall looks good

Comment threadairflow-core/newsfragments/62501.significant.rst Outdated
Comment threadairflow-core/tests/unit/models/test_taskinstance.py Outdated
Comment threadairflow-core/tests/unit/models/test_taskinstance.py Outdated
Comment threadairflow-core/tests/unit/models/test_taskinstance.py Outdated
Comment threadairflow-core/tests/unit/jobs/test_scheduler_job.py Outdated
Comment threadairflow-core/src/airflow/assets/manager.py Outdated
@dingo4dev

dingo4dev commented Jul 7, 2026

Copy link
Copy Markdown
ContributorAuthor

Hi @dingo4dev, As I see that you already mark the PR as "ready for review" and you've already got an approval, I need to ask if you still need help on the matter?

Hi @molcay ! Thanks for following up.

Even though we got an approval, I have a feeling this might not completely catch every edge case of the underlying issue. we should definitely keep a close eye on it and monitor how it behaves. Thanks again for the support. 🙌

@VladaZakharova

Copy link
Copy Markdown
Contributor

Hi there!
Thank you for the work you are doing, really waiting for the changes to be merged and available <3

Comment threadairflow-core/src/airflow/assets/manager.py

@Lee-WLee-W left a comment

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.

a few nits. mostly good

Comment threadairflow-core/src/airflow/assets/manager.py
Comment threadairflow-core/src/airflow/assets/manager.py Outdated
Comment threadairflow-core/src/airflow/assets/manager.py
Comment threadairflow-core/tests/unit/assets/test_manager.py Outdated
Comment threadairflow-core/tests/unit/models/test_taskinstance.py
Comment threadairflow-core/tests/unit/models/test_taskinstance.py Outdated
@VladaZakharova

Copy link
Copy Markdown
Contributor

Looks like green and beautiful :)
Are we close to merge this change?

dingo4devand others added 8 commits July 29, 2026 14:17
Fixes an issue where the Scheduler misses AssetEvents due to a
visibility gap between flush() and commit().
When multiple tasks create events for the same asset, the AssetEvent
timestamp is generated during flush, but the record remains invisible
to the Scheduler until the final commit. If the AssetDagRunQueue is
processed in the interim, the 'late-committing' event is orphaned.
The job scheduler will read the AssetDagRunQueue and fetch the created_at column as the triggered_date which will use for fetching the asset event and assign dag `run_after`.
This change ensures timestamps are synchronized and the transaction
boundary is tightened to prevent data-aware scheduling misses.
Add check for asset events before creating asset-triggered DAG runs
This is to prevent double triggering when the late-commit asset_dag_run_queue is exist in entry but the asset event is already process in prev dag
Fix update statement condition in AssetDagRunQueue to use correct column reference
Add tests for concurrent asset events and no asset event scenarios in DAG run creation
linting tests
make mypy happy
Add check for existing asset DAG run queue entry before merge
Co-authored-by: Kaxil Naik <kaxilnaik@gmail.com>
add log when no dag run without asset event
Support concurrent asset event queuing on MySQL
Implement a MySQL-specific optimization using `ON DUPLICATE KEY UPDATE` when queuing asset-triggered DAG runs. This prevents unique constraint violations during concurrent asset events, mirroring the existing PostgreSQL implementation. The concurrency tests are also refactored to verify correct behavior across both database dialects.
Refactor asset event handling and improve session management in AssetManager
make lint happy
Refactor asset event handling to streamline session management in AssetManager
Re-attach asset_event to caller's session to prevent DetachedInstanceError
fix: prevent race condition causing lost events in asset-triggered DAG run creation
- Remove CTE lower-bound filter from asset event query: the bound excluded
late-committed events that arrived after a concurrent DagRun was created,
causing those events to be permanently lost and no follow-up DagRun created.
- Add 'not consumed' filter using association_table to prevent duplicate
consumption of asset events across concurrent DagRuns.
- Always delete ADRQ rows after processing, even when no new DagRun is
created, to prevent stale entries accumulating and causing infinite
scheduler loops.
- Fix concurrent test to use seen_dr_ids set instead of prev_dr reference,
ensuring all DagRuns are counted regardless of creation order.
fix: Persist AssetEvent in independent session for immediate visibility
Persist AssetEvent using a truly independent session (`scoped=False`)
to ensure it's committed and visible to processes like the Scheduler
before other transaction operations proceed. This closes a critical
visibility gap for Asset-Triggered DagRuns (ADRQ).
The event is then explicitly reloaded into the caller's session
to prevent DetachedInstanceError and allow subsequent relationship
operations to function correctly.
make static check happy
refactor: Consolidate `AssetEvent` persistence logic
Extract the complex logic for persisting `AssetEvent` instances into a new
dedicated class method, `create_asset_event`.
This centralizes the handling of distinct session management strategies
(independent session for non-SQLite, direct flush for SQLite) and ensures
immediate visibility of asset events to the scheduler. It also re-attaches
the event to the caller's session to prevent `DetachedInstanceError`.
fix: Ensure DagRun updates and AssetEvents are visible across transaction boundaries
Add `session.flush()` after `DagRun` partition key updates to ensure these changes are persisted within the caller's transaction before an independent session for `AssetEvent` creation begins. This prevents stale `DagRun` data from being read.
Change `AssetEvent` creation to use `create_session(scoped=False)`, guaranteeing a truly independent session and immediate commit of the asset event. This ensures the event is visible to processes like the Scheduler for Asset-Triggered DagRuns (ADRQ) without transactional delays.
fix: Change log level from warning to info for DagRun creation status
apply copilot reviewer
test: Add backend markers for concurrent asset event tests
docs: Address asset event scheduling race condition by using independent session commits
Ensures the asset-triggered consumer DAG is registered in the session prior to creating the AssetEvent. This prevents potential test failures or incorrect behavior due to timing or visibility issues with DAG metadata, aligning the test setup with real-world scheduler requirements.
Per review feedback the change is not significant-tier; downgrade to a
bugfix newsfragment while keeping the operator-facing note about the
new duplicate/orphaned asset-event failure mode introduced by the
early-commit fix.
Reflects changes in asset manager's internal queueing helpers to standardize
conflict handling across dialects and to pass the AssetEvent context.
Co-authored-by: Copilot <copilot@github.com>
@Lee-W

Copy link
Copy Markdown
Member

Hey @dingo4dev , thanks for being so patient! We can merge this after the CI is green

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

Labels

area:Schedulerincluding HA (high availability) schedulerready for maintainer reviewSet after triaging when all criteria pass.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

8 participants

@dingo4dev@Lee-W@potiuk@molcay@VladaZakharova@uranusjr@kaxil
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Fix asset event scheduling race condition and consistency - #62501

Merged
Lee-W merged 8 commits into
apache:mainfrom
dingo4dev:fix/asset-triggered-dag-run
Jul 29, 2026
Merged

Fix asset event scheduling race condition and consistency#62501
Lee-W merged 8 commits into
apache:mainfrom
dingo4dev:fix/asset-triggered-dag-run

Conversation

@dingo4dev

@dingo4devdingo4dev commented Feb 26, 2026

Copy link
Copy Markdown
Contributor

This PR addresses a race condition in the Asset-based scheduling logic where AssetEvent records created by concurrent tasks can be "missed" or "orphaned" by the Scheduler.

The issue stems from the Visibility Gap between a database flush() (which generates the created_at timestamp) and the final commit() (which makes the record visible to other sessions).

This change ensures that the synchronization between the AssetEvent and the AssetDagRunQueue is atomic and that the timestamps are aligned to prevent the drift caused by performance issue.

  • Manually assigns the same timestamp to both the AssetEvent and the ADRQ before the flush.
  • Commit and populate the asset event record in database immediately, so that the event can be accessible by job scheduler
  • Prevent creating dag by empty asset event as the asset event is trigger in prev_dag_run

related: #54659, #56750, #56749

---
config:
theme: redux-dark-color
look: neo
---
sequenceDiagram
participant J1 as Task Instance 1
participant J2 as Task Instance 2
participant JS as Job Scheduler
participant DB as Database
Note over J1,DB: T0
Note left of J1: T1
J1-->>DB: Asset Event 1 (AE1) & commit
activate J1
activate DB
Note left of J1: T2
J2-->>DB: Asset Event 2 (AE2) & commit
activate J2
activate DB
Note left of J1: T3
J2-->>-DB: Asset Dag Run Queue (ADRQ)<br/>created_at = AE2.timestamp<br/>(flush & commit)
deactivate DB
Note left of J1: T4
activate JS
activate DB
JS-->>DB: Fetch created_at from ADRQ
JS-->>DB: Fetch events (AE1, AE2)
JS->>JS: Create DAG with run_after = ADRQ created_at (AE2.timestamp)
deactivate DB
deactivate JS
Note left of J1: T5
J1-->>DB: Asset Dag Run Queue (ADRQ)<br/>created_at = AE1.timestamp<br/>(flush & commit)
deactivate J1
deactivate DB
Note left of J1: T6
activate JS
activate DB
JS-->>DB: Fetch created_at from ADRQ
JS->>JS: No event found<br/>(AE1.timestamp < prev DAG run_after)
deactivate DB
deactivate JS
Loading

Durability tradeoff (new failure mode)

On Postgres/MySQL the AssetEvent is committed in a short-lived independent session so it is visible to the scheduler before the producing task's transaction commits the alias association, AssetDagRunQueue rows, and TI state change. This is intentional and required to fix the scheduling race. The tradeoff is that a failure after the event is committed but before the caller's transaction commits can leave:

  • an orphaned asset event — persisted with no AssetDagRunQueue row for the scheduler to consume, or
  • a duplicate asset event — if the producing task retries register_asset_change.

Operators should be aware of this new failure mode when diagnosing missing or duplicate asset-triggered Dag runs. This is also documented in the newsfragment for this change.

Was generative AI tooling used to co-author this PR?
  • Yes (please specify the tool below)
    Copilot

Open with GitKraken

Important

🛠️ Maintainer triage note for @dingo4dev · by @potiuk · 2026-06-17 14:51 UTC

Helpful heads-up from the maintainers — please address before this PR can be reviewed:

  • Static / docs checks failing (CI image checks / Static checks). Run them locally with prek run --all-files (or pre-commit run --all-files) and push the fixes.
  • Failing test jobs: MySQL tests: core / DB-core:MySQL:8.0:3.10:Core...Serialization. Reproduce and fix locally, then push.
  • See the Pull Request quality criteria.

The ball is in your court — you've been assigned to this PR. Fix the above, then mark it Ready for review.

Automated triage — may be imperfect; a maintainer takes the next look.

CopilotAI review requested due to automatic review settings February 26, 2026 04:41
@boring-cyborgboring-cyborgBot added the area:Scheduler including HA (high availability) scheduler label Feb 26, 2026
@dingo4dev
dingo4devforce-pushed the fix/asset-triggered-dag-run branch from 11ed83d to 624e1eaCompareFebruary 26, 2026 04:41
@dingo4devdingo4dev changed the title [WIP] Fix asset event scheduling race conditionFix asset event scheduling race conditionFeb 26, 2026

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR attempts to fix a race condition in asset-based scheduling where AssetEvent records created by concurrent tasks can be missed by the Scheduler due to a visibility gap between database flush() and commit() operations. The fix synchronizes timestamps between AssetEvent and AssetDagRunQueue records by manually assigning timestamps and committing the asset event immediately.

Changes:

  • Modified asset event registration to commit immediately after creating AssetEvent, ensuring early visibility to the scheduler
  • Updated AssetDagRunQueue creation to use the asset event's timestamp for synchronization across both PostgreSQL and slow-path implementations
  • Changed scheduler logic to conditionally create DAG runs only when asset events exist, and to delete ADRQ entries more selectively based on timestamps

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 7 comments.

FileDescription
airflow-core/src/airflow/jobs/scheduler_job_runner.pyModified DAG run creation to be conditional on asset_events existence and updated ADRQ deletion to filter by timestamp
airflow-core/src/airflow/assets/manager.pyChanged from session.flush() to session.commit() for early AssetEvent persistence and updated ADRQ timestamp synchronization logic

Comment threadairflow-core/src/airflow/assets/manager.py Outdated
Comment threadairflow-core/src/airflow/assets/manager.py
Comment threadairflow-core/src/airflow/assets/manager.py Outdated
Comment threadairflow-core/src/airflow/assets/manager.py Outdated
Comment threadairflow-core/src/airflow/jobs/scheduler_job_runner.py Outdated
Comment threadairflow-core/src/airflow/assets/manager.py Outdated
Comment threadairflow-core/src/airflow/assets/manager.py
@dingo4dev
dingo4devforce-pushed the fix/asset-triggered-dag-run branch 4 times, most recently from 2308c0b to 0019359CompareFebruary 27, 2026 02:42
@dingo4devdingo4dev changed the title Fix asset event scheduling race conditionFix asset event scheduling race condition and consistencyFeb 27, 2026
@dingo4dev
dingo4devforce-pushed the fix/asset-triggered-dag-run branch from 0019359 to e796ff5CompareMarch 5, 2026 14:17
@dingo4dev
dingo4devforce-pushed the fix/asset-triggered-dag-run branch from e796ff5 to bcdd0c5CompareMarch 6, 2026 02:03
@dingo4dev

Copy link
Copy Markdown
ContributorAuthor

@Lee-W@uranusjr Please take a time to review it for me. Thanks a lot ;p

joseotaviorf added a commit to joseotaviorf/airflow that referenced this pull request Mar 12, 2026
…rt of apache#62501)
Backports three targeted fixes to 2.10.4 to address the race condition reported
in GH#56541 and GH#41101, where a dataset-dependent DAG fires with one or more
upstream dataset events missing from consumed_dataset_events.
Root cause: stale `created_at` timestamps in DatasetDagRunQueue caused the
scheduler's event-window query to exclude valid events, yet the presence-based
readiness check still considered the DAG ready to run.
Changes:
- manager.py: replace ON CONFLICT DO NOTHING with ON CONFLICT DO UPDATE
(Postgres) and add a WHERE guard to prevent backwards timestamp drift;
pass explicit created_at=utcnow() on the non-Postgres merge path so
existing rows are always refreshed.
- scheduler_job_runner.py: wrap create_dagrun in `if dataset_events:` to
prevent phantom runs when the event window is empty; scope the DDRQ
DELETE to `created_at <= exec_date` instead of deleting all rows, so
events that arrived during processing are preserved for the next cycle.
- tests/datasets/test_manager.py: add WHERE-guard regression test for the
Postgres upsert path.
- dev/BUG_REPORT_DATASET_SCHEDULING.md: detailed bug report with timeline
diagrams, related issues (apache#56541, apache#41101, apache#35870), root cause analysis,
and explanation of each fix.
- reproduce_bug_56541.py: end-to-end reproduction and verification script.
Made-with: Cursor
@dingo4dev
dingo4devforce-pushed the fix/asset-triggered-dag-run branch from bcdd0c5 to c7dae2dCompareMarch 17, 2026 06:36
@Lee-W

Copy link
Copy Markdown
Member

will try to take a look this week

joseotaviorf added a commit to joseotaviorf/airflow that referenced this pull request Mar 19, 2026
…rt of apache#62501)
Backports three targeted fixes to 2.10.4 to address the race condition reported
in GH#56541 and GH#41101, where a dataset-dependent DAG fires with one or more
upstream dataset events missing from consumed_dataset_events.
Root cause: stale `created_at` timestamps in DatasetDagRunQueue caused the
scheduler's event-window query to exclude valid events, yet the presence-based
readiness check still considered the DAG ready to run.
Changes:
- manager.py: replace ON CONFLICT DO NOTHING with ON CONFLICT DO UPDATE
(Postgres) and add a WHERE guard to prevent backwards timestamp drift;
pass explicit created_at=utcnow() on the non-Postgres merge path so
existing rows are always refreshed.
- scheduler_job_runner.py: wrap create_dagrun in `if dataset_events:` to
prevent phantom runs when the event window is empty; scope the DDRQ
DELETE to `created_at <= exec_date` instead of deleting all rows, so
events that arrived during processing are preserved for the next cycle.
- tests/datasets/test_manager.py: add WHERE-guard regression test for the
Postgres upsert path.
- dev/BUG_REPORT_DATASET_SCHEDULING.md: detailed bug report with timeline
diagrams, related issues (apache#56541, apache#41101, apache#35870), root cause analysis,
and explanation of each fix.
- reproduce_bug_56541.py: end-to-end reproduction and verification script.

@kaxilkaxil left a comment

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.

Left a few comments

Comment threadairflow-core/src/airflow/assets/manager.py Outdated
Comment threadairflow-core/src/airflow/assets/manager.py
Comment threadairflow-core/src/airflow/jobs/scheduler_job_runner.py Outdated
Comment threadairflow-core/tests/unit/jobs/test_scheduler_job.py Outdated
Comment threadairflow-core/tests/unit/jobs/test_scheduler_job.py
@dingo4dev
dingo4devforce-pushed the fix/asset-triggered-dag-run branch 2 times, most recently from e674f0f to 1a08e6fCompareMarch 22, 2026 19:58
@dingo4dev
dingo4devforce-pushed the fix/asset-triggered-dag-run branch 2 times, most recently from 3a647bf to f0d5a65CompareMarch 29, 2026 16:48
@kaxil
kaxil requested a review from CopilotApril 2, 2026 01:03
Comment threadairflow-core/src/airflow/assets/manager.py Outdated
Comment threadairflow-core/src/airflow/assets/manager.py Outdated
Comment threadairflow-core/src/airflow/assets/manager.py
@molcay

Copy link
Copy Markdown
Contributor

Hi @dingo4dev, I wanted to ping you here if you missed my message on Slack.

I am repeating my message and offer for help in here as well:

Hi @Stanley Law, hope you're having a good week!

I have been following your PR, it seems great stuff and thanks for tackling this. I know bouncing between reviews and CI pipelines can take up a lot of time. If you're feeling swamped or just want someone to bounce ideas off of (since I am having trouble reproducing the issue), I'd be glad to step in and help in way that I can. If you've got it fully under control though, totally feel free to ignore this message 🙂
Just wanted to offer some support.

@dingo4dev

Copy link
Copy Markdown
ContributorAuthor

Hi @molcay, Thank you so much for checking in and offering help!
Some extra eyes would be amazing. Right now I am investing the failure ci test on Mysql, the concurrent tests sometimes failure and orphan few asset event, and I can't reproduce it in my local tho.
I had logged the asset event and dag run with id and timestamp.

_______________________________________________________________TestSchedulerJob.test_create_dag_runs_when_concurrent_asset_events_created_______________________________________________________________airflow-core/tests/unit/jobs/test_scheduler_job.py:5792: intest_create_dag_runs_when_concurrent_asset_events_createdasserttotal_consumed_asset_events==ASSET_EVENT_COUNTEassert29==30--------------------------------------------Capturedstdoutsetup-----------------------------------------
] FillinguptheDagBagfrom/dev/null [airflow.dag_processing.dagbag.DagBag]
--------------------------------------------Capturedstdoutcall------------------------------------------MissingAssetEventMetadata: {(16, '2026-06-23T05:02:05.983645+00:00')}
{'event_': (8, '2026-06-23T05:02:05.967063+00:00'), 'dagrun': (434, '2026-06-23T05:02:05.989838+00:00')}
{'event_': (9, '2026-06-23T05:02:05.972418+00:00'), 'dagrun': (434, '2026-06-23T05:02:05.989838+00:00')}
{'event_': (13, '2026-06-23T05:02:05.982971+00:00'), 'dagrun': (436, '2026-06-23T05:02:05.982971+00:00')}
{'event_': (16, '2026-06-23T05:02:05.983645+00:00'), 'dagrun': ()}
{'event_': (10, '2026-06-23T05:02:05.989838+00:00'), 'dagrun': (434, '2026-06-23T05:02:05.989838+00:00')}
{'event_': (15, '2026-06-23T05:02:05.994289+00:00'), 'dagrun': (435, '2026-06-23T05:02:06.034978+00:00')}
{'event_': (14, '2026-06-23T05:02:05.995763+00:00'), 'dagrun': (435, '2026-06-23T05:02:06.034978+00:00')}
{'event_': (11, '2026-06-23T05:02:06.025005+00:00'), 'dagrun': (435, '2026-06-23T05:02:06.034978+00:00')}
{'event_': (12, '2026-06-23T05:02:06.034978+00:00'), 'dagrun': (435, '2026-06-23T05:02:06.034978+00:00')}
{'event_': (18, '2026-06-23T05:02:06.059185+00:00'), 'dagrun': (437, '2026-06-23T05:02:07.001801+00:00')}
{'event_': (17, '2026-06-23T05:02:06.060623+00:00'), 'dagrun': (437, '2026-06-23T05:02:07.001801+00:00')}
{'event_': (19, '2026-06-23T05:02:06.099119+00:00'), 'dagrun': (437, '2026-06-23T05:02:07.001801+00:00')}
{'event_': (20, '2026-06-23T05:02:07.001801+00:00'), 'dagrun': (437, '2026-06-23T05:02:07.001801+00:00')}
{'event_': (21, '2026-06-23T05:02:07.022033+00:00'), 'dagrun': (438, '2026-06-23T05:02:07.090158+00:00')}
{'event_': (22, '2026-06-23T05:02:07.083870+00:00'), 'dagrun': (438, '2026-06-23T05:02:07.090158+00:00')}
{'event_': (23, '2026-06-23T05:02:07.090158+00:00'), 'dagrun': (438, '2026-06-23T05:02:07.090158+00:00')}
{'event_': (24, '2026-06-23T05:02:07.097476+00:00'), 'dagrun': (439, '2026-06-23T05:02:08.046920+00:00')}
{'event_': (25, '2026-06-23T05:02:07.116000+00:00'), 'dagrun': (439, '2026-06-23T05:02:08.046920+00:00')}
{'event_': (26, '2026-06-23T05:02:08.046920+00:00'), 'dagrun': (439, '2026-06-23T05:02:08.046920+00:00')}
{'event_': (27, '2026-06-23T05:02:08.054838+00:00'), 'dagrun': (440, '2026-06-23T05:02:08.092453+00:00')}
{'event_': (28, '2026-06-23T05:02:08.067990+00:00'), 'dagrun': (440, '2026-06-23T05:02:08.092453+00:00')}
{'event_': (29, '2026-06-23T05:02:08.092453+00:00'), 'dagrun': (440, '2026-06-23T05:02:08.092453+00:00')}
{'event_': (30, '2026-06-23T05:02:08.094004+00:00'), 'dagrun': (441, '2026-06-23T05:02:08.125311+00:00')}
{'event_': (31, '2026-06-23T05:02:08.118823+00:00'), 'dagrun': (441, '2026-06-23T05:02:08.125311+00:00')}
{'event_': (32, '2026-06-23T05:02:08.125311+00:00'), 'dagrun': (441, '2026-06-23T05:02:08.125311+00:00')}
{'event_': (33, '2026-06-23T05:02:08.127450+00:00'), 'dagrun': (442, '2026-06-23T05:02:09.073822+00:00')}
{'event_': (34, '2026-06-23T05:02:08.154315+00:00'), 'dagrun': (442, '2026-06-23T05:02:09.073822+00:00')}
{'event_': (35, '2026-06-23T05:02:09.073822+00:00'), 'dagrun': (442, '2026-06-23T05:02:09.073822+00:00')}
{'event_': (36, '2026-06-23T05:02:09.091265+00:00'), 'dagrun': (443, '2026-06-23T05:02:09.091265+00:00')}
{'event_': (37, '2026-06-23T05:02:09.118804+00:00'), 'dagrun': (444, '2026-06-23T05:02:09.118804+00:00')}

Comment threadairflow-core/tests/unit/jobs/test_scheduler_job.py Outdated
Comment threadairflow-core/tests/unit/jobs/test_scheduler_job.py Outdated
@molcay

molcay commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Hi @molcay, Thank you so much for checking in and offering help! Some extra eyes would be amazing. Right now I am investing the failure ci test on Mysql, the concurrent tests sometimes failure and orphan few asset event, and I can't reproduce it in my local tho. I had logged the asset event and dag run with id and timestamp.

_______________________________________________________________TestSchedulerJob.test_create_dag_runs_when_concurrent_asset_events_created_______________________________________________________________airflow-core/tests/unit/jobs/test_scheduler_job.py:5792: intest_create_dag_runs_when_concurrent_asset_events_createdasserttotal_consumed_asset_events==ASSET_EVENT_COUNTEassert29==30

Hi @dingo4dev,
As I see that you already mark the PR as "ready for review" and you've already got an approval, I need to ask if you still need help on the matter?

@Lee-WLee-W left a comment

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.

a few nits, but overall looks good

Comment threadairflow-core/newsfragments/62501.significant.rst Outdated
Comment threadairflow-core/tests/unit/models/test_taskinstance.py Outdated
Comment threadairflow-core/tests/unit/models/test_taskinstance.py Outdated
Comment threadairflow-core/tests/unit/models/test_taskinstance.py Outdated
Comment threadairflow-core/tests/unit/jobs/test_scheduler_job.py Outdated
Comment threadairflow-core/src/airflow/assets/manager.py Outdated
@dingo4dev

dingo4dev commented Jul 7, 2026

Copy link
Copy Markdown
ContributorAuthor

Hi @dingo4dev, As I see that you already mark the PR as "ready for review" and you've already got an approval, I need to ask if you still need help on the matter?

Hi @molcay ! Thanks for following up.

Even though we got an approval, I have a feeling this might not completely catch every edge case of the underlying issue. we should definitely keep a close eye on it and monitor how it behaves. Thanks again for the support. 🙌

@VladaZakharova

Copy link
Copy Markdown
Contributor

Hi there!
Thank you for the work you are doing, really waiting for the changes to be merged and available <3

Comment threadairflow-core/src/airflow/assets/manager.py

@Lee-WLee-W left a comment

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.

a few nits. mostly good

Comment threadairflow-core/src/airflow/assets/manager.py
Comment threadairflow-core/src/airflow/assets/manager.py Outdated
Comment threadairflow-core/src/airflow/assets/manager.py
Comment threadairflow-core/tests/unit/assets/test_manager.py Outdated
Comment threadairflow-core/tests/unit/models/test_taskinstance.py
Comment threadairflow-core/tests/unit/models/test_taskinstance.py Outdated
@VladaZakharova

Copy link
Copy Markdown
Contributor

Looks like green and beautiful :)
Are we close to merge this change?

dingo4devand others added 8 commits July 29, 2026 14:17
Fixes an issue where the Scheduler misses AssetEvents due to a
visibility gap between flush() and commit().
When multiple tasks create events for the same asset, the AssetEvent
timestamp is generated during flush, but the record remains invisible
to the Scheduler until the final commit. If the AssetDagRunQueue is
processed in the interim, the 'late-committing' event is orphaned.
The job scheduler will read the AssetDagRunQueue and fetch the created_at column as the triggered_date which will use for fetching the asset event and assign dag `run_after`.
This change ensures timestamps are synchronized and the transaction
boundary is tightened to prevent data-aware scheduling misses.
Add check for asset events before creating asset-triggered DAG runs
This is to prevent double triggering when the late-commit asset_dag_run_queue is exist in entry but the asset event is already process in prev dag
Fix update statement condition in AssetDagRunQueue to use correct column reference
Add tests for concurrent asset events and no asset event scenarios in DAG run creation
linting tests
make mypy happy
Add check for existing asset DAG run queue entry before merge
Co-authored-by: Kaxil Naik <kaxilnaik@gmail.com>
add log when no dag run without asset event
Support concurrent asset event queuing on MySQL
Implement a MySQL-specific optimization using `ON DUPLICATE KEY UPDATE` when queuing asset-triggered DAG runs. This prevents unique constraint violations during concurrent asset events, mirroring the existing PostgreSQL implementation. The concurrency tests are also refactored to verify correct behavior across both database dialects.
Refactor asset event handling and improve session management in AssetManager
make lint happy
Refactor asset event handling to streamline session management in AssetManager
Re-attach asset_event to caller's session to prevent DetachedInstanceError
fix: prevent race condition causing lost events in asset-triggered DAG run creation
- Remove CTE lower-bound filter from asset event query: the bound excluded
late-committed events that arrived after a concurrent DagRun was created,
causing those events to be permanently lost and no follow-up DagRun created.
- Add 'not consumed' filter using association_table to prevent duplicate
consumption of asset events across concurrent DagRuns.
- Always delete ADRQ rows after processing, even when no new DagRun is
created, to prevent stale entries accumulating and causing infinite
scheduler loops.
- Fix concurrent test to use seen_dr_ids set instead of prev_dr reference,
ensuring all DagRuns are counted regardless of creation order.
fix: Persist AssetEvent in independent session for immediate visibility
Persist AssetEvent using a truly independent session (`scoped=False`)
to ensure it's committed and visible to processes like the Scheduler
before other transaction operations proceed. This closes a critical
visibility gap for Asset-Triggered DagRuns (ADRQ).
The event is then explicitly reloaded into the caller's session
to prevent DetachedInstanceError and allow subsequent relationship
operations to function correctly.
make static check happy
refactor: Consolidate `AssetEvent` persistence logic
Extract the complex logic for persisting `AssetEvent` instances into a new
dedicated class method, `create_asset_event`.
This centralizes the handling of distinct session management strategies
(independent session for non-SQLite, direct flush for SQLite) and ensures
immediate visibility of asset events to the scheduler. It also re-attaches
the event to the caller's session to prevent `DetachedInstanceError`.
fix: Ensure DagRun updates and AssetEvents are visible across transaction boundaries
Add `session.flush()` after `DagRun` partition key updates to ensure these changes are persisted within the caller's transaction before an independent session for `AssetEvent` creation begins. This prevents stale `DagRun` data from being read.
Change `AssetEvent` creation to use `create_session(scoped=False)`, guaranteeing a truly independent session and immediate commit of the asset event. This ensures the event is visible to processes like the Scheduler for Asset-Triggered DagRuns (ADRQ) without transactional delays.
fix: Change log level from warning to info for DagRun creation status
apply copilot reviewer
test: Add backend markers for concurrent asset event tests
docs: Address asset event scheduling race condition by using independent session commits
Ensures the asset-triggered consumer DAG is registered in the session prior to creating the AssetEvent. This prevents potential test failures or incorrect behavior due to timing or visibility issues with DAG metadata, aligning the test setup with real-world scheduler requirements.
Per review feedback the change is not significant-tier; downgrade to a
bugfix newsfragment while keeping the operator-facing note about the
new duplicate/orphaned asset-event failure mode introduced by the
early-commit fix.
Reflects changes in asset manager's internal queueing helpers to standardize
conflict handling across dialects and to pass the AssetEvent context.
Co-authored-by: Copilot <copilot@github.com>
@Lee-W

Copy link
Copy Markdown
Member

Hey @dingo4dev , thanks for being so patient! We can merge this after the CI is green

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

Labels

area:Schedulerincluding HA (high availability) schedulerready for maintainer reviewSet after triaging when all criteria pass.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

8 participants

@dingo4dev@Lee-W@potiuk@molcay@VladaZakharova@uranusjr@kaxil
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Fix asset event scheduling race condition and consistency - #62501

Merged
Lee-W merged 8 commits into
apache:mainfrom
dingo4dev:fix/asset-triggered-dag-run
Jul 29, 2026
Merged

Fix asset event scheduling race condition and consistency#62501
Lee-W merged 8 commits into
apache:mainfrom
dingo4dev:fix/asset-triggered-dag-run

Conversation

@dingo4dev

@dingo4devdingo4dev commented Feb 26, 2026

Copy link
Copy Markdown
Contributor

This PR addresses a race condition in the Asset-based scheduling logic where AssetEvent records created by concurrent tasks can be "missed" or "orphaned" by the Scheduler.

The issue stems from the Visibility Gap between a database flush() (which generates the created_at timestamp) and the final commit() (which makes the record visible to other sessions).

This change ensures that the synchronization between the AssetEvent and the AssetDagRunQueue is atomic and that the timestamps are aligned to prevent the drift caused by performance issue.

  • Manually assigns the same timestamp to both the AssetEvent and the ADRQ before the flush.
  • Commit and populate the asset event record in database immediately, so that the event can be accessible by job scheduler
  • Prevent creating dag by empty asset event as the asset event is trigger in prev_dag_run

related: #54659, #56750, #56749

---
config:
theme: redux-dark-color
look: neo
---
sequenceDiagram
participant J1 as Task Instance 1
participant J2 as Task Instance 2
participant JS as Job Scheduler
participant DB as Database
Note over J1,DB: T0
Note left of J1: T1
J1-->>DB: Asset Event 1 (AE1) & commit
activate J1
activate DB
Note left of J1: T2
J2-->>DB: Asset Event 2 (AE2) & commit
activate J2
activate DB
Note left of J1: T3
J2-->>-DB: Asset Dag Run Queue (ADRQ)<br/>created_at = AE2.timestamp<br/>(flush & commit)
deactivate DB
Note left of J1: T4
activate JS
activate DB
JS-->>DB: Fetch created_at from ADRQ
JS-->>DB: Fetch events (AE1, AE2)
JS->>JS: Create DAG with run_after = ADRQ created_at (AE2.timestamp)
deactivate DB
deactivate JS
Note left of J1: T5
J1-->>DB: Asset Dag Run Queue (ADRQ)<br/>created_at = AE1.timestamp<br/>(flush & commit)
deactivate J1
deactivate DB
Note left of J1: T6
activate JS
activate DB
JS-->>DB: Fetch created_at from ADRQ
JS->>JS: No event found<br/>(AE1.timestamp < prev DAG run_after)
deactivate DB
deactivate JS
Loading

Durability tradeoff (new failure mode)

On Postgres/MySQL the AssetEvent is committed in a short-lived independent session so it is visible to the scheduler before the producing task's transaction commits the alias association, AssetDagRunQueue rows, and TI state change. This is intentional and required to fix the scheduling race. The tradeoff is that a failure after the event is committed but before the caller's transaction commits can leave:

  • an orphaned asset event — persisted with no AssetDagRunQueue row for the scheduler to consume, or
  • a duplicate asset event — if the producing task retries register_asset_change.

Operators should be aware of this new failure mode when diagnosing missing or duplicate asset-triggered Dag runs. This is also documented in the newsfragment for this change.

Was generative AI tooling used to co-author this PR?
  • Yes (please specify the tool below)
    Copilot

Open with GitKraken

Important

🛠️ Maintainer triage note for @dingo4dev · by @potiuk · 2026-06-17 14:51 UTC

Helpful heads-up from the maintainers — please address before this PR can be reviewed:

  • Static / docs checks failing (CI image checks / Static checks). Run them locally with prek run --all-files (or pre-commit run --all-files) and push the fixes.
  • Failing test jobs: MySQL tests: core / DB-core:MySQL:8.0:3.10:Core...Serialization. Reproduce and fix locally, then push.
  • See the Pull Request quality criteria.

The ball is in your court — you've been assigned to this PR. Fix the above, then mark it Ready for review.

Automated triage — may be imperfect; a maintainer takes the next look.

CopilotAI review requested due to automatic review settings February 26, 2026 04:41
@boring-cyborgboring-cyborgBot added the area:Scheduler including HA (high availability) scheduler label Feb 26, 2026
@dingo4dev
dingo4devforce-pushed the fix/asset-triggered-dag-run branch from 11ed83d to 624e1eaCompareFebruary 26, 2026 04:41
@dingo4devdingo4dev changed the title [WIP] Fix asset event scheduling race conditionFix asset event scheduling race conditionFeb 26, 2026

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR attempts to fix a race condition in asset-based scheduling where AssetEvent records created by concurrent tasks can be missed by the Scheduler due to a visibility gap between database flush() and commit() operations. The fix synchronizes timestamps between AssetEvent and AssetDagRunQueue records by manually assigning timestamps and committing the asset event immediately.

Changes:

  • Modified asset event registration to commit immediately after creating AssetEvent, ensuring early visibility to the scheduler
  • Updated AssetDagRunQueue creation to use the asset event's timestamp for synchronization across both PostgreSQL and slow-path implementations
  • Changed scheduler logic to conditionally create DAG runs only when asset events exist, and to delete ADRQ entries more selectively based on timestamps

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 7 comments.

FileDescription
airflow-core/src/airflow/jobs/scheduler_job_runner.pyModified DAG run creation to be conditional on asset_events existence and updated ADRQ deletion to filter by timestamp
airflow-core/src/airflow/assets/manager.pyChanged from session.flush() to session.commit() for early AssetEvent persistence and updated ADRQ timestamp synchronization logic

Comment threadairflow-core/src/airflow/assets/manager.py Outdated
Comment threadairflow-core/src/airflow/assets/manager.py
Comment threadairflow-core/src/airflow/assets/manager.py Outdated
Comment threadairflow-core/src/airflow/assets/manager.py Outdated
Comment threadairflow-core/src/airflow/jobs/scheduler_job_runner.py Outdated
Comment threadairflow-core/src/airflow/assets/manager.py Outdated
Comment threadairflow-core/src/airflow/assets/manager.py
@dingo4dev
dingo4devforce-pushed the fix/asset-triggered-dag-run branch 4 times, most recently from 2308c0b to 0019359CompareFebruary 27, 2026 02:42
@dingo4devdingo4dev changed the title Fix asset event scheduling race conditionFix asset event scheduling race condition and consistencyFeb 27, 2026
@dingo4dev
dingo4devforce-pushed the fix/asset-triggered-dag-run branch from 0019359 to e796ff5CompareMarch 5, 2026 14:17
@dingo4dev
dingo4devforce-pushed the fix/asset-triggered-dag-run branch from e796ff5 to bcdd0c5CompareMarch 6, 2026 02:03
@dingo4dev

Copy link
Copy Markdown
ContributorAuthor

@Lee-W@uranusjr Please take a time to review it for me. Thanks a lot ;p

joseotaviorf added a commit to joseotaviorf/airflow that referenced this pull request Mar 12, 2026
…rt of apache#62501)
Backports three targeted fixes to 2.10.4 to address the race condition reported
in GH#56541 and GH#41101, where a dataset-dependent DAG fires with one or more
upstream dataset events missing from consumed_dataset_events.
Root cause: stale `created_at` timestamps in DatasetDagRunQueue caused the
scheduler's event-window query to exclude valid events, yet the presence-based
readiness check still considered the DAG ready to run.
Changes:
- manager.py: replace ON CONFLICT DO NOTHING with ON CONFLICT DO UPDATE
(Postgres) and add a WHERE guard to prevent backwards timestamp drift;
pass explicit created_at=utcnow() on the non-Postgres merge path so
existing rows are always refreshed.
- scheduler_job_runner.py: wrap create_dagrun in `if dataset_events:` to
prevent phantom runs when the event window is empty; scope the DDRQ
DELETE to `created_at <= exec_date` instead of deleting all rows, so
events that arrived during processing are preserved for the next cycle.
- tests/datasets/test_manager.py: add WHERE-guard regression test for the
Postgres upsert path.
- dev/BUG_REPORT_DATASET_SCHEDULING.md: detailed bug report with timeline
diagrams, related issues (apache#56541, apache#41101, apache#35870), root cause analysis,
and explanation of each fix.
- reproduce_bug_56541.py: end-to-end reproduction and verification script.
Made-with: Cursor
@dingo4dev
dingo4devforce-pushed the fix/asset-triggered-dag-run branch from bcdd0c5 to c7dae2dCompareMarch 17, 2026 06:36
@Lee-W

Copy link
Copy Markdown
Member

will try to take a look this week

joseotaviorf added a commit to joseotaviorf/airflow that referenced this pull request Mar 19, 2026
…rt of apache#62501)
Backports three targeted fixes to 2.10.4 to address the race condition reported
in GH#56541 and GH#41101, where a dataset-dependent DAG fires with one or more
upstream dataset events missing from consumed_dataset_events.
Root cause: stale `created_at` timestamps in DatasetDagRunQueue caused the
scheduler's event-window query to exclude valid events, yet the presence-based
readiness check still considered the DAG ready to run.
Changes:
- manager.py: replace ON CONFLICT DO NOTHING with ON CONFLICT DO UPDATE
(Postgres) and add a WHERE guard to prevent backwards timestamp drift;
pass explicit created_at=utcnow() on the non-Postgres merge path so
existing rows are always refreshed.
- scheduler_job_runner.py: wrap create_dagrun in `if dataset_events:` to
prevent phantom runs when the event window is empty; scope the DDRQ
DELETE to `created_at <= exec_date` instead of deleting all rows, so
events that arrived during processing are preserved for the next cycle.
- tests/datasets/test_manager.py: add WHERE-guard regression test for the
Postgres upsert path.
- dev/BUG_REPORT_DATASET_SCHEDULING.md: detailed bug report with timeline
diagrams, related issues (apache#56541, apache#41101, apache#35870), root cause analysis,
and explanation of each fix.
- reproduce_bug_56541.py: end-to-end reproduction and verification script.

@kaxilkaxil left a comment

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.

Left a few comments

Comment threadairflow-core/src/airflow/assets/manager.py Outdated
Comment threadairflow-core/src/airflow/assets/manager.py
Comment threadairflow-core/src/airflow/jobs/scheduler_job_runner.py Outdated
Comment threadairflow-core/tests/unit/jobs/test_scheduler_job.py Outdated
Comment threadairflow-core/tests/unit/jobs/test_scheduler_job.py
@dingo4dev
dingo4devforce-pushed the fix/asset-triggered-dag-run branch 2 times, most recently from e674f0f to 1a08e6fCompareMarch 22, 2026 19:58
@dingo4dev
dingo4devforce-pushed the fix/asset-triggered-dag-run branch 2 times, most recently from 3a647bf to f0d5a65CompareMarch 29, 2026 16:48
@kaxil
kaxil requested a review from CopilotApril 2, 2026 01:03
Comment threadairflow-core/src/airflow/assets/manager.py Outdated
Comment threadairflow-core/src/airflow/assets/manager.py Outdated
Comment threadairflow-core/src/airflow/assets/manager.py
@molcay

Copy link
Copy Markdown
Contributor

Hi @dingo4dev, I wanted to ping you here if you missed my message on Slack.

I am repeating my message and offer for help in here as well:

Hi @Stanley Law, hope you're having a good week!

I have been following your PR, it seems great stuff and thanks for tackling this. I know bouncing between reviews and CI pipelines can take up a lot of time. If you're feeling swamped or just want someone to bounce ideas off of (since I am having trouble reproducing the issue), I'd be glad to step in and help in way that I can. If you've got it fully under control though, totally feel free to ignore this message 🙂
Just wanted to offer some support.

@dingo4dev

Copy link
Copy Markdown
ContributorAuthor

Hi @molcay, Thank you so much for checking in and offering help!
Some extra eyes would be amazing. Right now I am investing the failure ci test on Mysql, the concurrent tests sometimes failure and orphan few asset event, and I can't reproduce it in my local tho.
I had logged the asset event and dag run with id and timestamp.

_______________________________________________________________TestSchedulerJob.test_create_dag_runs_when_concurrent_asset_events_created_______________________________________________________________airflow-core/tests/unit/jobs/test_scheduler_job.py:5792: intest_create_dag_runs_when_concurrent_asset_events_createdasserttotal_consumed_asset_events==ASSET_EVENT_COUNTEassert29==30--------------------------------------------Capturedstdoutsetup-----------------------------------------
] FillinguptheDagBagfrom/dev/null [airflow.dag_processing.dagbag.DagBag]
--------------------------------------------Capturedstdoutcall------------------------------------------MissingAssetEventMetadata: {(16, '2026-06-23T05:02:05.983645+00:00')}
{'event_': (8, '2026-06-23T05:02:05.967063+00:00'), 'dagrun': (434, '2026-06-23T05:02:05.989838+00:00')}
{'event_': (9, '2026-06-23T05:02:05.972418+00:00'), 'dagrun': (434, '2026-06-23T05:02:05.989838+00:00')}
{'event_': (13, '2026-06-23T05:02:05.982971+00:00'), 'dagrun': (436, '2026-06-23T05:02:05.982971+00:00')}
{'event_': (16, '2026-06-23T05:02:05.983645+00:00'), 'dagrun': ()}
{'event_': (10, '2026-06-23T05:02:05.989838+00:00'), 'dagrun': (434, '2026-06-23T05:02:05.989838+00:00')}
{'event_': (15, '2026-06-23T05:02:05.994289+00:00'), 'dagrun': (435, '2026-06-23T05:02:06.034978+00:00')}
{'event_': (14, '2026-06-23T05:02:05.995763+00:00'), 'dagrun': (435, '2026-06-23T05:02:06.034978+00:00')}
{'event_': (11, '2026-06-23T05:02:06.025005+00:00'), 'dagrun': (435, '2026-06-23T05:02:06.034978+00:00')}
{'event_': (12, '2026-06-23T05:02:06.034978+00:00'), 'dagrun': (435, '2026-06-23T05:02:06.034978+00:00')}
{'event_': (18, '2026-06-23T05:02:06.059185+00:00'), 'dagrun': (437, '2026-06-23T05:02:07.001801+00:00')}
{'event_': (17, '2026-06-23T05:02:06.060623+00:00'), 'dagrun': (437, '2026-06-23T05:02:07.001801+00:00')}
{'event_': (19, '2026-06-23T05:02:06.099119+00:00'), 'dagrun': (437, '2026-06-23T05:02:07.001801+00:00')}
{'event_': (20, '2026-06-23T05:02:07.001801+00:00'), 'dagrun': (437, '2026-06-23T05:02:07.001801+00:00')}
{'event_': (21, '2026-06-23T05:02:07.022033+00:00'), 'dagrun': (438, '2026-06-23T05:02:07.090158+00:00')}
{'event_': (22, '2026-06-23T05:02:07.083870+00:00'), 'dagrun': (438, '2026-06-23T05:02:07.090158+00:00')}
{'event_': (23, '2026-06-23T05:02:07.090158+00:00'), 'dagrun': (438, '2026-06-23T05:02:07.090158+00:00')}
{'event_': (24, '2026-06-23T05:02:07.097476+00:00'), 'dagrun': (439, '2026-06-23T05:02:08.046920+00:00')}
{'event_': (25, '2026-06-23T05:02:07.116000+00:00'), 'dagrun': (439, '2026-06-23T05:02:08.046920+00:00')}
{'event_': (26, '2026-06-23T05:02:08.046920+00:00'), 'dagrun': (439, '2026-06-23T05:02:08.046920+00:00')}
{'event_': (27, '2026-06-23T05:02:08.054838+00:00'), 'dagrun': (440, '2026-06-23T05:02:08.092453+00:00')}
{'event_': (28, '2026-06-23T05:02:08.067990+00:00'), 'dagrun': (440, '2026-06-23T05:02:08.092453+00:00')}
{'event_': (29, '2026-06-23T05:02:08.092453+00:00'), 'dagrun': (440, '2026-06-23T05:02:08.092453+00:00')}
{'event_': (30, '2026-06-23T05:02:08.094004+00:00'), 'dagrun': (441, '2026-06-23T05:02:08.125311+00:00')}
{'event_': (31, '2026-06-23T05:02:08.118823+00:00'), 'dagrun': (441, '2026-06-23T05:02:08.125311+00:00')}
{'event_': (32, '2026-06-23T05:02:08.125311+00:00'), 'dagrun': (441, '2026-06-23T05:02:08.125311+00:00')}
{'event_': (33, '2026-06-23T05:02:08.127450+00:00'), 'dagrun': (442, '2026-06-23T05:02:09.073822+00:00')}
{'event_': (34, '2026-06-23T05:02:08.154315+00:00'), 'dagrun': (442, '2026-06-23T05:02:09.073822+00:00')}
{'event_': (35, '2026-06-23T05:02:09.073822+00:00'), 'dagrun': (442, '2026-06-23T05:02:09.073822+00:00')}
{'event_': (36, '2026-06-23T05:02:09.091265+00:00'), 'dagrun': (443, '2026-06-23T05:02:09.091265+00:00')}
{'event_': (37, '2026-06-23T05:02:09.118804+00:00'), 'dagrun': (444, '2026-06-23T05:02:09.118804+00:00')}

Comment threadairflow-core/tests/unit/jobs/test_scheduler_job.py Outdated
Comment threadairflow-core/tests/unit/jobs/test_scheduler_job.py Outdated
@molcay

molcay commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Hi @molcay, Thank you so much for checking in and offering help! Some extra eyes would be amazing. Right now I am investing the failure ci test on Mysql, the concurrent tests sometimes failure and orphan few asset event, and I can't reproduce it in my local tho. I had logged the asset event and dag run with id and timestamp.

_______________________________________________________________TestSchedulerJob.test_create_dag_runs_when_concurrent_asset_events_created_______________________________________________________________airflow-core/tests/unit/jobs/test_scheduler_job.py:5792: intest_create_dag_runs_when_concurrent_asset_events_createdasserttotal_consumed_asset_events==ASSET_EVENT_COUNTEassert29==30

Hi @dingo4dev,
As I see that you already mark the PR as "ready for review" and you've already got an approval, I need to ask if you still need help on the matter?

@Lee-WLee-W left a comment

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.

a few nits, but overall looks good

Comment threadairflow-core/newsfragments/62501.significant.rst Outdated
Comment threadairflow-core/tests/unit/models/test_taskinstance.py Outdated
Comment threadairflow-core/tests/unit/models/test_taskinstance.py Outdated
Comment threadairflow-core/tests/unit/models/test_taskinstance.py Outdated
Comment threadairflow-core/tests/unit/jobs/test_scheduler_job.py Outdated
Comment threadairflow-core/src/airflow/assets/manager.py Outdated
@dingo4dev

dingo4dev commented Jul 7, 2026

Copy link
Copy Markdown
ContributorAuthor

Hi @dingo4dev, As I see that you already mark the PR as "ready for review" and you've already got an approval, I need to ask if you still need help on the matter?

Hi @molcay ! Thanks for following up.

Even though we got an approval, I have a feeling this might not completely catch every edge case of the underlying issue. we should definitely keep a close eye on it and monitor how it behaves. Thanks again for the support. 🙌

@VladaZakharova

Copy link
Copy Markdown
Contributor

Hi there!
Thank you for the work you are doing, really waiting for the changes to be merged and available <3

Comment threadairflow-core/src/airflow/assets/manager.py

@Lee-WLee-W left a comment

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.

a few nits. mostly good

Comment threadairflow-core/src/airflow/assets/manager.py
Comment threadairflow-core/src/airflow/assets/manager.py Outdated
Comment threadairflow-core/src/airflow/assets/manager.py
Comment threadairflow-core/tests/unit/assets/test_manager.py Outdated
Comment threadairflow-core/tests/unit/models/test_taskinstance.py
Comment threadairflow-core/tests/unit/models/test_taskinstance.py Outdated
@VladaZakharova

Copy link
Copy Markdown
Contributor

Looks like green and beautiful :)
Are we close to merge this change?

dingo4devand others added 8 commits July 29, 2026 14:17
Fixes an issue where the Scheduler misses AssetEvents due to a
visibility gap between flush() and commit().
When multiple tasks create events for the same asset, the AssetEvent
timestamp is generated during flush, but the record remains invisible
to the Scheduler until the final commit. If the AssetDagRunQueue is
processed in the interim, the 'late-committing' event is orphaned.
The job scheduler will read the AssetDagRunQueue and fetch the created_at column as the triggered_date which will use for fetching the asset event and assign dag `run_after`.
This change ensures timestamps are synchronized and the transaction
boundary is tightened to prevent data-aware scheduling misses.
Add check for asset events before creating asset-triggered DAG runs
This is to prevent double triggering when the late-commit asset_dag_run_queue is exist in entry but the asset event is already process in prev dag
Fix update statement condition in AssetDagRunQueue to use correct column reference
Add tests for concurrent asset events and no asset event scenarios in DAG run creation
linting tests
make mypy happy
Add check for existing asset DAG run queue entry before merge
Co-authored-by: Kaxil Naik <kaxilnaik@gmail.com>
add log when no dag run without asset event
Support concurrent asset event queuing on MySQL
Implement a MySQL-specific optimization using `ON DUPLICATE KEY UPDATE` when queuing asset-triggered DAG runs. This prevents unique constraint violations during concurrent asset events, mirroring the existing PostgreSQL implementation. The concurrency tests are also refactored to verify correct behavior across both database dialects.
Refactor asset event handling and improve session management in AssetManager
make lint happy
Refactor asset event handling to streamline session management in AssetManager
Re-attach asset_event to caller's session to prevent DetachedInstanceError
fix: prevent race condition causing lost events in asset-triggered DAG run creation
- Remove CTE lower-bound filter from asset event query: the bound excluded
late-committed events that arrived after a concurrent DagRun was created,
causing those events to be permanently lost and no follow-up DagRun created.
- Add 'not consumed' filter using association_table to prevent duplicate
consumption of asset events across concurrent DagRuns.
- Always delete ADRQ rows after processing, even when no new DagRun is
created, to prevent stale entries accumulating and causing infinite
scheduler loops.
- Fix concurrent test to use seen_dr_ids set instead of prev_dr reference,
ensuring all DagRuns are counted regardless of creation order.
fix: Persist AssetEvent in independent session for immediate visibility
Persist AssetEvent using a truly independent session (`scoped=False`)
to ensure it's committed and visible to processes like the Scheduler
before other transaction operations proceed. This closes a critical
visibility gap for Asset-Triggered DagRuns (ADRQ).
The event is then explicitly reloaded into the caller's session
to prevent DetachedInstanceError and allow subsequent relationship
operations to function correctly.
make static check happy
refactor: Consolidate `AssetEvent` persistence logic
Extract the complex logic for persisting `AssetEvent` instances into a new
dedicated class method, `create_asset_event`.
This centralizes the handling of distinct session management strategies
(independent session for non-SQLite, direct flush for SQLite) and ensures
immediate visibility of asset events to the scheduler. It also re-attaches
the event to the caller's session to prevent `DetachedInstanceError`.
fix: Ensure DagRun updates and AssetEvents are visible across transaction boundaries
Add `session.flush()` after `DagRun` partition key updates to ensure these changes are persisted within the caller's transaction before an independent session for `AssetEvent` creation begins. This prevents stale `DagRun` data from being read.
Change `AssetEvent` creation to use `create_session(scoped=False)`, guaranteeing a truly independent session and immediate commit of the asset event. This ensures the event is visible to processes like the Scheduler for Asset-Triggered DagRuns (ADRQ) without transactional delays.
fix: Change log level from warning to info for DagRun creation status
apply copilot reviewer
test: Add backend markers for concurrent asset event tests
docs: Address asset event scheduling race condition by using independent session commits
Ensures the asset-triggered consumer DAG is registered in the session prior to creating the AssetEvent. This prevents potential test failures or incorrect behavior due to timing or visibility issues with DAG metadata, aligning the test setup with real-world scheduler requirements.
Per review feedback the change is not significant-tier; downgrade to a
bugfix newsfragment while keeping the operator-facing note about the
new duplicate/orphaned asset-event failure mode introduced by the
early-commit fix.
Reflects changes in asset manager's internal queueing helpers to standardize
conflict handling across dialects and to pass the AssetEvent context.
Co-authored-by: Copilot <copilot@github.com>
@Lee-W

Copy link
Copy Markdown
Member

Hey @dingo4dev , thanks for being so patient! We can merge this after the CI is green

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

Labels

area:Schedulerincluding HA (high availability) schedulerready for maintainer reviewSet after triaging when all criteria pass.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

8 participants

@dingo4dev@Lee-W@potiuk@molcay@VladaZakharova@uranusjr@kaxil