Queue, Scheduler, dan Horizon di VPS: Job Tidak Boleh Mati Diam-Diam
Gejalanya selalu mirip: pengguna mendaftar, halaman menampilkan pesan sukses, tapi email verifikasi tidak pernah sampai. Atau laporan harian yang biasanya terkirim pukul tujuh pagi, suatu hari tidak ada kabarnya sama sekali — dan tidak ada yang memberi tahu Anda. Masalahnya jarang terletak pada kode pengiriman email. Seringkali, worker queue yang seharusnya memproses job itu sudah berhenti berhari-hari, tanpa satu pun alarm yang berbunyi.
Ini artikel keempat seri Deploy Laravel ke VPS. Artikel sebelumnya membahas urutan deploy tanpa downtime, termasuk perintah queue:restart sesudah go live. Sekarang kita mengurus sisi yang membuat job tetap berjalan di antara dua deploy: worker yang dijaga process monitor, scheduler yang dipicu cron, dan — bila Anda memakai Redis — Horizon sebagai panel pantau.
Tiga komponen, tiga tanggung jawab berbeda
Banyak kebingungan berawal dari mencampuradukkan tiga hal ini. Mari pisahkan dengan tegas:
- Queue worker (
queue:work) adalah proses yang menunggu job masuk antrean, lalu mengerjakannya satu per satu. Tanpa worker yang hidup, job hanya menumpuk di tabel atau Redis. - Scheduler (
schedule:run) adalah pemicu. Cron sistem operasi memanggilnya tiap menit, ia memeriksa jadwal yang Anda tulis diroutes/console.php, dan menjalankan yang sudah waktunya — atau melempar job ke antrean. - Horizon adalah lapisan manajemen di atas queue Redis: dashboard, penyeimbang worker otomatis, dan metrik. Horizon bukan pengganti Supervisor; Horizon tetap butuh Supervisor untuk menjaganya tetap hidup.
Analogi sederhananya: scheduler adalah alarm yang berbunyi tiap menit, worker adalah orang yang bangun dan bekerja, dan Horizon adalah papan tulis tempat Anda melihat siapa sedang mengerjakan apa.
Prasyarat dan pilihan driver antrean
Sebelum menyentuh Supervisor, putuskan driver antrean di shared/.env:
QUEUE_CONNECTION=database
Driver database adalah titik awal paling gampang: tidak butuh layanan tambahan, job tersimpan di tabel jobs, dan tabel gagal di failed_jobs. Buat tabelnya dengan:
php artisan queue:table php artisan queue:failed-table php artisan migrate --force
Perintah queue:table dan queue:failed-table menghasilkan file migrasi untuk kedua tabel tersebut; perintah migrate kemudian membuatnya di database. Periksa bahwa tabel jobs serta failed_jobs muncul di database.
Bila trafik sudah lebih besar atau Anda ingin memakai Horizon, pilih Redis:
QUEUE_CONNECTION=redis REDIS_HOST=127.0.0.1 REDIS_PORT=6379
Redis lebih cepat dan tidak membebani database dengan polling, tapi menambah satu layanan yang harus dijaga tetap hidup.
Satu jebakan awal: mengubah QUEUE_CONNECTION di .env tanpa membangun ulang cache konfigurasi tidak berpengaruh bila config:cache aktif. Setelah mengubah .env, jalankan php artisan config:cache di folder rilis aktif. Dan pastikan nilai ini konsisten antara web dan worker: worker yang membaca konfigurasi lama akan memakai driver lama.
Langkah 1: Pasang Supervisor dan buat konfigurasi worker
Supervisor adalah process monitor yang menjaga proses tetap hidup: ia menjalankan worker saat boot, me-restart saat crash, dan mencatat lognya.
sudo apt update sudo apt install supervisor -y
Kemudian buat file konfigurasi worker:
sudo nano /etc/supervisor/conf.d/laravel-worker.conf
Isinya:
[program:laravel-worker] process_name=%(program_name)s_%(process_num)02d command=php /var/www/laravel-app/current/artisan queue:work database --sleep=3 --tries=3 --max-time=3600 autostart=true autorestart=true stopasgroup=true killasgroup=true user=www-data numprocs=2 redirect_stderr=true stdout_logfile=/var/www/laravel-app/shared/storage/logs/worker.log stopwaitsecs=3600
Mari bedah baris yang paling menentukan. command menunjuk ke current/artisan — bukan ke folder rilis tertentu — supaya sesudah deploy, worker baru yang di-restart otomatis memakai kode terbaru. --sleep=3 memberi jeda tiga detik antar polling saat antrean kosong, --tries=3 membatasi percobaan ulang sebuah job sampai tiga kali sebelum dipindah ke tabel gagal, dan --max-time=3600 membuat worker berhenti sendiri setelah satu jam agar memori yang menumpuk dibersihkan oleh proses baru. numprocs=2 menjalankan dua worker paralel; sesuaikan dengan jumlah CPU dan beban job Anda, bukan sekadar meniru contoh. stopwaitsecs=3600 memberi job panjang waktu menyelesaikan pekerjaannya sebelum Supervisor mematikan proses secara paksa.
Aktifkan konfigurasi:
sudo supervisorctl reread sudo supervisorctl update sudo supervisorctl start laravel-worker:*
Hasil yang diharapkan: reread melaporkan grup laravel-worker tersedia, update menambahkannya, dan perintah start tidak mengembalikan error. Periksa statusnya:
sudo supervisorctl status laravel-worker:*
Output yang sehat menampilkan kedua proses dalam keadaan RUNNING dengan uptime yang bertambah. Bila statusnya FATAL atau BACKOFF, baca log di shared/storage/logs/worker.log sebelum mengubah apa pun — pesan error pertama di log hampir selalu lebih jujur daripada tebakan.
Kesalahan umum: worker berjalan sebagai user yang salah sehingga tidak bisa menulis log atau membaca file aplikasi. Samakan user di konfigurasi Supervisor dengan pemilik file aplikasi dan user PHP-FPM — dalam contoh ini www-data. Kesalahan kedua: jalur artisan menunjuk ke folder rilis lama yang sudah dihapus saat pembersihan rilis. Itulah alasan memakai current/artisan: symlink selalu mengarah ke rilis aktif.
Langkah 2: Buktikan worker benar-benar memproses job
Worker yang RUNNING belum tentu memproses job. Buktikan dengan job sungguhan:
php artisan make:job SendWelcomeEmail
Lalu kirim job itu dari tinker:
php artisan tinker
dispatch(new App\Jobs\SendWelcomeEmail());
Periksa tiga tempat:
php artisan queue:monitor database:default sudo supervisorctl status laravel-worker:* tail -n 50 /var/www/laravel-app/shared/storage/logs/worker.log
Hasil yang diharapkan: job hilang dari tabel jobs, log tidak menunjukkan exception, dan worker tetap RUNNING. Bila job menumpuk, cek tiga hal: koneksi antrean yang dipakai worker, nama antrean yang didengarkan, dan cache konfigurasi yang belum dibangun ulang.
Job yang gagal masuk ke tabel gagal. Lihat daftarnya dan coba ulang satu job:
php artisan queue:failed php artisan queue:retry <id-job>
Langkah 3: Scheduler — satu baris cron untuk semua jadwal
Tulis jadwal di routes/console.php, bukan di crontab satu per satu:
use Illuminate\Support\Facades\Schedule;
Schedule::command('invoices:send-reminders')->dailyAt('07:00');
Schedule::command('reports:daily-summary')->daily()->withoutOverlapping();
Schedule::job(new CleanupOldUploads)->weekly();withoutOverlapping() mencegah tugas yang sama berjalan tumpang tindih bila eksekusi sebelumnya belum selesai, dan Schedule::job(...) cocok untuk tugas berat yang lebih baik dijalankan lewat antrean.
Lalu tambahkan satu baris cron ini, lewat crontab -e:
* * * * * cd /var/www/laravel-app/current && php artisan schedule:run >> /dev/null 2>&1
Lima bintang berarti tiap menit. Bagian cd penting: scheduler harus berjalan dari folder rilis aktif supaya memakai kode terbaru. Pengalihan ke /dev/null mencegah cron mengirim email setiap menit.
Verifikasi jadwalnya:
php artisan schedule:list php artisan schedule:test
Kesalahan umum: baris cron dipasang di crontab root, bukan di crontab user pemilik aplikasi, sehingga file yang ditulisnya dimiliki root. Dan ingat: jadwal baru hanya berlaku setelah rilis baru go live, karena scheduler membaca routes/console.php dari rilis aktif.
Langkah 4: Log, restart otomatis, dan cara cek worker masih hidup
Gunakan tiga lapisan pemeriksaan. Lapisan 1, status dan antrean:
sudo supervisorctl status php artisan queue:monitor database:default
Lapisan 2, log Laravel dan log worker:
tail -n 100 /var/www/laravel-app/shared/storage/logs/laravel.log tail -n 100 /var/www/laravel-app/shared/storage/logs/worker.log
Log yang diam total adalah sinyal, bukan ketenangan: worker yang sehat menulis sesuatu secara berkala, entah itu job yang diproses atau polling yang kosong.
Lapisan 3, restart sesudah deploy. Jangan me-restart Supervisor secara paksa kecuali darurat; pakai perintah yang dirancang Laravel:
cd /var/www/laravel-app/current php artisan queue:restart
Urutan yang benar sesudah mengubah konfigurasi: ubah shared/.env, jalankan php artisan config:cache di folder current, lalu queue:restart supaya worker baru membaca konfigurasi baru.
Langkah 5: Horizon — opsional, tapi mengubah cara Anda melihat antrean
Horizon hanya berlaku bila QUEUE_CONNECTION=redis. Pasang dan migrasikan:
composer require laravel/horizon php artisan horizon:install php artisan migrate --force
Atur supervisor Horizon di config/horizon.php, bagian environments untuk production dan supervisor-1:
'connection' => 'redis', 'queue' => ['default', 'notifications'], 'balance' => 'auto', 'minProcesses' => 1, 'maxProcesses' => 10, 'tries' => 3,
Strategi balance menentukan cara worker dibagi: auto menyesuaikan jumlah worker per antrean berdasarkan beban, simple membagi rata, dan false mematikan penyeimbangan otomatis.
Buat file Supervisor untuk Horizon:
sudo nano /etc/supervisor/conf.d/horizon.conf
[program:horizon] command=php /var/www/laravel-app/current/artisan horizon user=www-data autostart=true autorestart=true redirect_stderr=true stdout_logfile=/var/www/laravel-app/shared/storage/logs/horizon.log stopwaitsecs=3600
sudo supervisorctl reread sudo supervisorctl update sudo supervisorctl start horizon
Dashboard Horizon ada di /horizon. Kunci dengan gate di provider Horizon dan jangan pernah membiarkannya terbuka untuk publik — dashboard itu memperlihatkan isi job Anda.
Sesudah setiap deploy, jalankan:
php artisan horizon:terminate
Perintah ini menghentikan proses Horizon lama supaya Supervisor menjalankannya lagi dengan kode terbaru, sama seperti queue:restart untuk worker biasa.
Kapan tidak memakai Horizon
Horizon menambah Redis, satu proses Supervisor, dan satu dashboard yang perlu diamankan. Bila Anda hanya memproses puluhan job per hari lewat driver database dan 1–2 worker sudah cukup, Horizon adalah beban tambahan, bukan kebutuhan. Mulailah dari Supervisor + cron, ukur lewat queue:monitor dan log, dan pindah ke Horizon saat Anda benar-benar butuh visibilitas real-time atau penyeimbangan worker otomatis.
Tabel ringkasan
| Komponen | Konfigurasi | Perintah cek | Tanda sehat |
|---|---|---|---|
| Queue worker | Supervisor (laravel-worker.conf) | supervisorctl status laravel-worker:* | RUNNING, uptime bertambah |
| Job yang diproses | Worker + tabel jobs | queue:monitor, queue:failed | Job hilang dari antrean, gagal nol atau tertangani |
| Scheduler | Cron per menit → schedule:run | schedule:list | Tugas muncul dengan waktu jalan berikutnya |
| Restart sesudah deploy | queue:restart / horizon:terminate | Log worker / Horizon | Worker baru memakai kode terbaru |
| Horizon (opsional) | Supervisor (horizon.conf) | Dashboard /horizon + supervisorctl status horizon | Supervisor Horizon aktif, metrik bergerak |
Checklist akhir
QUEUE_CONNECTIONsama antara web dan worker, dan cache dibangun ulang.- Tabel
jobsdanfailed_jobsada (driver database) atau Redis hidup (driver Redis). - Supervisor terpasang,
laravel-worker:*berstatus RUNNING. - Satu job percobaan diproses sampai habis.
- Baris cron
schedule:runper menit ada di crontab user pemilik aplikasi. schedule:listmenampilkan semua tugas dengan waktu berikutnya yang masuk akal.withoutOverlapping()dipakai untuk tugas yang durasinya tidak menentu.queue:restartmasuk ke skrip deploy.- Log worker dan log Laravel dibaca berkala.
- Bila memakai Horizon:
horizon.confaktif, dashboard terkunci, danhorizon:terminatemasuk ke skrip deploy.
Job yang mati diam-diam adalah bug yang paling lama bertahan, justru karena tidak ada halaman error — hanya ketiadaan. Supervisor, satu baris cron, dan kebiasaan memeriksa status adalah penawarnya.
Pernah mengalami job berhenti tanpa kabar? Apa yang pertama kali Anda periksa — log worker, tabel jobs, atau status Supervisor? Ceritakan di komentar.
Baca juga: artikel seri 03 tentang deploy Laravel tanpa downtime tempat perintah queue:restart dan horizon:terminate masuk ke urutan go live, panduan deploy otomatis ke VPS dengan GitHub Actions untuk mengotomatiskan pemeriksaan ini tiap rilis, dan panduan membaca log server dengan journalctl dan logrotate untuk memeriksa keluaran tugas terjadwal.


Komentar
Posting Komentar