Dmitry Mikhailov

Все статьи
Дмитрий Михайлов8 мин чтения

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

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

Чёрный лаковый механический штамп с хромированными шарнирами и пружинами
В этой статье

Тайм-аут не доказывает, что действие не выполнено

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

Возьмём условную ситуацию. Автоматизация отправила запрос, сервис записал задачу, но соединение оборвалось до ответа. Отправитель видит тайм-аут и не знает, на каком этапе произошёл сбой. Новый запрос без связи с первым выглядит для сервиса как самостоятельная команда. Так появляются две задачи с одинаковым названием.

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

Свойство, при котором повтор одной операции сохраняет предусмотренный эффект, называют идемпотентностью. В RFC 9110 оно определяется через эффект повторных запросов, а не обязательное совпадение всех ответов. Для создания задачи его нужно обеспечить в контракте системы; само наличие кнопки «повторить» ничего не гарантирует.

Три доставки должны создать две задачи

В учебном приложении используются request-17, повтор request-17 и новая request-18. Первые две доставки относятся к одной заявке, третья — к другой. Во всех случаях название одинаковое: Prepare preview, то есть «Подготовить превью». Поэтому название намеренно не подходит для различения операций.

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

Пример устроен узко: задача и идентификатор события находятся в одной строке локальной базы SQLite. Первая запись уже сохранена, когда результат помечается как lost. Скрипт не обрывает реальное сетевое соединение и не обращается к таск-трекеру. Он показывает последствия повторной доставки для локальной записи.

Полезно проверить оба свойства сразу: повтор не создаёт лишнего объекта, а новая заявка не теряется из-за похожего содержания. Если проверять только два одинаковых запроса, ошибочное правило «вообще больше ничего не создавать» тоже может выглядеть успешным. Третья доставка выявляет такую потерю.

Сколько задач осталось после каждой доставки
3 / 3
ДоставкаБез защитыУникальный event_id
Первая request-17, ответ потерян11
Повтор request-1721
Новая request-1832

Ключ принадлежит операции, а не попытке

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

Обратная ошибка — один постоянный ключ для всех заявок. Тогда request-18 рискует быть подавлена вместе с повтором request-17. Правило должно различать самостоятельные действия, даже когда у них совпадают название, получатель и время.

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

В документации Stripe о вебхуках предлагается учитывать уже обработанные event ID; отдельно описан случай разных Event для одного объекта и типа события. Это полезный пример того, почему нельзя слепо применять один универсальный идентификатор ко всем интеграциям. Сначала нужно прочитать контракт источника и понять, что именно он считает повторной доставкой.

Проверка «найти, затем создать» оставляет гонку

Два параллельных запуска могут одновременно проверить отсутствие задачи. Оба получат ответ «не найдено», а затем оба создадут объект. Каждый по отдельности выполнил правило, но между поиском и записью осталось окно для второго запуска.

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

Учебный скрипт использует последовательные доставки и вставку INSERT OR IGNORE. Он не является нагрузочным тестом конкурентных обработчиков. Для реального сервиса нужно отдельно проверить параллельный запрос, конфликт уникальности и ответ вызывающей стороне. Не стоит переносить зелёный результат последовательного примера на все режимы работы.

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

Повтор с другим содержимым — отдельная ошибка

Предположим, request-17 сначала создавал задачу «Подготовить превью», а при повторе в нём появился другой срок или другое название. Молча вернуть старый результат неудобно: отправитель может решить, что изменения применились. Молча создать новую задачу тоже нарушит первоначальное назначение ключа.

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

Stripe описывает конкретные условия ключей, включая сравнение параметров и срок хранения для API v1. API v1 и v2 имеют различия в обработке повторов и ошибок. Поэтому правило выбранной версии нужно читать целиком; наличие слова idempotency в документации не обещает одинаковую семантику у всех API.

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

Отправка письма выходит за пределы локальной транзакции

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

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

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

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

Записанное намерение помогает продолжить после сбоя

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

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

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

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

Что показывать оператору при неопределённом результате

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

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

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

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

Как проверить сценарий перед рабочим запуском

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

Затем добавьте ситуации, которых нет в простом архивном примере:

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

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

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

Прочитать с ИИ

Разберите главную мысль и примените её к своей задаче.

ChatGPTClaude
Посмотреть промпт
Прочитай статью «Повторный запрос создал дубль: как сделать автоматизацию устойчивой к повторам»: https://dmitrymikhailov.com/insights/automation-retry-without-duplicates Объясни её основную мысль и назови конкретные примеры, на которых держатся выводы. Отдели факты статьи от своих предположений. Затем спроси, какую задачу я решаю, и помоги применить подход из статьи: предложи первый шаг и способ проверить результат. Если страница недоступна, попроси меня вставить текст. Не придумывай её содержание.