# Documentation Django 6.0.7

# L’infrastructure des tâches dans Django

New in Django 6.0:

Pour des versions plus anciennes de Django, le rétroportage django-tasks est disponible.

Pour une application web, il y a souvent du code à exécuter en plus du processus de transformation des requêtes HTTP en réponses HTTP. Pour certaines fonctionnalités, il peut être avantageux d’exécuter du code en dehors du cycle de requête-réponse.

C’est là qu’entrent en jeu les tâches d’arrière-plan.

Les tâches d’arrière-plan peuvent décharger le cycle de requête-réponse pour exécuter du code en dehors de ce cycle, potentiellement à une date ultérieure. Cela permet de garder les requêtes rapides, de réduire la latence et d’amélioration le ressenti des utilisateurs. Par exemple, une utilisatrice ne devrait pas avoir besoin d’attendre qu’un courriel soit envoyé avant que sa page termine de charger.

L’infrastructure des tâches de Django facilite la définition et la mise en file d’attente de ce genre de travail. Mais elle ne fournit pas de mécanisme d’exécution de ces tâches. L’exécution réelle doit être gérée par une infrastructure extérieure à Django, telle qu’un processus ou service séparé. À ce titre, un moteur de tâches capable de les exécuter sur ce service externe doit être choisi et configuré.

## Notions de base sur les tâches d’arrière-plan

Lorsque du code doit être exécuté en arrière-plan, Django crée une tâche `Task`, qui est stockée dans une file d’attente. Cette tâche contient toutes les métadonnées utiles à son exécution, de même qu’un identifiant unique pour que Django puisse plus tard obtenir son résultat.

Un processus de travail ira piocher de nouvelles tâches à exécuter dans la file d’attente. Lorsqu’une nouvelle tâches est ajoutée, le processus réserve la tâche, l’exécute, et enregistre son statut et son résultat dans la file d’attente. Ces processus s’exécutent en dehors du cycle de requête-réponse.

## Configuration d’un moteur de tâches

Le moteur de tâches détermine comment et où les tâches sont stockées en vue de leur exécution et comment elles sont exécutées. Différents moteurs de tâches possèdent différentes caractéristiques et options de configuration, ce qui peut influer sur la performance et la fiabilité de votre application. Django est livré avec des moteurs intégrés, mais ceux-ci sont uniquement pour du développement ou des tests.

Django gère la définition des tâches, leur validation, mise en file d’attente, et gestion de leurs résultats, pas leur exécution, donc les configurations de production ont besoin d’un moteur ou d’un processus de lancement qui exécute réellement ce qui se trouve en attente. Les options possibles sont énumérées sur la page Community Ecosystem.

Les moteurs de tâches sont configurés par le réglage `TASKS` de votre fichier de réglages. Même si la majorité des applications n’auront besoin que d’un seul moteur, il est possible d’en avoir plusieurs.

### Exécution immédiate

Il s’agit du moteur par défaut si aucun autre n’a été indiqué dans le fichier des réglages. Ce moteur `ImmediateBackend` exécute immédiatement les tâches en file d’attente, au lieu de le faire en arrière-plan. Cela permet notamment d’ajouter progressivement la fonctionnalité des tâches à une application, avant que l’infrastructure nécessaire ne soit mise en place.

Pour l’utiliser, définissez `BACKEND` à `"django.tasks.backends.immediate.ImmediateBackend"`:

    TASKS = {"default": {"BACKEND": "django.tasks.backends.immediate.ImmediateBackend"}}

Le moteur `ImmediateBackend` peut aussi être utile pour les tests, afin d’éviter d’avoir à exécuter un processus d’exécution externe au lancement des tests.

### Le moteur bidon

Le moteur `DummyBackend` n’exécute pas du tout les tâches en liste d’attente, elle ne fait que stocker leur résultat en vue d’une utilisation ultérieure. Ces résultats de tâches restent toujours dans l’état `READY` (prête).

Ce moteur n’est pas conçu pour être utilisé en production, il n’est fourni que par commodité et destiné à être utilisé durant le développement et les tests.

Pour l’utiliser, définissez `BACKEND` à `"django.tasks.backends.dummy.DummyBackend"`:

    TASKS = {"default": {"BACKEND": "django.tasks.backends.dummy.DummyBackend"}}

Les résultats des tâches en file d’attente peuvent être obtenus à partir de l’attribut `results`:

    >>> from django.tasks import default_task_backend
    >>> my_task.enqueue()
    >>> len(default_task_backend.results)
    1

Les résultats stockés peuvent être effacés en utilisant la méthode `clear()`:

    >>> default_task_backend.clear()
    >>> len(default_task_backend.results)
    0

### Moteurs de tierce partie

As mentioned at the beginning of this section, Django includes backends suitable for development and testing only. Production systems should rely on backends that supply a worker process and a durable queue implementation. Available third-party backends are listed on the Community Ecosystem page and the Tasks framework grid from Django Packages.

Pour utiliser un moteur de tâches externe avec Django, indiquez son chemin d’importation Python dans la clé setting:BACKEND <TASKS-BACKEND> du réglage setting:TASKS, comme ceci :

    TASKS = {
        "default": {
            "BACKEND": "path.to.backend",
        }
    }

Un moteur de tâches est une classe héritant de `BaseTaskBackend`. Au minimum, elle doit implémenter `BaseTaskBackend.enqueue()`. Si vous construisez votre propre moteur, vous pouvez utiliser les moteurs de tâches intégrés comme référence d’implémentation. Vous trouverez le code dans le répertoire django/tasks/backends/ du code source de Django.

### Gestion du code asynchrone

La prise en charge des moteurs de tâches asynchrones est en développement.

`BaseTaskBackend` possède des variantes asnychrones de toutes les méthodes de base. Par convention, les versions asynchrones de toutes les méthodes sont préfixées par `a`. Les arguments des deux variantes sont identiques.

### Obtention des moteurs

Les moteurs peuvent être obtenus en utilisant le gestionnaire de connexion `task_backends`:

    from django.tasks import task_backends

    task_backends["default"]  # The default backend
    task_backends["reserve"]  # Another backend

Le moteur par défaut est disponible en tant que `default_task_backend`:

    from django.tasks import default_task_backend

## Définition des tâches

Les tâches sont définies en utilisant le décorateur `django.tasks.task()` sur une fonction au niveau d’un module :

    from django.core.mail import send_mail
    from django.tasks import task

    @task
    def email_users(emails, subject, message):
        return send_mail(
            subject=subject, message=message, from_email=None, recipient_list=emails
        )

La valeur renvoyée par le décorateur est une instance `Task`.

Les attributs de `Task` peuvent être personnalisés via les paramètres du décorateur `@task`:

    from django.core.mail import send_mail
    from django.tasks import task

    @task(priority=2, queue_name="emails")
    def email_users(emails, subject, message):
        return send_mail(
            subject=subject, message=message, from_email=None, recipient_list=emails
        )

Par convention, les tâches sont définies dans un fichier `tasks.py`, mais ceci n’est pas obligatoire.

### Contexte des tâches

Parfois, la tâche à exécuter peut avoir besoin de connaître le contexte dans lequel elle a été mise en file d’attente, et comment elle est exécutée. Elle peut pour cela accepter un argument `context` qui est une instance de `TaskContext`.

Pour recevoir le contexte de la tâche comme argument d’une fonction de tâche, passez `takes_context` au moment de sa définition :

    import logging
    from django.core.mail import send_mail
    from django.tasks import task

    logger = logging.getLogger(__name__)

    @task(takes_context=True)
    def email_users(context, emails, subject, message):
        logger.debug(
            f"Attempt {context.attempt} to send user email. Task result id: {context.task_result.id}."
        )
        return send_mail(
            subject=subject, message=message, from_email=None, recipient_list=emails
        )

### Modification des tâches

Avant de mettre une tâche en file d’attente, il peut être nécessaire de modifier certains paramètres de la tâche. Par exemple, pour lui donner une priorité plus haute que la normale.

Une instance `Task` ne peut pas être modifiée directement. Mais on peut par contre créer une instance modifiée avec la méthode `using()`, l’originale restant intacte. Par exemple :

    >>> email_users.priority
    0
    >>> email_users.using(priority=10).priority
    10

## Mise en file d’attente des tâches

Pour ajouter une tâche dans le stockage de la file d’attente en vue de son exécution, appelez la méthode `enqueue()` sur elle. Si la tâche accepte des arguments, ils peuvent être transmis tels quels. Par exemple :

    result = email_users.enqueue(
        emails=["user@example.com"],
        subject="You have a message",
        message="Hello there!",
    )

Cela renvoie un objet `TaskResult`, qui peut être utilisé ensuite pour récupérer le résultat de la tâche après qu’elle a terminé son exécution.

Pour mettre en file d’attente des tâches dans un contexte asynchrone, `aenqueue()` est disponible comme variante `async` de `enqueue()`.

Comme les arguments et les valeurs de renvoi des tâches sont toutes sérialisées en JSON, elles doivent pouvoir être sérialisées en JSON :

    >>> process_data.enqueue(datetime.now())
    Traceback (most recent call last):
    ...
    TypeError: Object of type datetime is not JSON serializable

Les arguments doivent aussi être capable de faire un cycle aller-retour par `json.dumps()`/ `json.loads()` sans changer de type. Par exemple, considérez cette tâche :

    @task()
    def double_dictionary(key):
        return {key: key * 2}

Si le moteur `ImmediateBackend` a été configuré comme moteur par défaut :

    >>> result = double_dictionary.enqueue((1, 2, 3))
    >>> result.status
    FAILED
    >>> result.errors[0].traceback
    Traceback (most recent call last):
    ...
    TypeError: unhashable type: 'list'

La tâche `double_dictionary` échoue parce qu’après l’aller-retour JSON, le tuple `(1, 2, 3)` devient la liste `[1, 2, 3]`, qui ne peut pas être utilisée comme clé de dictionnaire.

En général, les objets complexes tels que des instances de modèles ou des types Python comme `datetime` ou `tuple` ne peuvent pas être utilisés dans le système des tâches sans une conversion spécifique.

### Transactions

Pour la plupart des moteurs, les tâches sont exécutées dans un processus séparé, utilisant une connexion de base de données différente. Lorsqu’on utilise une transaction, sans attendre que celle-ci soit commitée, des processus d’exécution pourraient démarrer le traitement d’une tâche utilisant des objets auxquels elle ne peut pas encore accéder.

Par exemple, considérons cet exemple simplifié :

    @task
    def my_task(thing_num):
        Thing.objects.get(num=thing_num)

    with transaction.atomic():
        Thing.objects.create(num=1)
        my_task.enqueue(thing_num=1)

Pour éviter le scénario où `my_task` s’exécute avant que `Thing` ne soit commité dans la base de données, utilisez `transaction.on_commit()`, en liant tous les arguments à destination de `enqueue()` via `functools.partial()`:

    from functools import partial

    from django.db import transaction

    with transaction.atomic():
        Thing.objects.create(num=1)
        transaction.on_commit(partial(my_task.enqueue, thing_num=1))

## Résultats des tâches

Lors de la mise en file d’attente d’une tâche `Task`, vous obtenez une instance `TaskResult`. Cette instance permet ensuite de récupérer le résultat de la tâche depuis un autre endroit (par exemple une autre requête ou une autre tâche).

Chaque `TaskResult` possède un `id` unique, qui peut être utilisé ensuite pour récupérer le résultat une fois que le code qui a mis la tâche en file d’attente ait terminé son travail.

La méthode `get_result()` peut récupérer un résultat sur la base de son `id`:

    # Later, somewhere else...
    result = email_users.get_result(result_id)

Pour obtenir un résultat `TaskResult`, quel que soit le type de tâche dont il provient, utilisez la méthode `get_result()` sur le moteur :

    from django.tasks import default_task_backend

    result = default_task_backend.get_result(result_id)

Pour récupérer des résultats dans un contexte asynchrone, `aget_result()` est disponible comme variante `async` de `get_result()`, aussi bien pour le moteur que pour une tâche `Task`.

Certains moteurs, tels que le moteur intégré `ImmediateBackend`, ne prennent pas en charge `get_result()`. Si on appelle cette méthode avec ces moteurs, une erreur `NotImplementedError` sera générée.

### Mise à jour des résultats

Un objet `TaskResult` contient l’état d’exécution d’une tâche au moment où il est obtenu. Si la tâche se termine après que `get_result()` est appelée, l’objet ne se met pas à jour tout seul.

Pour actualiser les valeurs, appelez la méthode `django.tasks.TaskResult.refresh()`:

    >>> result.status
    RUNNING
    >>> result.refresh()  # or await result.arefresh()
    >>> result.status
    SUCCESSFUL

### Valeurs de renvoi

Si votre fonction de tâche renvoie quelque chose, on peut le récupérer à partir de l’attribut `django.tasks.TaskResult.return_value`:

    >>> result.status
    SUCCESSFUL
    >>> result.return_value
    42

Si la tâche n’a pas terminé son exécution, ou qu’elle a échoué, une exception `ValueError` est générée.

    >>> result.status
    RUNNING
    >>> result.return_value
    Traceback (most recent call last):
    ...
    ValueError: Task has not finished yet

### Erreurs

Si la tâche échoue et produit une exception, soit dans le contexte de la tâche ou lors de son exécution, l’exception et la trace d’erreur sont enregistrés dans la liste `django.tasks.TaskResult.errors`.

Chaque élément de `errors` est un objet `TaskError` contenant des informations au sujet de l’erreur produite durant l’exécution :

    >>> result.errors[0].exception_class
    <class 'ValueError'>

Notez qu’il ne s’agit que du type de l’exception et ne contient pas d’autres valeurs. Les informations de trace d’erreur sont réduites à une chaîne qui peut être utile en vue du débogage :

    >>> result.errors[0].traceback
    Traceback (most recent call last):
    ...
    TypeError: Object of type datetime is not JSON serializable