Фреймворк задач Django
Веб-приложение не всегда ограничивается преобразованием 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/