Повторы и идемпотентность callback
Один callback может прийти повторно. Это нормальная часть надежной доставки, а не ошибка Системы расчета купонов SportAPI.
Например, партнер мог успешно сохранить пакет, но ответ HTTP 200 потерялся в сети. Система не знает, был ли запрос обработан, поэтому отправляет тот же пакет еще раз.
Обработчик должен быть идемпотентным: повтор одного пакета не должен повторно изменять купоны, ставки или баланс.
Два вида повторного появления купона
Важно различать:
| Ситуация | coupon_code | batchId | Что делать |
|---|---|---|---|
| Повторная доставка того же HTTP-пакета | Тот же | Тот же | Не выполнять бизнес-операции повторно, вернуть HTTP 200. |
| Новое расчетное состояние купона | Тот же | Новый | Обновить купон и ставки как новую версию. |
Один coupon_code может законно приходить несколько раз с разными batchId, особенно для экспресса.
Назначение batchId
batchId идентифицирует конкретную версию пакета callback.
Гарантируется следующее:
- повторная HTTP-доставка одного пакета сохраняет тот же
batchId; - новое расчетное состояние получает новый
batchId; - один пакет может содержать до 100 купонов;
batchIdне является ID купона или ставки.
Создайте уникальный индекс по batchId в таблице входящих callback.
Пример:
CREATE TABLE coupon_callback_batches (
batch_id VARCHAR(64) PRIMARY KEY,
received_at TIMESTAMP NOT NULL,
coupon_count INTEGER NOT NULL
);
Это только пример структуры. Названия таблиц и типы данных партнер выбирает самостоятельно.
Почему недостаточно coupon_code
Нельзя использовать coupon_code как уникальный ID доставки:
coupon_code = 000000000272
batchId = version_A
coupon_code = 000000000272
batchId = version_B
Обе версии относятся к одному купону, но содержат разные состояния ставок. Если отклонить вторую версию как дубликат по coupon_code, локальная запись не будет обновлена.
Используйте:
batchId— для дедупликации HTTP-пакетов;coupon_code— для поиска и обновления купона;uuid— для обновления конкретной ставки внутри купона.
Транзакционная обработка
Регистрацию batchId, обновление купонов и финансовые операции нужно выполнять в одной транзакции.
Рекомендуемый алгоритм:
- проверить HMAC-подпись;
- разобрать JSON;
- проверить
couponCountи массивcoupons; - начать транзакцию;
- попытаться вставить
batchIdс уникальным ограничением; - если
batchIdуже существует, не повторять бизнес-операции; - обработать все элементы
coupons; - выполнить upsert купонов по
coupon_code; - выполнить upsert ставок по сочетанию
coupon_code + uuid; - выполнить необходимые финансовые операции;
- зафиксировать транзакцию;
- вернуть HTTP
200.
Упрощенная схема:
BEGIN
INSERT batchId
если уже существует:
COMMIT
вернуть 200
FOR EACH coupon:
UPSERT coupon BY coupon_code
FOR EACH event:
UPSERT event BY coupon_code + uuid
APPLY financial changes idempotently
COMMIT
вернуть 200
Не фиксируйте batchId отдельной транзакцией до сохранения купонов. Если приложение завершится после записи batchId, но до обновления купонов, повторная доставка будет ошибочно считаться полностью обработанной.
Атомарная обработка пакета
В пакете может быть несколько купонов. HTTP 200 означает, что партнер успешно зафиксировал весь пакет.
Если хотя бы один элемент не удалось сохранить:
- откатите транзакцию;
- не возвращайте HTTP
200; - верните временный серверный код, при котором предусмотрен повтор.
Если архитектура не позволяет обновить все системы одной транзакцией, сначала атомарно сохраните входящий пакет во внутреннюю inbox-очередь партнера, а бизнес-обработку выполняйте из нее идемпотентно.
Идемпотентность финансовых операций
Уникальность batchId защищает от повторной доставки пакета, но финансовым операциям нужна дополнительная защита на уровне купона.
Причина: один купон может получить несколько разных batchId, например промежуточный и финальный снимки.
Партнер должен отдельно фиксировать:
- был ли уже обработан текущий финальный результат;
- было ли уже выполнено повторное списание при переходе в статус
15; - было ли начислено новое
realWinпосле перерасчета.
Одна из возможных схем — хранить номер локального цикла расчета купона:
coupon_code = 000000000272
settlement_generation = 0
При первом переходе в статус 15 номер увеличивается:
settlement_generation = 1
Для финансовых операций можно сформировать уникальные ключи:
000000000272:1:recalculation_debit
000000000272:1:final_credit
Это защищает баланс не только от повторного batchId, но и от повторной обработки одинакового состояния в разных участках системы.
Конкретную схему партнер выбирает самостоятельно. Главное правило: один расчетный переход должен создавать не более одной финансовой операции каждого типа.
Когда выполняются повторы
Повторная доставка выполняется при:
- timeout;
- ошибке соединения или другой транспортной ошибке;
- HTTP
500; - HTTP
502; - HTTP
503; - HTTP
504.
Эти ситуации считаются временными.
Расписание попыток
Первая отправка выполняется сразу. После неудачи используются интервалы:
1-я попытка: сразу
2-я попытка: через 1 минуту
3-я попытка: через 5 минут
4-я попытка: через 15 минут
5-я попытка: через 1 час
Всего выполняется не более пяти HTTP-отправок с учетом первой.
После исчерпания попыток автоматическая доставка прекращается. Для восстановления и сверки используйте методы чтения купонов, описанные в разделе «Резервный polling».
Таймаут
Полный HTTP-запрос callback ожидается не более 10 секунд.
Обработчик партнера должен успеть:
- проверить подпись;
- атомарно сохранить пакет;
- вернуть HTTP
200.
Долгие дополнительные операции рекомендуется выполнять после надежного сохранения пакета во внутреннюю очередь партнера.
Если endpoint обработал пакет, но не успел вернуть ответ, запрос может прийти повторно с тем же batchId.
Поведение для HTTP-ответов
| Ответ или ситуация | Поведение доставки |
|---|---|
HTTP 200, пустое тело | Успешно. |
HTTP 200, success: true, processed = couponCount | Успешно. |
HTTP 200, success: false | Финальная ошибка без автоматического повтора. |
HTTP 200, processed < couponCount | Финальная ошибка без автоматического повтора. |
HTTP 401 | Неверная или отсутствующая подпись; финальная ошибка. |
HTTP 403 | Запрос запрещен IP-фильтром; финальная ошибка. |
HTTP 500, 502, 503, 504 | Временная ошибка; выполняются повторы. |
| Timeout или транспортная ошибка | Выполняются повторы. |
| Любой другой HTTP-код | Финальная ошибка без автоматического повтора. |
Текущий контракт требует именно HTTP 200. Ответы 201, 202 и 204 не считаются успешными и автоматически не повторяются.
Как отвечать при временной ошибке
Не возвращайте HTTP 200 с телом:
{
"success": false
}
Такой ответ считается финальным и не запускает повтор.
Если пакет временно невозможно сохранить, верните предусмотренный временный HTTP-код, например:
HTTP/1.1 503 Service Unavailable
После восстановления endpoint система повторит доставку по расписанию.
Успешное подтверждение
Минимальный ответ:
HTTP/1.1 200 OK
Тело может быть пустым.
Рекомендуемый расширенный ответ:
{
"success": true,
"processed": 3
}
processed должен быть равен полученному couponCount.
При повторном batchId, который уже полностью обработан, также верните HTTP 200. Повторять сохранение и финансовые операции не нужно.
Типичные ошибки интеграции
- Уникальность установлена только по
coupon_code. batchIdсохраняется до бизнес-данных в отдельной транзакции.- HTTP
200возвращается до фиксации транзакции. - При повторном
batchIdснова выполняется начисление. - Каждый новый
batchIdавтоматически считается новым финансовым результатом. - Обрабатывается только
coupons[0], а остальные элементы пакета пропускаются. - Для временной ошибки возвращается
success: falseс HTTP200. - Endpoint возвращает
204, хотя контракт требует200. - Долгая бизнес-обработка превышает таймаут и вызывает повтор.
Контрольный список
batchIdимеет уникальное ограничение.- Повторный
batchIdвозвращает HTTP200без повторной обработки. - Один
coupon_codeможет иметь несколько версий. - Весь массив
couponsобрабатывается атомарно. - Купоны обновляются по
coupon_code. - Ставки обновляются по
coupon_code + uuid. - Финансовые операции имеют собственные уникальные ключи.
- HTTP
200возвращается только после надежного сохранения. - Временная ошибка возвращает
500,502,503или504. success: falseне используется для запроса автоматического повтора.
Следующий раздел: «Резервный polling».