Команда настроила ретраи для исходящих email: любая ошибка от SMTP-шлюза — повтор через 5 секунд, до 5 попыток. Через месяц под нагрузкой провайдер начал иногда отвечать таймаутом на пике, не отказом — письмо реально уходило, просто ответ приходил с опозданием.

Ретрай видел таймаут как ошибку и слал письмо повторно. Клиенты получали дубликаты транзакционных писем: подтверждение заказа дважды, сброс пароля дважды. Баг не воспроизводился на стейджинге, потому что там провайдер отвечал быстро и таймаутов не было вовсе.

Проблема не в самом ретрае — он нужен. Проблема в том, что таймаут SMTP означает «неизвестно, отправлено или нет», а не «точно не отправлено». Решение: перед повтором проверять идемпотентный ключ письма (связка order_id + тип письма) в своей БД, а не слепо доверять ответу внешнего сервиса.

String idempotencyKey = orderId + ":" + EmailType.ORDER_CONFIRMED;

if (sentEmailRepository.existsByKey(idempotencyKey)) { return; // уже отправлено, повтор не нужен }

try { smtpClient.send(email); sentEmailRepository.markSent(idempotencyKey); } catch (SmtpTimeoutException e) { scheduleRetry(email, idempotencyKey); }

Следом спросят: а если запись в sentEmailRepository и реальная отправка разъехались — например, упали между send() и markSent()? Тогда нужен либо outbox с отдельным воркером, либо проверка статуса доставки у самого провайдера перед повтором, если он такую проверку предоставляет. Чистого решения без внешнего подтверждения доставки не существует — только снижение вероятности дубля.

Таймаут внешнего сервиса — это неизвестность, а не сигнал для безусловного повтора.

Тренажёр: 900+ вопросов и задач, мок с таймером, план повторов

senior·base — что спрашивают на самом деле


В этом посте были ссылки, но мы их удалили по правилам Сетки