Урок 05

Laravel Horizon

Что такое Horizon

Horizon — панель управления Redis-очередями в Laravel.

Что даёт:

  • UI — веб-интерфейс для мониторинга.
  • Метрики — throughput, wait time, jobs processed.
  • Failed jobs — retry из UI.
  • Auto-balance — распределение воркеров.
  • Supervisor — вместо supervisor из ОС.
  • Теги — группировка.
  • Алерты — при сбоях.

Установка

composer require laravel/horizon
php artisan horizon:install

Что создаёт:

  • config/horizon.php — конфиг.
  • app/Providers/HorizonServiceProvider.php — доступ.
  • Миграции (если нужны).

Запуск

php artisan horizon

Что происходит: запускаются воркеры согласно конфигу.

UI: /horizon.

Доступ

HorizonServiceProvider::gate:

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

Что даёт: только админы видят UI.

Конфиг — environments

config/horizon.php:

'environments' => [
    'production' => [
        'supervisor-1' => [
            'connection' => 'redis',
            'queue' => ['default'],
            'balance' => 'auto',
            'maxProcesses' => 10,
            'maxTime' => 0,
            'maxJobs' => 0,
            'memory' => 128,
            'tries' => 1,
            'timeout' => 60,
            'nice' => 0,
        ],
    ],

    'local' => [
        'supervisor-1' => [
            'connection' => 'redis',
            'queue' => ['default'],
            'balance' => 'simple',
            'maxProcesses' => 3,
            'tries' => 3,
        ],
    ],
],

Supervisor — логическая группа

Можно несколько:

'production' => [
    // Срочные задачи
    'supervisor-critical' => [
        'connection' => 'redis',
        'queue' => ['critical'],
        'balance' => 'false',
        'processes' => 5,
        'tries' => 3,
        'timeout' => 30,
    ],

    // Обычные
    'supervisor-default' => [
        'connection' => 'redis',
        'queue' => ['default'],
        'balance' => 'auto',
        'maxProcesses' => 10,
        'tries' => 3,
        'timeout' => 60,
    ],

    // Долгие (email, PDF)
    'supervisor-long' => [
        'connection' => 'redis',
        'queue' => ['long-running'],
        'balance' => 'auto',
        'maxProcesses' => 3,
        'timeout' => 900,
        'tries' => 1,
    ],
],

Что даёт: разные настройки для разных типов задач.

Balance

Три режима:

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

false — предсказуемо. simple — ок для равных очередей. auto — оптимально.

Параметры

  • connection — Redis.
  • queue — какие очереди слушать.
  • balance — стратегия.
  • processes — количество (для false).
  • minProcesses / maxProcesses — для auto.
  • maxTime — сколько секунд работает процесс.
  • maxJobs — сколько задач обработать и перезапуститься.
  • memory — лимит MB.
  • tries — попытки.
  • timeout — таймаут.
  • nice — приоритет.

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

Через supervisor:

/etc/supervisor/conf.d/horizon.conf:

[program:horizon]
process_name=%(program_name)s
command=php /var/www/app/artisan horizon
user=www-data
autostart=true
autorestart=true
redirect_stderr=true
stdout_logfile=/var/www/app/storage/logs/horizon.log
stopwaitsecs=3600

Или systemd:

/etc/systemd/system/horizon.service:

[Unit]
Description=Horizon
After=network.target

[Service]
Type=simple
User=www-data
WorkingDirectory=/var/www/app
ExecStart=/usr/bin/php /var/www/app/artisan horizon
Restart=always
RestartSec=5

[Install]
WantedBy=multi-user.target

Рестарт после деплоя

php artisan horizon:terminate

Что делает: все воркеры завершают текущие задачи и перезапускаются.

Через supervisor:

sudo supervisorctl restart horizon

Правило: после деплоя — horizon:terminate.

Метрики

Что видно в UI:

  • Throughput — сколько задач в час.
  • Wait time — сколько ждёт задача.
  • Runtime — сколько выполняется.
  • Failed rate — % ошибок.

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

  • Wait time растёт → воркеров мало.
  • Failed rate растёт → ошибка в коде.
  • Throughput падает → внешний сервис тормозит.

Failed jobs

UI → Failed Jobs:

  • Retry — перезапустить.
  • Retry All — все.
  • Delete — удалить.
  • View — посмотреть exception.

Теги

В Job:

public function tags(): array
{
    return ['user:' . $this->user->id, 'email'];
}

В UI: фильтр по тегам.

Notifications

Алерты при сбое:

// AppServiceProvider::boot()
use Laravel\Horizon\Events\LongWaitDetected;
use Illuminate\Support\Facades\Notification;

Event::listen(LongWaitDetected::class, function ($event) {
    Notification::route('mail', 'ops@example.com')
        ->notify(new LongWaitNotification($event));
});

Что даёт: узнаешь, что очередь встала.

События

use Laravel\Horizon\Events\JobFailed;
use Laravel\Horizon\Events\JobProcessed;

Event::listen(JobFailed::class, function ($event) {
    Log::error('Horizon job failed', [
        'job' => $event->job->payload(),
        'exception' => $event->exception,
    ]);
});

Tags — продвинуто

Динамические:

class ProcessOrder implements ShouldQueue
{
    public function tags(): array
    {
        return [
            'order:' . $this->order->id,
            'user:' . $this->order->user_id,
            'status:' . $this->order->status,
        ];
    }
}

В UI: фильтр по каждому.

Auto-scaling

Что делает:

  1. Смотрит длину очереди.
  2. Если растёт — добавляет воркеров (до maxProcesses).
  3. Если падает — уменьшает (до minProcesses).

Результат:

  • Ночью — 2 воркера.
  • Днём — 10.
  • В пик — 20.

Metrics и Prometheus

Horizon Metrics:

'fast_termination' => false,

'metrics' => [
    'trim' => [
        'recent' => 60,       // минуты
        'completed' => 10080, // неделя
        'failed' => 10080,
    ],
],

Trim: сколько хранить метрики.

Waits

Пороги:

'waits' => [
    'redis:default' => 60,
    'redis:critical' => 30,
],

Что даёт: алерт при превышении.

Практика: полная настройка

1. Установка:

composer require laravel/horizon
php artisan horizon:install

2. Конфиг — production:

'environments' => [
    'production' => [
        'supervisor-critical' => [
            'connection' => 'redis',
            'queue' => ['critical'],
            'balance' => 'false',
            'processes' => 5,
            'tries' => 3,
            'timeout' => 30,
        ],
        'supervisor-default' => [
            'connection' => 'redis',
            'queue' => ['default'],
            'balance' => 'auto',
            'minProcesses' => 2,
            'maxProcesses' => 10,
            'tries' => 3,
            'timeout' => 60,
            'nice' => 0,
        ],
        'supervisor-long' => [
            'connection' => 'redis',
            'queue' => ['long-running'],
            'balance' => 'auto',
            'maxProcesses' => 3,
            'tries' => 1,
            'timeout' => 900,
        ],
    ],
],

3. Запуск:

php artisan horizon
# UI: /horizon

4. Job с тегами:

class SendEmail implements ShouldQueue
{
    public int $tries = 3;
    public int $timeout = 30;

    public function __construct(public User $user) { }

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

    public function tags(): array
    {
        return ['email', 'user:' . $this->user->id];
    }
}

5. Dispatch:

SendEmail::dispatch($user)->onQueue('default');

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

1. Horizon без Redis:

Horizon requires Redis

Решение: QUEUE_CONNECTION=redis.

2. Забыли horizon:terminate:

Воркеры работают со старым кодом

3. Все процессы умерли:

supervisor` не настроен

Решение: supervisor или systemd.

4. Только один supervisor:

Долгая задача блокирует остальные

Решение: разные очереди.

5. Забыли Gate:

/horizon открыт для всех

Итог

  • Horizon — UI и метрики для Redis.
  • horizon:install — установка.
  • horizon — запуск.
  • environments — конфиг по среде.
  • supervisor — логическая группа.
  • balance — auto / simple / false.
  • Auto-scaling — встроен.
  • Failed jobs — retry из UI.
  • Tags — группировка.
  • horizon:terminate — рестарт после деплоя.

Дальше — Schedule (расписание).