Spec-Zone.ru › Django 6.0

Фреймворк задач Django

Добавлено в Django 6.0.

Веб-приложение не всегда ограничивается преобразованием HTTP-запросов в HTTP-ответы. Для некоторых функций может быть полезно выполнять код за пределами цикла обработки запросов и ответов.

В этом случае на помощь приходят фоновые задачи.

Фоновые задачи позволяют выполнять работу за пределами цикла обработки запросов и ответов, в другом месте и, возможно, позднее. Это ускоряет обработку запросов, снижает задержки и улучшает пользовательский опыт. Например, пользователю не должно приходиться ждать отправки письма, прежде чем страница завершит загрузку.

Новый фреймворк задач Django упрощает определение и постановку таких задач в очередь. Он не предоставляет механизм для запуска задач рабочими процессами. Фактическое выполнение должно обрабатываться инфраструктурой за пределами Django, например отдельным процессом или службой.

Основы фоновых задач

Когда работу необходимо выполнить в фоновом режиме, Django создает Task, который хранится в хранилище очереди. Этот Task содержит все метаданные, необходимые для выполнения задачи, а также уникальный идентификатор, с помощью которого Django позднее сможет получить результат.

Рабочий процесс проверяет хранилище очереди на наличие новых задач. Когда добавляется новая задача, рабочий процесс захватывает ее, выполняет и сохраняет статус и результат обратно в хранилище очереди. Эти рабочие процессы выполняются за пределами жизненного цикла обработки запросов и ответов.

Настройка бэкенда задач

Бэкенд задач определяет, как и где хранятся задачи для выполнения и каким образом они выполняются. Разные бэкенды задач имеют разные характеристики и параметры настройки, которые могут влиять на производительность и надежность приложения. Django поставляется с несколькими встроенными бэкендами. Django не предоставляет универсального способа выполнения задач, а только постановки их в очередь.

Бэкенды задач настраиваются с помощью параметра TASKS в файле настроек. Хотя большинству приложений нужен только один бэкенд, поддерживается использование нескольких.

Немедленное выполнение

Это бэкенд по умолчанию, если в файле настроек не указан другой. ImmediateBackend выполняет поставленные в очередь задачи немедленно, а не в фоновом режиме. Это позволяет постепенно добавлять в приложение функциональность фоновых задач до появления необходимой инфраструктуры.

Чтобы использовать его, задайте для BACKEND значение "django.tasks.backends.immediate.ImmediateBackend":

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

ImmediateBackend также может быть полезен в тестах, позволяя обойти необходимость запуска настоящего фонового рабочего процесса.

Пустой бэкенд

DummyBackend вообще не выполняет поставленные в очередь задачи, а сохраняет результаты для последующего использования. Результаты задач навсегда остаются в состоянии READY.

Этот бэкенд не предназначен для использования в production-среде — он предоставлен для удобства при разработке и тестировании.

Чтобы использовать его, задайте для BACKEND значение "django.tasks.backends.dummy.DummyBackend":

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

Результаты поставленных в очередь задач можно получить из атрибута results бэкенда:

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

Сохраненные результаты можно удалить с помощью метода clear():

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

Использование пользовательского бэкенда

Хотя Django изначально поддерживает несколько бэкендов задач, иногда может понадобиться настроить бэкенд самостоятельно. Чтобы использовать внешний бэкенд задач с Django, укажите путь импорта Python в качестве значения BACKEND параметра TASKS, например:

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

Бэкенд задач — это класс, наследующий BaseTaskBackend. Как минимум, он должен реализовать BaseTaskBackend.enqueue(). Если вы создаете собственный бэкенд, в качестве примеров реализации можно использовать встроенные бэкенды задач. Их код находится в каталоге django/tasks/backends/ исходного кода Django.

Поддержка асинхронного выполнения

Поддержка асинхронных бэкендов задач в Django находится в разработке.

BaseTaskBackend содержит асинхронные варианты всех базовых методов. По соглашению, названия асинхронных версий всех методов имеют префикс a. Аргументы обеих версий одинаковы.

Получение бэкендов

Бэкенды можно получить с помощью обработчика подключений task_backends:

from django.tasks import task_backends

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

Бэкенд «default» доступен как default_task_backend:

from django.tasks import default_task_backend

Определение задач

Задачи определяются с помощью декоратора django.tasks.task(), примененного к функции уровня модуля:

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
    )

Декоратор возвращает экземпляр Task.

Атрибуты Task можно настроить с помощью аргументов декоратора @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
    )

По соглашению, задачи определяются в файле tasks.py, однако это не является обязательным.

Контекст задачи

Иногда выполняющейся Task может понадобиться узнать контекст ее постановки в очередь и выполнения. Его можно получить, передав аргумент context — экземпляр TaskContext.

Чтобы получить контекст задачи в качестве аргумента функции задачи, передайте takes_context при ее определении:

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
    )

Изменение задач

Перед постановкой задач в очередь может потребоваться изменить некоторые параметры задачи. Например, задать ей более высокий приоритет, чем обычно.

Нельзя изменить экземпляр Task напрямую. Вместо этого можно создать измененный экземпляр с помощью метода using(), оставив исходный без изменений. Например:

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

Постановка задач в очередь

Чтобы добавить задачу в хранилище очереди для последующего выполнения, вызовите для нее метод enqueue(). Если задача принимает аргументы, их можно передать как есть. Например:

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

Метод возвращает TaskResult, который можно использовать для получения результата задачи после завершения ее выполнения.

Для постановки задач в контексте async доступен aenqueue() — вариант метода enqueue() для async.

Поскольку аргументы задач и возвращаемые значения сериализуются в JSON, они должны поддерживать сериализацию в JSON:

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

Аргументы также должны проходить цикл json.dumps()/ json.loads() без изменения типа. Например, рассмотрим такую задачу:

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

Если для ImmediateBackend задан бэкенд по умолчанию:

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

Задача double_dictionary завершается с ошибкой, поскольку после преобразования JSON кортеж (1, 2, 3) становится списком [1, 2, 3], который нельзя использовать в качестве ключа словаря.

Как правило, сложные объекты, например экземпляры моделей, или встроенные типы, такие как datetime и tuple, нельзя использовать в задачах без дополнительного преобразования.

Транзакции

Для большинства бэкендов задачи выполняются в отдельном процессе с использованием другого подключения к базе данных. При использовании транзакции, если не дождаться ее фиксации, рабочие процессы могут начать выполнять задачу, использующую объекты, которые пока недоступны.

Рассмотрим упрощенный пример:

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

Чтобы предотвратить ситуацию, при которой my_task выполняется до фиксации Thing в базе данных, используйте transaction.on_commit(), связав все аргументы enqueue() с помощью 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))

Результаты задач

При постановке Task в очередь вы получаете TaskResult, однако часто бывает полезно получить результат откуда-то еще (например, из другого запроса или другой задачи).

У каждого TaskResult есть уникальный id, который можно использовать для идентификации и получения результата после завершения кода, поставившего задачу в очередь.

Метод get_result() позволяет получить результат по его id:

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

Чтобы получить TaskResult независимо от того, из какого Task он был получен, используйте метод get_result() бэкенда:

from django.tasks import default_task_backend

result = default_task_backend.get_result(result_id)

Для получения результатов в контексте async доступен aget_result() — вариант метода get_result() для async, доступный как в бэкенде, так и в Task.

Некоторые бэкенды, например встроенный ImmediateBackend, не поддерживают get_result(). Вызов get_result() для таких бэкендов приведет к возникновению исключения NotImplementedError.

Обновление результатов

TaskResult содержит статус выполнения задачи на момент его получения. Если задача завершится после вызова get_result(), статус не обновится.

Чтобы обновить значения, вызовите метод django.tasks.TaskResult.refresh():

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

Возвращаемые значения

Если функция задачи что-либо возвращает, это значение можно получить из атрибута django.tasks.TaskResult.return_value:

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

Если выполнение задачи не завершено или завершилось с ошибкой, возникает исключение ValueError.

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

Ошибки

Если задача завершается неудачно и вызывает исключение — в самой задаче или при ее выполнении — исключение и трассировка сохраняются в списке django.tasks.TaskResult.errors.

Каждая запись в errors представляет собой TaskError с информацией об ошибке, возникшей во время выполнения:

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

Обратите внимание, что здесь указана только разновидность исключения, без каких-либо других значений. Информация трассировки сокращена до строки, которая может помочь при отладке:

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

© Django Software Foundation and individual contributors
Licensed under the BSD License.
https://docs.djangoproject.com/en/6.0/topics/tasks/

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API