Я хочу, чтобы тест повторов отвечал на конкретный вопрос: сколько записей или действий существует в целевой системе после того, как отправитель повторил попытку?
Один лишь успешный 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 раньше версии 1 | 200 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-интеграциям и автоматизации описывает объём работ и передачу. Эти проверки согласуются в брифе на интеграцию. Если вы отправляете данные в платформу измерений, используйте сверку заказов, чтобы проверить, какие заказы дошли, и отделяйте продления подписок от повторных доставок.