This is an automated email from the ASF dual-hosted git repository.
pierrejeambrun pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/airflow.git
The following commit(s) were added to refs/heads/main by this push:
new bb29517c618 Allow keeping finished task states on Dag run patch
(#68666)
bb29517c618 is described below
commit bb29517c618f43f5150df66cab910fb1b3d23ad1
Author: Guan-Ming Chiu <[email protected]>
AuthorDate: Thu Jul 9 23:31:38 2026 +0800
Allow keeping finished task states on Dag run patch (#68666)
---
airflow-core/src/airflow/api/common/mark_tasks.py | 145 +++++++++++----------
.../tests/unit/api/common/test_mark_tasks.py | 25 ++--
2 files changed, 87 insertions(+), 83 deletions(-)
diff --git a/airflow-core/src/airflow/api/common/mark_tasks.py
b/airflow-core/src/airflow/api/common/mark_tasks.py
index 7004156ef24..5274d8c706e 100644
--- a/airflow-core/src/airflow/api/common/mark_tasks.py
+++ b/airflow-core/src/airflow/api/common/mark_tasks.py
@@ -215,80 +215,26 @@ def _set_dag_run_state(dag_id: str, run_id: str, state:
DagRunState, session: SA
session.merge(dag_run)
-@provide_session
-def set_dag_run_state_to_success(
+def _set_dag_run_terminal_state(
*,
dag: SerializedDAG,
- run_id: str | None = None,
- commit: bool = False,
- session: SASession = NEW_SESSION,
-) -> list[TaskInstance]:
- """
- Set the dag run's state to success.
-
- Set for a specific logical date and its task instances to success.
-
- :param dag: the Dag of which to alter state
- :param run_id: the run_id to start looking from
- :param commit: commit Dag and tasks to be altered to the database
- :param session: database session
- :return: If commit is true, list of tasks that have been updated,
- otherwise list of tasks that will be updated
- :raises: ValueError if dag or logical_date is invalid
- """
- if not dag:
- return []
- if not run_id:
- raise ValueError(f"Invalid dag_run_id: {run_id}")
-
- tasks = dag.tasks
-
- # Mark all task instances of the dag run to success - except for
unfinished teardown as they need to complete work.
- teardown_tasks = [task for task in tasks if task.is_teardown]
- unfinished_teardown_task_ids = set(
- session.scalars(
- select(TaskInstance.task_id).where(
- TaskInstance.dag_id == dag.dag_id,
- TaskInstance.run_id == run_id,
- TaskInstance.task_id.in_(task.task_id for task in
teardown_tasks),
- or_(TaskInstance.state.is_(None),
TaskInstance.state.in_(State.unfinished)),
- )
- )
- )
-
- # Mark the dag run to success if there are no unfinished teardown tasks.
- if commit and len(unfinished_teardown_task_ids) == 0:
- _set_dag_run_state(dag.dag_id, run_id, DagRunState.SUCCESS, session)
-
- tasks_to_mark_success = [task for task in tasks if not task.is_teardown] +
[
- task for task in teardown_tasks if task.task_id not in
unfinished_teardown_task_ids
- ]
- for task in tasks_to_mark_success:
- task.dag = dag
- return set_state(
- tasks=tasks_to_mark_success,
- run_id=run_id,
- state=TaskInstanceState.SUCCESS,
- commit=commit,
- session=session,
- )
-
-
-@provide_session
-def set_dag_run_state_to_failed(
- *,
- dag: SerializedDAG,
- run_id: str | None = None,
- commit: bool = False,
- session: SASession = NEW_SESSION,
+ run_id: str | None,
+ run_state: DagRunState,
+ ti_state: TaskInstanceState,
+ commit: bool,
+ session: SASession,
) -> list[TaskInstance]:
"""
- Set the dag run's state to failed.
+ Set the dag run's state to the given terminal state.
- Set for a specific logical date and its task instances to failed.
+ Running task instances are set to ``ti_state`` and non-finished ones to
+ SKIPPED; finished task instances keep their state. Teardown tasks are left
+ untouched so they can complete their work.
:param dag: the Dag of which to alter state
:param run_id: the Dag run_id to start looking from
+ :param run_state: terminal state to set on the dag run
+ :param ti_state: state to set on running task instances
:param commit: commit Dag and tasks to be altered to the database
:param session: database session
:return: If commit is true, list of tasks that have been updated,
@@ -352,14 +298,75 @@ def set_dag_run_state_to_failed(
for ti in pending_normal_tis:
ti.set_state(TaskInstanceState.SKIPPED)
- # Mark the dag run to failed if there is no pending teardown (else
this would not be scheduled later).
+ # Set the dag run state only if there is no pending teardown (else
this would not be scheduled later).
if not any(dag.task_dict[ti.task_id].is_teardown for ti in
(running_tis + pending_tis)):
- _set_dag_run_state(dag.dag_id, run_id, DagRunState.FAILED, session)
+ _set_dag_run_state(dag.dag_id, run_id, run_state, session)
return pending_normal_tis + set_state(
tasks=running_tasks,
run_id=run_id,
- state=TaskInstanceState.FAILED,
+ state=ti_state,
+ commit=commit,
+ session=session,
+ )
+
+
+@provide_session
+def set_dag_run_state_to_success(
+ *,
+ dag: SerializedDAG,
+ run_id: str | None = None,
+ commit: bool = False,
+ session: SASession = NEW_SESSION,
+) -> list[TaskInstance]:
+ """
+ Set the dag run's state to success.
+
+ Set for a specific logical date and its task instances to success.
+
+ :param dag: the Dag of which to alter state
+ :param run_id: the run_id to start looking from
+ :param commit: commit Dag and tasks to be altered to the database
+ :param session: database session
+ :return: If commit is true, list of tasks that have been updated,
+ otherwise list of tasks that will be updated
+ :raises: ValueError if dag or logical_date is invalid
+ """
+ return _set_dag_run_terminal_state(
+ dag=dag,
+ run_id=run_id,
+ run_state=DagRunState.SUCCESS,
+ ti_state=TaskInstanceState.SUCCESS,
+ commit=commit,
+ session=session,
+ )
+
+
+@provide_session
+def set_dag_run_state_to_failed(
+ *,
+ dag: SerializedDAG,
+ run_id: str | None = None,
+ commit: bool = False,
+ session: SASession = NEW_SESSION,
+) -> list[TaskInstance]:
+ """
+ Set the dag run's state to failed.
+
+ Set for a specific logical date and its task instances to failed.
+
+ :param dag: the Dag of which to alter state
+ :param run_id: the Dag run_id to start looking from
+ :param commit: commit Dag and tasks to be altered to the database
+ :param session: database session
+ :return: If commit is true, list of tasks that have been updated,
+ otherwise list of tasks that will be updated
+ """
+ return _set_dag_run_terminal_state(
+ dag=dag,
+ run_id=run_id,
+ run_state=DagRunState.FAILED,
+ ti_state=TaskInstanceState.FAILED,
commit=commit,
session=session,
)
diff --git a/airflow-core/tests/unit/api/common/test_mark_tasks.py
b/airflow-core/tests/unit/api/common/test_mark_tasks.py
index 4504ddceda0..e1f5695ada8 100644
--- a/airflow-core/tests/unit/api/common/test_mark_tasks.py
+++ b/airflow-core/tests/unit/api/common/test_mark_tasks.py
@@ -91,21 +91,23 @@ def test_set_dag_run_state_to_success_unfinished_teardown(
assert len(updated_tis) == 2
task_dict = {ti.task_id: ti for ti in updated_tis}
assert task_dict["running"].state == TaskInstanceState.SUCCESS
- assert task_dict["pending"].state == TaskInstanceState.SUCCESS
+ assert task_dict["pending"].state == TaskInstanceState.SKIPPED
assert "teardown" not in task_dict
@pytest.mark.parametrize("finished_state", sorted(list(State.finished)))
-def test_set_dag_run_state_to_success_finished_teardown(dag_maker:
DagMaker[SerializedDAG], finished_state):
+def test_set_dag_run_state_to_success_keeps_finished_task_states(
+ dag_maker: DagMaker[SerializedDAG], finished_state
+):
with dag_maker("TEST_DAG_1") as dag:
with EmptyOperator(task_id="teardown").as_teardown():
- EmptyOperator(task_id="failed")
+ EmptyOperator(task_id="finished")
dr = dag_maker.create_dagrun()
for ti in dr.get_task_instances():
- if ti.task_id == "failed":
- ti.set_state(TaskInstanceState.FAILED)
- if ti.task_id == "teardown":
+ if ti.task_id == "finished":
ti.set_state(finished_state)
+ if ti.task_id == "teardown":
+ ti.set_state(TaskInstanceState.SUCCESS)
dag_maker.session.flush()
dr.set_state(DagRunState.FAILED)
@@ -115,14 +117,9 @@ def
test_set_dag_run_state_to_success_finished_teardown(dag_maker: DagMaker[Seri
run = dag_maker.session.scalar(select(DagRun).filter_by(dag_id=dr.dag_id,
run_id=dr.run_id))
assert run is not None
assert run.state == DagRunState.SUCCESS
- if finished_state == TaskInstanceState.SUCCESS:
- assert len(updated_tis) == 1
- else:
- assert len(updated_tis) == 2
- task_dict = {ti.task_id: ti for ti in updated_tis}
- assert task_dict["failed"].state == TaskInstanceState.SUCCESS
- if finished_state != TaskInstanceState.SUCCESS:
- assert task_dict["teardown"].state == TaskInstanceState.SUCCESS
+ assert updated_tis == []
+ states = {ti.task_id: ti.state for ti in
dr.get_task_instances(session=dag_maker.session)}
+ assert states == {"finished": finished_state, "teardown":
TaskInstanceState.SUCCESS}
def test_find_task_relatives_downstream_skips_teardowns(dag_maker:
DagMaker[SerializedDAG]):