Увійти Реєстрація
Блог Серії
Кар'єра
Вакансії Компанії
Навчання
Документація Співбесіди Тестування Відео
Екосистема
Пакети Ресурси Проєкти Інструменти Події
Інше
Про нас Реклама

Senior: питання на співбесіді з теми «Черги»

Питання з реальних співбесід з відповідями: Laravel і PHP, бази даних, JavaScript і фронтенд, Git, Docker, API, безпека й архітектура. Тими самими темами, що й тести.

6 питань

Horizon - панель і конфігурація черг на Redis. Дає те, чого немає в базовому queue:work.

// config/horizon.php
'supervisor-1' => [
    'connection' => 'redis',
    'queue' => ['high', 'default'],
    'balance' => 'auto', // авто-балансування воркерів
    'maxProcesses' => 10,
],

Можливості:

  • Реалтайм-метрики: throughput, час очікування, runtime завдань.
  • Авто-балансування процесів між чергами за навантаженням.
  • Керування невдалими завданьами, теги, сповіщення про довге очікування (LongWaitDetected).

Запуск - php artisan horizon; під капотом це менеджер довготривалих воркерів. Дашборд захищають gate viewHorizon.

Докладніше в документації: Laravel Horizon

Завдання можуть падати через тимчасові збої (мережа, rate limit) - потрібна стратегія повторів і обробки остаточних провалів.

class CallApi implements ShouldQueue
{
    public int $tries = 5; // спроб
    public int $maxExceptions = 2;
    public int $timeout = 30;

    // прогресивна затримка між спробами
    public function backoff(): array
    {
        return [10, 30, 60]; // 10с, 30с, 60с...
    }

    public function failed(Throwable $e): void
    {
        // викликається після вичерпання спроб
    }
}
  • Остаточно провалені завдання осідають у таблиці failed_jobs.
  • php artisan queue:retry all - повторити, queue:flush - очистити.
  • releaseAfter, WithoutOverlapping, RateLimited middleware керують поведінкою.
  • Ідемпотентність (idempotency) обов'язкова - бо завдання може виконатися повторно.

Докладніше в документації: Обробка невдалих завдань

Черги гарантують доставку «хоча б раз», а не «рівно раз»: завдання може виконатися повторно після таймауту, падіння воркера чи ручного queue:retry. Ідемпотентне завдання при повторі не робить шкоди - не списує гроші двічі й не шле другий лист.

Прийоми, від простого до надійного:

1. Перевірка стану перед дією:

public function handle(): void
{
    if ($this->order->paid_at !== null) {
        return;   // уже оброблено
    }
    // ...
}

Сама по собі має вікно гонки: два воркери можуть одночасно побачити «не оплачено».

2. Обмеження бази як гарантія. Унікальний індекс на payments.order_id перетворює другу вставку на помилку, а не на дубль. Перевірку в коді можна обійти, обмеження в базі - ні.

3. Ключ ідемпотентності для зовнішніх API. Платіжні сервіси приймають заголовок на кшталт Idempotency-Key: повторний запит з тим самим ключем поверне перший результат, а не створить новий платіж.

4. Поділ на кроки. Не «спиши кошти й надішли лист» в одному завданні, а запис наміру в базу, списання, і лист окремим завданням після успіху - щоб повтор листа не тягнув повтор списання.

Плюс afterCommit(), щоб завдання не стартувало раніше, ніж закомічено транзакцію, яка його породила.

Докладніше в документації: Робота з невдалими завданнями

Це два різні механізми, і їх неправильне поєднання дає подвійне виконання.

  • --timeout (або #[Timeout] на завданні) - скільки воркер дозволяє завданню працювати, перш ніж убити процес. За замовчуванням 60 секунд.
  • retry_after у config/queue.php - через скільки секунд підключення вважає завдання загубленим і віддає його іншому воркеру.

Що буде, якщо retry_after менший за реальну тривалість: завдання на 120 секунд при retry_after = 90 на 91-й секунді отримає другий воркер. Перше ще працює - і тепер їх два.

Правило: --timeout має бути на кілька секунд меншим за retry_after. Тоді зависле завдання гарантовано вбивається до того, як його повторять.

Для довгих завдань:

  • Підняти обидва значення для окремого підключення чи черги з довгими завданнями, не чіпаючи решту.
  • Краще - розбити роботу на частини (Bus::batch), щоб кожне завдання вкладалося в хвилину.
  • #[FailOnTimeout] позначає завдання невдалим після таймауту замість повторів - для операцій, які небезпечно повторювати наосліп.

У SQS замість retry_after діє visibility timeout черги, і правило те саме.

Докладніше в документації: Терміни дії й таймаути завдань

Усі три борються з «зайвими» завданнями, але на різних етапах.

ShouldBeUnique - не пускає дубль у чергу. При dispatch() береться блокування за uniqueId(); поки перше завдання не виконане, друге просто не потрапить у чергу.

class RebuildSitemap implements ShouldQueue, ShouldBeUnique
{
    public int $uniqueFor = 3600;
}

Підходить, коли результат від повтору не зміниться: перебудувати сайтмап двічі - те саме, що раз. ShouldBeUniqueUntilProcessing знімає блокування на початку виконання, а не наприкінці, тож нова зміна під час роботи вже може стати в чергу.

WithoutOverlapping - дублі можуть стояти в черзі, але не виконуються одночасно. Підходить, коли кожне завдання важливе, але паралельно вони зламають дані: два оновлення одного рахунку.

Дебаунс (#[DebounceFor], Laravel 13) - виконується лише останнє. Кожен новий виклик відсуває виконання, тож за серію змін товару індекс оновиться один раз - з найсвіжішими даними. ShouldBeUnique тут гірший: він лишив би перший виклик, і пізніші зміни загубилися б. Параметр maxWait не дає відкладати нескінченно.

Коротко: результат однаковий - unique; порядок важливий, паралель шкодить - overlapping; важливий лише останній стан - дебаунс.

Докладніше в документації: Унікальні завдання

Воркер queue:work завантажує застосунок один раз і тримає його в пам'яті. Після деплою він продовжує виконувати старий код, поки його не перезапустити.

Перезапуск:

php artisan queue:restart     # звичайні воркери
php artisan horizon:terminate # якщо черги веде Horizon

Обидві команди просять воркери завершити поточне завдання й вийти, а Supervisor піднімає їх уже з новим кодом. queue:restart передає сигнал через кеш, тож усі сервери мають бачити спільне сховище кешу.

Що ламається тихіше - сумісність payload. У черзі можуть лежати завдання, серіалізовані старою версією класу:

  • Перейменували чи видалили клас завдання - старі завдання впадуть, бо класу немає.
  • Додали обов'язковий параметр конструктора - десеріалізоване старе завдання не матиме властивості.

Як робити безпечно:

  • Зміни завдань - у два кроки: спершу код, що розуміє і старий, і новий формат, і лише після того, як черга спорожніє, прибрати старе.
  • Для перейменування - залишити старий клас-обгортку, що делегує новому, на один реліз.
  • Перед ризиковим деплоєм можна призупинити чергу (queue:pause), дати воркерам добити поточні завдання й поновити після.

Докладніше в документації: Воркери черги й деплой