-
Notifications
You must be signed in to change notification settings - Fork 14.3k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add Listener Plugin API that tracks TaskInstance state changes (#20443)
This adds new Plugin API - "listeners". It enables plugin authors to write [pluggy hook implementation][1] that will be called on certain formalized extension points. To differentiate between current Airflow extension points, like plugins, and current Airflow hooks, implementations of those hooks are called listeners. The API is ment to be called across all dags, and all operators - in contrast to current on_success_callback, pre_execute and related family which are meant to provide callbacks for particular dag authors, or operator creators. pluggy mechanism enables us to execute multiple, or none, listeners that implement particular extension point, so that users can use multiple listeners seamlessly. In this PR, three such extension points are added. When TaskInstance's state is changed to RUNNING, on_task_instance_running hook is called. On change toSUCCESS on_task_instance_success is called, similarly on FAILED on_task_instance_failed is called. Actual notification mechanism is be implemented using [SQLAlchemy’s events mechanism][2]. This ensures that plugins will get every change of state, regardless of where in the codebase it happened, and not require manual annotation of TI state changes across the codebase. To make sure that this change is not affecting performance, running this mechanism on scheduler is disabled by default. The SQLAlchemy event mechanism is also not affected by default - the event listener is only added if we have any plugin which actually provides any listener. [1]: https://pluggy.readthedocs.io/en/stable/ [2]: https://docs.sqlalchemy.org/en/13/orm/session_events.html#after-flush Signed-off-by: Maciej Obuchowski <obuchowski.maciej@gmail.com>
- Loading branch information
1 parent
ce06e6b
commit dba00ce
Showing
20 changed files
with
570 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
# | ||
# Licensed to the Apache Software Foundation (ASF) under one | ||
# or more contributor license agreements. See the NOTICE file | ||
# distributed with this work for additional information | ||
# regarding copyright ownership. The ASF licenses this file | ||
# to you under the Apache License, Version 2.0 (the | ||
# "License"); you may not use this file except in compliance | ||
# with the License. You may obtain a copy of the License at | ||
# | ||
# http://www.apache.org/licenses/LICENSE-2.0 | ||
# | ||
# Unless required by applicable law or agreed to in writing, | ||
# software distributed under the License is distributed on an | ||
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY | ||
# KIND, either express or implied. See the License for the | ||
# specific language governing permissions and limitations | ||
# under the License. | ||
from pluggy import HookimplMarker | ||
|
||
hookimpl = HookimplMarker("airflow") |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,79 @@ | ||
# Licensed to the Apache Software Foundation (ASF) under one | ||
# or more contributor license agreements. See the NOTICE file | ||
# distributed with this work for additional information | ||
# regarding copyright ownership. The ASF licenses this file | ||
# to you under the Apache License, Version 2.0 (the | ||
# "License"); you may not use this file except in compliance | ||
# with the License. You may obtain a copy of the License at | ||
# | ||
# http://www.apache.org/licenses/LICENSE-2.0 | ||
# | ||
# Unless required by applicable law or agreed to in writing, | ||
# software distributed under the License is distributed on an | ||
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY | ||
# KIND, either express or implied. See the License for the | ||
# specific language governing permissions and limitations | ||
# under the License. | ||
import logging | ||
|
||
from sqlalchemy import event | ||
from sqlalchemy.orm import Session | ||
|
||
from airflow.listeners.listener import get_listener_manager | ||
from airflow.models import TaskInstance | ||
from airflow.utils.state import State | ||
|
||
_is_listening = False | ||
|
||
|
||
def on_task_instance_state_session_flush(session, flush_context): | ||
""" | ||
Listens for session.flush() events that modify TaskInstance's state, and notify listeners that listen | ||
for that event. Doing it this way enable us to be stateless in the SQLAlchemy event listener. | ||
""" | ||
logger = logging.getLogger(__name__) | ||
if not get_listener_manager().has_listeners: | ||
return | ||
for state in flush_context.states: | ||
if isinstance(state.object, TaskInstance) and session.is_modified( | ||
state.object, include_collections=False | ||
): | ||
added, unchanged, deleted = flush_context.get_attribute_history(state, 'state') | ||
|
||
logger.debug( | ||
"session flush listener: added %s unchanged %s deleted %s - %s", | ||
added, | ||
unchanged, | ||
deleted, | ||
state.object, | ||
) | ||
if not added: | ||
continue | ||
|
||
previous_state = deleted[0] if deleted else State.NONE | ||
|
||
if State.RUNNING in added: | ||
get_listener_manager().hook.on_task_instance_running( | ||
previous_state=previous_state, task_instance=state.object, session=session | ||
) | ||
elif State.FAILED in added: | ||
get_listener_manager().hook.on_task_instance_failed( | ||
previous_state=previous_state, task_instance=state.object, session=session | ||
) | ||
elif State.SUCCESS in added: | ||
get_listener_manager().hook.on_task_instance_success( | ||
previous_state=previous_state, task_instance=state.object, session=session | ||
) | ||
|
||
|
||
def register_task_instance_state_events(): | ||
global _is_listening | ||
if not _is_listening: | ||
event.listen(Session, 'after_flush', on_task_instance_state_session_flush) | ||
_is_listening = True | ||
|
||
|
||
def unregister_task_instance_state_events(): | ||
global _is_listening | ||
event.remove(Session, 'after_flush', on_task_instance_state_session_flush) | ||
_is_listening = False |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,71 @@ | ||
# | ||
# Licensed to the Apache Software Foundation (ASF) under one | ||
# or more contributor license agreements. See the NOTICE file | ||
# distributed with this work for additional information | ||
# regarding copyright ownership. The ASF licenses this file | ||
# to you under the Apache License, Version 2.0 (the | ||
# "License"); you may not use this file except in compliance | ||
# with the License. You may obtain a copy of the License at | ||
# | ||
# http://www.apache.org/licenses/LICENSE-2.0 | ||
# | ||
# Unless required by applicable law or agreed to in writing, | ||
# software distributed under the License is distributed on an | ||
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY | ||
# KIND, either express or implied. See the License for the | ||
# specific language governing permissions and limitations | ||
# under the License. | ||
import logging | ||
from types import ModuleType | ||
from typing import TYPE_CHECKING | ||
|
||
import pluggy | ||
|
||
from airflow.plugins_manager import integrate_listener_plugins | ||
|
||
if TYPE_CHECKING: | ||
from pluggy._hooks import _HookRelay | ||
|
||
log = logging.getLogger(__name__) | ||
|
||
|
||
_listener_manager = None | ||
|
||
|
||
class ListenerManager: | ||
"""Class that manages registration of listeners and provides hook property for calling them""" | ||
|
||
def __init__(self): | ||
from airflow.listeners import spec | ||
|
||
self.pm = pluggy.PluginManager("airflow") | ||
self.pm.add_hookspecs(spec) | ||
|
||
@property | ||
def has_listeners(self) -> bool: | ||
return len(self.pm.get_plugins()) > 0 | ||
|
||
@property | ||
def hook(self) -> "_HookRelay": | ||
"""Returns hook, on which plugin methods specified in spec can be called.""" | ||
return self.pm.hook | ||
|
||
def add_listener(self, listener): | ||
if not isinstance(listener, ModuleType): | ||
raise TypeError("Listener %s is not module", str(listener)) | ||
if self.pm.is_registered(listener): | ||
return | ||
self.pm.register(listener) | ||
|
||
def clear(self): | ||
"""Remove registered plugins""" | ||
for plugin in self.pm.get_plugins(): | ||
self.pm.unregister(plugin) | ||
|
||
|
||
def get_listener_manager() -> ListenerManager: | ||
global _listener_manager | ||
if not _listener_manager: | ||
_listener_manager = ListenerManager() | ||
integrate_listener_plugins(_listener_manager) | ||
return _listener_manager |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
# | ||
# Licensed to the Apache Software Foundation (ASF) under one | ||
# or more contributor license agreements. See the NOTICE file | ||
# distributed with this work for additional information | ||
# regarding copyright ownership. The ASF licenses this file | ||
# to you under the Apache License, Version 2.0 (the | ||
# "License"); you may not use this file except in compliance | ||
# with the License. You may obtain a copy of the License at | ||
# | ||
# http://www.apache.org/licenses/LICENSE-2.0 | ||
# | ||
# Unless required by applicable law or agreed to in writing, | ||
# software distributed under the License is distributed on an | ||
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY | ||
# KIND, either express or implied. See the License for the | ||
# specific language governing permissions and limitations | ||
# under the License. | ||
from typing import TYPE_CHECKING, Optional | ||
|
||
from pluggy import HookspecMarker | ||
|
||
if TYPE_CHECKING: | ||
from sqlalchemy.orm.session import Session | ||
|
||
from airflow.models.taskinstance import TaskInstance | ||
from airflow.utils.state import TaskInstanceState | ||
|
||
hookspec = HookspecMarker("airflow") | ||
|
||
|
||
@hookspec | ||
def on_task_instance_running( | ||
previous_state: "TaskInstanceState", task_instance: "TaskInstance", session: Optional["Session"] | ||
): | ||
"""Called when task state changes to RUNNING. Previous_state can be State.NONE.""" | ||
|
||
|
||
@hookspec | ||
def on_task_instance_success( | ||
previous_state: "TaskInstanceState", task_instance: "TaskInstance", session: Optional["Session"] | ||
): | ||
"""Called when task state changes to SUCCESS. Previous_state can be State.NONE.""" | ||
|
||
|
||
@hookspec | ||
def on_task_instance_failed( | ||
previous_state: "TaskInstanceState", task_instance: "TaskInstance", session: Optional["Session"] | ||
): | ||
"""Called when task state changes to FAIL. Previous_state can be State.NONE.""" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
.. Licensed to the Apache Software Foundation (ASF) under one | ||
or more contributor license agreements. See the NOTICE file | ||
distributed with this work for additional information | ||
regarding copyright ownership. The ASF licenses this file | ||
to you under the Apache License, Version 2.0 (the | ||
"License"); you may not use this file except in compliance | ||
with the License. You may obtain a copy of the License at | ||
.. http://www.apache.org/licenses/LICENSE-2.0 | ||
.. Unless required by applicable law or agreed to in writing, | ||
software distributed under the License is distributed on an | ||
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY | ||
KIND, either express or implied. See the License for the | ||
specific language governing permissions and limitations | ||
under the License. | ||
Listeners | ||
========= | ||
|
||
Airflow gives you an option to be notified of events happening in Airflow | ||
by writing listeners. Listeners are powered by `pluggy <https://pluggy.readthedocs.io/en/stable/>`__ | ||
|
||
Listener API is meant to be called across all dags, and all operators - in contrast to methods like | ||
``on_success_callback``, ``pre_execute`` and related family which are meant to provide callbacks | ||
for particular dag authors, or operator creators. There is no possibility to listen on events generated | ||
by particular dag. | ||
|
||
To include listener in your Airflow installation, include it as a part of an :doc:`Airflow Plugin </plugins>` | ||
|
||
|experimental| | ||
|
||
Interface | ||
--------- | ||
|
||
To create a listener you will need to derive the | ||
create python module, import ``airflow.listeners.hookimpl`` and implement the ``hookimpls`` for | ||
events you want to be notified at. | ||
|
||
Right now Airflow exposes TaskInstance state change events. | ||
Their specification is defined as ``hookspec`` in ``airflow/listeners/spec.py`` file. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.