-
Notifications
You must be signed in to change notification settings - Fork 17.5k
Reference asset events from asset_dag_run_queue #70972
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -28,7 +28,7 @@ | |
| from collections import Counter, defaultdict, deque | ||
| from collections.abc import Callable, Collection, Iterable, Iterator | ||
| from contextlib import ExitStack | ||
| from datetime import date, datetime, timedelta | ||
| from datetime import datetime, timedelta | ||
| from functools import lru_cache, partial | ||
| from itertools import groupby | ||
| from typing import TYPE_CHECKING, Any, cast | ||
|
|
@@ -135,10 +135,10 @@ | |
| if TYPE_CHECKING: | ||
| from types import FrameType | ||
|
|
||
| from pendulum.datetime import DateTime | ||
| from sqlalchemy.engine import CursorResult | ||
| from sqlalchemy.orm import Session | ||
| from sqlalchemy.orm.interfaces import LoaderOption | ||
| from sqlalchemy.sql.elements import ColumnElement | ||
| from sqlalchemy.sql.selectable import Subquery | ||
|
|
||
| from airflow._shared.logging.types import Logger | ||
|
|
@@ -2641,9 +2641,7 @@ def _create_dag_runs_asset_triggered( | |
|
|
||
| queued_adrqs = session.scalars( | ||
| with_row_locks( | ||
| select(AssetDagRunQueue) | ||
| .where(AssetDagRunQueue.target_dag_id == dag.dag_id) | ||
| .order_by(AssetDagRunQueue.created_at.desc()), | ||
| select(AssetDagRunQueue).where(AssetDagRunQueue.target_dag_id == dag.dag_id), | ||
| of=AssetDagRunQueue, | ||
| skip_locked=True, | ||
| key_share=False, | ||
|
|
@@ -2658,53 +2656,31 @@ def _create_dag_runs_asset_triggered( | |
| ) | ||
| continue | ||
|
|
||
| triggered_date: DateTime = timezone.coerce_datetime(queued_adrqs[0].created_at) | ||
| self.log.debug( | ||
| "Creating asset-triggered DagRun for '%s': %d queued assets, triggered_date=%s", | ||
| dag.dag_id, | ||
| len(queued_adrqs), | ||
| triggered_date, | ||
| ) | ||
| cte = ( | ||
| select(func.max(DagRun.run_after).label("previous_dag_run_run_after")) | ||
| .where( | ||
| DagRun.dag_id == dag.dag_id, | ||
| DagRun.run_type == DagRunType.ASSET_TRIGGERED, | ||
| DagRun.run_after < triggered_date, | ||
| ) | ||
| .cte() | ||
| ) | ||
|
|
||
| # A first asset-triggered run has no previous run to floor the event window. With | ||
| # catchup off, floor it at when the Dag started scheduling on its assets so the | ||
| # backlog is skipped; with catchup on, only date.min applies and the backlog replays. | ||
| event_window_floor: list[Any] = [cte.c.previous_dag_run_run_after] | ||
| if not dag.catchup: | ||
| event_window_floor.append( | ||
| select(func.min(DagScheduleAssetReference.created_at)) | ||
| .where(DagScheduleAssetReference.dag_id == dag.dag_id) | ||
| .scalar_subquery() | ||
| referenced_event_ids = {adrq.asset_event_id for adrq in queued_adrqs} | ||
| event_predicate: ColumnElement[bool] = AssetEvent.id.in_(referenced_event_ids) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. One behaviour delta worth confirming: the old select required the event's asset to still be in the dag's schedule (the DagScheduleAssetReference/alias join). Consuming by reference means a queued event for an asset that was since removed from the dag's schedule now gets consumed and shows up in the run's consumed_asset_events / triggering_asset_events, where before it was silently dropped along with its ADRQ row. Intended? |
||
| if dag.catchup: | ||
| # With catchup on, also consume events recorded before the Dag started | ||
| # scheduling on its assets/aliases, not just those with a queue row. (With catchup | ||
| # off only queued events are consumed.) The not-consumed filter below dedupes | ||
| # across runs, so no event window is needed. | ||
| event_predicate = or_( | ||
| event_predicate, | ||
| AssetEvent.asset_id.in_( | ||
| select(DagScheduleAssetReference.asset_id).where( | ||
| DagScheduleAssetReference.dag_id == dag.dag_id | ||
| ) | ||
| ), | ||
| AssetEvent.source_aliases.any( | ||
| AssetAliasModel.scheduled_dags.any( | ||
| DagScheduleAssetAliasReference.dag_id == dag.dag_id | ||
| ) | ||
| ), | ||
| ) | ||
| event_window_floor.append(date.min) | ||
|
|
||
| asset_events = list( | ||
| session.scalars( | ||
| select(AssetEvent) | ||
| .where( | ||
| or_( | ||
| AssetEvent.asset_id.in_( | ||
| select(DagScheduleAssetReference.asset_id).where( | ||
| DagScheduleAssetReference.dag_id == dag.dag_id | ||
| ) | ||
| ), | ||
| AssetEvent.source_aliases.any( | ||
| AssetAliasModel.scheduled_dags.any( | ||
| DagScheduleAssetAliasReference.dag_id == dag.dag_id | ||
| ) | ||
| ), | ||
| ), | ||
| AssetEvent.timestamp > func.coalesce(*event_window_floor), | ||
| AssetEvent.timestamp <= triggered_date, | ||
| event_predicate, | ||
| ~( | ||
| select(association_table.c.event_id) | ||
| .join(DagRun, DagRun.id == association_table.c.dag_run_id) | ||
|
|
@@ -2719,6 +2695,13 @@ def _create_dag_runs_asset_triggered( | |
| ) | ||
| ) | ||
| if asset_events: | ||
| triggered_date = timezone.coerce_datetime(max(event.timestamp for event in asset_events)) | ||
| self.log.debug( | ||
| "Creating asset-triggered DagRun for '%s': %d queued assets, triggered_date=%s", | ||
| dag.dag_id, | ||
| len(queued_adrqs), | ||
| triggered_date, | ||
| ) | ||
| dag_run = dag.create_dagrun( | ||
| run_id=DagRun.generate_run_id( | ||
| run_type=DagRunType.ASSET_TRIGGERED, logical_date=None, run_after=triggered_date | ||
|
|
@@ -2747,19 +2730,19 @@ def _create_dag_runs_asset_triggered( | |
| ) | ||
| else: | ||
| self.log.info( | ||
| "No DagRun created for '%s' at '%s' - asset events already consumed or none found", | ||
| "No DagRun created for '%s' - asset events already consumed or none found", | ||
| dag.dag_id, | ||
| triggered_date, | ||
| ) | ||
| # Always delete ADRQ rows for this batch to prevent stale entries accumulating, | ||
| # including when all events were already consumed by a concurrent DagRun. | ||
| adrq_pks = [(record.asset_id, record.target_dag_id) for record in queued_adrqs] | ||
| result = cast( | ||
| "CursorResult", | ||
| session.execute( | ||
| delete(AssetDagRunQueue).where( | ||
| tuple_(AssetDagRunQueue.asset_id, AssetDagRunQueue.target_dag_id).in_(adrq_pks), | ||
| AssetDagRunQueue.created_at <= triggered_date, | ||
| tuple_( | ||
| AssetDagRunQueue.target_dag_id, | ||
| AssetDagRunQueue.asset_event_id, | ||
| ).in_((adrq.target_dag_id, adrq.asset_event_id) for adrq in queued_adrqs) | ||
| ) | ||
| ), | ||
| ) | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
INSERT IGNORE downgrades more than duplicate-key errors to warnings on MySQL (FK violations, NOT NULL, truncation are all silently swallowed), while the postgres/sqlite path only ignores the PK conflict. A bad event id, for example, would raise on postgres but insert nothing here without a sound.
on_duplicate_key_update(asset_id=stmt.inserted.asset_id)is the usual no-op trick that keeps the ignore scoped to duplicates.