Как связать заказ и отправку SMS через transactional outbox

Как связать заказ и отправку SMS через transactional outbox

Сохраняйте заказ и задание на отправку SMS в одной транзакции базы данных, затем передавайте сообщение шлюзу через отдельный процесс. Такой подход называют transactional outbox: он помогает избежать ситуации, когда заказ уже есть, а приложение потеряло задание на уведомление. Ниже разберём, как устроить эту связку для сайта или интернет-магазина в Беларуси, что предусмотреть при сбоях API или SMPP и как проверить результат без обещаний доставки «ровно один раз».

Почему отправки SMS сразу после сохранения заказа недостаточно?

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

Если поменять действия местами, появится другая проблема. Шлюз примет SMS, а база отклонит заказ из-за ошибки. Клиент получит подтверждение покупки, которую магазин не сохранил. Обычная транзакция базы данных не охватывает внешний SMS-шлюз, поэтому объединить эти действия одной командой не получится.

Transactional outbox решает задачу через таблицу исходящих заданий в той же базе, где хранится заказ. Приложение записывает заказ и задание в общей транзакции. База либо подтверждает обе записи, либо отменяет обе. Отдельный процесс, который дальше будем называть обработчиком, читает задания и отправляет SMS.

Начните с проверки доступа к системе заказов. Для этой схемы разработчику нужна возможность менять логику сохранения заказа и структуру базы. Если магазин работает на конструкторе, сначала выясните, какие события и способы подключения он поддерживает. Этот вопрос подробнее разобран в статье про SMS-уведомления о заказах из конструкторов сайтов через SMPP.

Как записать заказ и задание в общую транзакцию?

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

  1. Откройте транзакцию базы перед записью заказа.
  2. Сохраните заказ с его внутренним идентификатором.
  3. Добавьте в outbox задание с типом уведомления и ссылкой на заказ.
  4. Зафиксируйте транзакцию только после успешной записи обеих сущностей.

У задания должен быть собственный уникальный идентификатор. Также нужны состояние обработки, время следующей попытки и причина последней ошибки. Отдельно предусмотрите поле для идентификатора сообщения, который вернёт SMS-провайдер: по нему затем сопоставляют отправку и отчёт о доставке.

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

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

Как обработчику отправлять SMS через API или SMPP?

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

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

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

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

Скорость обработки очереди согласуйте с лимитами подключения. Они зависят от API-поставщика, шлюза и маршрута, поэтому их берут из технических условий и проверяют тестовой отправкой (источник: smpp.by, «Как построить очередь SMS в API и SMPP: rate limit и приоритеты»). В задании на API-интеграцию полезно сразу закрепить эти параметры вместе с правилами повторов.

Что делать при тайм-ауте и как проверить доставку?

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

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

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

Разделите подтверждение приёма шлюзом и результат доставки. Ответ API или SMPP сообщает о приёме запроса на соответствующем этапе обработки. DLR, отчёт о доставке, приходит отдельно. Он не подтверждает, что человек прочитал SMS.

  • Повторяйте отправку после временной ошибки по расписанию с увеличением интервалов.
  • Останавливайте автоматические повторы при ошибке, которая требует исправить настройки или само задание.
  • Связывайте DLR с заданием через идентификатор сообщения провайдера.
  • Обрабатывайте повторный отчёт без повторного запуска действий по заказу.
  • Показывайте в аналитике отдельно очередь, приём шлюзом и итог доставки.

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

Типичные ошибки при внедрении transactional outbox

  • Записывают задание после завершения транзакции заказа. Исправление: перенести обе записи в общую транзакцию той же базы.
  • Помечают SMS как доставленное сразу после ответа шлюза. Исправление: разделить приём запроса и статус из DLR.
  • Повторяют запрос после любого тайм-аута. Исправление: сначала проверить механизм идемпотентности или возможность уточнить результат.
  • Оставляют задание навсегда закреплённым за процессом. Исправление: добавить срок закрепления и правило возврата в обработку.
  • Удаляют задание сразу после отправки. Исправление: сохранить связь с идентификатором провайдера для сопоставления статусов.

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

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