Messenger, который списал карту дважды
Провайдер вернул 200. Воркер умер до ack. Redis отдал сообщение снова. Unique middleware не закрыл SIGKILL.
Магазин крутил Symfony Messenger на Redis рядом с каталогом Laravel на Horizon. Checkout слал PayOrder. Обработчик звонил провайдеру, потом ack. Docker на деплое слал SIGTERM. У одного воркера был 200 и не было ack. Следующий consumer списал снова.
Unique middleware Messenger был включён. Он сериализует пересекающихся consumer. Он не переживает падение после HTTP. Поддержка видела два id провайдера на один заказ. Клиент видел два списания.
Проблема была в списании без постоянного забора. Unique middleware это не журнал. Нужен insert с unique (order_id, step) до HTTP, потом ack. Повтор должен быть пустым.
Insert до HTTP
Таблица с unique (order_id, step) это забор. Insert в транзакции до вызова провайдера. При конфликте берём сохранённый provider_ref, не списываем, ack. Три ретрая, потом failure transport. Timeout короче резерва Redis.
Один Redis, два воркера
Horizon и Messenger делили инстанс. Префиксы отделяли job от кэша и сессий. Забор живёт в MySQL, не в Redis SET NX, который умрёт на maxmemory. Очередь Laravel на этом сайте использует ту же таблицу. Symfony не был особенным. Особенным был отсутствующий insert.
Какой опыт из этого
Ack после HTTP это окно. SIGTERM в этом окне это второе списание, если забора ещё нет в базе.
Unique middleware сериализует consumer. Он не переживает SIGKILL. Уникальная строка переживает.
Денежные шаги живут в MySQL. Redis это транспорт. Две работы не должны делить lock, который maxmemory может снять.
