Письма, отчёты, обработка файлов, вызовы медленных API: всё, что не должно держать HTTP-запрос, уходит в очередь задач. В Python-мире это чаще всего Celery. На собесе проверяют не синтаксис @shared_task, а понимание гарантий: что будет при падении воркера, повторе задачи, перезапуске брокера.
Как устроено
веб-процесс --(сообщение)--> брокер (RabbitMQ, Redis) --> воркеры Celery
\--> backend результатов (опционально)
@shared_task(bind=True, autoretry_for=(httpx.TransportError,),
retry_backoff=True, retry_backoff_max=600, retry_jitter=True, max_retries=5)
def send_receipt(self, order_id: int) -> None:
order = Order.objects.get(pk=order_id)
mailer.send(order.email, render_receipt(order))
send_receipt.delay(order.id) # поставить в очередь
send_receipt.apply_async(args=[order.id], countdown=60, queue='mail')
- Брокер хранит и доставляет сообщения. RabbitMQ даёт подтверждения и маршрутизацию, Redis проще в эксплуатации.
- Воркер забирает сообщения и выполняет задачи. Параллелизм задаёт
--concurrencyи пул (preforkпроцессы по умолчанию,threads,gevent,solo). - Result backend хранит результаты, если они нужны. Не нужны:
ignore_result=True. - Beat это планировщик периодических задач (аналог cron). Запускается одним экземпляром, иначе задачи задвоятся.
Что передавать в задачу
Только идентификаторы и простые данные: send_receipt.delay(order.id), а не объект модели. Объект сериализуется на момент постановки, к выполнению он устареет, а сериализатор json его вообще не примет.
Ставить задачу, которая читает только что созданные данные, нужно после коммита: transaction.on_commit(lambda: send_receipt.delay(order.id)). Иначе воркер может прийти раньше коммита и не найти заказ.
Гарантии и подтверждения
По умолчанию Celery подтверждает (ack) сообщение брокеру до выполнения задачи. Если воркер упадёт посередине, задача потеряна: это семантика «не более одного раза».
acks_late=True подтверждает после выполнения: при падении воркера сообщение вернётся в очередь и выполнится снова. Это «как минимум один раз», и задача обязана быть идемпотентной. Вместе с ним часто включают task_reject_on_worker_lost=True и worker_prefetch_multiplier=1 для длинных задач.
У Redis-брокера есть visibility_timeout (по умолчанию час): если задача не подтверждена за это время, её отдадут другому воркеру. Задачи с eta или countdown дальше этого таймаута начнут выполняться повторно.
Идемпотентность
Повторное выполнение не должно давать повторный эффект:
- проверять состояние перед действием (
if order.receipt_sent: return); - уникальные ключи и
INSERT ... ON CONFLICT DO NOTHING; - ключ идемпотентности во внешних API (платёжные провайдеры его поддерживают);
- атомарный захват:
UPDATE ... SET status='processing' WHERE id=... AND status='new'и проверка числа обновлённых строк.
Ретраи
Повторяют только временные ошибки (таймаут, 503, обрыв соединения), с экспоненциальной задержкой и джиттером, с ограничением попыток. Ошибки данных (404, валидация) ретраить бессмысленно. После исчерпания попыток задачу логируют, алертят и откладывают для ручного разбора.
Альтернативы и паттерны
- RQ, Dramatiq, arq, Taskiq: проще или асинхронные.
- Kafka для потоков событий, где важны порядок внутри партиции и повторное чтение.
- Transactional outbox: событие пишется в таблицу в той же транзакции, что и данные, отдельный процесс публикует его в брокер. Решает проблему «данные сохранили, а сообщение потеряли».
Частые ошибки
- Передача ORM-объектов в задачу.
- Постановка задачи до коммита транзакции.
acks_lateбез идемпотентности: двойные письма и списания.- Ретраи всех исключений подряд без ограничения.
- Несколько экземпляров beat.
- Долгие задачи без
time_limit, зависшие навсегда.
Что спросят на собесе
- Как устроен Celery: брокер, воркер, backend, beat.
- Что передавать в задачу и почему не объект модели.
- Что такое
acks_late, at-most-once и at-least-once. - Как сделать задачу идемпотентной.
- Как настроить ретраи и что ретраить нельзя.