Skip to content

Очереди, таймеры и отложенные задачи

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

Механизм

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

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

Задачи, от которых зависит платёж

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

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

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

Доставка уведомления мерчанту. Финализация не отправляет колбек сама, а ставит задачу. Разбирая её, ядро собирает тело, подписывает его и кладёт в очередь исходящих уведомлений, которую читает отдельный сервис колбеков — он и делает HTTP-запросы мерчанту с повторами. Если подготовить уведомление не удалось, ядро планирует новую попытку с растущей паузой и ограниченным числом повторов. Подробности — в Колбеке мерчанту.

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

Что происходит, когда попытки исчерпаны

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

Следы при этом остаются. Итог задачи пишется в журнал очередей в базе ядра — неудачи фиксируются всегда, у большинства очередей туда же попадают и успешные выполнения, — а подробности видны в обычных логах сервиса. Отдельные задачи, потеря которых блокирует деньги, намеренно сделаны громче: они не удаляются при неудаче и оставляют запись об ошибке, чтобы человек заметил и разобрался вручную.

Как это выглядит при разборе инцидента

Жалоба «платёж завис» почти всегда означает, что ожидание не закончилось или задача не выполнилась. Разбор идёт по шагам.

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

Ручной инструмент на этот случай — принудительная проверка статуса из бэкофиса: она делает то же, что делала бы отложенная задача. Помните, что для уже закрытой транзакции это режим, снимающий защиту от повторной финализации.

Кто разбирает очереди

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

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

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

Внутренняя база знаний. Нашли неточность — поправьте страницу или заведите вопрос в разделе «Открытые вопросы».