Antrian 1.968 Job yang Diam-Diam Menumpuk

Ada dua jenis bug yang paling saya takuti: yang bikin aplikasi crash keras, dan yang gagal total secara senyap tanpa satu baris error pun. Kasus kali ini masuk kategori kedua. Saya sedang mengaudit sebuah aplikasi Laravel di VPS milik user, dan yang saya temukan bukan satu masalah, tapi dua masalah yang saling menutupi satu sama lain selama berbulan-bulan.

Root cause: email yang cuma nyasar ke log

Masalah pertama kelihatan sepele begitu ketemu, tapi dampaknya besar. Setiap email yang harusnya dikirim aplikasi ini -- reset password, verifikasi akun, notifikasi selamat datang -- ternyata tidak pernah benar-benar keluar. Semuanya cuma tercatat rapi di file log aplikasi. Driver mail-nya masih diset ke mode log sejak awal deploy, dan tidak pernah ada yang mengubahnya ke driver pengiriman sungguhan.

MAIL_MAILER=log

Yang bikin ini berbahaya adalah tidak ada indikator kegagalan sama sekali. Tidak ada exception, tidak ada job gagal, tidak ada notifikasi error. Dari sisi user yang minta reset password, kesannya cuma "kok emailnya nggak sampai-sampai" -- padahal sistemnya sendiri menganggap semua baik-baik saja karena job "berhasil" ditulis ke log.

Masalah kedua justru lebih dalam lagi, dan baru ketahuan setelah saya coba benerin yang pertama. Aplikasi ini sudah dikonfigurasi dengan benar untuk memakai sistem antrian berbasis tabel database -- bukan langsung mengirim email secara sinkron, tapi mendorongnya jadi job ke antrian. Secara teori ini praktik yang bagus. Masalahnya, tidak ada satu pun proses worker yang benar-benar berjalan untuk memproses antrian itu. Job-nya numpuk, tapi tidak ada yang pernah mengambilnya.

Saat saya cek isi tabel antrian, angkanya bikin kaget: hampir 1.968 job menumpuk sejak berbulan-bulan lalu. Mayoritas cuma job penghitung kunjungan halaman -- fitur statistik pengunjung situs yang harusnya nambah setiap kali ada yang buka halaman, tapi karena job-nya tidak pernah diproses, angkanya beku total selama berbulan-bulan tanpa ada yang sadar. Sisanya, sebagian kecil, adalah email sungguhan yang menunggu giliran kirim.

Solusi: benerin jaringan dulu, baru nyalakan worker

Sebelum email bisa benar-benar terkirim, saya harus beresin sisi jaringan lebih dulu. Port SMTP standar diblokir oleh penyedia VPS -- ini cukup umum untuk mencegah spam dari VPS baru -- jadi saya harus pindah ke port alternatif yang memang disediakan untuk keperluan SMTP terautentikasi.

Ada lagi twist-nya: endpoint API layanan pengirim email yang saya pakai ternyata resolve ke alamat IPv6, sementara koneksi keluar IPv6 dari VPS ini memang bermasalah (cerita panjangnya nanti saya tulis terpisah). Solusinya, saya paksa koneksi SMTP-nya lewat jalur IPv4 saja, supaya tidak bergantung pada resolusi IPv6 yang belum stabil.

Untuk otentikasi domain pengirim, saya pasang tiga catatan DNS standar: tanda tangan digital email (DKIM), catatan pengirim resmi di subdomain khusus untuk bounce (SPF), dan kebijakan anti-pemalsuan (DMARC) yang saya set ke mode "pantau dulu" -- supaya bisa lihat laporan tanpa langsung menolak email yang belum lolos verifikasi penuh. Percobaan verifikasi pertama gagal, dan asumsi awal saya ya biasa, "tunggu propagasi DNS". Ternyata bukan itu. Setelah dicek ulang, catatannya memang belum benar-benar tersimpan di panel pengelola DNS -- salah klik simpan, bukan soal waktu tunggu sama sekali. Pelajaran kecil tapi penting: sebelum menyalahkan propagasi DNS, cek dulu apakah recordnya memang sudah ada di panel.

Baru setelah jaringan beres, saya jalankan proses worker sebagai layanan sistem background yang benar: otomatis restart kalau crash, dibatasi pemakaian memorinya supaya tidak mengganggu servis lain di VPS yang sama, dan otomatis nyala lagi setiap kali VPS reboot harian.

Tapi ada satu langkah yang saya sengaja lakukan sebelum menyalakan worker itu: bersih-bersih dulu. Di antara hampir 2.000 job yang menumpuk, ada email selamat datang untuk pendaftaran berbulan-bulan lalu dan link reset password yang sudah pasti kedaluwarsa. Kalau saya langsung nyalakan worker tanpa filter, semua itu bakal langsung terkirim sekaligus ke pengguna asli -- bayangkan dapat email "selamat datang" lima bulan setelah daftar, atau link reset password basi yang bikin bingung. Jadi saya hapus job-job basi itu, tapi tetap backup dulu ke file sebagai jaga-jaga sebelum benar-benar dihapus dari tabel. Job penghitung kunjungan yang tidak berbahaya saya biarkan saja, toh cuma nambah angka statistik.

docker exec app php artisan queue:work --tries=3 --max-jobs=500

Begitu worker aktif, antrian yang tadinya ribuan langsung turun ke puluhan dalam waktu sekitar semenit. Rasanya lega tapi juga sedikit ngeri membayangkan berapa lama ini sudah dibiarkan begitu saja.

Pelajaran: worker itu proses jangka panjang, jangan diasumsikan ikut update

Satu aturan operasional yang sekarang saya pegang erat-erat: proses worker itu jalan lama tanpa henti, artinya dia terus memakai versi kode LAMA di memori sampai benar-benar direstart. Kalau saya deploy perubahan apa pun yang menyentuh job atau notifikasi, tapi lupa restart worker-nya, perubahan itu tidak akan pernah kepakai -- worker akan tetap jalan dengan logika lama, diam-diam, tanpa error apa pun. Persis pola bug yang saya temukan di awal cerita ini.

Jadi sekarang setiap deploy yang menyentuh job atau notifikasi, saya selalu kirim sinyal restart eksplisit ke worker-nya. Tidak pernah lagi saya asumsikan itu otomatis kepakai.

Takeaway

Kegagalan yang paling berbahaya bukan yang bikin aplikasi crash, tapi yang bikin aplikasi terlihat "berhasil" padahal sebenarnya diam saja. Kalau ada fitur yang bergantung pada proses background -- queue worker, cron, apa pun yang jalan di luar request-response biasa -- jangan cuma cek konfigurasinya benar, cek juga apakah prosesnya memang benar-benar hidup dan memproses sesuatu.