Uh oh!
There was an error while loading. Please reload this page.
Load the correct DAG version when a task starts from a trigger - #69988
Load the correct DAG version when a task starts from a trigger#69988seanghaeli wants to merge 3 commits into
Conversation
f157049 to
ecbf158Compareseanghaeli
commented
Jul 17, 2026
@o-nikolas this one is similar in spirit to your PR #69941 |
Uh oh!
There was an error while loading. Please reload this page.
| version_id=trigger.task_instance.get_dagrun(session=session).created_dag_version_id | ||
| or trigger.task_instance.dag_version_id, |
There was a problem hiding this comment.
created_dag_version_id is populated even for Dags with disable_bundle_versioning=True; dag_run.bundle_version is what determines
whether the run is pinned. This unconditional preference for created_dag_version_id therefore makes an unpinned start_from_trigger task load the Dag version from when the run was created, even after the scheduler has advanced the unfinished TI's dag_version_id following a reparse.
This differs from DBDagBag._version_from_dag_run(), which intentionally resolves the latest version when bundle_version is absent. Please use the run's created_dag_version_id only when the run is pinned, retain the TI version for unpinned runs, and cover both cases in the regression test.
There was a problem hiding this comment.
I believe this is addressed now, could you take a look?
Uh oh!
There was an error while loading. Please reload this page.
potiuk
commented
Jul 20, 2026
@seanghaeli — There are 3 unresolved review thread(s) on this PR from @viiccwen. Could you either push a fix or reply in each thread explaining why the feedback doesn't apply? Once you believe the feedback is addressed, mark the thread as resolved so the reviewer isn't re-pinged needlessly. Thanks! Note: This comment was drafted by an AI-assisted triage tool and may contain mistakes. Once you have addressed the points above, an Apache Airflow maintainer — a real person — will take the next look at your PR. We use this two-stage triage process so that our maintainers' limited time is spent where it matters most: the conversation with you. |
o-nikolas
commented
Jul 24, 2026
@viiccwen do the changes look good to you now? |
vatsrahul1001
commented
Jul 29, 2026
LGTM, I think we are good to merge after code owners review @dstandish@hussein-awala |
jason810496
left a comment
There was a problem hiding this comment.
Thanks for the fix.
IMO, we should introduce something like get_serialized_dag_model_for_run rather than calling the private method of the DagBag.
# airflow-core/src/airflow/models/dagbag.py, next to get_dag_for_rundefget_serialized_dag_model_for_run(
self, dag_run: DagRun, *, session: Session
) ->SerializedDagModel|None:
"""Return the SerializedDagModel for the version a run executes against."""ifversion_id:=self._version_from_dag_run(dag_run=dag_run, session=session):
returnself.get_serialized_dag_model(version_id=version_id, session=session)
returnNoneairflow/airflow-core/src/airflow/models/dagbag.py
Lines 218 to 221 in b3cc48d
Uh oh!
There was an error while loading. Please reload this page.
vatsrahul1001
commented
Aug 5, 2026
Moving to 3.3.2 as this is still pending code owner review and do not want to rush on merging this without that |
vatsrahul1001
commented
Aug 11, 2026
@dstandish@hussein-awala can you review this? |
hussein-awala
left a comment
There was a problem hiding this comment.
Thanks for the fix, resolving the version from the run is the right direction and it
matches what workers already do. One thing worries me though. This applies to every
trigger, not only the start_from_trigger ones, and that makes the get_task call
able to kill the triggerer.
Here is the scenario. Take a DAG in a bundle without versioning, so its runs havebundle_version = None.
my_dagv1 has a deferrable taskwait. A run is created at v1 andwait
defers. The TI isDEFERREDwithdag_version_id = v1and there is aTrigger
row for it.- Someone edits the file and renames
waittowait_for_data. The dag processor
creates v2 and it becomes the latest. The run is stillRUNNING, itsbundle_versionis stillNoneandcreated_dag_version_idis still v1. - The scheduler does not clean the TI up yet.
_check_for_removed_or_restored_tasksonly marks itREMOVEDwhenself.state != DagRunState.RUNNING, so nothing happens until the nexttask_instance_scheduling_decisionspass over that run. - In the meantime the triggerer builds the workload for that trigger, which happens
on a restart, on a rolling deploy, or on HA failover afterassign_unassigned._version_from_dag_runsees nobundle_version, returns v2 as the latest, andserialized_dag_model.dag.get_task("wait")raisesTaskNotFound. - Nobody catches it. Not
_create_workload, notbuild_trigger_workloads, notrun_once.TriggererJobRunner.run()logs it and re-raises, so the process exits
and every other trigger it was running goes down with it. After the restartassign_unassignedgives it the same trigger back and it crashes again, until the
scheduler gets around to marking the TIREMOVEDandclean_unuseddeletes the
trigger.
Steps 2 and 4 are really the same event in practice. A rolling deploy of new DAG
code is exactly when new versions show up and triggerers restart.
Before this change the version came from the TI, so the task was always there and
step 4 just worked. We already guard this pattern elsewhere. Intask_instances.py the same resolution is followed bywith contextlib.suppress(TaskNotFound).
The simplest fix I can think of is to keep the flag check on the TI version and only
re-resolve for the case the title is about:
serialized_dag_model=dag_bag.get_serialized_dag_model(
version_id=trigger.task_instance.dag_version_id, session=session
)
ifserialized_dag_model:
task=serialized_dag_model.dag.get_task(trigger.task_instance.task_id)
iftask.start_from_trigger:
dag_run=trigger.task_instance.get_dagrun(session=session)
run_version_id=DBDagBag._version_from_dag_run(dag_run=dag_run, session=session)
ifrun_version_idandrun_version_id!=trigger.task_instance.dag_version_id:
serialized_dag_model= (
dag_bag.get_serialized_dag_model(version_id=run_version_id, session=session)
orserialized_dag_model
)Uh oh!
There was an error while loading. Please reload this page.
0a7a1ff to
4ce124cCompareseanghaeli
commented
Aug 18, 2026
@hussein-awala good catch, it can crash in this specific sequence. In fact, the current version of main can fail for a similar reason noted in issue #69841. The latest commit introduces a guard so it falls back to running the trigger without Dag context instead of killing the triggerer |
For an unpinned run the resolved (latest) Dag version may no longer contain a renamed or removed deferred task. get_task() then raised TaskNotFound, which nothing on the load_triggers path caught, killing the whole triggerer; assign_unassigned re-handed the same trigger to the restarted triggerer, producing a crash loop. Guard the lookup and fall through to the plain workload, mirroring the guard in the execution API. Also introduce DBDagBag.get_serialized_dag_model_for_run so the triggerer uses a public API instead of the private _version_from_dag_run helper, as requested in review.
4ce124c to
05ebadcCompare
If a Dag run without a
bundle_versionand one of its tasks point at different Dag versions, the trigger should load the latest version. Currently, it loads the task's possibly outdated version.