โ† Semua pembelajaran / Laravel Nol โ†’ Enterprise
Fase 3 ยท Arsitektur Laravel

Queue & job

Antrean mengubah "pengguna menunggu 8 detik" jadi "pengguna menunggu 80 milidetik dan pekerjaannya selesai belakangan". Yang perlu dipelajari adalah kegagalan, pengulangan, dan idempotensi.

Sumber asli laravel.com Resmi Rangkuman ~9 menit baca

Intisari

  • Job adalah kelas dengan handle(). Job::dispatch($data) menaruhnya di antrean.
  • Driver: sync (langsung, untuk lokal), database, redis, sqs. Produksi di AWS memakai Redis atau SQS.
  • Job akan dijalankan lebih dari sekali. Tulis supaya aman diulang โ€” atau pakai ShouldBeUnique.
  • Selalu sediakan tries, backoff, timeout, dan tabel failed jobs.
  • Laravel 13 menambahkan Queue::route() dan atribut #[Tries], #[Backoff], #[Timeout].

Sebuah job

use Illuminate\Queue\Attributes\Tries;
use Illuminate\Queue\Attributes\Backoff;
use Illuminate\Queue\Attributes\Timeout;

#[Tries(5)]
#[Backoff([10, 60, 300])]
#[Timeout(120)]
class BuatInvoicePdf implements ShouldQueue
{
    use Queueable;

    public function __construct(public readonly int $pesananId) {}

    public function handle(PembuatPdf $pdf): void      // dependensi diinjeksi container
    {
        $pesanan = Pesanan::findOrFail($this->pesananId);

        // Idempoten: kalau sudah pernah dibuat, berhenti โ€” job bisa jalan dua kali.
        if ($pesanan->invoice_path) {
            return;
        }

        $path = $pdf->buat($pesanan);

        $pesanan->update(['invoice_path' => $path]);
    }

    public function failed(Throwable $e): void
    {
        Log::error('invoice gagal', ['pesanan' => $this->pesananId, 'sebab' => $e->getMessage()]);
    }
}
BuatInvoicePdf::dispatch($pesanan->id);
BuatInvoicePdf::dispatch($id)->onQueue('berat')->delay(now()->addMinutes(5));
BuatInvoicePdf::dispatch($id)->afterCommit();       // hanya kalau transaksi berhasil

Kirim ID, bukan objek besar. Job diserialisasi jadi teks di dalam antrean. Trait SerializesModels memang menyimpan ID saja, tapi properti lain โ€” array besar, koleksi, closure โ€” ikut diserialisasi utuh. Payload SQS dibatasi 256 KB, dan payload gemuk memperlambat semuanya. Kirim ID, ambil datanya di dalam handle().

Idempotensi: aturan yang tidak bisa ditawar

Job akan dijalankan lebih dari sekali. Bukan karena kodemu buruk, melainkan karena: worker mati di tengah jalan setelah pekerjaan selesai tapi sebelum sempat menghapus pesannya; SQS Standard memang menjamin at-least-once; percobaan ulang terjadi setelah timeout padahal yang pertama masih berjalan.

PolaBahaya kalau berjalan dua kaliPerbaikan
Kirim emailPengguna menerima dua kaliCatat penanda kirim, cek dulu
Tagih kartuDitagih dua kaliKunci idempotensi di sisi gerbang pembayaran
increment('poin', 10)Poin dobelSimpan sebagai baris transaksi, bukan penambahan
Buat PDFBoros, tapi tidak merusakCek keberadaan berkasnya lebih dulu
// Satu job aktif per kunci โ€” mencegah tumpang tindih
class SinkronKatalog implements ShouldQueue, ShouldBeUnique
{
    public $uniqueFor = 3600;

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

Memilih driver

DriverKapanPerlu diingat
syncLokal & tesBukan antrean โ€” berjalan seketika di proses yang sama
databaseAplikasi kecil, satu serverSetiap worker mem-polling database
redisDefault yang baik di AWSLatensi rendah, dan mendukung Horizon (Fase 7)
sqsSkala besar, lintas layananTerkelola penuh; batas payload 256 KB, tanpa Horizon
// Laravel 13: routing terpusat, tidak perlu diulang di tiap dispatch
Queue::route(BuatInvoicePdf::class, connection: 'redis', queue: 'berat');
Queue::route(KirimEmail::class,     connection: 'sqs',   queue: 'notifikasi');

Menjalankan worker

# produksi: proses sekali jalan lalu keluar, dijaga supervisor/ECS
php artisan queue:work redis --queue=penting,default --tries=3 --max-time=3600

# lokal: memuat ulang kode tiap job
php artisan queue:listen
OpsiGunanya
--queue=penting,defaultPrioritas: penting dihabiskan dulu
--tries=3Batas percobaan sebelum masuk failed jobs
--max-time=3600Keluar setelah sejam โ€” mengatasi kebocoran memori
--max-jobs=1000Keluar setelah sekian job; efek yang sama
--timeout=60Bunuh job yang menggantung

queue:work memuat kode ke memori dan tidak melihat perubahanmu. Setelah deploy, worker wajib di-restart (php artisan queue:restart) atau task-nya diganti โ€” kalau tidak, worker lama akan memproses job baru dengan kode lama. Di Fase 9, ini jadi satu poin eksplisit dalam urutan deploy ECS.

Job yang gagal

php artisan queue:failed              # daftar
php artisan queue:retry all           # coba lagi semuanya
php artisan queue:retry 5             # satu job
php artisan queue:flush               # buang

Tabel failed_jobs adalah jaring pengaman terakhir. Kalau ia tidak pernah dibaca, kegagalan berarti pekerjaan yang hilang diam-diam. Pasang alarm atas jumlah barisnya โ€” di Fase 9 ini menjadi metrik CloudWatch.

Batch dan rantai

// Berurutan: berhenti kalau salah satu gagal
Bus::chain([
    new TagihPembayaran($id),
    new BuatInvoicePdf($id),
    new KirimStruk($id),
])->dispatch();

// Paralel, dengan callback setelah semuanya selesai
Bus::batch($produk->map(fn ($p) => new SinkronKeMarketplace($p->id)))
    ->then(fn (Batch $b) => Log::info('sinkron selesai', ['total' => $b->totalJobs]))
    ->catch(fn (Batch $b, Throwable $e) => Log::error('sinkron gagal'))
    ->allowFailures()
    ->dispatch();

Latihan: buat job BuatInvoicePdf yang sengaja throw pada percobaan pertama dan berhasil pada percobaan kedua. Jalankan dengan --tries=3, amati jeda backoff-nya, lalu ubah supaya selalu gagal dan periksa isi tabel failed_jobs.

Rangkuman ini sengaja dipangkas ke bagian yang dipakai di roadmap. Buka sumber aslinya saat kamu butuh detail lengkap atau referensi parameter.