Задачи и приложение¶
Приложение Akadze хранит engine, зарегистрированные задачи и расписания. Воркер загружает его по пути импорта (модуль:атрибут).
from datetime import timedelta
from akadze import Akadze
app = Akadze(database_url="postgresql://akadze:akadze@localhost:5432/akadze")
| Параметр | По умолчанию | Смысл |
|---|---|---|
database_url или engine |
— | Нужен один из двух. С database_url приложение само создаёт async engine и владеет им; передайте свой engine, чтобы делить пул |
pool_size |
5, как в SQLAlchemy | Размер пула engine, который создаётся из database_url, плюс 10 overflow; с engine не передаётся. См. Пул соединений |
grace |
1 минута | Насколько поздно периодический запуск ещё можно поставить в очередь |
retention |
7 дней | Завершённые задания и запуски расписаний старше этого воркер удаляет |
Engine, который создаёт приложение, отключает кэш prepared statements в asyncpg, поэтому перед базой может стоять пул в transaction-режиме, например PgBouncer.
Объявить задачу¶
@app.task("mail.send", queue="mail", max_attempts=6, timeout=timedelta(seconds=60), priority=0)
async def send_mail(user_id: int) -> None:
...
| Параметр | По умолчанию | Смысл |
|---|---|---|
queue |
"default" |
Воркер берёт задания из очередей, с которыми запущен |
max_attempts |
3 | Бюджет попыток, включая первый запуск |
timeout |
нет | Запуск дольше этого считается ошибкой и уходит в ретрай |
priority |
0 | Больший забирается раньше; можно переопределить для задания |
backoff |
экспонента с джиттером | Функция attempt -> timedelta, см. Ретраи |
Аргументы проверяются моделью, построенной по сигнатуре, и хранятся как JSON. С 0.6.0 значения возвращаются со своими типами: вложенные pydantic-модели, dataclass, datetime, UUID и Decimal приходят в задачу именно этими типами.
Синхронная функция тоже подходит: она выполняется в потоке.
Синхронные задачи и timeout
Таймаут не останавливает поток. Запуск считается ошибкой, ретрай может начаться, пока старый поток ещё работает, и этот поток больше не учитывается в slots. При регистрации синхронной задачи с timeout в логгер akadze.app пишется предупреждение. Дайте синхронной задаче собственный дедлайн.
Разнести задачи по модулям¶
Каждый модуль объявляет register(app) и регистрирует задачи там. Приложение подключает модули по имени: без обхода пакета и без импорта ради побочных эффектов.
# tasks/hello.py
def register(app: Akadze) -> None:
@app.task("hello")
async def hello(name: str) -> str:
return f"hello {name}"
# app.py
app = Akadze(database_url="postgresql://akadze:akadze@localhost:5432/akadze")
app.include("tasks.hello")
Передать зависимости¶
Если зависимости создаются раньше приложения, объявите обычную функцию и зарегистрируйте замыкание позже. app.register(fn, *, name, ...) принимает те же параметры, что @app.task, и возвращает Task.
# tasks/hello.py: без app и без декоратора
async def hello(name: str, *, runner: Runner) -> str:
return await runner.greet(name)
def create_app(deps: Deps) -> Akadze:
app = Akadze(database_url="postgresql://akadze:akadze@localhost:5432/akadze")
app.include("tasks.ops")
async def _hello(name: str) -> str:
return await hello(name, runner=deps.runner)
app.register(_hello, name="hello")
return app
Глобального приложения и shared_task нет.
Хуки¶
app.hooks — четыре списка функций. Каждая может быть синхронной или async.
| Хук | Аргументы | Когда |
|---|---|---|
on_enqueue |
(connection, job) |
в транзакции постановки, по разу на задание |
before_run |
(job) |
перед телом задачи |
after_run |
(job) |
после возврата из тела, внутри его таймаута; ошибка здесь считается неудачным запуском: попытка тратится, и при ретрае тело выполняется заново |
on_transition |
(connection, job, from_state, to_state) |
в транзакции, меняющей состояние |
async def count(connection, job, from_state, to_state):
outcomes.labels(job.task, to_state).inc()
app.hooks.on_transition.append(count)
Не пишите аргументы и результаты задач в логи из хуков.