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.
Завдання можуть падати через тимчасові збої (мережа, 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,RateLimitedmiddleware керують поведінкою.- Ідемпотентність (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), дати воркерам добити поточні завдання й поновити після.