From aec44f16912a8b75680443b813099cab6782fb09 Mon Sep 17 00:00:00 2001 From: George Leslie-Waksman Date: Fri, 19 Aug 2016 18:17:53 -0700 Subject: [PATCH] Implement latest_only_operator --- .gitignore | 1 + airflow/example_dags/example_latest_only.py | 34 +++++++ .../example_latest_only_with_trigger.py | 43 ++++++++ airflow/operators/__init__.py | 1 + airflow/operators/latest_only_operator.py | 57 +++++++++++ docs/concepts.rst | 74 ++++++++++++++ docs/img/latest_only_with_trigger.png | Bin 0 -> 40034 bytes setup.py | 2 +- tests/core.py | 2 +- tests/operators/latest_only_operator.py | 93 ++++++++++++++++++ 10 files changed, 305 insertions(+), 2 deletions(-) create mode 100644 airflow/example_dags/example_latest_only.py create mode 100644 airflow/example_dags/example_latest_only_with_trigger.py create mode 100644 airflow/operators/latest_only_operator.py create mode 100644 docs/img/latest_only_with_trigger.png create mode 100644 tests/operators/latest_only_operator.py diff --git a/.gitignore b/.gitignore index 48af479ee14a9..ca6de2b0644fe 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ .DS_Store .ipynb* .coverage +.python-version airflow/git_version airflow/www/static/coverage/ airflow.db diff --git a/airflow/example_dags/example_latest_only.py b/airflow/example_dags/example_latest_only.py new file mode 100644 index 0000000000000..9ce03b9aa6a7f --- /dev/null +++ b/airflow/example_dags/example_latest_only.py @@ -0,0 +1,34 @@ +# -*- coding: utf-8 -*- +# +# Licensed 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. +""" +Example of the LatestOnlyOperator +""" +import datetime as dt + +from airflow.models import DAG +from airflow.operators.dummy_operator import DummyOperator +from airflow.operators.latest_only_operator import LatestOnlyOperator +from airflow.utils.trigger_rule import TriggerRule + + +dag = DAG( + dag_id='latest_only', + schedule_interval=dt.timedelta(hours=4), + start_date=dt.datetime(2016, 9, 20), +) + +latest_only = LatestOnlyOperator(task_id='latest_only', dag=dag) + +task1 = DummyOperator(task_id='task1', dag=dag) +task1.set_upstream(latest_only) diff --git a/airflow/example_dags/example_latest_only_with_trigger.py b/airflow/example_dags/example_latest_only_with_trigger.py new file mode 100644 index 0000000000000..e3a88b7b0b85b --- /dev/null +++ b/airflow/example_dags/example_latest_only_with_trigger.py @@ -0,0 +1,43 @@ +# -*- coding: utf-8 -*- +# +# Licensed 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. +""" +Example LatestOnlyOperator and TriggerRule interactions +""" +import datetime as dt + +from airflow.models import DAG +from airflow.operators.dummy_operator import DummyOperator +from airflow.operators.latest_only_operator import LatestOnlyOperator +from airflow.utils.trigger_rule import TriggerRule + + +dag = DAG( + dag_id='latest_only_with_trigger', + schedule_interval=dt.timedelta(hours=4), + start_date=dt.datetime(2016, 9, 20), +) + +latest_only = LatestOnlyOperator(task_id='latest_only', dag=dag) + +task1 = DummyOperator(task_id='task1', dag=dag) +task1.set_upstream(latest_only) + +task2 = DummyOperator(task_id='task2', dag=dag) + +task3 = DummyOperator(task_id='task3', dag=dag) +task3.set_upstream([task1, task2]) + +task4 = DummyOperator(task_id='task4', dag=dag, + trigger_rule=TriggerRule.ALL_DONE) +task4.set_upstream([task1, task2]) diff --git a/airflow/operators/__init__.py b/airflow/operators/__init__.py index f39ad01671cb5..4cfac7b8cb451 100644 --- a/airflow/operators/__init__.py +++ b/airflow/operators/__init__.py @@ -57,6 +57,7 @@ 'dummy_operator': ['DummyOperator'], 'email_operator': ['EmailOperator'], 'hive_to_samba_operator': ['Hive2SambaOperator'], + 'latest_only_operator': ['LatestOnlyOperator'], 'mysql_operator': ['MySqlOperator'], 'sqlite_operator': ['SqliteOperator'], 'mysql_to_hive': ['MySqlToHiveTransfer'], diff --git a/airflow/operators/latest_only_operator.py b/airflow/operators/latest_only_operator.py new file mode 100644 index 0000000000000..49ba2a339533c --- /dev/null +++ b/airflow/operators/latest_only_operator.py @@ -0,0 +1,57 @@ +# -*- coding: utf-8 -*- +# +# Licensed 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 datetime +import logging + +from airflow.models import BaseOperator, TaskInstance +from airflow.utils.state import State +from airflow import settings + + +class LatestOnlyOperator(BaseOperator): + """ + Allows a workflow to skip tasks that are not running during the most + recent schedule interval. + + If the task is run outside of the latest schedule interval, all + directly downstream tasks will be skipped. + """ + + ui_color = '#e9ffdb' # nyanza + + def execute(self, context): + now = datetime.datetime.now() + left_window = context['dag'].following_schedule( + context['execution_date']) + right_window = context['dag'].following_schedule(left_window) + logging.info( + 'Checking latest only with left_window: %s right_window: %s ' + 'now: %s', left_window, right_window, now) + if not left_window < now <= right_window: + logging.info('Not latest execution, skipping downstream.') + session = settings.Session() + for task in context['task'].downstream_list: + ti = TaskInstance( + task, execution_date=context['ti'].execution_date) + logging.info('Skipping task: %s', ti.task_id) + ti.state = State.SKIPPED + ti.start_date = now + ti.end_date = now + session.merge(ti) + session.commit() + session.close() + logging.info('Done.') + else: + logging.info('Latest, allowing execution to proceed.') diff --git a/docs/concepts.rst b/docs/concepts.rst index 8cfc8aba74b61..82d52488ccc98 100644 --- a/docs/concepts.rst +++ b/docs/concepts.rst @@ -594,6 +594,80 @@ that, when set to ``True``, keeps a task from getting triggered if the previous schedule for the task hasn't succeeded. +Latest Run Only +=============== + +Standard workflow behavior involves running a series of tasks for a +particular date/time range. Some workflows, however, perform tasks that +are independent of run time but need to be run on a schedule, much like a +standard cron job. In these cases, backfills or running jobs missed during +a pause just wastes CPU cycles. + +For situations like this, you can use the ``LatestOnlyOperator`` to skip +tasks that are not being run during the most recent scheduled run for a +DAG. The ``LatestOnlyOperator`` skips all immediate downstream tasks, and +itself, if the time right now is not between its ``execution_time`` and the +next scheduled ``execution_time``. + +One must be aware of the interaction between skipped tasks and trigger +rules. Skipped tasks will cascade through trigger rules ``all_success`` +and ``all_failed`` but not ``all_done``, ``one_failed``, ``one_success``, +and ``dummy``. If you would like to use the ``LatestOnlyOperator`` with +trigger rules that do not cascade skips, you will need to ensure that the +``LatestOnlyOperator`` is **directly** upstream of the task you would like +to skip. + +It is possible, through use of trigger rules to mix tasks that should run +in the typical date/time dependent mode and those using the +``LatestOnlyOperator``. + +For example, consider the following dag: + +.. code:: python + + #dags/latest_only_with_trigger.py + import datetime as dt + + from airflow.models import DAG + from airflow.operators.dummy_operator import DummyOperator + from airflow.operators.latest_only_operator import LatestOnlyOperator + from airflow.utils.trigger_rule import TriggerRule + + + dag = DAG( + dag_id='latest_only_with_trigger', + schedule_interval=dt.timedelta(hours=4), + start_date=dt.datetime(2016, 9, 20), + ) + + latest_only = LatestOnlyOperator(task_id='latest_only', dag=dag) + + task1 = DummyOperator(task_id='task1', dag=dag) + task1.set_upstream(latest_only) + + task2 = DummyOperator(task_id='task2', dag=dag) + + task3 = DummyOperator(task_id='task3', dag=dag) + task3.set_upstream([task1, task2]) + + task4 = DummyOperator(task_id='task4', dag=dag, + trigger_rule=TriggerRule.ALL_DONE) + task4.set_upstream([task1, task2]) + +In the case of this dag, the ``latest_only`` task will show up as skipped +for all runs except the latest run. ``task1`` is directly downstream of +``latest_only`` and will also skip for all runs except the latest. +``task2`` is entirely independent of ``latest_only`` and will run in all +scheduled periods. ``task3`` is downstream of ``task1`` and ``task2`` and +because of the default ``trigger_rule`` being ``all_success`` will receive +a cascaded skip from ``task1``. ``task4`` is downstream of ``task1`` and +``task2`` but since its ``trigger_rule`` is set to ``all_done`` it will +trigger as soon as ``task1`` has been skipped (a valid completion state) +and ``task2`` has succeeded. + +.. image:: img/latest_only_with_trigger.png + + Zombies & Undeads ================= diff --git a/docs/img/latest_only_with_trigger.png b/docs/img/latest_only_with_trigger.png new file mode 100644 index 0000000000000000000000000000000000000000..629adfa9079643d9989ffe46a8415b7df8d829c4 GIT binary patch literal 40034 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 literal 0 HcmV?d00001 diff --git a/setup.py b/setup.py index 225573465846c..a63c1beb37d9e 100644 --- a/setup.py +++ b/setup.py @@ -159,7 +159,7 @@ def write_version(filename=os.path.join(*['airflow', cloudant = ['cloudant>=0.5.9,<2.0'] # major update coming soon, clamp to 0.x all_dbs = postgres + mysql + hive + mssql + hdfs + vertica + cloudant -devel = ['lxml>=3.3.4', 'nose', 'nose-parameterized', 'mock', 'click', 'jira', 'moto'] +devel = ['lxml>=3.3.4', 'nose', 'nose-parameterized', 'mock', 'click', 'jira', 'moto', 'freezegun'] devel_minreq = devel + mysql + doc + password + s3 devel_hadoop = devel_minreq + hive + hdfs + webhdfs + kerberos devel_all = devel + all_dbs + doc + samba + s3 + slack + crypto + oracle + docker diff --git a/tests/core.py b/tests/core.py index e443a03b1f585..cffdc1f284c7d 100644 --- a/tests/core.py +++ b/tests/core.py @@ -60,7 +60,7 @@ import six -NUM_EXAMPLE_DAGS = 16 +NUM_EXAMPLE_DAGS = 18 DEV_NULL = '/dev/null' TEST_DAG_FOLDER = os.path.join( os.path.dirname(os.path.realpath(__file__)), 'dags') diff --git a/tests/operators/latest_only_operator.py b/tests/operators/latest_only_operator.py new file mode 100644 index 0000000000000..37aec38d4cac5 --- /dev/null +++ b/tests/operators/latest_only_operator.py @@ -0,0 +1,93 @@ +# -*- coding: utf-8 -*- +# +# Licensed 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 __future__ import print_function, unicode_literals + +import datetime +import logging +import unittest + +from airflow import configuration, DAG, settings +from airflow.jobs import BackfillJob +from airflow.models import TaskInstance +from airflow.operators.latest_only_operator import LatestOnlyOperator +from airflow.operators.dummy_operator import DummyOperator +from freezegun import freeze_time + +DEFAULT_DATE = datetime.datetime(2016, 1, 1) +END_DATE = datetime.datetime(2016, 1, 2) +INTERVAL = datetime.timedelta(hours=12) +FROZEN_NOW = datetime.datetime(2016, 1, 2, 12, 1, 1) + + +def get_task_instances(task_id): + session = settings.Session() + return session \ + .query(TaskInstance) \ + .filter(TaskInstance.task_id == task_id) \ + .order_by(TaskInstance.execution_date) \ + .all() + + +class LatestOnlyOperatorTest(unittest.TestCase): + + def setUp(self): + super(LatestOnlyOperatorTest, self).setUp() + configuration.load_test_config() + self.dag = DAG( + 'test_dag', + default_args={ + 'owner': 'airflow', + 'start_date': DEFAULT_DATE}, + schedule_interval=INTERVAL) + self.addCleanup(self.dag.clear) + freezer = freeze_time(FROZEN_NOW) + freezer.start() + self.addCleanup(freezer.stop) + + def test_run(self): + task = LatestOnlyOperator( + task_id='latest', + dag=self.dag) + task.run(start_date=DEFAULT_DATE, end_date=DEFAULT_DATE) + + def test_skipping(self): + latest_task = LatestOnlyOperator( + task_id='latest', + dag=self.dag) + downstream_task = DummyOperator( + task_id='downstream', + dag=self.dag) + downstream_task.set_upstream(latest_task) + + latest_task.run(start_date=DEFAULT_DATE, end_date=END_DATE) + downstream_task.run(start_date=DEFAULT_DATE, end_date=END_DATE) + + latest_instances = get_task_instances('latest') + exec_date_to_latest_state = { + ti.execution_date: ti.state for ti in latest_instances} + assert exec_date_to_latest_state == { + datetime.datetime(2016, 1, 1): 'success', + datetime.datetime(2016, 1, 1, 12): 'success', + datetime.datetime(2016, 1, 2): 'success', + } + + downstream_instances = get_task_instances('downstream') + exec_date_to_downstream_state = { + ti.execution_date: ti.state for ti in downstream_instances} + assert exec_date_to_downstream_state == { + datetime.datetime(2016, 1, 1): 'skipped', + datetime.datetime(2016, 1, 1, 12): 'skipped', + datetime.datetime(2016, 1, 2): 'success', + }