Скачать CV

Backend

Очередь Redis, которая переживает второй запуск

Воркер может умереть после вызова провайдера и до коммита. Redis отдаст job снова. Второй запуск должен быть пустым.

Списание ушло дважды после того, как я убил контейнер queue посреди job. Очередь в этом стеке это Redis. Доставка минимум один раз. Воркер php-fpm в контейнере queue могут убить после того, как письмо или webhook ушли из нашей сети, и до коммита локальной транзакции. Redis отдаст job снова. Если обработчик к этому не готов, отправите дважды.

Провайдер уже вернул 200. MySQL ссылку не сохранил. Пользователи увидели два списания. Хост увидел два job с одним payload и без уникального ключа.

Одному ShouldBeUnique я не верю. Он закрывает пересечение, не ретрай после падения. Ограждение это таблица с уникальным ключом. Ключ это id заказа плюс имя шага, никогда случайный uuid. Вставляйте ключ до side-effect. Если вставка ударилась в unique, работа сделана и job выходит.

Ключ из id заказа и имени шага. Никогда из случайного uuid.
Ключ из id заказа и имени шага. Никогда из случайного uuid.

Держать забор рядом

Локальная запись в транзакции. Внешний вызов после захвата ключа и до отметки о завершении. Id ответа провайдера лежит рядом с ключом. Повтор использует его, а не списывает снова.

Ретраи ограничены. Три попытки с backoff, потом failed_jobs и алерт. Timeout job короче резерва Redis. Backoff короче TTL очереди. Упавший payload должен запускаться руками после фикса.

Я воспроизвёл это SIGKILL после HTTP 200 и до коммита. Второй воркер ударился в unique, прочитал сохранённый id провайдера и вышел. Без сохранённого id он списал бы снова даже с unique-ключом, потому что первый запуск ссылку не записал. Захват, вызов, сохранить id, потом завершить. Ack или удаление job в Redis в конце.

Три попытки с backoff, потом failed_jobs и алерт.
Три попытки с backoff, потом failed_jobs и алерт.

Что в очередь не кладу

Логин, 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, префиксы отдельно от кэша. Логин в очередь не класть.

К заметкам