Перейти к содержимому

ГайдыИнтеграции и автоматизация5 мин чтения

Повторы без дублей: приёмочные тесты интеграции

Локально выполненная матрица тестов: повторная доставка, неверный порядок, таймаут и восстановление. Привожу полученные результаты и ограничения.

Опубликовано
Проверено
Повторяющиеся карточки-квитанции рядом с одной ячейкой для подшивки, стрелка возврата и песочные часы.

Daniil MaximkinРазработка продуктов и интеграций

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

Обработку повторов я принимаю, проверяя бизнес-результат после дубля, более старого обновления, таймаута и перезапуска. Повторная доставка не должна повторно выполнять действие, а старое обновление не должно перезаписывать более новое состояние. Эти проверки я прогнал на синтетическом локальном HTTP-приёмнике на Node 22. Результаты ниже относятся к этому приёмнику, ограничения я указал.

Даниил

Главное

  • Проверяйте бизнес-эффект, а не только HTTP-ответ. Таймаут может случиться уже после того, как работа зафиксирована.
  • Ключ доставки отвечает за повторную доставку. Бизнес-ключ отвечает за повторные попытки одной и той же бизнес-операции.
  • Обнаружение дублей не решает проблему обновлений в неверном порядке: приёмнику нужно явное правило для состояния.
  • После перезапуска должны сохраниться и результат выполненной работы, и запись о дубле.
  • Успешный локальный тест подтверждает поведение только этого стенда. Внешние записи и гонки между несколькими воркерами требуют своих тестов.
В этом гайде

Я хочу, чтобы тест повторов отвечал на конкретный вопрос: сколько записей или действий существует в целевой системе после того, как отправитель повторил попытку?

Один лишь успешный HTTP-ответ на него не отвечает. Не отвечает и лог «дубль проигнорирован», если исходная работа так и не завершилась. Этот гайд превращает раздел об обработке ошибок из брифа на интеграцию в конкретные приёмочные проверки.

Чего приёмнику ждать от Shopify?

Shopify документирует повторную доставку, события в неверном порядке и ограниченное окно повторов. Я принимаю это как исходные условия для плана приёмки и не исхожу из того, что доставка происходит один раз и по порядку.

При доставке по HTTPS Shopify задаёт таймаут соединения в одну секунду и общий таймаут запроса в пять секунд. Отсутствие ответа или ошибка запускают восемь повторов в течение четырёх часов. Подписка, настроенная через Admin API, удаляется после восьми неудач подряд. Источник: verify deliveries.

В той же документации для дедупликации доставок указан заголовок X-Shopify-Webhook-Id. Отдельные подписки на один и тот же топик могут иметь разные ID доставки, но общий X-Shopify-Event-Id, который относится к одному действию продавца. Для операции, которую я принимаю, я всё равно определяю бизнес-ключ.

Shopify не гарантирует порядок внутри топика и между топиками для одного и того же ресурса. Он рекомендует упорядочивать вебхуки по заголовку X-Shopify-Triggered-At или по полю updated_at в теле запроса. Поскольку доставка гарантируется не всегда, он также рекомендует периодическую сверку через API. Источник: about webhooks.

Что я на самом деле запускал?

Я прогнал четыре сценария сбоев 8 октября 2026 на Node v22.22.2. Фикстуры синтетические. HTTP-запросы, таймаут клиента, остановка приёмника и перезапуск процесса выполнялись по-настоящему, но локально. Ни магазин Shopify, ни данные клиентов, ни внешние сервисы в этом не участвовали.

Стенд запускает один приёмник на свободном порту 127.0.0.1. В его сохранённом снимке находятся ключи доставки, текущее состояние заказа, локальные эффекты и очередь входящих. В этом однопроцессном тесте замена одного снимка сохраняет всё это вместе. Успешный ответ отправляется после сохранения.

У каждой фикстуры есть синтетическое целочисленное поле version. Это правило теста, а не поле Shopify. Более высокая версия может обновить состояние. Более низкая записывается как устаревшая и перезаписать его не может. Под эффектом я понимаю запись в локальном журнале эффектов, а не платёж, письмо или вызов внешнего API.

Я запускал node run.mjs из каталога стенда на Node v22.22.2. Это один скрипт без пакетов. Скрипт и результаты входят в пакет материалов для проверки владельцем, публичной загрузки на этой странице нет. Скрипт выводит JSON и пишет results.json с полученными HTTP-статусами, исходами, количеством сохранённых состояний и результатами проверок. Если проверка безопасного приёмника не проходит, скрипт завершается с ошибкой.

Что показала матрица сбоев?

Все четыре сценария с безопасным приёмником прошли заявленные проверки. Таблица ниже взята из table.md, который скрипт сгенерировал из собранных значений JSON. Колонка с фактическим выводом приведена дословно. Таблица фиксирует локальные ответы и сохранённые эффекты, а не ожидаемое поведение Shopify и не оценку надёжности на продакшене.

Внесённый сбойФактический выводПроверка приёмки
Две доставки внахлёст200 applied; 200 duplicate; effects=1Один сохранённый эффект
Версия 2 раньше версии 1200 applied; 200 stale; final=paid v2; effects=1Новое состояние и один эффект
Таймаут после фиксации, затем повторCLIENT_TIMEOUT; 200 duplicate; effects=1Фиксация до таймаута; один эффект
Очередь, остановка и перезапуск202 queued; ECONNREFUSED; pending=1 to 0; 200 duplicate; 200 applied; effects=1/1Очередь разобрана; один эффект на заказ

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

В строке с таймаутом приёмник сообщает о фиксации после сохранения. Скрипт записывает время этого уведомления и время таймаута клиента по собственным монотонным часам и проверяет, что уведомление пришло раньше. Клиент сдаётся через 300 мс, приёмник задерживает ответ на 1200 мс. Затем скрипт повторяет тот же ключ. «Ответа я не получил» не значит «ничего не произошло».

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

Те же приёмочные проверки я прогнал на четырёх намеренно сломанных HTTP-приёмниках. Безусловный приёмник без идемпотентности применил одну и ту же доставку дважды: 200 applied; 200 applied; effects=2. Приёмник, который опирается на порядок поступления, сохранил created v1 поверх paid v2. Оба не прошли проверки. Ещё два приёмника вернули HTTP 500 или повторно выполнили действие при обработке устаревшего обновления; эти тоже не прошли. В JSON записана каждая проваленная проверка. Если отрицательный контроль неожиданно проходит, скрипт завершается с ошибкой.

Как превратить это в приёмку проекта?

Я записываю ожидаемое состояние целевой системы до теста и проверяю его после каждого сбоя. Число результатов, запись о восстановлении и ответственный за нерешённую работу входят в соглашение о приёмке.

Что согласоватьКакие подтверждения я бы запросил
Одна операция повторяется с тем же и с другим ключом доставкиОдин бизнес-эффект, привязанный к согласованному ключу операции
Два воркера одновременно обрабатывают одну операциюОдин зафиксированный результат; второй воркер записывает причину своего результата
Старое обновление приходит после новогоТекущее состояние остаётся верным; неоднозначные версии уходят на проверку или чтение из источника
Процесс останавливается до и после записи в целевую системуПовтор либо доделывает недостающее, либо находит уже зафиксированный результат
Простой дольше окна повторов отправителяСверка находит пропущенные записи; повторная отправка безопасна и ограничена
Повторы исчерпаныВидимая запись о неудаче, назначенный ответственный и задокументированное решение о повторной отправке

Это предлагаемые проверки для реальной интеграции. Локальный стенд не выполнял строки о нескольких воркерах, внешней записи и долгом простое. Для прямых API-интеграций я бы согласовывал их с учётом конкретных систем и задачи.

Различайте идентификаторы. Ключ доставки идентифицирует входящую доставку. Бизнес-ключ идентифицирует действие, например однократный выпуск счёта. У event ID или transaction ID в целевой системе свои правила. Один ключ не решает автоматически все три задачи.

Что этот стенд не доказывает?

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

Я не тестировал отключение питания, гарантии сброса данных файловой системой, принудительное завершение процесса во время сохранения, проверку HMAC, некорректные тела запросов, конкуренцию за базу данных и случай, когда удалённый API принимает запись раньше нашей локальной фиксации. Штатный перезапуск проверяет восстановление сохранённого состояния и очереди входящих. Это не сертификация устойчивости к аварийным остановкам.

Для внешнего эффекта я бы использовал ключ операции, который поддерживает целевая система, если он есть, или восстанавливаемый outbox с поиском в целевой системе и явной обработкой неопределённого исхода. Если сохранить «видел» до выполнения работы, работу можно потерять. Если выполнить работу до сохранения «видел», её можно продублировать. Этот переход нужно проверять отдельно.

Какой следующий шаг полезен?

Я начинаю с операции, её ключа и результатов, которые нужно подтвердить при приёмке. Затем выбираю механизм восстановления, который целевая система реально поддерживает.

Услуга по API-интеграциям и автоматизации описывает объём работ и передачу. Эти проверки согласуются в брифе на интеграцию. Если вы отправляете данные в платформу измерений, используйте сверку заказов, чтобы проверить, какие заказы дошли, и отделяйте продления подписок от повторных доставок.

Вопросы

Вопросы, на которые отвечает этот гайд

Что здесь значит идемпотентность?

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

Доказывает ли успешный повтор, что первый запрос не прошёл?

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

Достаточно ли ID доставки для любого дубля?

Нет. Второй ID доставки всё равно может относиться к той же бизнес-операции. В брифе определите ключ операции и ключ доставки отдельно.

Daniil Maximkin

Здравствуйте, я Даниил.

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

Попробовали и всё равно не выходит?

Опишите задачу

Отвечу в течение одного рабочего дня. Первый ответ бесплатный. Или напишите напрямую: next@taskfordaniel.com

Обсудить задачу

Расскажите, что нужно разработать, исправить или подключить.

Описать задачу next@taskfordaniel.com