Коротко
- Повторная доставка события нормальна для распределённой системы; опасен не повтор, а повторное бизнес-действие.
- Одна заявка должна иметь стабильный идентификатор, а обработчик — атомарно фиксировать идентификатор и результат.
- Временные ошибки требуют ограниченных повторов с задержкой, постоянные — отдельной очереди ошибок и понятной процедуры возврата.
- Даже при надёжной доставке нужна регулярная сверка источника и CRM: она находит редкие расхождения, которые не видны по отдельным запросам.
Как выглядит проблема для бизнеса
Клиент отправил форму, увидел сообщение об успехе, но менеджер не получил карточку. Или одна заявка появилась в CRM дважды: первый раз от вебхука, второй — после повторной отправки формы либо автоматической повторной попытки. Иногда запись есть, но без телефона, источника или выбранной услуги. В других случаях CRM показывает новый лид, а уведомление в Telegram не пришло, поэтому команда реагирует слишком поздно.
Общая черта этих ситуаций — разрыв между фактом приёма обращения и подтверждением его обработки. Интерфейс сайта знает, что запрос ушёл. Интеграционный сервис знает, что получил HTTP-ответ. CRM знает, что создала запись. Но если у систем нет общего идентификатора и журнала переходов, никто не может доказать, относится ли повторный запрос к той же заявке и на каком шаге остановился исходный.
- Количество отправок формы не совпадает с количеством уникальных заявок в CRM.
- У одного контакта появляются несколько активных лидов с одинаковым содержанием.
- Статусы в CRM, платёжной системе и кабинете клиента расходятся.
- Менеджеры вручную переносят данные из почты или мессенджера после жалобы клиента.
- Техническая команда видит только ошибки запросов, но не может восстановить бизнес-путь конкретной заявки.
Почему обещание «отправить один раз» не работает
Между сайтом и CRM находятся сеть, балансировщик, приложение, очередь и база данных. Любой участник может завершить работу в момент, когда соседняя система ещё не получила подтверждение. Например, CRM уже создала лид, но ответ потерялся по сети. Отправитель видит тайм-аут и повторяет запрос. Если получатель не распознаёт повтор, появляется вторая карточка.
Обратная ошибка возникает, если обработчик подтверждает получение сообщения до фиксации данных. После подтверждения процесс аварийно завершается, запись не создаётся, а очередь считает работу выполненной. Поэтому семантику доставки выбирают вместе с правилами обработки, а не как отдельную настройку транспорта.
| Подход | Что гарантирует | Риск для заявок |
|---|---|---|
| At most once | Сообщение обрабатывается не более одного раза | При сбое заявка может исчезнуть без повторной попытки |
| At least once | Сообщение повторяется, пока не будет подтверждено | Получатель обязан безопасно распознавать повтор |
| Exactly once | Конкретная платформа ограничивает повторную доставку в оговорённых границах | Гарантия транспорта не означает автоматически однократный внешний эффект |
Надёжная интеграция не пытается исключить все повторы. Она делает повтор предсказуемым и безопасным.
Принцип проектирования Agentix Labs
Где именно возникают дубли и потери
Полезно разложить передачу заявки на отдельные переходы: источник сохраняет обращение, публикует событие, интеграция принимает его, CRM создаёт или обновляет сущность, уведомление уходит менеджеру. На каждой границе существует окно неопределённости. Ответ мог не дойти, процесс мог перезапуститься, внешний API мог временно ограничить запросы, а формат данных — перестать соответствовать новой схеме.
- Форма вызывает CRM напрямую и не сохраняет обращение локально до внешнего запроса.
- Пользователь повторно нажимает кнопку, а интерфейс создаёт новый идентификатор для каждой попытки.
- Webhook отправляется повторно после тайм-аута, но CRM всегда выполняет команду создания.
- Очередь повторяет сообщение после истечения срока подтверждения, хотя обработчик уже записал результат.
- Ошибка в обязательном поле бесконечно повторяется как временная и блокирует полезные сообщения.
- Сотрудник вручную создаёт лид из письма, не видя, что автоматическая интеграция восстановится позже.
- Системы по-разному нормализуют телефон, email или внешний номер и ошибочно считают одного клиента разными людьми.
Ключ идемпотентности: как отличить повтор от нового обращения
Ключ идемпотентности — стабильный ключ одной бизнес-команды. Источник создаёт его при первом приёме заявки и повторно использует во всех повторных попытках. Получатель перед изменением данных проверяет ключ: если команда уже выполнена, он возвращает ранее сохранённый результат вместо второго создания. AWS Builders' Library отдельно подчёркивает роль клиентского идентификатора и необходимость различать одинаковый ключ с разными параметрами.
Ключ нельзя генерировать заново на каждой сетевой попытке. Для интеграции нескольких компаний обычно важен контекст владельца: комбинация организации, источника и внешнего идентификатора события безопаснее глобальной проверки короткого номера. Вместе с ключом полезно хранить отпечаток значимых параметров, статус, итоговый идентификатор CRM и время обработки.
-
01
Создать ключ в точке первого приёма
Сайт или шлюз сохраняет заявку и назначает неизменяемый event_id до обращения к CRM.
-
02
Передать ключ через весь маршрут
Один идентификатор попадает в очередь, логи, CRM и уведомления, чтобы события можно было связать.
-
03
Проверить ключ до побочного эффекта
Обработчик ищет ранее завершённую команду и не создаёт вторую карточку, платёж или уведомление.
-
04
Зафиксировать ключ и изменение атомарно
Запись о выполнении и бизнес-изменение должны сохраняться как одна транзакционная операция либо через надёжный inbox/outbox-контур.
-
05
Отклонить несовпадающий повтор
Если тот же ключ пришёл с другим содержанием, система сигнализирует о конфликте вместо молчаливого объединения.
| Событие | Подходящий ключ | Поведение при повторе |
|---|---|---|
| Отправка формы | Идентификатор сохранённой заявки | Вернуть существующую заявку |
| Webhook оплаты | Идентификатор события провайдера и организация | Не проводить операцию повторно |
| Импорт строки | Идентификатор пакета и номер строки | Показать ранее записанный результат |
| Смена статуса | Идентификатор команды, а не только новый статус | Не запускать повторные уведомления и задачи |
Повторы, задержки и очередь необработанных событий
Повторные попытки нужны при временных отказах: сетевом тайм-ауте, краткой недоступности сервиса или ограничении частоты. Повторять запрос сразу и бесконечно опасно. Во время сбоя множество процессов создают дополнительную нагрузку и мешают системе восстановиться. При обработке временных отказов число попыток ограничивают, интервалы увеличивают, учитывают Retry-After и добавляют случайное смещение, чтобы клиенты не повторяли запросы одновременно.
Постоянные ошибки нужно отделять от временных. Если в событии отсутствует обязательное поле или указан неизвестный ответственный, новый запрос с теми же данными ситуацию не исправит. После заданного числа попыток событие переводят в очередь необработанных событий (DLQ). Правильно настроенная DLQ сохраняет сообщение и метаданные отказа от брокера; число прикладных попыток и контекст трассировки нужно явно включить в заголовки или данные события и проверить надёжность доставки в DLQ. Сама очередь не восстанавливает заявку.
- Задать тайм-аут каждому внешнему вызову и определить, какие коды разрешают повтор.
- Использовать ограниченные увеличивающиеся интервалы со случайным смещением, а не фиксированный частый интервал.
- Не повторять ошибки валидации и конфликты идемпотентного ключа как временные.
- Показывать размер DLQ, возраст самого старого события и причины отказов.
- Предусмотреть безопасную повторную обработку после исправления причины; она использует исходный ключ идемпотентности.
- Назначить ответственного и срок реакции, иначе DLQ превращается в скрытый архив потерянных заявок.
Сверка данных: как найти тихие потери
Логи отвечают на вопрос, что происходило внутри конкретного компонента. Сверка отвечает на более важный вопрос: совпадает ли бизнес-состояние между системами. Даже хорошо спроектированная доставка может столкнуться с ручным изменением, ошибкой миграции, истёкшим сроком хранения ключа или недоступным внешним сервисом. Поэтому критичные интеграции дополняют периодической сверкой данных.
Для заявок сверка может сравнивать принятые источником event_id с внешними идентификаторами в CRM за выбранный интервал. Отдельно проверяются допустимые переходы статусов и обязательные связи: у оплаченного заказа есть платёжное событие, у переданной заявки — владелец или очередь, у завершённой операции — запись в журнале. Найденное расхождение не нужно автоматически исправлять без правил: сначала его классифицируют, чтобы не перезаписать корректное ручное решение.
-
01
Определить источник истины
Для каждого поля и статуса зафиксировать систему-владельца, а не объявлять всю CRM единственным источником.
-
02
Сравнить идентификаторы и состояния
Проверять не только количество записей, но и соответствие event_id, внешнего номера, версии и статуса.
-
03
Разделить типы расхождений
Пропуск, дубль, конфликт содержимого и задержка требуют разных действий и уровней срочности.
-
04
Восстановить через штатный обработчик
Исправление повторно проходит идемпотентный контур, а не меняет таблицы CRM вручную.
-
05
Сохранить результат сверки
Журнал показывает, что было найдено, кто подтвердил действие и к какому состоянию пришли системы.
Как провести диагностику без переписывания всей интеграции
Диагностика начинается с одного реального маршрута, например «форма на сайте — CRM — Telegram менеджера». Команда выбирает несколько обращений: успешное, пропавшее и продублированное. Затем по временным меткам и идентификаторам восстанавливает каждый переход. Если единого идентификатора нет, это уже первый вывод: наблюдаемость нужно исправить до сложной оптимизации.
| Проверка | Что нужно увидеть | Тревожный признак |
|---|---|---|
| Приём источником | Сохранённые данные и event_id до внешнего вызова | Форма показывает успех только по факту отправки запроса |
| Доставка | Попытки, ответы, задержки и единый trace_id | Есть только финальная ошибка без истории |
| Обработка | Идемпотентная запись и внешний CRM id | Каждый повтор вызывает create |
| Ошибки | Разделение временных и постоянных причин | Все ошибки повторяются одинаково |
| Восстановление | очередь необработанных событий, повторная обработка и результат сверки | Исправление выполняется ручным копированием |
- Посчитайте уникальные event_id, а не только число HTTP-запросов или строк CRM.
- Проверьте, что повтор после тайм-аута возвращает тот же бизнес-результат.
- Смоделируйте остановку процесса после записи в CRM, но до подтверждения очереди.
- Отправьте невалидное событие и убедитесь, что оно не повторяется бесконечно.
- Запустите повторную обработку одного события из DLQ и проверьте отсутствие второго побочного эффекта.
- Сверьте принятые обращения с CRM за контрольный период и сохраните список расхождений.
Как эти принципы применяются в проектах Agentix Labs
В QR Oplata одна заявка проходит резервирование, очередь, захват трейдером, холд, спор и финансовый журнал. По подтверждённому описанию проекта конкурентный захват защищён Redis-lock, а финальный статус проверяется в PostgreSQL. Денежные изменения проходят через LedgerEntry и транзакции, поэтому контроль строится вокруг единого состояния заявки и проверяемой истории, а не вокруг доверия к отдельному интерфейсному запросу.
В VPN Service повторные платёжные и трафиковые события затрагивают подписку и отдельный LTE-лимит. В проекте предусмотрены paid_event_id, idempotency_key и идентификатор usage event: повтор не должен второй раз выдать LTE-лимит или повторно списать трафик. Этот пример показывает, почему дедупликация должна находиться на уровне бизнес-операции, а не только в очереди или веб-сервере.
Чек-лист требований к интеграции заявок
Не каждой компании сразу нужна отдельная событийная платформа. Для небольшого потока достаточно устойчивого приёмника, локального сохранения, фоновой доставки и понятного журнала ошибок. Но основные контракты стоит определить заранее: идентификатор заявки, владелец данных, допустимые повторы, конечное состояние и способ восстановления. Тогда архитектуру можно усложнять по фактической нагрузке, не меняя смысл событий.
- Заявка сохраняется до вызова внешней CRM и получает постоянный event_id.
- Все повторные попытки передают тот же ключ идемпотентности.
- Получатель атомарно фиксирует ключ и бизнес-результат.
- Повтор с тем же ключом и другим содержанием возвращает явный конфликт.
- Повторные попытки ограничены, используют нарастающую паузу со случайным смещением и учитывают тип ошибки.
- После исчерпания попыток событие попадает в наблюдаемую DLQ.
- Replay не создаёт второй лид, платёж, задачу или уведомление.
- Логи связываются единым trace_id и не раскрывают лишние персональные данные.
- Для каждого статуса определена система-владелец и допустимые переходы.
- Периодическая сверка находит пропуски, дубли и конфликтующие состояния.
- У команды есть ответственный за DLQ и регламент восстановления.
- Метрики показывают число принятых событий, повторов, ошибок, возраст очереди и расхождения сверки.
Частые вопросы
Можно ли убрать дубли уникальным номером телефона?
Телефон помогает связать контакты, но не определяет уникальность бизнес-намерения. Один человек может отправить две разные заявки, а номер может быть общим для компании. Для защиты повторной обработки нужен идентификатор события или команды; правила объединения контактов задаются отдельно.
Что делать, если CRM не поддерживает ключ идемпотентности?
Поставить перед CRM интеграционный слой, который сохраняет внешний event_id, состояние обработки и полученный CRM id. При повторе слой ищет завершённую операцию и не вызывает создание повторно. Если CRM позволяет искать по пользовательскому внешнему полю, его также используют для сверки.
Достаточно ли настроить автоматические повторные попытки?
Нет. Повтор без идемпотентности увеличивает риск дублей, а бесконечное повторение постоянной ошибки создаёт нагрузку и скрывает проблему. Нужны классификация ошибок, ограничение попыток, увеличивающиеся интервалы, очередь необработанных событий и безопасная повторная обработка.
Чем DLQ отличается от журнала ошибок?
Журнал помогает расследовать, а DLQ сохраняет необработанное событие как единицу будущей работы. Для DLQ нужны просмотр причины, исправление, повторная обработка с исходным ключом и подтверждение конечного состояния.
Нужна ли сверка, если очередь обещает однократную доставку?
Да, если бизнес-процесс затрагивает несколько систем или внешние побочные эффекты. Гарантия брокера действует в определённых границах и не защищает от ручных изменений, ошибок схемы, неверного сопоставления сущностей или отдельного сбоя внешнего API. Сверка проверяет итоговое бизнес-состояние.
Источники
- AWS Builders' Library: Making retries safe with idempotent APIsПроверено 30 июля 2026 г.
- Microsoft Azure Architecture Center: Transient fault handlingПроверено 30 июля 2026 г.
- Google Cloud Pub/Sub: Exactly-once deliveryПроверено 30 июля 2026 г.
- RabbitMQ: Dead Letter ExchangesПроверено 30 июля 2026 г.