Méthode appropriée pour créer des flux de travail dynamiques dans Airflow


96

Problème

Existe-t-il un moyen dans Airflow de créer un flux de travail tel que le nombre de tâches B. * soit inconnu jusqu'à la fin de la tâche A? J'ai regardé les subdags mais il semble que cela ne puisse fonctionner qu'avec un ensemble statique de tâches qui doivent être déterminées lors de la création de Dag.

Les déclencheurs de dag fonctionneraient-ils? Et si oui, pourriez-vous donner un exemple.

J'ai un problème où il est impossible de connaître le nombre de tâches B qui seront nécessaires pour calculer la tâche C jusqu'à ce que la tâche A soit terminée. Chaque tâche B. * prendra plusieurs heures à calculer et ne peut pas être combinée.

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

Idée n ° 1

Je n'aime pas cette solution car je dois créer un ExternalTaskSensor bloquant et toute la tâche B. * prendra entre 2 et 24 heures. Je ne considère donc pas cela comme une solution viable. Il existe sûrement un moyen plus simple? Ou Airflow n'a-t-il pas été conçu pour cela?

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 --|

Modifier 1:

Pour le moment, cette question n'a toujours pas de bonne réponse . J'ai été contacté par plusieurs personnes à la recherche d'une solution.


Toutes les tâches B * sont-elles similaires, en ce sens qu'elles peuvent être créées en boucle?
Daniel Lee

Oui, toutes les tâches B. * peuvent être créées rapidement en boucle une fois la tâche A terminée. La tâche A prend environ 2 heures.
costrouc

Avez-vous trouvé une solution au problème? cela vous dérangerait-il de le poster peut-être?
Daniel Dubovski

3
Une ressource utile pour l'idée n ° 1: linkedin.com/pulse/…
Juan Riaza

1
Voici un article que j'ai écrit expliquant comment faire LinkedIn.com/pulse/dynamic-workflows-airflow-kyle-bridenstine
Kyle Bridenstine

Réponses:


30

Voici comment je l'ai fait avec une demande similaire sans aucun sous-marqueur:

Commencez par créer une méthode qui renvoie les valeurs souhaitées

def values_function():
     return values

Ensuite, créez la méthode qui générera les travaux de manière dynamique:

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)

Et puis combinez-les:

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

Où les valeurs sont-elles définies?
monksy

11
Au lieu de for i in values_function()j'attendrais quelque chose comme for i in push_func_output. Le problème est que je ne trouve pas de moyen d'obtenir cette sortie de manière dynamique. La sortie de PythonOperator sera dans le Xcom après l'exécution mais je ne sais pas si je peux le référencer à partir de la définition du DAG.
Ena le

@Ena Avez-vous trouvé un moyen d'y parvenir?
eldos

1
@eldos voir ma réponse ci
Ena

1
Et si nous devions effectuer une série d'étapes dépendantes de la boucle? Y aurait-il une deuxième chaîne de dépendances au sein de la groupfonction?
CodingInCircles

12

J'ai trouvé un moyen de créer des flux de travail basés sur le résultat des tâches précédentes.
Fondamentalement, ce que vous voulez faire est d'avoir deux sous-balises avec les éléments suivants:

  1. Xcom envoie une liste (ou ce dont vous avez besoin pour créer le flux de travail dynamique plus tard) dans le sous-dag qui est exécuté en premier (voir test1.py def return_list() )
  2. Passez l'objet principal dag en tant que paramètre à votre deuxième subdag
  3. Maintenant, si vous avez l'objet principal dag, vous pouvez l'utiliser pour obtenir une liste de ses instances de tâches. À partir de cette liste d'instances de tâches, vous pouvez filtrer une tâche de l'exécution actuelle en utilisant parent_dag.get_task_instances(settings.Session, start_date=parent_dag.get_active_runs()[-1])[-1]), on pourrait probablement ajouter plus de filtres ici.
  4. Avec cette instance de tâche, vous pouvez utiliser xcom pull pour obtenir la valeur dont vous avez besoin en spécifiant le dag_id à celui du premier sous-dag: dag_id='%s.%s' % (parent_dag_name, 'test1')
  5. Utilisez la liste / valeur pour créer vos tâches de manière dynamique

Maintenant, j'ai testé cela dans mon installation locale de flux d'air et cela fonctionne très bien. Je ne sais pas si la partie pull xcom aura des problèmes s'il y a plus d'une instance du dag en cours d'exécution en même temps, mais alors vous utiliseriez probablement une clé unique ou quelque chose comme ça pour identifier de manière unique le xcom valeur que vous voulez. On pourrait probablement optimiser l'étape 3. pour être sûr à 100% d'obtenir une tâche spécifique du dag principal actuel, mais pour mon utilisation, cela fonctionne assez bien, je pense qu'il suffit d'un seul objet task_instance pour utiliser xcom_pull.

De plus, je nettoie les xcom pour le premier sous-dag avant chaque exécution, juste pour m'assurer que je n'obtiens pas accidentellement une valeur erronée.

Je suis assez mauvais pour expliquer, donc j'espère que le code suivant clarifiera tout:

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

et le flux de travail principal:

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

sur Airflow 1.9, ceux-ci ne se sont pas chargés lorsqu'ils sont ajoutés au dossier DAG, quelque chose me manque?
Anthony Keane

@AnthonyKeane avez-vous mis test1.py et test2.py dans un dossier appelé subdags dans votre dossier dag?
Christopher Beck

J'ai fait oui. Copié les deux fichiers dans les sous-balises et placé le test.py dans le dossier dag, obtenez toujours cette erreur. DAG cassé: [/home/airflow/gcs/dags/test.py] Aucun module nommé subdags.test1 Remarque J'utilise Google Cloud Composer (Airflow 1.9.0 géré par Google)
Anthony Keane

@AnthonyKeane est-ce la seule erreur que vous voyez dans les journaux? Le DAG cassé peut être causé par une erreur de compilation du sous-dag.
Christopher Beck

3
Salut @Christopher Beck J'ai trouvé mon erreur que je devais ajouter _ _init_ _.pyau dossier subdags. rookie error
Anthony Keane

8

Oui, c'est possible, j'ai créé un exemple de DAG qui le démontre.

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)

Avant d'exécuter le DAG, créez ces trois variables de flux d'air

airflow variables --set DynamicWorkflow_Group1 1

airflow variables --set DynamicWorkflow_Group2 0

airflow variables --set DynamicWorkflow_Group3 0

Vous verrez que le DAG part de ça

entrez la description de l'image ici

À ceci après qu'il ait couru

entrez la description de l'image ici

Vous pouvez voir plus d'informations sur ce DAG dans mon article sur la création de flux de travail dynamiques sur Airflow .


1
Mais que se passe-t-il si vous avez plusieurs DagRun de ce DAG. Partagent-ils tous les mêmes variables?
Mar-k

1
Oui, ils utiliseraient la même variable; J'aborde cela dans mon article à la toute fin. Vous devrez créer dynamiquement la variable et utiliser l'ID d'exécution dag dans le nom de la variable. Mon exemple est simple juste pour démontrer la possibilité dynamique mais vous devrez en faire une production de qualité :)
Kyle Bridenstine

Les ponts sont-ils nécessaires lors de la création de tâches dynamiques? Je lirai votre article entièrement momentanément, mais je voulais demander. J'ai du mal à créer une tâche dynamique basée sur une tâche en amont en ce moment, et je commence à comprendre où je me suis trompé. Mon problème actuel est que, pour une raison quelconque, je ne parviens pas à synchroniser le DAG dans le DAG-Bag. Mon DAG s'est synchronisé lorsque j'utilisais une liste statique dans le module, mais s'est arrêté lorsque j'ai changé cette liste statique pour qu'elle soit créée à partir d'une tâche en amont.
lucid_goose le

6

OA: "Est-il possible dans Airflow de créer un flux de travail tel que le nombre de tâches B. * soit inconnu jusqu'à la fin de la tâche A?"

La réponse courte est non. Airflow créera le flux DAG avant de commencer à l'exécuter.

Cela dit, nous sommes arrivés à une conclusion simple, à savoir que nous n'avons pas un tel besoin. Lorsque vous souhaitez paralléliser certains travaux, vous devez évaluer les ressources dont vous disposez et non le nombre d'éléments à traiter.

Nous l'avons fait comme ceci: nous générons dynamiquement un nombre fixe de tâches, disons 10, qui diviseront le travail. Par exemple, si nous devons traiter 100 fichiers, chaque tâche en traitera 10. Je publierai le code plus tard aujourd'hui.

Mettre à jour

Voici le code, désolé pour le retard.

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

Explication du code:

Ici, nous avons une seule tâche de début et une seule tâche de fin (toutes deux factices).

Ensuite, à partir de la tâche de démarrage avec la boucle for, nous créons 10 tâches avec le même appel python. Les tâches sont créées dans la fonction create_dynamic_task.

À chaque appelable en python, nous passons en arguments le nombre total de tâches parallèles et l'index de la tâche en cours.

Supposons que vous ayez 1000 éléments à élaborer: la première tâche recevra en entrée qu'elle doit élaborer le premier morceau sur 10 morceaux. Il divisera les 1000 éléments en 10 morceaux et élaborera le premier.


1
C'est une bonne solution, tant que vous n'avez pas besoin d'une tâche spécifique par élément (comme la progression, le résultat, le succès / échec, les tentatives, etc.)
Alonzzo2

@Ena parallelTaskn'est pas définie: est-ce que je manque quelque chose?
Anthony Keane

2
@AnthonyKeane C'est la fonction python que vous devez appeler pour faire quelque chose. Comme indiqué dans le code, il faudra comme entrée le nombre total et le nombre actuel pour élaborer un morceau d'éléments totaux.
Ena

4

Ce que je pense que vous cherchez, c'est de créer DAG dynamiquement J'ai rencontré ce type de situation il y a quelques jours après quelques recherches, j'ai trouvé ce blog .

Génération de tâches dynamiques

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

Définition du flux de travail 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

Voici à quoi ressemble notre DAG après avoir assemblé le code entrez la description de l'image ici

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

C'était très utile, j'espère que cela aidera aussi quelqu'un d'autre


L'avez-vous réalisé par vous-même? Je suis fatiguée. Mais j'ai échoué.
Newt le

Oui, cela a fonctionné pour moi. À quel problème faites-vous face?
Muhammad Bin Ali

1
J? ai compris. Mon problème a été résolu. Merci. Je n'ai tout simplement pas trouvé la bonne façon de lire les variables d'environnement dans les images docker.
Newt le

Que faire si les éléments de la table peuvent changer, nous ne pouvons donc pas les mettre dans un fichier yaml statique?
FrankZhu

3

Je pense avoir trouvé une meilleure solution à cela sur https://github.com/mastak/airflow_multi_dagrun , qui utilise une simple mise en file d'attente de DagRuns en déclenchant plusieurs dagruns, similaires à TriggerDagRuns . La plupart des crédits vont à https://github.com/mastak , même si j'ai dû corriger certains détails pour que cela fonctionne avec le flux d'air le plus récent.

La solution utilise un opérateur personnalisé qui déclenche plusieurs 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()

Vous pouvez ensuite soumettre plusieurs dagruns à partir de la fonction appelable dans votre PythonOperator, par exemple:

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
)

J'ai créé un fork avec le code sur https://github.com/flinz/airflow_multi_dagrun


3

Le graphique des travaux n'est pas généré au moment de l'exécution. Le graphique est plutôt créé lorsqu'il est récupéré par Airflow à partir de votre dossier dags. Par conséquent, il ne sera pas vraiment possible d'avoir un graphique différent pour le travail à chaque fois qu'il s'exécute. Vous pouvez configurer un travail pour créer un graphique basé sur une requête au moment du chargement . Ce graphique restera le même pour chaque exécution après cela, ce qui n'est probablement pas très utile.

Vous pouvez concevoir un graphique qui exécute différentes tâches à chaque exécution en fonction des résultats de la requête à l'aide d'un opérateur de branche.

Ce que j'ai fait, c'est de préconfigurer un ensemble de tâches, puis de prendre les résultats de la requête et de les répartir entre les tâches. C'est probablement mieux de toute façon parce que si votre requête renvoie beaucoup de résultats, vous ne voudrez probablement pas inonder le planificateur avec beaucoup de tâches simultanées de toute façon. Pour être encore plus sûr, j'ai également utilisé un pool pour m'assurer que ma concurrence ne devienne pas incontrôlable avec une requête d'une taille inattendue.

"""
 - 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)


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

Notez qu'il semble qu'il soit possible de créer des sous-balises à la volée à la suite d'une tâche, cependant, la plupart de la documentation sur les sous-balises que j'ai trouvée recommande fortement de rester à l'écart de cette fonctionnalité car elle pose plus de problèmes qu'elle n'en résout dans la plupart des cas. J'ai vu des suggestions selon lesquelles les sous-balises pourraient être supprimées en tant que fonctionnalité intégrée prochainement.
pourri le

Notez également que dans la for tasks in tasksboucle de mon exemple, je supprime l'objet sur lequel je suis en train d'itérer. C'est une mauvaise idée. Au lieu de cela, obtenez une liste des clés et répétez-y - ou ignorez les suppressions. De même, si xcom_pull renvoie None (au lieu d'une liste ou d'une liste vide), la boucle for échoue également. On peut vouloir exécuter xcom_pull avant le 'for', puis vérifier s'il est None - ou s'assurer qu'il y a au moins une liste vide. YMMV. Bonne chance!
rotten le

1
qu'est-ce qu'il y a dans le open_order_task?
alltej

Vous avez raison, c'est une faute de frappe dans mon exemple. Ce devrait être get_orders_task.set_upstream (). Je le réparerai.
pourri

0

Vous ne comprenez pas quel est le problème?

Voici un exemple standard. Maintenant, si dans la fonction subdag remplacez for i in range(5):par for i in range(random.randint(0, 10)):alors tout fonctionnera. Imaginez maintenant que l'opérateur 'start' place les données dans un fichier, et au lieu d'une valeur aléatoire, la fonction lira ces données. Ensuite, l'opérateur «start» affectera le nombre de tâches.

Le problème ne sera que dans l'affichage dans l'interface utilisateur car lors de la saisie du sous-dag, le nombre de tâches sera égal à la dernière lecture du fichier / base de données / XCom pour le moment. Ce qui donne automatiquement une restriction sur plusieurs lancements d'un dag à la fois.


-1

J'ai trouvé ce post Medium qui est très similaire à cette question. Cependant, il est plein de fautes de frappe et ne fonctionne pas lorsque j'ai essayé de l'implémenter.

Ma réponse à ce qui précède est la suivante:

Si vous créez des tâches de manière dynamique, vous devez le faire en itérant sur quelque chose qui n'est pas créé par une tâche en amont ou qui peut être défini indépendamment de cette tâche. J'ai appris que vous ne pouvez pas passer des dates d'exécution ou d'autres variables de flux d'air à quelque chose en dehors d'un modèle (par exemple, une tâche) comme beaucoup d'autres l'ont souligné auparavant. Voir aussi cet article .


Si vous regardez mon commentaire, vous verrez qu'il est en fait possible de créer des tâches en fonction du résultat des tâches en amont.
Christopher Beck
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.