ТестированиеТестирование API и интеграцийИнженер по интеграционному тестированию

Сервис потребитель получил одно событие дважды. Какой контракт события позволит отличить повторную доставку...

Сервис-потребитель получил одно событие дважды. Какой контракт события позволит отличить повторную доставку от двух разных событий?

Проходите собеседования с ИИ помощником Hintsage

Краткий ответ

Контракт должен требовать стабильный уникальный идентификатор события: при повторной доставке того же события его event_id не меняется, а разные бизнес-события получают разные идентификаторы. Интеграционный тест должен отправить два сообщения с одинаковым event_id и проверить, что потребитель распознаёт второе как повтор, а не как новую бизнес-операцию.

Сам идентификатор не устраняет дублирование. Он лишь даёт потребителю надёжный критерий идентичности события; реакция на повторную доставку также должна быть определена контрактом.

Исторический контекст

В распределённых системах доставка сообщений часто строится по модели at-least-once: брокер или отправитель повторяет доставку, если не получил подтверждение. Это снижает риск потери события, но допускает повторное получение после сбоя между выполнением обработки и подтверждением.

Поэтому контракт событий должен описывать не только структуру полезной нагрузки, но и её идентичность. Без стабильного идентификатора потребитель вынужден угадывать, являются ли два похожих сообщения повтором или самостоятельными событиями.

Постановка проблемы

Предположим, событие ПлатёжЗавершён повторно доставлено из-за сетевого сбоя. Если потребитель ориентируется только на содержимое или на время создания, он может дважды начислить бонус, отправить два уведомления или повторно изменить состояние заказа.

Если же потребитель ошибочно сочтёт два разных события одним, legitimate повторная операция будет пропущена. Например, два отдельных платежа могут иметь одинаковую сумму и один идентификатор клиента, но должны обрабатываться независимо.

Подробное решение

В контракте следует зафиксировать следующие свойства:

  • event_id уникален в пределах оговорённой области уникальности;
  • повторная доставка того же события сохраняет тот же event_id;
  • новое бизнес-событие получает новый event_id;
  • идентификатор не зависит от порядка доставки и не вычисляется из случайно изменяемых полей;
  • поведение потребителя при повторе явно определено: повтор игнорируется, безопасно возвращается прежний результат или выполняется другая идемпотентная реакция.

Минимальная форма сообщения может выглядеть так:

{ "event_id": "8f1c2a7e-6d3b-4c91-a2f0-51b7d9e204aa", "event_type": "PaymentCompleted", "occurred_at": "2025-03-08T10:15:00Z", "payload": { "payment_id": "pay-42", "amount": 1000 } }

Интеграционная проверка должна доставить один и тот же экземпляр события повторно, не меняя event_id, и убедиться, что побочный эффект возникает не более одного раза. Отдельно нужно проверить два разных события с разными идентификаторами, даже если их полезная нагрузка совпадает.

Дедупликация обычно требует сохранения обработанных идентификаторов в хранилище. Запись идентификатора и бизнес-эффект должны быть согласованы: если эффект уже выполнен, но отметка о событии не сохранена, повтор снова вызовет эффект; если отметка сохранена первой, а обработка прервалась, событие может быть ошибочно пропущено.

На практике это решают транзакцией, охватывающей таблицу обработанных событий и бизнес-изменение, либо специальным inbox-паттерном. Если побочный эффект выполняется во внешней системе, одной локальной транзакции недостаточно: понадобится идемпотентный контракт внешней операции или механизм повторной сверки.

Срок хранения идентификаторов — компромисс. Короткий срок уменьшает объём хранилища, но допускает повторную обработку старого события после удаления записи. Бессрочное хранение надёжнее с точки зрения дедупликации, однако требует политики очистки, архивирования и определения допустимого окна повторной доставки.

Ситуация из практики

Платёжный сервис публиковал событие о завершённой оплате. При временной недоступности брокера отправитель не получал подтверждение и повторял сообщение. Потребитель начислял бонусы при каждом получении, поэтому часть клиентов получила их дважды.

Рассматривались три варианта. Сравнение полного содержимого события оказалось ненадёжным: поля могли меняться при сериализации, а одинаковые платежи не всегда означали один бизнес-факт. Дедупликация только в памяти была простой, но теряла данные при перезапуске экземпляра. Использование стабильного event_id с уникальной записью обработанных событий требовало изменения хранилища, зато работало после рестартов и не зависело от формата остальных полей.

Выбрали третий вариант. Тесты проверили повторную доставку с тем же идентификатором, два самостоятельных события с разными идентификаторами и восстановление после сбоя между бизнес-записью и подтверждением сообщения. В результате повторная доставка стала безопасной, а разные платежи перестали ошибочно объединяться.

Что кандидаты часто упускают

  1. Достаточно ли времени создания события для выявления повтора?

    Нет. Время может совпадать у разных событий, иметь разную точность или измениться при преобразовании сообщения. Оно полезно для диагностики и упорядочивания, но не заменяет стабильную идентичность события.

  2. Должен ли повторно опубликованный экземпляр события получать новый идентификатор?

    Если это повторная доставка того же бизнес-события, идентификатор должен сохраниться. Новый идентификатор означает для потребителя новое событие и может привести к повторному эффекту. Если же создаётся новый бизнес-факт, даже похожий на предыдущий, ему нужен новый идентификатор; это различие должно быть определено правилами домена.

  3. Гарантирует ли уникальный event_id, что побочный эффект выполнится один раз?

    Нет. Идентификатор позволяет обнаружить повтор, но не делает обработку атомарной. Потребитель должен согласовать запись о выполненном событии с бизнес-изменением, а для внешнего побочного эффекта — использовать идемпотентный ключ, повторяемую операцию или механизм компенсации. Иначе сбой между этими шагами всё ещё создаст либо повторный эффект, либо потерю обработки.