Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions airflow-core/newsfragments/65422.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fix scheduler crash on asset-triggered DagRuns by eager-loading ``AssetEvent.source_aliases`` in ``SchedulerJobRunner.process_executor_events``.
3 changes: 2 additions & 1 deletion airflow-core/src/airflow/jobs/scheduler_job_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -1231,12 +1231,13 @@ def process_executor_events(
filter_for_tis = TI.filter_for_tis(tis_with_right_state)
if filter_for_tis is None:
return len(event_buffer)
asset_loader, _ = _eager_load_dag_run_for_validation()
asset_loader, alias_loader = _eager_load_dag_run_for_validation()
query = (
select(TI)
.where(filter_for_tis)
.options(selectinload(TI.dag_model))
.options(asset_loader)
.options(alias_loader)
.options(joinedload(TI.dag_run).selectinload(DagRun.created_dag_version))
.options(joinedload(TI.dag_version))
)
Expand Down
21 changes: 15 additions & 6 deletions airflow-core/tests/unit/jobs/test_scheduler_job.py
Original file line number Diff line number Diff line change
Expand Up @@ -849,11 +849,15 @@ def test_process_executor_events_with_asset_events(self, mock_stats_incr, sessio
Test that _process_executor_events handles asset events without DetachedInstanceError.

Regression test for scheduler crashes when task callbacks are built with
consumed_asset_events that weren't eager-loaded.
consumed_asset_events that weren't eager-loaded. Exercises both
``AssetEvent.asset`` and ``AssetEvent.source_aliases`` so that missing
either loader option on the TI query surfaces as a ``DetachedInstanceError``
when the callback's ``DRDataModel`` is built.
"""
asset1 = Asset(uri="test://asset1", name="test_asset_executor", group="test_group")
asset_model = AssetModel(name=asset1.name, uri=asset1.uri, group=asset1.group)
session.add(asset_model)
asset_alias = AssetAliasModel(name="test_alias_executor", group="test_group")
session.add_all([asset_model, asset_alias])
session.flush()

with dag_maker(dag_id="test_executor_events_with_assets", schedule=[asset1], fileloc="/test_path1/"):
Expand All @@ -865,14 +869,17 @@ def test_process_executor_events_with_asset_events(self, mock_stats_incr, sessio

dr = dag_maker.create_dagrun()

# Create asset event and attach to dag run
# Create asset event with an attached source alias so the lazy-loaded
# AssetEvent.source_aliases relationship is non-empty and must be
# eager-loaded to survive a detached ORM instance in the callback.
asset_event = AssetEvent(
asset_id=asset_model.id,
source_task_id="upstream_task",
source_dag_id="upstream_dag",
source_run_id="upstream_run",
source_map_index=-1,
)
asset_event.source_aliases.append(asset_alias)
session.add(asset_event)
session.flush()
dr.consumed_asset_events.append(asset_event)
Expand All @@ -896,12 +903,14 @@ def test_process_executor_events_with_asset_events(self, mock_stats_incr, sessio
ti1.refresh_from_db(session=session)
assert ti1.state == State.FAILED

# Verify callback was created with asset event data
# Verify callback was created with asset event data including aliases
self.job_runner.executor.callback_sink.send.assert_called_once()
callback_request = self.job_runner.executor.callback_sink.send.call_args.args[0]
assert callback_request.context_from_server is not None
assert len(callback_request.context_from_server.dag_run.consumed_asset_events) == 1
assert callback_request.context_from_server.dag_run.consumed_asset_events[0].asset.uri == asset1.uri
events = callback_request.context_from_server.dag_run.consumed_asset_events
assert len(events) == 1
assert events[0].asset.uri == asset1.uri
assert [alias.name for alias in events[0].source_aliases] == [asset_alias.name]

@pytest.mark.usefixtures("testing_dag_bundle")
def test_schedule_dag_run_with_asset_event(self, session: Session, dag_maker: DagMaker):
Expand Down
Loading