Ack в Symfony Messenger это не забор платежа
Redis-транспорт отдаёт сообщение после падения снова. У Horizon то же правило. Ack после вызова провайдера это второй платёж.
Карту списали дважды на хосте Symfony, где уже крутился Horizon на Laravel. Laravel Horizon и Symfony Messenger в этом стеке оба сидят на Redis. Доставка минимум один раз. Воркер php-fpm в контейнере queue умер после 200 от провайдера и до ack. Redis отдал сообщение следующему. У обработчика не было забора. Карту списали снова.
Пользователи увидели две выписки. В логе воркера один успех и один ретрай. Ack после HTTP это как ждёт второй платёж. Middleware unique забором не считаю. Оно закрывает пересечение двух живых воркеров. Не закрывает ретрай после SIGKILL.
Забор это таблица с уникальным ключом id заказа плюс имя шага. Insert до HTTP. Если insert ударился в unique, выходим и ack.
Ack в конце, захват в начале
Локальный insert в транзакции. Вызов провайдера после захвата ключа. Ссылка провайдера лежит рядом с ключом. Повтор использует этот id. Ретраи Messenger ограничены. Три попытки, потом failure transport и алерт. Timeout job короче резерва Redis.
Тот же шаблон в заметке про Horizon на этом сайте. Symfony это не другая продуктовая задача. Это тот же список Redis с другим бинарником воркера.
Я сравнил auto-ack на Redis-транспорте с ack после обработчика. Auto-ack до HTTP теряет сообщение, если процесс умрёт посреди вызова. Ack после вызова без забора дублирует вызов. Сначала захват в MySQL, потом HTTP, сохранить ссылку, потом ack. Failure transport не должен снова бить HTTP, не попав в unique.
Что на шину не кладу
Логин, CSRF и /api/user остаются синхронными. Сообщение, которое должно закончиться до HTML, это медленный запрос с лишними отказами. Письмо с контакта может подождать. Пересборка кэша может подождать. Платёж без забора ждать не может.
Очередь, кэш и сессии делят Redis. Префиксы разделены, чтобы вытеснение slab листинга не сняло зарезервированное сообщение. Этот префикс часть обработчика, не строка в логе после инцидента.
Упавшие сообщения Messenger выглядели здоровыми, а в таблице платежей было две строки на один шаг. Считаю insert забора против 200 провайдера. Если 200 больше, ack прошёл после вызова без unique-захвата.
Какой опыт из этого
Меряю 200 провайдера против уникальных строк на шаг заказа, не throughput Messenger. Redis отдаст снова. Второй запуск обработчика должен сделать ack без списания.
Middleware unique и ack после HTTP забором не считаю. Сначала insert ключа. Сохранить id провайдера. Ack в конце.
Правило, которое оставляю: тот же забор, что у Horizon, префиксы отдельно от кэша, логин не на шину. Timeout короче резерва Redis. Три попытки, потом failure transport.
