Skip to content

Commit a8bb94a

Browse files
kacpermudajason810496
authored andcommitted
feat: Add listener notification in API endpoint when skipped (apache#60585)
1 parent 1595710 commit a8bb94a

5 files changed

Lines changed: 59 additions & 4 deletions

File tree

airflow-core/docs/administration-and-deployment/listeners.rst

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,12 @@ of a :class:`~airflow.sdk.execution_time.task_runner.RuntimeTaskInstance` instan
9696
:start-after: [START howto_listen_ti_failure_task]
9797
:end-before: [END howto_listen_ti_failure_task]
9898

99+
- ``on_task_instance_skipped``
100+
101+
.. exampleinclude:: /../src/airflow/example_dags/plugins/event_listener.py
102+
:language: python
103+
:start-after: [START howto_listen_ti_skipped_task]
104+
:end-before: [END howto_listen_ti_skipped_task]
99105

100106
Asset Events
101107
--------------
@@ -199,3 +205,5 @@ List of changes in the listener interfaces since 2.8.0 when they were introduced
199205
| 3.0.0 | ``on_task_instance_failed``, | ``session`` argument removed from task instance listeners, |
200206
| | ``on_task_instance_success`` | ``task_instance`` object is now an instance of ``RuntimeTaskInstance`` when on worker and ``TaskInstance`` when on API server |
201207
+-----------------+--------------------------------------------+-------------------------------------------------------------------------------------------------------------------------------+
208+
| 3.2.0 | ``on_task_instance_skipped`` | New listener method added to the interface |
209+
+-----------------+--------------------------------------------+-------------------------------------------------------------------------------------------------------------------------------+

airflow-core/src/airflow/api_fastapi/core_api/services/public/task_instances.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,8 @@ def _patch_task_instance_state(
132132
task_instance=ti,
133133
error=f"TaskInstance's state was manually set to `{TaskInstanceState.FAILED}`.",
134134
)
135+
elif data["new_state"] == TaskInstanceState.SKIPPED:
136+
get_listener_manager().hook.on_task_instance_skipped(previous_state=None, task_instance=ti)
135137
except Exception:
136138
log.exception("error calling listener")
137139

airflow-core/src/airflow/example_dags/plugins/event_listener.py

Lines changed: 43 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,9 @@
3030

3131
# [START howto_listen_ti_running_task]
3232
@hookimpl
33-
def on_task_instance_running(previous_state: TaskInstanceState, task_instance: RuntimeTaskInstance):
33+
def on_task_instance_running(
34+
previous_state: TaskInstanceState | None, task_instance: RuntimeTaskInstance | TaskInstance
35+
):
3436
"""
3537
Called when task state changes to RUNNING.
3638
@@ -63,7 +65,7 @@ def on_task_instance_running(previous_state: TaskInstanceState, task_instance: R
6365
# [START howto_listen_ti_success_task]
6466
@hookimpl
6567
def on_task_instance_success(
66-
previous_state: TaskInstanceState, task_instance: RuntimeTaskInstance | TaskInstance
68+
previous_state: TaskInstanceState | None, task_instance: RuntimeTaskInstance | TaskInstance
6769
):
6870
"""
6971
Called when task state changes to SUCCESS.
@@ -95,7 +97,7 @@ def on_task_instance_success(
9597
# [START howto_listen_ti_failure_task]
9698
@hookimpl
9799
def on_task_instance_failed(
98-
previous_state: TaskInstanceState,
100+
previous_state: TaskInstanceState | None,
99101
task_instance: RuntimeTaskInstance | TaskInstance,
100102
error: None | str | BaseException,
101103
):
@@ -133,6 +135,44 @@ def on_task_instance_failed(
133135
# [END howto_listen_ti_failure_task]
134136

135137

138+
# [START howto_listen_ti_skipped_task]
139+
@hookimpl
140+
def on_task_instance_skipped(
141+
previous_state: TaskInstanceState | None, task_instance: RuntimeTaskInstance | TaskInstance
142+
):
143+
"""
144+
Called when a task instance skips itself during execution.
145+
146+
This hook is called only when a task has started execution and then
147+
intentionally skips itself (e.g., by raising AirflowSkipException).
148+
149+
Note: This function will NOT cover tasks that were skipped by scheduler, before execution began, such as:
150+
- Skips due to trigger rules (e.g., upstream failures)
151+
- Skips from operators like BranchPythonOperator, ShortCircuitOperator, or similar mechanisms
152+
- Any other situation in which the scheduler decides not to schedule a task for execution
153+
154+
For comprehensive tracking of skipped tasks, use DAG-level listeners
155+
(on_dag_run_success/on_dag_run_failed) which may have access to all task states.
156+
"""
157+
print("Task instance was skipped")
158+
159+
if isinstance(task_instance, TaskInstance):
160+
print("Task instance's state was changed through the API.")
161+
return
162+
163+
context = task_instance.get_template_context()
164+
task = context["task"]
165+
166+
if TYPE_CHECKING:
167+
assert task
168+
169+
print("Task start")
170+
print(f"Task:{task}")
171+
172+
173+
# [END howto_listen_ti_skipped_task]
174+
175+
136176
# [START howto_listen_dagrun_success_task]
137177
@hookimpl
138178
def on_dag_run_success(dag_run: DagRun, msg: str):

airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_task_instances.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3924,7 +3924,8 @@ class TestPatchTaskInstance(TestTaskInstanceEndpoint):
39243924
[
39253925
("success", [TaskInstanceState.SUCCESS]),
39263926
("failed", [TaskInstanceState.FAILED]),
3927-
("skipped", []),
3927+
("skipped", [TaskInstanceState.SKIPPED]),
3928+
("running", []),
39283929
],
39293930
)
39303931
def test_patch_task_instance_notifies_listeners(

airflow-core/tests/unit/listeners/class_listener.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,10 @@ def on_task_instance_success(self, previous_state, task_instance):
4949
def on_task_instance_failed(self, previous_state, task_instance, error: None | str | BaseException):
5050
self.state.append(TaskInstanceState.FAILED)
5151

52+
@hookimpl
53+
def on_task_instance_skipped(self, previous_state, task_instance):
54+
self.state.append(TaskInstanceState.SKIPPED)
55+
5256
@hookimpl
5357
def on_dag_run_running(self, dag_run, msg: str):
5458
self.state.append(DagRunState.RUNNING)

0 commit comments

Comments
 (0)