This is an automated email from the ASF dual-hosted git repository.
pierrejeambrun pushed a commit to branch v3-3-test
in repository https://gitbox.apache.org/repos/asf/airflow.git
The following commit(s) were added to refs/heads/v3-3-test by this push:
new ce4af16cc8f [v3-3-test] Populate dag and note before firing dag run
state-change listeners (#69873) (#70245)
ce4af16cc8f is described below
commit ce4af16cc8fc36ee19996efd8fc975078f88a46c
Author: github-actions[bot]
<41898282+github-actions[bot]@users.noreply.github.com>
AuthorDate: Wed Jul 22 19:18:46 2026 +0200
[v3-3-test] Populate dag and note before firing dag run state-change
listeners (#69873) (#70245)
* Populate dag and note before firing dag run state-change listeners
* Address PR review
(cherry picked from commit ec8da5c25eb23ca67665d3cbc323ea7b21eec0fc)
Co-authored-by: Kacper Muda <[email protected]>
---
.../api_fastapi/core_api/routes/public/dag_run.py | 14 +++++------
.../core_api/services/public/dag_run.py | 14 +++++++++--
.../core_api/routes/public/test_dag_run.py | 28 ++++++++++++++++++----
.../tests/unit/listeners/class_listener.py | 9 +++++++
4 files changed, 51 insertions(+), 14 deletions(-)
diff --git
a/airflow-core/src/airflow/api_fastapi/core_api/routes/public/dag_run.py
b/airflow-core/src/airflow/api_fastapi/core_api/routes/public/dag_run.py
index 617b70ea894..955c7aa34ce 100644
--- a/airflow-core/src/airflow/api_fastapi/core_api/routes/public/dag_run.py
+++ b/airflow-core/src/airflow/api_fastapi/core_api/routes/public/dag_run.py
@@ -222,13 +222,13 @@ def patch_dag_run(
data = patch_body.model_dump(include=fields_to_update, by_alias=True)
- for attr_name, attr_value_raw in data.items():
- if attr_name == "state" and patch_body.state is not None:
- patch_dag_run_state(dag=dag, dag_run=dag_run,
state=patch_body.state, session=session)
- elif attr_name == "note":
- updated_dag_run = session.get(DagRun, dag_run.id)
- if updated_dag_run is not None:
- patch_dag_run_note(dag_run=updated_dag_run,
note=attr_value_raw, user=user)
+ # Apply "note" before "state" so listeners fired inside
patch_dag_run_state() see the updated note.
+ if "note" in data:
+ updated_dag_run = session.get(DagRun, dag_run.id)
+ if updated_dag_run is not None:
+ patch_dag_run_note(dag_run=updated_dag_run, note=data["note"],
user=user)
+ if "state" in data and patch_body.state is not None:
+ patch_dag_run_state(dag=dag, dag_run=dag_run, state=patch_body.state,
session=session)
final_dag_run = session.get(DagRun, dag_run.id)
if not final_dag_run:
diff --git
a/airflow-core/src/airflow/api_fastapi/core_api/services/public/dag_run.py
b/airflow-core/src/airflow/api_fastapi/core_api/services/public/dag_run.py
index 6d43d18cd34..f0fa4b76f9f 100644
--- a/airflow-core/src/airflow/api_fastapi/core_api/services/public/dag_run.py
+++ b/airflow-core/src/airflow/api_fastapi/core_api/services/public/dag_run.py
@@ -195,7 +195,12 @@ def patch_dag_run_state(
if state == DagRunMutableStates.SUCCESS:
set_dag_run_state_to_success(dag=dag, run_id=dag_run.run_id,
commit=True, session=session)
try:
- get_listener_manager().hook.on_dag_run_success(dag_run=dag_run,
msg="")
+ if dag_run.dag is None:
+ dag_run.dag = dag
+ get_listener_manager().hook.on_dag_run_success(
+ dag_run=dag_run,
+ msg=f"Dag Run's state was manually set to
`{DagRunMutableStates.SUCCESS.value}`.",
+ )
except Exception:
log.exception("error calling listener")
elif state == DagRunMutableStates.QUEUED:
@@ -206,7 +211,12 @@ def patch_dag_run_state(
elif state == DagRunMutableStates.FAILED:
set_dag_run_state_to_failed(dag=dag, run_id=dag_run.run_id,
commit=True, session=session)
try:
- get_listener_manager().hook.on_dag_run_failed(dag_run=dag_run,
msg="")
+ if dag_run.dag is None:
+ dag_run.dag = dag
+ get_listener_manager().hook.on_dag_run_failed(
+ dag_run=dag_run,
+ msg=f"Dag Run's state was manually set to
`{DagRunMutableStates.FAILED.value}`.",
+ )
except Exception:
log.exception("error calling listener")
diff --git
a/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_dag_run.py
b/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_dag_run.py
index 1f3bfc887bc..6ace0a04bbc 100644
--- a/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_dag_run.py
+++ b/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_dag_run.py
@@ -1553,20 +1553,38 @@ class TestPatchDagRun:
assert body["detail"][0]["msg"] == "Input should be 'queued',
'success' or 'failed'"
@pytest.mark.parametrize(
- ("state", "listener_state"),
+ ("state", "listener_state", "expected_msg"),
[
- ("queued", []),
- ("success", [DagRunState.SUCCESS]),
- ("failed", [DagRunState.FAILED]),
+ ("queued", [], None),
+ ("success", [DagRunState.SUCCESS], "Dag Run's state was manually
set to `success`."),
+ ("failed", [DagRunState.FAILED], "Dag Run's state was manually set
to `failed`."),
],
)
@pytest.mark.usefixtures("configure_git_connection_for_dag_bundle")
- def test_patch_dag_run_notifies_listeners(self, test_client, state,
listener_state, listener_manager):
+ def test_patch_dag_run_notifies_listeners(
+ self, test_client, state, listener_state, expected_msg,
listener_manager
+ ):
listener = ClassBasedListener()
listener_manager(listener)
response =
test_client.patch(f"/dags/{DAG1_ID}/dagRuns/{DAG1_RUN1_ID}", json={"state":
state})
assert response.status_code == 200
assert listener.state == listener_state
+ if expected_msg is not None:
+ assert listener.dag_run_msg == expected_msg
+ assert listener.dag_has_dag_attr is True
+
+ @pytest.mark.usefixtures("configure_git_connection_for_dag_bundle")
+ def test_patch_dag_run_listener_sees_note_when_note_and_state_both_patched(
+ self, test_client, listener_manager
+ ):
+ listener = ClassBasedListener()
+ listener_manager(listener)
+ response = test_client.patch(
+ f"/dags/{DAG1_ID}/dagRuns/{DAG1_RUN2_ID}",
+ json={"state": "success", "note": "listener_note"},
+ )
+ assert response.status_code == 200
+ assert listener.dag_run_note_at_listener == "listener_note"
class TestDeleteDagRun:
diff --git a/airflow-core/tests/unit/listeners/class_listener.py
b/airflow-core/tests/unit/listeners/class_listener.py
index 4b9ef3a9f26..152f199cb80 100644
--- a/airflow-core/tests/unit/listeners/class_listener.py
+++ b/airflow-core/tests/unit/listeners/class_listener.py
@@ -26,6 +26,9 @@ class ClassBasedListener:
self.started_component = None
self.stopped_component = None
self.state = []
+ self.dag_run_msg: str | None = None
+ self.dag_has_dag_attr: bool | None = None
+ self.dag_run_note_at_listener: str | None = None
@hookimpl
def on_starting(self, component):
@@ -60,10 +63,16 @@ class ClassBasedListener:
@hookimpl
def on_dag_run_success(self, dag_run, msg: str):
self.state.append(DagRunState.SUCCESS)
+ self.dag_run_msg = msg
+ self.dag_has_dag_attr = dag_run.dag is not None
+ self.dag_run_note_at_listener = dag_run.note
@hookimpl
def on_dag_run_failed(self, dag_run, msg: str):
self.state.append(DagRunState.FAILED)
+ self.dag_run_msg = msg
+ self.dag_has_dag_attr = dag_run.dag is not None
+ self.dag_run_note_at_listener = dag_run.note
def clear():