Переезд с Celery¶
Как перенести приложение с Celery на akadze. Брокер — Postgres приложения, поэтому переезд — это akadze migrate при деплое, перенос объявлений задач и мест вызова и замена celery beat расписаниями в самом воркере.
1. Объявление задач¶
Задачи Celery — синхронные функции, привязанные к глобальному приложению. Задачи akadze обычно async def; синхронная функция тоже подходит и выполняется в потоке. Аргументы должны сериализоваться в JSON: они проверяются моделью по сигнатуре и хранятся в akadze.jobs.args.
# Celery
@shared_task(bind=True, max_retries=5, queue="mail", time_limit=60)
def send_mail(self, user_id: int) -> None: ...
# akadze
@app.task("mail.send", queue="mail", max_attempts=6, timeout=timedelta(seconds=60))
async def send_mail(user_id: int) -> None: ...
max_attempts — бюджет попыток, включая первый запуск, поэтому max_retries=5 в Celery — это max_attempts=6. По умолчанию 3: задаче Celery без ретраев, которую нельзя выполнить дважды (списание, SMS), нужен max_attempts=1. Ошибка, Retry, таймаут и потеря воркера (по проверке heartbeat другим воркером) тратят попытку; Snooze и штатная остановка — нет.
timeout — не жёсткий time_limit. Когда он срабатывает, запуск считается ошибкой и уходит в ретрай с backoff, а не в failed. Синхронная задача после таймаута продолжает работать в своём потоке, поэтому ретрай может пойти параллельно, а поток больше не учитывается в slots (повторные таймауты могут забить пул потоков по умолчанию); дайте синхронным задачам собственный дедлайн. shared_task нет: модули объявляют register(app), приложение вызывает include, или фабрика регистрирует обычные функции через app.register (см. Задачи и приложение).
2. Постановка в очередь¶
| Celery | akadze |
|---|---|
send_mail.delay(7) |
await send_mail.using(session=connection).enqueue(user_id=7) |
apply_async(countdown=30) |
using(..., delay=timedelta(seconds=30)) |
apply_async(eta=dt) |
using(..., run_at=dt) (с часовым поясом) |
apply_async(expires=dt) |
using(..., expires_at=dt) |
apply_async(priority=…) |
using(..., priority=…) — больший забирается раньше, диапазон smallint; Celery на Redis считает 0 наивысшим, так что значения нужно инвертировать |
apply_async(task_id=…) для дедупликации |
using(..., unique_key=…) → DuplicateJob, пока задание активно |
transaction.on_commit(lambda: task.delay()) |
не нужно: enqueue выполняется в транзакции вызывающего кода |
group(task.s(x) for x in xs).apply_async() (fan-out без результата) |
await task.using(session=connection).enqueue_many({"x": x} for x in xs) |
Аргументы enqueue(...) передаются только по имени. Параметры постановки никогда не попадают в аргументы задачи.
3. Ретраи и управление выполнением¶
| Celery | akadze |
|---|---|
raise self.retry(countdown=10) |
raise Retry(timedelta(seconds=10)) — тратит попытку |
autoretry_for=(Exception,), retry_backoff, retry_jitter |
по умолчанию любое исключение уходит в ретрай с экспоненциальным backoff и джиттером до max_attempts; своя кривая — @app.task(..., backoff=fn) |
| ретрай без траты бюджета (опрос) | raise Snooze(timedelta(...)) — попытку не тратит |
raise Reject(requeue=False) / сдаться |
raise Fail("reason") |
revoke(task_id) |
request_cancel(session, job_id) или akadze jobs cancel JOB_ID; внутри задачи raise Cancel() |
acks_late + visibility timeout |
аренда с heartbeat; другой воркер возвращает задания потерянного в очередь и тратит попытку (на последней задание failed) |
request_cancel не прерывает выполняющуюся задачу. Задание, для которого отмену запросили или expires_at прошёл до запуска, отменяется без запуска. Во время запуска решает тело: если оно вернуло значение, задание succeeded с результатом (побочные эффекты уже случились); если бросило что угодно, включая Retry и Snooze, или вернуло не-JSON, задание cancelled, попытка не тратится. После коммита блока complete_tx задание succeeded, что бы тело ни делало дальше.
Доставка at least once: задание может выполниться снова, если пропал его воркер, поэтому задачи должны быть идемпотентными, как и с acks_late. Закладывайте max_attempts и на падения.
4. Цепочки и результаты¶
Canvas (chain, group, chord), result backend и .get() нет. Чтобы следующий шаг выполнился только при успехе этого, поставьте его на соединении complete_tx. Исход пишите в свои таблицы в той же транзакции, а не читайте его потом из очереди.
from akadze import current
@app.task("report.build")
async def build(report_id: int) -> None:
...
async with current().complete_tx() as connection:
await connection.execute(...) # отметить отчёт готовым
await notify.using(session=connection).enqueue(report_id=report_id)
5. Лимиты и маршрутизация¶
| Celery | akadze |
|---|---|
rate_limit="10/m" |
set_limit(session, "vendor", max_starts=10, window=timedelta(minutes=1)) + using(..., limit_key="vendor") — на весь кластер |
| отдельный пул воркеров под вендора | set_limit(session, "vendor", max_running=N) + using(..., limit_key="vendor") |
-Q mail / маршрутизация |
@app.task(..., queue="mail") и akadze worker app:app --queue mail |
--concurrency=8 |
akadze worker app:app --slots 8 — слоты на каждую очередь: с тремя --queue воркер выполняет до 24 заданий |
| остановить очередь везде | akadze queues pause mail / resume — на весь кластер, а не на один воркер |
celery control cancel_consumer на одном хосте |
остановить этот воркер (Ctrl-C / SIGTERM): он не берёт новые задания и даёт выполняющимся завершиться |
6. Beat¶
celery beat и его расписание переезжают в воркер: @app.periodic("name", cron="0 * * * *") (с timezone="Europe/Berlin" вместо beat_cron_timezone / CELERY_TIMEZONE) или every=timedelta(...), либо app.add_schedules([...]) из конфига приложения. Расписание выполняет каждый воркер; наступивший слот ставится в очередь один раз. Отдельного процесса beat нет (akadze beat — заглушка).
7. Эксплуатация¶
Вместо экранов Flower — CLI и запросы: akadze jobs list, akadze workers list, akadze limits list, akadze jobs requeue JOB_ID и akadze metrics для sidecar Prometheus. Сигналы: task_prerun → app.hooks.before_run; task_success → app.hooks.after_run, который вызывается, только когда задача вернула значение, и внутри её таймаута (ошибка в нём тратит попытку, и при ретрае тело выполняется заново); task_postrun, task_failure и task_retry → app.hooks.on_transition, который видит каждую смену состояния в транзакции, где она происходит.
Чего нет и почему¶
- Брокеры Redis/RabbitMQ — брокер Postgres, чтобы постановка шла в транзакции приложения.
- Canvas и
.get()— chord держит слоты воркера, пока ждёт; следующий шаг черезcomplete_txзакрывает цепочки без этого. - Result backend — результатам место в таблицах приложения;
resultв строке задания необязателен. - HTTP / веб-интерфейс — его может построить другой пакет на запросах выше.