taskiq¶
hare.contrib.taskiq запускает hare в воркерах taskiq: HareContext
воркера, команды каждой задачи с меткой задачи, арендатор задачи, транзакция на задачу с повтором после
конфликта и задачи, которые отправляются только после фиксации запросившей их транзакции. Установите
дополнение:
Подключение к брокеру¶
from taskiq_redis import ListQueueBroker
from hare.contrib.taskiq import HareTaskiq
broker = ListQueueBroker("redis://localhost:6379")
hare_taskiq = HareTaskiq(broker, HARE_ORM, atomic_tasks=True, transaction_retries=3)
@broker.task
async def send_due_date_reminder(loan_id: int) -> None:
loan = await Loan.objects.get(id=loan_id)
...
class HareTaskiq:
def __init__(
self,
broker: AsyncBroker,
config: Mapping[str, Any] | HareConfig | str,
*,
atomic_tasks: bool | Sequence[str] = False,
tenant_label: str | None = "tenant",
transaction_retries: int = 0,
) -> None: ...
HareTaskiq у брокера:
- открывает
HareContextпоconfig(какHare.init(config=...)), когда воркер запускается (WORKER_STARTUP), и закрывает его подключения, когда воркер останавливается (WORKER_SHUTDOWN); контекст — общий запасной, его видит каждая задача воркера; - добавляет брокеру
HareTaskiqMiddleware(hare_taskiq.middleware) — создавайтеHareTaskiqв модуле, из которого и приложение, и воркер берут брокер: отправляющей стороне middleware тоже нужен, чтобы записывать арендатора в отправляемые задачи.
Приложение, которое отправляет задачи, сохраняет свой HareContext (Hare.init(), плагин
веб-фреймворка); HareTaskiq открывает только контекст воркера.
Что получает каждая задача¶
HareTaskiqMiddleware(*, atomic_tasks=False, tenant_label="tenant", transaction_retries=0):
- Метки запросов — каждая команда задачи несёт
task_nameиtask_idв завершающем комментарии (метки в комментариях SQL), рядом с уже активными метками. - Арендатор — при отправке задачи middleware записывает активную тогда область арендатора
(
Tenancy.scope(...)) в меткуtenant_label: значение арендатора — строку, целое число или UUID (отправляется текстом) — как есть,Tenancy.any_of(...)— как{"any_of": [...]},Tenancy.ALL— как{"all": true}, область по моделям — как{"models": {"app.Model": ...}}. Значение другого типа или область, заданная для абстрактной модели, даютUnSupportedErrorпри отправке — задача никогда не выполняется без области отправителя. Метка, заданная вручную (task.kicker().with_labels(tenant=...)), сохраняется. Воркер выполняет задачу внутри области, которую называет метка.tenant_label=Noneарендаторов не трогает. - Транзакция — с
atomic_tasks=Trueзадача выполняется в транзакции на подключении по умолчанию (имена подключений — по транзакции на каждом); транзакция фиксируется, когда задача возвращает результат, и откатывается, когда задача падает. Неудачная фиксация роняет задачу. ОбработчикиTransactions.on_commit()выполняются после фиксации —TaskiqDelivery.kiq_on_commit()из задачи будит relay именно тогда. - Повтор после конфликта — задача, транзакция которой получила
TransactionRetryError(сбой сериализации, взаимная блокировка — в задаче или при фиксации), откатывается и отправляется заново под своим же id, не большеtransaction_retriesраз; меткаhare_transaction_retriesсчитает повторы. Результат неудачной попытки не сохраняется, поэтомуwait_result()возвращает удачную попытку — или последний сбой. Вместе сSimpleRetryMiddlewareиз taskiq не включайтеTransactionRetryErrorв его исключения, иначе конфликт повторится дважды.
Всё, что middleware настроил, отменяется, когда задача заканчивается, — метки, область арендатора, выполняемая задача. Синхронная задача выполняется в потоке, где асинхронный API hare недоступен.
Имя подключения из atomic_tasks, которого нет в конфиге, отклоняется при запуске воркера.
Отправка задач после фиксации¶
Задача, отправленная изнутри транзакции, может выполниться до её фиксации — и не увидеть её записей — или
выполниться для транзакции, которая потом откатится. TaskiqDelivery отправляет её через
транзакционный outbox — только после фиксации:
from hare.contrib.outbox import OutboxEvent
from hare.contrib.taskiq import TaskiqDelivery
class TaskOutboxEvent(OutboxEvent):
class Meta(OutboxEvent.Meta):
table = "task_outbox"
tasks = TaskiqDelivery(broker, model=TaskOutboxEvent)
async with tasks.get_relay(poll_interval_seconds=1.0):
async with Transactions.atomic():
loan = await Loan.objects.create(book=book, reader=reader)
await tasks.kiq_on_commit(send_due_date_reminder, loan.id)
# send_due_date_reminder отправляется здесь, сразу после фиксации.
TaskiqDelivery(broker, *, model, topic="taskiq", using=None, wakeup=None) — доставка outbox,
отправляющая задачи, и отправитель, который их записывает; model — класс модели outbox или её метка
"app.Model", которая ищется в текущем контексте, когда модель впервые понадобится.
await kiq_on_commit(task, *args, labels=None, **kwargs) принимает задачу или её имя, её аргументы —
значения JSON — и метки и записывает задачу событием outbox с темой topic (OutboxEvent.enqueue()) — в
текущей транзакции, поэтому откат не оставляет ни события, ни задачи; вне транзакции событие фиксируется
сразу. HareTaskiqMiddleware брокера записывает активную в этот момент область арендатора в метку
арендатора события, поэтому задача выполняется в арендаторе отправителя, а не relay. Возвращает событие.
Задачи отправляет relay из get_relay(**relay_options) — только события темы доставки, поэтому в таблице
могут быть и другие темы — relay остальных событий таблицы получает
exclude_topics=[delivery.topic]. Будильник доставки — по умолчанию свой InProcessWakeup — запускает relay
сразу после фиксации, поэтому запускайте relay в процессе, который отправляет задачи; чтобы отправлять
сразу из другого процесса, передайте будильник брокера (RedisWakeup и т. п.). Задачу, которую брокер не
принял (был недоступен), relay повторяет, как любое событие outbox.
Доставка — как минимум один раз: задача может прийти дважды, поэтому делайте задачи безопасными для
повтора. using задаёт подключение, на котором пишутся задачи, — по умолчанию подключение модели. Пустой
topic даёт ConfigurationError.
Тестирование¶
InMemoryBroker(await_inplace=True) из taskiq выполняет каждую задачу при отправке, в отправляющей
задаче, — с добавленным HareTaskiqMiddleware тесты видят те же метки, арендаторов и транзакции, что и
воркеры:
broker = InMemoryBroker(await_inplace=True)
broker.add_middlewares(HareTaskiqMiddleware(atomic_tasks=True))
HareTaskiq, запущенный в тесте под hare_app_db pytest-плагина hare, берёт контекст теста вместо баз
своей конфигурации — записи задач откатываются вместе с записями теста; см.
Тесты приложения.