Опашката, която никога не спря

Паметта на Redis продължаваше да расте. Horizon светеше зелено. Двадесет и девет email класа повтаряха опитите безкрайно и никой не забеляза.

Имейлите се проваляха. Тази част беше очаквана — счупени SMTP данни за достъп по време на миграция. Неочакваното беше друго: те никога не спряха да се провалят.


Таблото на Horizon: зелено. Работниците: здрави. Redis: расте бавно. Никакви аларми, никакви грешки в логовете. Само тихо натрупване на задачи, които опитваха, опитваха и пак опитваха.

Забелязах го само защото паметта на Redis не падна обратно, след като поправих SMTP конфигурацията. Нещо още беше вътре и дъвчеше повторни опити. Хиляди.


Предположих, че опашката ще се справи. Това е сделката: една задача се проваля, пробва още няколко пъти, пада в failed_jobs. Продължаваш нататък.

Освен ако задачата не е Mailable.

Когато изпратиш Mailable към опашка, Laravel го обвива в задача. maxTries на тази задача идва от свойството $tries на Mailable-а. Ако не го зададеш — а защо би, документацията едва го споменава — то се сериализира като null.

null не значи „използвай стойността по подразбиране на supervisor-а“. null значи „без лимит“. Horizon вижда null и си мисли: тази задача иска да опитва отново завинаги. И го прави.


Оказа се, че е известен бъг. Laravel Horizon issue #1346. Флагът --tries на supervisor-а се игнорира, когато сериализираният payload на задачата носи maxTries: null. Собствената декларация на задачата печели, а тя казва: никога не спирай.

Двадесет и девет Mailable класа. Всеки един без изрично свойство $tries. Всеки един потенциално безсмъртен.


Поправката е почти обидно проста:

class WelcomeEmail extends Mailable implements ShouldQueue
{
    public int $tries = 2;
    public int $maxExceptions = 2;
}

Две свойства. Двадесет и девет файла. Това е.

Един първоначален опит, един retry, после failed_jobs. Така, както предполагах, че винаги е работело.


Тествам го така, както би тествал капан за мишки. Чупя SMTP конфигурацията нарочно. Пускам един имейл към опашката. Гледам Horizon. Два опита. Failed job. Край. Без призраци в опашката.

После поправям останалите двадесет и осем.


Три урока, сгъстени:

  1. null не е „default“. В сериализираните payload-и на задачи maxTries: null значи без лимит. Supervisor конфигурацията ти е предложение, не правило.
  2. Зелените dashboard-и лъжат. Horizon показваше здрави worker-и, които щастливо обработваха задачи, които никога нямаше да приключат.
  3. Framework default-ите не винаги са разумни. Laravel не задава $tries на Mailables. Ти трябва да го направиш. Документацията няма да те предупреди, докато вече нямаш пожар.

Най-страшните бъгове са онези, които изглеждат като нормална работа. Този изглеждаше така — седмици наред.


Коментари

Boris D. Teoharov

Автор

Здравей, аз съм Борис

Не съм писател. Не съм философ. Просто съм backend инженер от България, който живее между Laravel опашки и индекси със стотици милиони редове. През останалото време чета медицина, която няма работа да чета, френски романи, които разбирам наполовина, и каквото още малката ми гумена глава реши да дъвче. Две спасени кучета ме държат честен.