Кейс: очередь уведомлений разослала одно письмо семь раз подряд
Пользователи начали жаловаться на дублирующиеся письма о смене пароля — не два-три, а до семи одинаковых писем за минуту.
Очередь была на RabbitMQ, consumer подтверждал сообщение вручную через basicAck после успешной отправки письма. Отправка письма шла через внешний SMTP-провайдер с таймаутом в 30 секунд. Prefetch count consumer'а стоял на значении по умолчанию — не единица, а больше, так что несколько сообщений уходили воркеру одновременно.
При всплеске нагрузки провайдер начал отвечать медленнее обычного. Consumer не успевал обработать батч сообщений в отведённый heartbeat timeout соединения с брокером. RabbitMQ считал consumer мёртвым, разрывал соединение и возвращал все неподтверждённые сообщения обратно в очередь — включая те, письма по которым уже реально ушли, просто ack не успел долететь до брокера.
Новый consumer забирал те же сообщения и честно слал письма заново. Идемпотентности на уровне бизнес-логики не было: отправка письма не проверяла, было ли уже отправлено сообщение с этим requestId.
Починили двумя слоями. Первый — снизили prefetch до значения, при котором consumer гарантированно укладывается в heartbeat даже при деградации внешнего провайдера. Второй, более важный — добавили idempotency key на основе requestId с TTL в Redis: перед отправкой письма consumer проверяет, было ли уже отправлено сообщение с этим ключом, и если да — просто подтверждает получение без повторной отправки.
At-least-once delivery в очереди — это гарантия брокера, а не гарантия бизнес-логики. Идемпотентность на стороне consumer нужна независимо от того, как аккуратно настроен prefetch.
Тренажёр: 600 вопросов, мок с таймером, план повторов
senior·base — что спрашивают на самом деле
В этом посте были ссылки, но мы их удалили по правилам Сетки