Идемпотентность API-интеграции: как не создать дубли при повторной отправке

Интеграция с внешним API редко работает в идеальном мире. Запрос может уйти, а ответ потеряться. Сервис может вернуть временную ошибку. Очередь может повторить задачу. Пользователь может нажать кнопку два раза. Если система к этому не готова, появляются дубли: две сделки, два заказа, два платежа, две заявки в CRM.

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

Плохой сценарий

Допустим, сайт отправляет заказ в CRM. Код делает запрос, CRM создаёт сделку, но ответ не доходит до сайта из-за таймаута. Сайт считает, что отправка не удалась, и повторяет запрос. CRM создаёт вторую сделку.

createDeal($order);

if ($responseFailed) {
    createDeal($order);
}

С точки зрения сайта это retry. С точки зрения CRM — две разные сделки. Так появляется дубль, который потом приходится чистить вручную.

Стабильный внешний идентификатор

У каждой отправляемой сущности должен быть внешний ключ. Например, order_id сайта, uuid заявки или составной ключ источника.

external_id = site_order_12345

Перед созданием записи во внешней системе нужно проверить, нет ли уже сущности с таким external_id. Если есть — обновить или вернуть найденную запись.

Idempotency key

Некоторые API поддерживают специальный idempotency key. Тогда один и тот же ключ при повторном запросе не создаёт новую операцию.

Idempotency-Key: order-12345-payment-1

Если API такого механизма не даёт, его нужно реализовать на своей стороне: хранить статус отправки и ID внешней сущности.

Таблица связей

Для интеграций полезно хранить отдельную таблицу соответствий.

local_type: order
local_id: 12345
external_service: crm
external_id: 987
status: success
last_error: null

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

Статусы отправки

Нельзя хранить только “отправлено” или “не отправлено”. Лучше иметь несколько состояний:

  • new — нужно отправить;
  • processing — сейчас отправляется;
  • success — успешно отправлено;
  • failed — ошибка после всех попыток;
  • retry — можно повторить позже.

Состояние processing должно иметь timeout. Если процесс умер во время отправки, такая запись не должна зависнуть навсегда.

Уникальные индексы

Защита должна быть не только в PHP-коде. В базе нужен уникальный индекс на ключ, который запрещает дубль.

CREATE UNIQUE INDEX ux_external_map
ON external_map (local_type, local_id, external_service);

Если два процесса одновременно попытаются создать связь, база остановит дубль.

Повторные попытки

Retry нужен только для временных ошибок: timeout, 502, 503, лимит запросов. Если API вернул ошибку валидации, повторять тот же запрос бессмысленно.

if ($errorCode === 'timeout' || $httpCode >= 500) {
    scheduleRetry($jobId);
}

if ($httpCode === 400) {
    markAsFailed($jobId, 'Validation error');
}

Количество попыток должно быть ограничено. Иначе плохая заявка будет бесконечно грузить очередь.

Логи интеграции

В логе должны быть локальный ID, внешний сервис, метод, номер попытки, HTTP-код, внешний ID и короткая ошибка.

[
    'order_id' => 12345,
    'service' => 'crm',
    'method' => 'deal.create',
    'attempt' => 2,
    'http_code' => 504,
    'external_id' => null,
]

Не нужно писать в лог токены, пароли и полные персональные данные.

Чек-лист

  1. Добавить стабильный external_id для каждой сущности.
  2. Хранить связь локальной и внешней записи.
  3. Проверять связь перед созданием новой сущности.
  4. Добавить уникальный индекс на связь.
  5. Разделять временные и постоянные ошибки.
  6. Ограничить количество retry.
  7. Логировать попытки и результат отправки.
  8. Проверить сценарий “запрос ушёл, ответ потерялся”.

Идемпотентность кажется лишней только до первого массового дубля. Если интеграция отправляет заказы, оплаты, лиды или остатки, повтор запроса должен быть штатным сценарием, а не аварией.

Комментарии (0)

Пока нет комментариев. Будьте первым!