Appearance
Horizon
Laravel Horizon: the dashboard and workers of queues on Redis.
kitloom/horizon · admins: kitloom/horizon-filament, kitloom/horizon-nova
Laravel Horizon on a kitloom site. Horizon runs the workers of a queue kept in Redis and shows them in a dashboard of its own: jobs waiting, running, done and failed, retries, throughput and wait times. This module brings Horizon in, lets into its dashboard the users whose role allows it, and shows in the admin whether Horizon is running. The admin section is kitloom/horizon-filament or kitloom/horizon-nova.
Horizon works only when the site's queue is on Redis. A site whose queue stays in the database (QUEUE_CONNECTION=database) does not need this module: Horizon would not see its jobs.
What the server needs
| What | Why |
|---|---|
Redis, and the phpredis extension (or the predis/predis package) | The queue and Horizon's own records live there |
PHP extensions pcntl and posix | Horizon starts and stops worker processes. Linux and macOS have them; Windows does not, so Horizon does not run there |
A process manager that keeps php artisan horizon running | Horizon is one long-lived process. If it stops, jobs wait in the queue until it is started again |
php artisan schedule:run every minute (cron) | The module schedules horizon:snapshot, which feeds the Metrics screen; it runs on one server of several, by a lock in the cache, so a site on several servers needs a shared cache store (Redis, the database) — with each server's own cache the lock is local and the task runs on every one |
Setting it up
Point the queue to Redis in
.env:dotenvQUEUE_CONNECTION=redis REDIS_HOST=127.0.0.1 REDIS_PORT=6379 REDIS_PASSWORD=nullStart Horizon under a process manager (below). Locally, run
php artisan horizonin a terminal instead.Open the admin: the "Horizon" section has the dashboard, and its badge says running.
php artisan horizon:install is not needed. It would publish a provider into the application to decide who opens the dashboard, and the module decides that already.
supervisor
/etc/supervisor/conf.d/horizon.conf. Change the path, the user and the log file to match the server:
ini
[program:horizon]
process_name=%(program_name)s
command=php /var/www/site/artisan horizon
user=www-data
autostart=true
autorestart=true
redirect_stderr=true
stdout_logfile=/var/www/site/storage/logs/horizon.log
stopwaitsecs=3600Then run supervisorctl reread, supervisorctl update and supervisorctl start horizon. stopwaitsecs gives a long job time to finish before supervisor kills it; it should be longer than the longest job.
systemd
/etc/systemd/system/horizon.service:
ini
[Unit]
Description=Laravel Horizon
After=network.target redis.service
[Service]
User=www-data
WorkingDirectory=/var/www/site
ExecStart=/usr/bin/php artisan horizon
Restart=always
TimeoutStopSec=3600
[Install]
WantedBy=multi-user.targetThen run systemctl enable --now horizon.
Forge, Ploi, Docker
- Forge, Ploi: add a daemon with the command
php artisan horizon. They run it under supervisor for you. - Docker: a container of the site's image with the command
php artisan horizonandrestart: unless-stopped. The image needspcntlandposix(docker-php-ext-install pcntl posix).
On every deploy
bash
php artisan horizon:terminateHorizon finishes the jobs it is running and exits; the process manager starts it again with the new code. Without this, the workers keep running the old code.
The badge in the admin
| Badge | What it means | What to do |
|---|---|---|
| running | Horizon processes the queue | — |
| paused | Someone ran horizon:pause; jobs wait | php artisan horizon:continue |
| not running | Nothing processes the queue; jobs wait | Start php artisan horizon, check the process manager and its log |
| Redis is down | Redis does not answer: neither the queue nor Horizon works | Start Redis, check REDIS_* in .env |
| queue not on Redis | The site queues jobs somewhere else; Horizon does not see them | QUEUE_CONNECTION=redis, or remove the module |
The same in the terminal: php artisan horizon:status.
Who opens the dashboard
/horizon opens for users whose role has the Open the Horizon dashboard permission (view_horizon). Administrators have it from the start; it is given to other roles on the roles screen. The dashboard shows the jobs' data and can retry and delete them, so give it with care. Everyone else gets "403 Forbidden", in every environment, local included.
An application that decides this itself with Horizon::auth() sets kitloom-horizon.authorize to false.
Deactivating the module
Deactivating Horizon on the Modules screen also stops loading laravel/horizon: the dashboard, its routes and the horizon commands go with the module (unless the application requires laravel/horizon in its own composer.json). The process manager's php artisan horizon then fails at its next restart, and the switch restarts it — so first stop that program, or change its command to php artisan queue:work if the site keeps a queue. Horizon's records in Redis stay, and activating the module brings everything back.
Workers, environments, alerts
How many workers run, on which queues, and when Horizon adds more is Horizon's own configuration: php artisan vendor:publish --tag=horizon-config writes config/horizon.php, documented in Horizon's docs. Two things to know:
- Horizon starts workers only for the environments listed under
environmentsin that file:productionandlocalout of the box. A site withAPP_ENV=stagingpublishes the file and adds its environment (or'*', which matches any), or Horizon starts, says it is running, and processes nothing. - The dashboard's address is
horizon.path(/horizon), and the admin links follow it.
Alerts about long waits (horizon.waits) are sent once the application tells Horizon where: Horizon::routeMailNotificationsTo('[email protected]') in a service provider.
Configuration
php artisan vendor:publish --tag=kitloom-horizon-config writes kitloom-horizon.php:
| Key | Default | What |
|---|---|---|
authorize | true | The module decides who opens the dashboard (view_horizon) |
snapshot | true | horizon:snapshot every five minutes while the queue is on Redis |
filament.register, nova.register | true | The "Horizon" section of each admin |
For developers
Kitloom\Horizon\HorizonDashboard (bound per request) is what the admins read: status() (running, paused, inactive, unreachable, not-on-redis), label(), tone() (success, warning, danger), explanation(), url(), allows($user), onRedis(). It asks Redis once per request and does not ask at all when the queue is not on Redis, so an admin page never waits for a Redis that is not there.