Успешный платеж, который так и не стал бронированием
Клиент оплатил. Razorpay показал успех. Webhook отправил HTTP 200. Платеж был подтвержден (captured).
Тем не менее, статус бронирования застыл на «confirming».
Ошибок не было. Исключений в коде не возникало. Алерты не срабатывали. Все метрики показывали, что система работает исправно.
Но у клиента ничего не было. У автора не было бронирования.
Принимать деньги — легко. Обеспечить, чтобы каждый платеж приводил к бронированию — вот настоящая задача.
Большинство туториалов предлагают следующий процесс:
- Webhook получает событие
- Webhook обновляет бронирование
Это опасно. Если бизнес-логика находится внутри webhook, вы полностью зависите от успешности доставки. Webhook-и подвержены повторным попыткам (retries), дублированию и частичным сбоям.
Мы изменили архитектуру, чтобы разделить эти задачи. Теперь webhook-и только записывают события. Они не выполняют бизнес-логику.
Мы внедрили журнал событий (event ledger) с тремя таблицами:
- payment_orders: источник истины от провайдера
- payment_events: неизменяемый журнал событий
- bookings: источник истины бизнеса
Теперь у webhook-а одна задача:
- Проверить подпись
- Сохранить событие
- Вернуть 200
Это защищает систему. Если webhook даст сбой, событие все равно будет в безопасности.
Мы также поняли, что состояние платежа и состояние бронирования — это разные вещи. Подтвержденный платеж (captured payment) — это входные данные. Подтвержденное бронирование — это результат. Разделение этих состояний позволяет проводить сверку (reconciliation).
В ходе расследования мы обнаружили баг. События были в базе данных. Процессор работал исправно. Webhook работал исправно.
Но процессор никогда не запускался. Никто не вызывал функцию для обработки ожидающих событий.
Разделение приема данных (ingestion) и их обработки — это правильный дизайн. Но это создает новое требование: что-то должно запускать процесс обработки.
Мы внедрили планировщик (scheduler) для выполнения нескольких задач:
- Обработка событий платежей
- Восстановление пропущенных webhook-ов
- Проверка согласованности системы
Чтобы избежать ошибок при повторных попытках, мы используем следующую логику:
- Выбрать необработанные события
- Использовать "SKIP LOCKED", чтобы разрешить работу нескольким воркерам
- Гарантировать, что дубликаты доставки ничего не меняют
Система, которая работает только тогда, когда каждый webhook приходит вовремя — это хрупкая система. Если в вашей очереди некому вычитывать задачи, работа будет ждать вечно.
Надежность означает проектирование с учетом того, что сбои неизбежны.