Правильний спосіб створення динамічних робочих процесів у Airflow


98

Проблема

Чи існує спосіб Airflow створити такий робочий процес, щоб кількість завдань B. * була невідомою до завершення завдання A? Я розглядав підтеги, але схоже, що він може працювати лише зі статичним набором завдань, які повинні бути визначені при створенні Dag.

Чи спрацюють тригери dag? І якщо так, то будь ласка, наведіть приклад.

У мене виникла проблема, коли неможливо знати кількість завдань B, які будуть потрібні для обчислення завдання C, поки завдання A не буде виконане. Для обчислення кожного завдання B. * потрібно кілька годин, і їх не можна поєднувати.

              |---> Task B.1 --|
              |---> Task B.2 --|
 Task A ------|---> Task B.3 --|-----> Task C
              |       ....     |
              |---> Task B.N --|

Ідея No1

Мені не подобається це рішення, оскільки мені доводиться створювати блокуючий ExternalTaskSensor, і виконання Завдання B. * триватиме від 2 до 24 годин. Тому я не вважаю це життєздатним рішенням. Напевно, є простіший спосіб? Або Airflow не призначений для цього?

Dag 1
Task A -> TriggerDagRunOperator(Dag 2) -> ExternalTaskSensor(Dag 2, Task Dummy B) -> Task C

Dag 2 (Dynamically created DAG though python_callable in TriggerDagrunOperator)
               |-- Task B.1 --|
               |-- Task B.2 --|
Task Dummy A --|-- Task B.3 --|-----> Task Dummy B
               |     ....     |
               |-- Task B.N --|

Редагувати 1:

На сьогодні це питання все ще не має чудової відповіді . До мене зв’язалося кілька людей, які шукали рішення.


Чи всі завдання B * подібні тим, що їх можна створити в циклі?
Daniel Lee

Так, усі завдання B. * можна швидко створити в циклі, як тільки завдання A виконано. Виконання завдання A займає близько 2 годин.
costrouc

Ви знайшли рішення проблеми? не могли б ви його опублікувати?
Даніель Дубовський

3
Корисний ресурс для Ідеї №1: linkedin.com/pulse/…
Хуан Ріаза,

1
Ось стаття, яку я написав, що пояснює, як це зробити linkedin.com/pulse/dynamic-workflows-airflow-kyle-bridenstine
Kyle Bridenstine

Відповіді:


33

Ось як я це зробив із подібним запитом без жодних підтегів:

Спочатку створіть метод, який повертає будь-які значення, які ви хочете

def values_function():
     return values

Далі створіть метод, який генеруватиме завдання динамічно:

def group(number, **kwargs):
        #load the values if needed in the command you plan to execute
        dyn_value = "{{ task_instance.xcom_pull(task_ids='push_func') }}"
        return BashOperator(
                task_id='JOB_NAME_{}'.format(number),
                bash_command='script.sh {} {}'.format(dyn_value, number),
                dag=dag)

А потім об’єднайте їх:

push_func = PythonOperator(
        task_id='push_func',
        provide_context=True,
        python_callable=values_function,
        dag=dag)

complete = DummyOperator(
        task_id='All_jobs_completed',
        dag=dag)

for i in values_function():
        push_func >> group(i) >> complete

Де визначаються значення?
monksy

11
Замість цього for i in values_function()я би очікував чогось подібного for i in push_func_output. Проблема в тому, що я не можу знайти спосіб отримати цей результат динамічно. Результат роботи PythonOperator буде в Xcom після виконання, але я не знаю, чи можу я посилатися на нього з визначення DAG.
Ена

@Ena Ви знайшли спосіб досягти цього?
eldos

1
@eldos див. мою відповідь нижче
Ена

1
Що, якби нам довелося виконати ряд кроків, залежних від кроків у циклі? Чи існував би другий ланцюжок залежностей у межах groupфункції?
CodingInCircles

12

Я придумав спосіб створення робочих процесів на основі результатів попередніх завдань.
В основному, що ви хочете зробити, це мати два підтеги з наступним:

  1. Xcom натискає список (або що-небудь, що потрібно для подальшого створення динамічного робочого процесу) у підтезі, який виконується першим (див. Test1.py def return_list())
  2. Передайте основний об’єкт dag як параметр другому підпрограмі
  3. Тепер, якщо у вас є основний об’єкт dag, ви можете використовувати його для отримання списку його екземплярів завдань. З цього списку екземплярів завдань ви можете відфільтрувати завдання поточного запуску, використовуючи parent_dag.get_task_instances(settings.Session, start_date=parent_dag.get_active_runs()[-1])[-1]), можливо, тут можна додати більше фільтрів.
  4. За допомогою цього екземпляра завдання ви можете використовувати xcom pull, щоб отримати потрібне значення, вказавши dag_id одному з перших підпрограм: dag_id='%s.%s' % (parent_dag_name, 'test1')
  5. Використовуйте список / значення для динамічного створення завдань

Зараз я перевірив це у своїй локальній установці повітряного потоку, і воно працює нормально. Я не знаю, чи виникне у xcom pull частини проблем, якщо одночасно працює більше одного екземпляра дага, але тоді ви, мабуть, використовуєте унікальний ключ або щось подібне для однозначної ідентифікації xcom значення, яке ви хочете. Ймовірно, можна було б оптимізувати 3. крок, щоб бути впевненим на 100%, щоб отримати конкретне завдання поточного основного дага, але для мого використання це працює досить добре, я думаю, що для використання xcom_pull потрібен лише один об’єкт task_instance.

Також я очищаю xcoms для першого підпрограми перед кожним виконанням, щоб переконатися, що випадково не отримаю неправильне значення.

Я досить погано пояснюю, тому я сподіваюся, що наступний код все зрозуміє:

test1.py

from airflow.models import DAG
import logging
from airflow.operators.python_operator import PythonOperator
from airflow.operators.postgres_operator import PostgresOperator

log = logging.getLogger(__name__)


def test1(parent_dag_name, start_date, schedule_interval):
    dag = DAG(
        '%s.test1' % parent_dag_name,
        schedule_interval=schedule_interval,
        start_date=start_date,
    )

    def return_list():
        return ['test1', 'test2']

    list_extract_folder = PythonOperator(
        task_id='list',
        dag=dag,
        python_callable=return_list
    )

    clean_xcoms = PostgresOperator(
        task_id='clean_xcoms',
        postgres_conn_id='airflow_db',
        sql="delete from xcom where dag_id='{{ dag.dag_id }}'",
        dag=dag)

    clean_xcoms >> list_extract_folder

    return dag

test2.py

from airflow.models import DAG, settings
import logging
from airflow.operators.dummy_operator import DummyOperator

log = logging.getLogger(__name__)


def test2(parent_dag_name, start_date, schedule_interval, parent_dag=None):
    dag = DAG(
        '%s.test2' % parent_dag_name,
        schedule_interval=schedule_interval,
        start_date=start_date
    )

    if len(parent_dag.get_active_runs()) > 0:
        test_list = parent_dag.get_task_instances(settings.Session, start_date=parent_dag.get_active_runs()[-1])[-1].xcom_pull(
            dag_id='%s.%s' % (parent_dag_name, 'test1'),
            task_ids='list')
        if test_list:
            for i in test_list:
                test = DummyOperator(
                    task_id=i,
                    dag=dag
                )

    return dag

та основний робочий процес:

test.py

from datetime import datetime
from airflow import DAG
from airflow.operators.subdag_operator import SubDagOperator
from subdags.test1 import test1
from subdags.test2 import test2

DAG_NAME = 'test-dag'

dag = DAG(DAG_NAME,
          description='Test workflow',
          catchup=False,
          schedule_interval='0 0 * * *',
          start_date=datetime(2018, 8, 24))

test1 = SubDagOperator(
    subdag=test1(DAG_NAME,
                 dag.start_date,
                 dag.schedule_interval),
    task_id='test1',
    dag=dag
)

test2 = SubDagOperator(
    subdag=test2(DAG_NAME,
                 dag.start_date,
                 dag.schedule_interval,
                 parent_dag=dag),
    task_id='test2',
    dag=dag
)

test1 >> test2

на Airflow 1.9 вони не завантажувались при додаванні до папки DAG, i щось не вистачає?
Ентоні Кін

@AnthonyKeane ви помістили test1.py та test2.py в папку, що називається підданими у вашій папці dag?
Крістофер Бек,

Я зробив так. Скопіював обидва файли до підтегів і помістив test.py у папку dag, все одно отримайте цю помилку. Broken DAG: [/home/airflow/gcs/dags/test.py] Немає модуля з іменем subdags.test1 Примітка. Я використовую Google Cloud Composer (керований Google Airflow 1.9.0)
Ентоні Кін

@AnthonyKeane - це єдина помилка, яку ви бачите в журналах? Пошкоджений DAG може бути спричинений тим, що підтег має помилку компіляції.
Крістофер Бек,

3
Привіт, @Christopher Beck, я знайшов СВОЮ помилку, яку мені потрібно було додати _ _init_ _.pyдо папки підданих. помилка новачка
Ентоні Кін

9

Так, це можливо. Я створив приклад DAG, який демонструє це.

import airflow
from airflow.operators.python_operator import PythonOperator
import os
from airflow.models import Variable
import logging
from airflow import configuration as conf
from airflow.models import DagBag, TaskInstance
from airflow import DAG, settings
from airflow.operators.bash_operator import BashOperator

main_dag_id = 'DynamicWorkflow2'

args = {
    'owner': 'airflow',
    'start_date': airflow.utils.dates.days_ago(2),
    'provide_context': True
}

dag = DAG(
    main_dag_id,
    schedule_interval="@once",
    default_args=args)


def start(*args, **kwargs):

    value = Variable.get("DynamicWorkflow_Group1")
    logging.info("Current DynamicWorkflow_Group1 value is " + str(value))


def resetTasksStatus(task_id, execution_date):
    logging.info("Resetting: " + task_id + " " + execution_date)

    dag_folder = conf.get('core', 'DAGS_FOLDER')
    dagbag = DagBag(dag_folder)
    check_dag = dagbag.dags[main_dag_id]
    session = settings.Session()

    my_task = check_dag.get_task(task_id)
    ti = TaskInstance(my_task, execution_date)
    state = ti.current_state()
    logging.info("Current state of " + task_id + " is " + str(state))
    ti.set_state(None, session)
    state = ti.current_state()
    logging.info("Updated state of " + task_id + " is " + str(state))


def bridge1(*args, **kwargs):

    # You can set this value dynamically e.g., from a database or a calculation
    dynamicValue = 2

    variableValue = Variable.get("DynamicWorkflow_Group2")
    logging.info("Current DynamicWorkflow_Group2 value is " + str(variableValue))

    logging.info("Setting the Airflow Variable DynamicWorkflow_Group2 to " + str(dynamicValue))
    os.system('airflow variables --set DynamicWorkflow_Group2 ' + str(dynamicValue))

    variableValue = Variable.get("DynamicWorkflow_Group2")
    logging.info("Current DynamicWorkflow_Group2 value is " + str(variableValue))

    # Below code prevents this bug: https://issues.apache.org/jira/browse/AIRFLOW-1460
    for i in range(dynamicValue):
        resetTasksStatus('secondGroup_' + str(i), str(kwargs['execution_date']))


def bridge2(*args, **kwargs):

    # You can set this value dynamically e.g., from a database or a calculation
    dynamicValue = 3

    variableValue = Variable.get("DynamicWorkflow_Group3")
    logging.info("Current DynamicWorkflow_Group3 value is " + str(variableValue))

    logging.info("Setting the Airflow Variable DynamicWorkflow_Group3 to " + str(dynamicValue))
    os.system('airflow variables --set DynamicWorkflow_Group3 ' + str(dynamicValue))

    variableValue = Variable.get("DynamicWorkflow_Group3")
    logging.info("Current DynamicWorkflow_Group3 value is " + str(variableValue))

    # Below code prevents this bug: https://issues.apache.org/jira/browse/AIRFLOW-1460
    for i in range(dynamicValue):
        resetTasksStatus('thirdGroup_' + str(i), str(kwargs['execution_date']))


def end(*args, **kwargs):
    logging.info("Ending")


def doSomeWork(name, index, *args, **kwargs):
    # Do whatever work you need to do
    # Here I will just create a new file
    os.system('touch /home/ec2-user/airflow/' + str(name) + str(index) + '.txt')


starting_task = PythonOperator(
    task_id='start',
    dag=dag,
    provide_context=True,
    python_callable=start,
    op_args=[])

# Used to connect the stream in the event that the range is zero
bridge1_task = PythonOperator(
    task_id='bridge1',
    dag=dag,
    provide_context=True,
    python_callable=bridge1,
    op_args=[])

DynamicWorkflow_Group1 = Variable.get("DynamicWorkflow_Group1")
logging.info("The current DynamicWorkflow_Group1 value is " + str(DynamicWorkflow_Group1))

for index in range(int(DynamicWorkflow_Group1)):
    dynamicTask = PythonOperator(
        task_id='firstGroup_' + str(index),
        dag=dag,
        provide_context=True,
        python_callable=doSomeWork,
        op_args=['firstGroup', index])

    starting_task.set_downstream(dynamicTask)
    dynamicTask.set_downstream(bridge1_task)

# Used to connect the stream in the event that the range is zero
bridge2_task = PythonOperator(
    task_id='bridge2',
    dag=dag,
    provide_context=True,
    python_callable=bridge2,
    op_args=[])

DynamicWorkflow_Group2 = Variable.get("DynamicWorkflow_Group2")
logging.info("The current DynamicWorkflow value is " + str(DynamicWorkflow_Group2))

for index in range(int(DynamicWorkflow_Group2)):
    dynamicTask = PythonOperator(
        task_id='secondGroup_' + str(index),
        dag=dag,
        provide_context=True,
        python_callable=doSomeWork,
        op_args=['secondGroup', index])

    bridge1_task.set_downstream(dynamicTask)
    dynamicTask.set_downstream(bridge2_task)

ending_task = PythonOperator(
    task_id='end',
    dag=dag,
    provide_context=True,
    python_callable=end,
    op_args=[])

DynamicWorkflow_Group3 = Variable.get("DynamicWorkflow_Group3")
logging.info("The current DynamicWorkflow value is " + str(DynamicWorkflow_Group3))

for index in range(int(DynamicWorkflow_Group3)):

    # You can make this logic anything you'd like
    # I chose to use the PythonOperator for all tasks
    # except the last task will use the BashOperator
    if index < (int(DynamicWorkflow_Group3) - 1):
        dynamicTask = PythonOperator(
            task_id='thirdGroup_' + str(index),
            dag=dag,
            provide_context=True,
            python_callable=doSomeWork,
            op_args=['thirdGroup', index])
    else:
        dynamicTask = BashOperator(
            task_id='thirdGroup_' + str(index),
            bash_command='touch /home/ec2-user/airflow/thirdGroup_' + str(index) + '.txt',
            dag=dag)

    bridge2_task.set_downstream(dynamicTask)
    dynamicTask.set_downstream(ending_task)

# If you do not connect these then in the event that your range is ever zero you will have a disconnection between your stream
# and your tasks will run simultaneously instead of in your desired stream order.
starting_task.set_downstream(bridge1_task)
bridge1_task.set_downstream(bridge2_task)
bridge2_task.set_downstream(ending_task)

Перш ніж запускати DAG, створіть ці три змінні повітряного потоку

airflow variables --set DynamicWorkflow_Group1 1

airflow variables --set DynamicWorkflow_Group2 0

airflow variables --set DynamicWorkflow_Group3 0

Ви побачите, що DAG виходить із цього

введіть тут опис зображення

До цього після того, як його побігли

введіть тут опис зображення

Ви можете побачити більше інформації про цей DAG у моїй статті про створення динамічних робочих процесів у потоці повітря .


1
Але що станеться, якщо у вас є кілька DagRun цього DAG. Чи всі вони мають однакові змінні?
Мар-к

1
Так, вони б використовували ту саму змінну; Я звертаюся до цього у своїй статті в самому кінці. Вам потрібно буде динамічно створювати змінну та використовувати ідентифікатор запуску dag в імені змінної. Мій приклад простий лише для демонстрації динамічної можливості, але вам потрібно буде зробити це якісним у виробництві :)
Кайл Бріденстайн,

Чи необхідні мости при створенні динамічних завдань? Прочитаю вашу статтю повністю миттєво, але хотів запитати. Зараз я намагаюся створити динамічне завдання на основі попереднього завдання, і я починаю розбиратися, де я помилився. Моє поточне питання полягає в тому, що з якихось причин я не можу змусити DAG синхронізуватися з DAG-пакетом. Мій DAG синхронізувався, коли я використовував статичний список у модулі, але зупинявся, коли я перемикав цей статичний список на збірку із попереднього завдання.
lucid_goose

Це дуже розумно
jvans

1
@jvans дякує, що це розумно, але, ймовірно, не якість виробництва
Кайл Бріденстайн,

6

OA: "Чи можна в Airflow створити такий робочий процес, щоб кількість завдань B. * була невідомою до завершення завдання A?"

Коротка відповідь - ні. Потік повітря створить потік DAG перед початком його запуску.

Це означає, що ми дійшли простого висновку, тобто у нас немає такої потреби. Коли ви хочете розпаралелювати певну роботу, слід оцінити наявні у вас ресурси, а не кількість елементів для обробки.

Ми зробили це так: ми динамічно генеруємо фіксовану кількість завдань, скажімо 10, які розділять роботу. Наприклад, якщо нам потрібно обробити 100 файлів, кожне завдання обробить 10 з них. Я опублікую код пізніше сьогодні.

Оновлення

Ось код, вибачте за затримку.

from datetime import datetime, timedelta

import airflow
from airflow.operators.dummy_operator import DummyOperator

args = {
    'owner': 'airflow',
    'depends_on_past': False,
    'start_date': datetime(2018, 1, 8),
    'email': ['myemail@gmail.com'],
    'email_on_failure': True,
    'email_on_retry': True,
    'retries': 1,
    'retry_delay': timedelta(seconds=5)
}

dag = airflow.DAG(
    'parallel_tasks_v1',
    schedule_interval="@daily",
    catchup=False,
    default_args=args)

# You can read this from variables
parallel_tasks_total_number = 10

start_task = DummyOperator(
    task_id='start_task',
    dag=dag
)


# Creates the tasks dynamically.
# Each one will elaborate one chunk of data.
def create_dynamic_task(current_task_number):
    return DummyOperator(
        provide_context=True,
        task_id='parallel_task_' + str(current_task_number),
        python_callable=parallelTask,
        # your task will take as input the total number and the current number to elaborate a chunk of total elements
        op_args=[current_task_number, int(parallel_tasks_total_number)],
        dag=dag)


end = DummyOperator(
    task_id='end',
    dag=dag)

for page in range(int(parallel_tasks_total_number)):
    created_task = create_dynamic_task(page)
    start_task >> created_task
    created_task >> end

Пояснення коду:

Тут ми маємо єдине початкове завдання та єдине кінцеве завдання (обидва фіктивні).

Потім із початкового завдання за допомогою циклу for ми створюємо 10 завдань з тим самим пітоном, що викликається. Завдання створюються у функції create_dynamic_task.

Кожному пітону, що викликається, ми передаємо як аргументи загальну кількість паралельних завдань та поточний індекс завдань.

Припустимо, у вас є 1000 деталей для розробки: перше завдання отримає в якості вхідних даних, що воно має опрацювати перший шматок із 10 шматочків. Він поділить 1000 предметів на 10 шматочків і розробить перший.


1
Це хороше рішення, якщо вам не потрібне конкретне завдання на предмет (наприклад, прогрес, результат, успіх / невдача, повторні спроби тощо)
Alonzzo2,

@Ena parallelTaskне визначено: мені чогось не вистачає?
Ентоні Кін,

2
@AnthonyKeane Це функція python, яку ви повинні викликати, щоб насправді щось зробити. Як зазначається в коді, для введення загальної кількості та поточного числа буде використано загальну кількість і поточне число для опрацювання частини загальних елементів.
Ena

4

Я думаю, що ви шукаєте, це динамічне створення DAG. Я зіткнувся з подібною ситуацією кілька днів тому після певного пошуку, який я знайшов у цьому блозі .

Динамічне створення завдань

start = DummyOperator(
    task_id='start',
    dag=dag
)

end = DummyOperator(
    task_id='end',
    dag=dag)

def createDynamicETL(task_id, callableFunction, args):
    task = PythonOperator(
        task_id = task_id,
        provide_context=True,
        #Eval is used since the callableFunction var is of type string
        #while the python_callable argument for PythonOperators only receives objects of type callable not strings.
        python_callable = eval(callableFunction),
        op_kwargs = args,
        xcom_push = True,
        dag = dag,
    )
    return task

Налаштування робочого процесу DAG

with open('/usr/local/airflow/dags/config_files/dynamicDagConfigFile.yaml') as f:
    # Use safe_load instead to load the YAML file
    configFile = yaml.safe_load(f)

    # Extract table names and fields to be processed
    tables = configFile['tables']

    # In this loop tasks are created for each table defined in the YAML file
    for table in tables:
        for table, fieldName in table.items():
            # In our example, first step in the workflow for each table is to get SQL data from db.
            # Remember task id is provided in order to exchange data among tasks generated in dynamic way.
            get_sql_data_task = createDynamicETL('{}-getSQLData'.format(table),
                                                 'getSQLData',
                                                 {'host': 'host', 'user': 'user', 'port': 'port', 'password': 'pass',
                                                  'dbname': configFile['dbname']})

            # Second step is upload data to s3
            upload_to_s3_task = createDynamicETL('{}-uploadDataToS3'.format(table),
                                                 'uploadDataToS3',
                                                 {'previous_task_id': '{}-getSQLData'.format(table),
                                                  'bucket_name': configFile['bucket_name'],
                                                  'prefix': configFile['prefix']})

            # This is where the magic lies. The idea is that
            # once tasks are generated they should linked with the
            # dummy operators generated in the start and end tasks. 
            # Then you are done!
            start >> get_sql_data_task
            get_sql_data_task >> upload_to_s3_task
            upload_to_s3_task >> end

Ось так виглядає наш DAG після складання коду введіть тут опис зображення

import yaml
import airflow
from airflow import DAG
from datetime import datetime, timedelta, time
from airflow.operators.python_operator import PythonOperator
from airflow.operators.dummy_operator import DummyOperator

start = DummyOperator(
    task_id='start',
    dag=dag
)


def createDynamicETL(task_id, callableFunction, args):
    task = PythonOperator(
        task_id=task_id,
        provide_context=True,
        # Eval is used since the callableFunction var is of type string
        # while the python_callable argument for PythonOperators only receives objects of type callable not strings.
        python_callable=eval(callableFunction),
        op_kwargs=args,
        xcom_push=True,
        dag=dag,
    )
    return task


end = DummyOperator(
    task_id='end',
    dag=dag)

with open('/usr/local/airflow/dags/config_files/dynamicDagConfigFile.yaml') as f:
    # use safe_load instead to load the YAML file
    configFile = yaml.safe_load(f)

    # Extract table names and fields to be processed
    tables = configFile['tables']

    # In this loop tasks are created for each table defined in the YAML file
    for table in tables:
        for table, fieldName in table.items():
            # In our example, first step in the workflow for each table is to get SQL data from db.
            # Remember task id is provided in order to exchange data among tasks generated in dynamic way.
            get_sql_data_task = createDynamicETL('{}-getSQLData'.format(table),
                                                 'getSQLData',
                                                 {'host': 'host', 'user': 'user', 'port': 'port', 'password': 'pass',
                                                  'dbname': configFile['dbname']})

            # Second step is upload data to s3
            upload_to_s3_task = createDynamicETL('{}-uploadDataToS3'.format(table),
                                                 'uploadDataToS3',
                                                 {'previous_task_id': '{}-getSQLData'.format(table),
                                                  'bucket_name': configFile['bucket_name'],
                                                  'prefix': configFile['prefix']})

            # This is where the magic lies. The idea is that
            # once tasks are generated they should linked with the
            # dummy operators generated in the start and end tasks. 
            # Then you are done!
            start >> get_sql_data_task
            get_sql_data_task >> upload_to_s3_task
            upload_to_s3_task >> end

Це дуже допомогло повній надії. Це також допоможе комусь іншому


Ви самі цього досягли? Я втомився. Але мені не вдалося.
Ньют,

Так, це спрацювало у мене. З якою проблемою ви стикаєтесь?
Мухаммед бен Алі

1
Зрозумів. Мою проблему вирішено. Дякую. Я просто не отримав правильного способу читати змінні середовища на зображеннях докера.
Ньют,

1
що, якщо елементи таблиці можуть змінитися, тому ми не можемо помістити їх у статичний файл yaml?
FrankZhu

Це насправді залежить, де ви його використовуєте. Хоча мені було б цікаво, що ви пропонуєте. @FrankZhu як це слід робити правильно?
Мухаммед бен Алі

3

Думаю, я знайшов для цього більш приємне рішення за адресою https://github.com/mastak/airflow_multi_dagrun , яке використовує просте чергування DagRuns за допомогою запуску декількох дагрунів, подібних до TriggerDagRuns . Більшість кредитів надходять на https://github.com/mastak , хоча мені довелося виправити деякі деталі, щоб це працювало з останнім потоком повітря.

Рішення використовує спеціальний оператор, який запускає кілька DagRuns :

from airflow import settings
from airflow.models import DagBag
from airflow.operators.dagrun_operator import DagRunOrder, TriggerDagRunOperator
from airflow.utils.decorators import apply_defaults
from airflow.utils.state import State
from airflow.utils import timezone


class TriggerMultiDagRunOperator(TriggerDagRunOperator):
    CREATED_DAGRUN_KEY = 'created_dagrun_key'

    @apply_defaults
    def __init__(self, op_args=None, op_kwargs=None,
                 *args, **kwargs):
        super(TriggerMultiDagRunOperator, self).__init__(*args, **kwargs)
        self.op_args = op_args or []
        self.op_kwargs = op_kwargs or {}

    def execute(self, context):

        context.update(self.op_kwargs)
        session = settings.Session()
        created_dr_ids = []
        for dro in self.python_callable(*self.op_args, **context):
            if not dro:
                break
            if not isinstance(dro, DagRunOrder):
                dro = DagRunOrder(payload=dro)

            now = timezone.utcnow()
            if dro.run_id is None:
                dro.run_id = 'trig__' + now.isoformat()

            dbag = DagBag(settings.DAGS_FOLDER)
            trigger_dag = dbag.get_dag(self.trigger_dag_id)
            dr = trigger_dag.create_dagrun(
                run_id=dro.run_id,
                execution_date=now,
                state=State.RUNNING,
                conf=dro.payload,
                external_trigger=True,
            )
            created_dr_ids.append(dr.id)
            self.log.info("Created DagRun %s, %s", dr, now)

        if created_dr_ids:
            session.commit()
            context['ti'].xcom_push(self.CREATED_DAGRUN_KEY, created_dr_ids)
        else:
            self.log.info("No DagRun created")
        session.close()

Потім ви можете надіслати кілька дагрунів із функції, що викликається, у вашому PythonOperator, наприклад:

from airflow.operators.dagrun_operator import DagRunOrder
from airflow.models import DAG
from airflow.operators import TriggerMultiDagRunOperator
from airflow.utils.dates import days_ago


def generate_dag_run(**kwargs):
    for i in range(10):
        order = DagRunOrder(payload={'my_variable': i})
        yield order

args = {
    'start_date': days_ago(1),
    'owner': 'airflow',
}

dag = DAG(
    dag_id='simple_trigger',
    max_active_runs=1,
    schedule_interval='@hourly',
    default_args=args,
)

gen_target_dag_run = TriggerMultiDagRunOperator(
    task_id='gen_target_dag_run',
    dag=dag,
    trigger_dag_id='common_target',
    python_callable=generate_dag_run
)

Я створив вилку з кодом на https://github.com/flinz/airflow_multi_dagrun


3

Графік завдань не генерується під час виконання. Швидше графік будується, коли його забирає Airflow з вашої папки dags. Тому насправді не буде можливо мати інший графік для роботи кожного разу, коли вона виконується. Ви можете налаштувати завдання на побудову графіка на основі запиту при завантаженні час . Цей графік залишатиметься незмінним для кожного запуску після цього, що, мабуть, не дуже корисно.

Ви можете створити графік, який виконує різні завдання при кожному запуску на основі результатів запиту, використовуючи оператор відділення.

Що я зробив, це попередньо налаштувати набір завдань, а потім взяти результати запиту та розподілити їх між завданнями. Це, мабуть, краще, оскільки, якщо ваш запит повертає багато результатів, ви, мабуть, не хочете заливати планувальник великою кількістю одночасних завдань. Щоб бути ще безпечнішим, я також використовував пул, щоб переконатися, що моя паралельність не виходить з-під контролю за несподівано великим запитом.

"""
 - This is an idea for how to invoke multiple tasks based on the query results
"""
import logging
from datetime import datetime

from airflow import DAG
from airflow.hooks.postgres_hook import PostgresHook
from airflow.operators.mysql_operator import MySqlOperator
from airflow.operators.python_operator import PythonOperator, BranchPythonOperator
from include.run_celery_task import runCeleryTask

########################################################################

default_args = {
    'owner': 'airflow',
    'catchup': False,
    'depends_on_past': False,
    'start_date': datetime(2019, 7, 2, 19, 50, 00),
    'email': ['rotten@stackoverflow'],
    'email_on_failure': True,
    'email_on_retry': False,
    'retries': 0,
    'max_active_runs': 1
}

dag = DAG('dynamic_tasks_example', default_args=default_args, schedule_interval=None)

totalBuckets = 5

get_orders_query = """
select 
    o.id,
    o.customer
from 
    orders o
where
    o.created_at >= current_timestamp at time zone 'UTC' - '2 days'::interval
    and
    o.is_test = false
    and
    o.is_processed = false
"""

###########################################################################################################

# Generate a set of tasks so we can parallelize the results
def createOrderProcessingTask(bucket_number):
    return PythonOperator( 
                           task_id=f'order_processing_task_{bucket_number}',
                           python_callable=runOrderProcessing,
                           pool='order_processing_pool',
                           op_kwargs={'task_bucket': f'order_processing_task_{bucket_number}'},
                           provide_context=True,
                           dag=dag
                          )


# Fetch the order arguments from xcom and doStuff() to them
def runOrderProcessing(task_bucket, **context):
    orderList = context['ti'].xcom_pull(task_ids='get_open_orders', key=task_bucket)

    if orderList is not None:
        for order in orderList:
            logging.info(f"Processing Order with Order ID {order[order_id]}, customer ID {order[customer_id]}")
            doStuff(**op_kwargs)


# Discover the orders we need to run and group them into buckets for processing
def getOpenOrders(**context):
    myDatabaseHook = PostgresHook(postgres_conn_id='my_database_conn_id')

    # initialize the task list buckets
    tasks = {}
    for task_number in range(0, totalBuckets):
        tasks[f'order_processing_task_{task_number}'] = []

    # populate the task list buckets
    # distribute them evenly across the set of buckets
    resultCounter = 0
    for record in myDatabaseHook.get_records(get_orders_query):

        resultCounter += 1
        bucket = (resultCounter % totalBuckets)

        tasks[f'order_processing_task_{bucket}'].append({'order_id': str(record[0]), 'customer_id': str(record[1])})

    # push the order lists into xcom
    for task in tasks:
        if len(tasks[task]) > 0:
            logging.info(f'Task {task} has {len(tasks[task])} orders.')
            context['ti'].xcom_push(key=task, value=tasks[task])
        else:
            # if we didn't have enough tasks for every bucket
            # don't bother running that task - remove it from the list
            logging.info(f"Task {task} doesn't have any orders.")
            del(tasks[task])

    return list(tasks.keys())

###################################################################################################


# this just makes sure that there aren't any dangling xcom values in the database from a crashed dag
clean_xcoms = MySqlOperator(
    task_id='clean_xcoms',
    mysql_conn_id='airflow_db',
    sql="delete from xcom where dag_id='{{ dag.dag_id }}'",
    dag=dag)


# Ideally we'd use BranchPythonOperator() here instead of PythonOperator so that if our
# query returns fewer results than we have buckets, we don't try to run them all.
# Unfortunately I couldn't get BranchPythonOperator to take a list of results like the
# documentation says it should (Airflow 1.10.2). So we call all the bucket tasks for now.
get_orders_task = PythonOperator(
                                 task_id='get_orders',
                                 python_callable=getOpenOrders,
                                 provide_context=True,
                                 dag=dag
                                )
get_orders_task.set_upstream(clean_xcoms)

# set up the parallel tasks -- these are configured at compile time, not at run time:
for bucketNumber in range(0, totalBuckets):
    taskBucket = createOrderProcessingTask(bucketNumber)
    taskBucket.set_upstream(get_orders_task)


###################################################################################################

Зауважте, що, здається, можливо створити підтеги на льоту в результаті виконання завдання, однак більшість документації щодо підданих, яку я знайшов, настійно рекомендує триматися подалі від цієї функції, оскільки вона викликає більше проблем, ніж вирішує. в більшості випадків. Я бачив пропозиції щодо того, що підтеги можуть бути вилучені як вбудована функція найближчим часом.
гнилий

Також зауважте, що у for tasks in tasksциклі в моєму прикладі я видаляю об’єкт, для якого я ітератую. Це погана ідея. Натомість отримайте список клавіш і перегляньте їх - або пропустіть видалення. Подібним чином, якщо xcom_pull повертає None (замість списку або порожнього списку), то цикл for теж не вдається. Можливо, потрібно запустити xcom_pull перед символом "for", а потім перевірити, чи не є значення None, або переконатися, що там є хоча б порожній список. YMMV. Щасти!
гнилий

1
що в open_order_task?
alltej

Ви маєте рацію, це помилка в моєму прикладі. Це має бути get_orders_task.set_upstream (). Я це виправлю.
гнилий

0

Не розумієте, в чому проблема?

Ось стандартний приклад. Тепер, якщо у функції підпрограм замінити for i in range(5):наfor i in range(random.randint(0, 10)): тоді все буде працювати. А тепер уявіть, що оператор 'start' поміщає дані у файл, і замість випадкового значення функція буде зчитувати ці дані. Тоді оператор 'start' вплине на кількість завдань.

Проблема буде лише на дисплеї в інтерфейсі, оскільки при введенні підпрограми кількість завдань буде дорівнює останньому прочитаному з файлу / бази даних / XCom на даний момент. Що автоматично дає обмеження на кілька запусків одного дага одночасно.


-1

Я знайшов цей допис Medium, який дуже схожий на це питання. Однак він повний друкарських помилок і не працює, коли я намагався його реалізувати.

Моя відповідь на вищезазначене така:

Якщо ви створюєте завдання динамічно, ви повинні робити це , перебираючи щось, що не створене попереднім завданням або може бути визначене незалежно від цього завдання. Я дізнався, що ви не можете передавати дати виконання чи інші змінні повітряного потоку чомусь поза шаблоном (наприклад, завданням), як багато хто вказував раніше. Дивіться також цю публікацію .


Якщо ви подивитесь на мій коментар, то побачите, що насправді можливо створювати завдання на основі результатів попередніх завдань.
Крістофер Бек,
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.