Очередь Redis, которая переживает второй запуск
Воркер может умереть после вызова провайдера и до коммита. Redis отдаст job снова. Второй запуск должен быть пустым.
Списание ушло дважды после того, как я убил контейнер queue посреди job. Очередь в этом стеке это Redis. Доставка минимум один раз. Воркер php-fpm в контейнере queue могут убить после того, как письмо или webhook ушли из нашей сети, и до коммита локальной транзакции. Redis отдаст job снова. Если обработчик к этому не готов, отправите дважды.
Провайдер уже вернул 200. MySQL ссылку не сохранил. Пользователи увидели два списания. Хост увидел два job с одним payload и без уникального ключа.
Одному ShouldBeUnique я не верю. Он закрывает пересечение, не ретрай после падения. Ограждение это таблица с уникальным ключом. Ключ это id заказа плюс имя шага, никогда случайный uuid. Вставляйте ключ до side-effect. Если вставка ударилась в unique, работа сделана и job выходит.
Держать забор рядом
Локальная запись в транзакции. Внешний вызов после захвата ключа и до отметки о завершении. Id ответа провайдера лежит рядом с ключом. Повтор использует его, а не списывает снова.
Ретраи ограничены. Три попытки с backoff, потом failed_jobs и алерт. Timeout job короче резерва Redis. Backoff короче TTL очереди. Упавший payload должен запускаться руками после фикса.
Я воспроизвёл это SIGKILL после HTTP 200 и до коммита. Второй воркер ударился в unique, прочитал сохранённый id провайдера и вышел. Без сохранённого id он списал бы снова даже с unique-ключом, потому что первый запуск ссылку не записал. Захват, вызов, сохранить id, потом завершить. Ack или удаление job в Redis в конце.
Что в очередь не кладу
Логин, CSRF и эндпоинт текущего пользователя остаются синхронными. Очередь, которая должна закончиться до HTML, это медленный запрос с лишними отказами. Письмо с формы контакта может подождать. Пересборка кэша после сохранения заметки может подождать. Платёж без ограждения ждать не может.
Контейнер queue это второй процесс php в Docker. Он делит Redis с кэшем и сессиями. Ключи сессий и payload job живут в разных префиксах, чтобы вытеснение большого кэша списка не сняло зарезервированный job. Этот префикс часть устройства, не дописка в логе воркера.
Метрики Horizon по throughput двойное списание не показали. Таблица платежей показала. Считаю строки на id заказа плюс шаг и колбэки провайдера. Если числа расходятся, забора нет или id не сохранили.
Какой опыт из этого
Меряю списания и строки забора на шаг заказа, не throughput очереди. Redis с at-least-once прогонит обработчик дважды. Второй запуск должен быть пустым.
ShouldBeUnique и uuid id job я не доверяю. Unique на id заказа плюс шаг, insert до HTTP, сохранить id провайдера.
Правило, которое оставляю: timeout короче резерва Redis, три попытки потом failed_jobs, префиксы отдельно от кэша. Логин в очередь не класть.
