Как гарантировать атомарность выполнения задачи и отправки уведомления пользователю в системе с очередями
Что проверяют
Понимаете ли вы, что транзакция базы и отправка сообщения в брокер или пользователю относятся к разным системам и общей атомарности между ними нет. Интервьюер ждёт слов «проблема двойной записи», «outbox» и «идемпотентность». Следом обычно спрашивают, что будет, если воркер упадёт между завершением задачи и отправкой уведомления.
Как отвечать
Атомарно в одной транзакции записывается только то, что лежит в одной базе. Уведомление (письмо, пуш, сообщение в мессенджер) это внешний побочный эффект, откатить его нельзя. Поэтому цель не «ровно один раз», а «хотя бы один раз плюс защита от дублей». Каркас:
- transactional outbox: результат задачи и запись «нужно уведомить» пишутся одной транзакцией в две таблицы. Отдельный процесс читает
outbox, публикует в брокер и помечает отправленное. Событие не потеряется: закоммичено либо всё, либо ничего; - публикация после коммита: если outbox избыточен, в Django есть
transaction.on_commit(lambda: notify.delay(task_id)), в SQLAlchemy событие сессииafter_commit. Но падение между коммитом и публикацией теряет событие; - подтверждение после выполнения: в Celery по умолчанию сообщение подтверждается до запуска задачи.
acks_late=Trueпереносит подтверждение на после выполнения, упавший воркер задачу не теряет, зато она может выполниться повторно; - идемпотентный отправитель: у уведомления есть ключ, например
(task_id, тип), и уникальный индекс в таблице отправленных. Повторная доставка упирается в ограничение и ничего не шлёт.
Окно между отметкой об отправке и приёмом письма провайдером остаётся всегда, поэтому выбирайте осознанно: редкий дубль или редкая потеря.
Где спотыкаются
Пишут, что BEGIN и COMMIT гарантируют атомарность задачи и уведомления. Транзакция базы не покрывает ни RabbitMQ, ни SMTP, ни API пуш-сервиса.
Ставят задачу в очередь внутри открытой транзакции. Воркер берёт её раньше коммита и не находит данных, а при откате пользователь получает уведомление о том, чего не произошло.
Обещают exactly-once. Даже у Kafka эта гарантия действует только внутри цепочки «прочитал из топика, записал в топик», а до внешнего получателя доходит at-least-once.