Урок 04

Очереди и Horizon

Зачем очереди

Что даёт:

  • Ответ сразу.
  • Тяжёлое — в фон.
  • Retry при сбое.
  • Масштаб воркеров.
  • Приоритеты.

Что вынести:

  • Email.
  • Webhooks.
  • Отчёты.
  • Ресайз.
  • Индексация.

Правило: что > 200 мс и не нужно в ответе — в очередь.

Драйверы очередей

Доступные:

  • sync — сразу, для тестов.
  • database — таблица.
  • redis — стандарт.
  • sqs — AWS.
  • beanstalkd.

Правило: в проде — Redis или SQS.

Установка

.env:

QUEUE_CONNECTION=redis

Миграция (для database):

php artisan queue:table
php artisan migrate

Первая job

php artisan make:job SendWelcomeEmail
class SendWelcomeEmail implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    public function __construct(public User $user) {}

    public function handle(): void
    {
        Mail::to($this->user)->send(new WelcomeMail($this->user));
    }
}

Dispatch:

SendWelcomeEmail::dispatch($user);

Правило: ShouldQueue — обязательно для async.

Запуск воркера

php artisan queue:work
php artisan queue:work --queue=high,default
php artisan queue:work --tries=3 --timeout=60

Что важно:

  • --tries — попыток.
  • --timeout — макс. сек.
  • --memory — лимит памяти воркера.

Правило: воркер в проде — через Supervisor / Horizon.

Supervisor

/etc/supervisor/conf.d/laravel-worker.conf:

[program:laravel-worker]
process_name=%(program_name)s_%(process_num)02d
command=php /var/www/skills/api/artisan queue:work --sleep=3 --tries=3 --max-time=3600
autostart=true
autorestart=true
numprocs=4
user=www-data
redirect_stderr=true
stdout_logfile=/var/log/laravel-worker.log
stopwaitsecs=3600

Что даёт: авто-рестарт, логи.

Правило: Supervisor — базовый вариант.

Horizon

Что это: дашборд и менеджер очередей Redis.

Что даёт:

  • UI для очередей.
  • Auto-balance воркеров.
  • Метрики — throughput, latency.
  • Failed jobs в UI.
  • Тэги для группировки.

Правило: Horizon — стандарт для Redis.

Horizon: установка

composer require laravel/horizon
php artisan horizon:install
php artisan migrate

Что создаёт:

  • config/horizon.php.
  • app/Providers/HorizonServiceProvider.php.
  • Дашборд на /horizon.

Horizon: доступ

HorizonServiceProvider:

protected function gate(): void
{
    Gate::define('viewHorizon', function (User $user) {
        return $user->is_admin;
    });
}

Правило: Horizon — только для админов.

Horizon: конфиг

config/horizon.php:

'environments' => [
    'production' => [
        'supervisor-default' => [
            'connection' => 'redis',
            'queue' => ['default'],
            'balance' => 'auto',
            'minProcesses' => 1,
            'maxProcesses' => 10,
            'maxTime' => 3600,
            'maxJobs' => 1000,
            'memory' => 256,
            'tries' => 3,
            'timeout' => 60,
            'nice' => 0,
        ],
        'supervisor-high' => [
            'connection' => 'redis',
            'queue' => ['high'],
            'balance' => 'simple',
            'processes' => 4,
        ],
    ],
],

Что важно: разные supervisor для разных очередей.

Balance strategies

  • simple — фиксированное число процессов.
  • auto — по нагрузке.
  • false — один процесс на очередь.

Правило: auto — для смешанных очередей.

Приоритеты очередей

Dispatch:

SendEmail::dispatch($user)->onQueue('high');
GenerateReport::dispatch($report)->onQueue('low');

Воркер:

php artisan queue:work --queue=high,default,low

Что даёт: слева — раньше.

Правило: 3 очереди максимум.

Задержка

SendEmail::dispatch($user)->delay(now()->addMinutes(5));
SendEmail::dispatch($user)->delay(300);

Что важно: для database — свой планировщик.

Правило: Redis — delay из коробки.

Retry

В job:

public int $tries = 5;
public array $backoff = [10, 30, 60, 120, 300];
public int $maxExceptions = 3;

Что даёт: умный retry с backoff.

Правило: $backoff — растущий.

Failed jobs

Таблица failed_jobs:

php artisan queue:failed
php artisan queue:retry all
php artisan queue:forget 5
php artisan queue:flush

В Horizon: вкладка Failed.

Правило: failed — не забывать, иначе растёт.

unique jobs

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

    public function uniqueId(): string
    {
        return 'order:' . $this->order->id;
    }
}

Что даёт: job не запустится дважды.

Правило: идемпотентность — обязательна.

Middleware jobs

Rate limiting:

public function middleware(): array
{
    return [new RateLimited('api')];
}

WithoutOverlapping:

public function middleware(): array
{
    return [(new WithoutOverlapping($this->user->id))->expireAfter(300)];
}

Правило: для пользовательских операций — без overlap.

Batches

Bus::batch([
    new ProcessFile('a.csv'),
    new ProcessFile('b.csv'),
])
->then(fn (Batch $batch) => Log::info('Done'))
->catch(fn (Batch $batch, $e) => Log::error('Failed'))
->finally(fn (Batch $batch) => Log::info('Finished'))
->dispatch();

Что даёт: группа задач как одна.

Правило: batch — когда важно знать итог группы.

Chains

Bus::chain([
    new ChargeOrder($order),
    new SendReceipt($order),
    new UpdateAnalytics($order),
])->dispatch();

Что даёт: последовательное выполнение.

Правило: chain — порядок важен.

Scheduler

routes/console.php:

Schedule::job(new CleanupOldData)->daily();
Schedule::command('cache:prune-stale-tags')->hourly();

Воркер:

php artisan schedule:work

Cron:

* * * * * cd /var/www/skills/api && php artisan schedule:run >> /dev/null 2>&1

Правило: schedule:work — в dev, cron — в проде.

Дебаг job

public function handle(): void
{
    Log::info('Job started', ['id' => $this->order->id]);

    try {
        // работа
    } catch (\Throwable $e) {
        Log::error('Job failed', ['error' => $e->getMessage()]);
        throw $e;
    }
}

Правило: логировать начало и ошибки.

Job в Horizon UI

Что видно:

  • Pending jobs.
  • Completed.
  • Failed.
  • Silenced.
  • Runtime.

Правило: Horizon → Jobs — первый экран при инциденте.

Метрики Horizon

Что смотреть:

  • Throughput — jobs/min.
  • Wait time — latency.
  • Runtime — длительность.
  • Failed rate.

Правило: wait time растёт → воркеров мало.

Очереди в Laravel 11+

Что нового:

  • Queue::route() — централизованно.
  • Connection в каждом классе.

AppServiceProvider:

Queue::route(SendEmail::class, 'emails');
Queue::route(GenerateReport::class, 'reports');

Правило: маршрутизация — в одном месте.

Практика

1. Установите Horizon.

2. Настройте 3 очереди: high, default, low.

3. Настройте Gate для Horizon.

4. Вынесите email в очередь.

5. Вынесите отчёт в очередь.

6. Настройте retry с backoff.

7. Сделайте batch для импорта.

8. Настройте schedule:work в dev.

9. Проверьте Supervisor в проде.

10. Смотрите throughput в Horizon.

Частые ошибки

1. Job без ShouldQueue:

Синхронно

Решение: implements.

2. Публичный Horizon:

Доступ всем

Решение: Gate.

3. Одна очередь для всего:

High ждёт low

Решение: разделить.

4. Без retry:

Потеря job

Решение: $tries + $backoff.

5. Без unique:

Дубли

Решение: ShouldBeUnique.

6. Воркер без memory:

OOM

Решение: --memory=256.

7. Failed не смотрят:

Растёт таблица

Решение: алерт.

8. Schedule без cron:

Не работает в проде

Решение: cron.

Итог

  • Очереди — async обработка.
  • Redis — драйвер в проде.
  • Horizon — UI + метрики.
  • Приоритеты — high/default/low.
  • Retry + backoff — обязательно.
  • Unique jobs — идемпотентность.
  • Batches — группы.
  • Chains — последовательности.
  • Scheduler — cron + schedule:run.
  • Gate — доступ к Horizon.

Дальше — Laravel Octane: Swoole и RoadRunner.