Сервис повторяет HTTP-запрос после тайм-аута. Какой контракт должен защитить операцию от двойного эффекта?
Операция должна поддерживать идемпотентный контракт повторения: одинаковый ключ идемпотентности для одной логической операции должен приводить к одному результату, даже если запрос фактически принят несколько раз. Сервер обязан связать ключ с результатом первой обработки и вернуть совместимый результат при повторе, а тест должен проверять отсутствие второго побочного эффекта.
Сетевой тайм-аут не показывает, выполнилась операция или нет: ответ мог потеряться уже после успешной записи на сервере. Поэтому распределённые клиенты используют повторные попытки, особенно для временных ошибок и разрывов соединения.
Без специального контракта повтор безопасен только для операций, которые сами по себе не меняют состояние повторно. Для создания платежа, заказа или резервирования этого обычно недостаточно: повтор может породить второй объект или повторно списать средства.
Клиент отправляет запрос на создание операции, но не получает ответ из-за тайм-аута. Он повторяет запрос, считая, что первая попытка могла не дойти, а сервер может уже выполнять или завершить её.
Если сервер обрабатывает повторы как новые независимые запросы, появляются дубли, двойные списания, повторные уведомления или рассинхронизация между системами. Проверка только HTTP-статуса не выявляет проблему: оба запроса могут вернуть успешный статус, хотя бизнес-эффект возник дважды.
Контракт обычно включает ключ идемпотентности, передаваемый клиентом для каждой логической операции. При повторной отправке после тайм-аута клиент использует тот же ключ, а для новой операции создаёт новый.
Сервер должен атомарно зарегистрировать ключ вместе с параметрами операции и состоянием обработки. Если такой ключ уже завершён, сервер возвращает сохранённый результат; если обработка ещё идёт, он не запускает вторую операцию, а сообщает о продолжающейся обработке или применяет другой явно описанный механизм ожидания.
Минимальный пример контракта:
Повтор этого запроса с тем же ключом должен вернуть результат первой операции, а не создать новый платёж. Повтор с тем же ключом, но с другим содержимым запроса следует отклонять: молчаливое переиспользование ключа скрывает ошибку клиента.
Интеграционная проверка должна подтвердить как минимум три свойства: повтор не создаёт второй бизнес-объект, повтор возвращает согласованный результат, а конфликт параметров для уже использованного ключа получает предсказуемую ошибку. Важно проверять состояние внешней системы или базы, а не только ответы HTTP.
У механизма есть ограничения. Хранилище ключей должно иметь подходящий срок хранения, иначе поздний повтор может быть принят за новую операцию; его очистка не должна происходить раньше периода возможных повторов. Ключ должен иметь корректную область уникальности, например на уровне клиента или платёжного счёта, иначе независимые операции могут ошибочно столкнуться.
Идемпотентность не означает, что запрос можно бесконечно повторять без условий. Нужно определить, какие ошибки допускают повтор, как обрабатывается незавершённая операция, что происходит после окончательного отказа и как согласуются побочные действия, например публикация события или отправка уведомления.
Платёжный сервис иногда возвращал тайм-аут после передачи запроса в банковский шлюз. Рассматривались два варианта: увеличить тайм-аут и отключить повторы либо добавить ключ идемпотентности на границе платёжного API.
Увеличение тайм-аута уменьшало число повторов, но не устраняло неопределённость при сетевом разрыве и ухудшало время реакции клиента. Отключение повторов снижало риск дублей, однако оставляло пользователя без надёжного способа узнать результат операции.
Выбрали идемпотентный контракт. Сервис сохранял ключ, нормализованный набор параметров и итог операции, а интеграционный тест имитировал потерю ответа после обработки первого запроса и затем отправлял повтор.
Тест проверял, что в платёжной системе существует ровно одна операция, повтор получает тот же идентификатор результата, а запрос с тем же ключом и другой суммой отклоняется. Это защищало не только HTTP-слой, но и фактический бизнес-эффект.
Нет. Одинаковый статус не доказывает отсутствие двойного эффекта: два независимых запроса могут оба вернуть успешный ответ. Проверка должна наблюдать внешний результат — количество созданных объектов, число списаний, состояние резервирования или опубликованные события.
Он не должен позволить двум обработчикам одновременно пройти проверку отсутствия ключа и создать две операции. Регистрация ключа и переход к обработке должны быть защищены атомарностью, уникальным ограничением или эквивалентным механизмом; иначе последовательный тест повторов может проходить, а гонка в производственной системе — нет.
Не обязательно. Идемпотентность должна распространяться на наблюдаемый бизнес-эффект либо иметь явно определённый контракт для побочных действий. Для событий применяют уникальный идентификатор операции, дедупликацию у потребителя или транзакционный механизм публикации, например запись события в надёжное хранилище с последующей доставкой; выбор зависит от требований к доставке и согласованности.