Queue Laravel harus tetap berjalan meski terjadi retry, deadlock, atau beban tinggi. Dengan menggabungkan cache state job, locking yang elegan, dan pemantauan worker, tim dapat menekan duplikasi serta memastikan data tetap konsisten.

Artikel ini membahas pola cache agar job tidak dieksekusi ganda, mekanisme lock agar satu worker menangani satu job, tata cara menangani retry secara konsisten, serta langkah operasional untuk menghindari deadlock atau cache stampede.

Mengamankan Queue Laravel dengan Cache State

Gunakan cache untuk menyimpan status hidup job: pending, processing, failed. Ini membantu mencegah job yang sama diproses ulang ketika ada restart worker atau retry otomatis. Skema sederhana adalah menulis key seperti queue:job-status:{job_id} dalam Redis saat job diambil, lalu hapus ketika selesai.

Contoh implementasi di dalam job:

public function handle()
{
    $cacheKey = "queue:job-status:" . $this->job->getJobId();

    if (Cache::get($cacheKey) === 'processing') {
        return; // sudah ditangani worker lain
    }

    Cache::put($cacheKey, 'processing', now()->addMinutes(5));

    try {
        // logika job
    } finally {
        Cache::forget($cacheKey);
    }
}

Dengan demikian status job terbaca oleh sistem monitoring atau job tandem. Pastikan TTL cache lebih lama dari waktu eksekusi rata-rata, agar status tidak hilang saat job masih berjalan.

Locking Job dan Worker untuk Mencegah Duplikasi

Laravel menyediakan Cache::lock() untuk membuat distributed lock berbasis Redis atau memcached. Pola ini berguna saat job mungkin dipanggil oleh lebih dari satu worker atau ketika job disubmits ulang akibat timeout queue.

Contoh lock di job:

$lockKey = "queue:job-lock:" . $this->job->getJobId();
$lock = Cache::lock($lockKey, 60);

if (! $lock->get()) {
    return; // worker lain sudah memegang lock
}

try {
    // proses job
} finally {
    $lock->release();
}

Gunakan lock dengan TTL minimal satu kali durasi eksekusi untuk menghindari deadlock karena worker mati sebelum release(). Jika job bisa memakan waktu lama, pertimbangkan blockFor untuk menunggu lock yang dilepaskan daripada langsung skip.

Diagram alur cache + lock

[Job Dispatched]
      |
[Add cache status: pending]
      |
[Worker mengambil job]
      |---> [Cache::lock] ----> [Lock berhasil?]
      |                          |
      |                         Yes
      |                          |
      |                   [Status jadi processing]
      |                          |
      |                [Eksekusi job, update DB]
      |                          |
[Lock release] <---------------+ 
      |
      +--> [Cache::forget status]
      |
[Job selesai atau retry]

Diagram ini memastikan tidak ada dua worker yang mengubah status sekaligus, dan cache status mencerminkan kondisi real-time.

Strategi Konsistensi Setelah Retry

Saat job gagal lalu retry, jangan biarkan state cache menyebabkan job dianggap sudah selesai. Kita perlu mereset status sebelum job dijalankan ulang, lalu memastikan job idempotent.

  • Reset state: gunekan listener pada event JobProcessing dan JobFailed untuk menghapus atau mengubah status cache.
  • Idempotensi: pastikan operasi database bisa dijalankan berkali-kali dengan hasil sama, atau gunakan flag di database untuk menandai progress.
  • Retry policy yang adaptif: gunakan backoff bertingkat untuk menghindari beban spike.

Misalnya, saat job gagal karena masalah koneksi, listener bisa memperbarui cache status menjadi failed_retry, lalu job otomatis di-queue ulang setelah backoff. Jangan lupa update attempts untuk membatasi retry agar tidak looping.

Konfigurasi Queue Driver dan Cache Store

Gunakan driver Redis dengan konfigurasi yang konsisten antara queue dan cache:

// config/queue.php
'connections' => [
    'redis' => [
        'driver' => 'redis',
        'connection' => 'default',
        'queue' => env('QUEUE_NAME', 'default'),
        'retry_after' => 90,
    ],
],

// config/cache.php
'stores' => [
    'redis' => [
        'driver' => 'redis',
        'connection' => 'default',
    ],
],

Pastikan retry_after lebih besar dari waktu eksekusi maksimal job. Jika worker mati sebelum TTL lock habis, konfigurasi ini mencegah job diambil worker lain secara prematur.

Monitor Worker dan Deteksi Responsifitas

Worker yang tidak responsif menjadi risiko utama. Gunakan alat seperti Laravel Horizon atau supervisor (systemd) untuk menjaga jumlah worker, logs, dan metrics.

Tips monitoring:

  • Gunakan php artisan queue:restart untuk gracefully restart worker setelah deploy; ini memastikan instance baru memegang environment terbaru.
  • Pantau failed_jobs; jika job sering gagal di tahap locking atau cache, cek status Redis atau issue deadlock.
  • Pastikan worker log mencatat waktu mulai dan akhir job serta ID lock, agar mudah runut saat debugging.
  • Gunakan redis-cli MONITOR secara periodik untuk memastikan key lock dan status tidak menumpuk.

Worker harus memiliki health-check yang memeriksa pembacaan queue. Jika menggunakan Horizon, atur alert saat job backlog tinggi atau worker tidak online.

Solusi Deadlock dan Cache Stampede

Deadlock: terjadi saat lock tidak dilepas. Terapkan lock timeout dan pastikan finally selalu menjalankan release(). Jika worker mati sebelum selesai, TTL lock otomatis habis, namun add callback untuk log kejadian agar bisa dievaluasi.

Cache stampede: terjadi saat banyak worker memproses job serupa dan secara serentak mengisi cache. Gunakan lock->blockFor(5) agar worker menunggu lock ketimbang langsung menulis ulang cache.

Operasionalnya:

  • Atur alert jika jumlah cache key queue:job-lock aktif melebihi jumlah worker. Ini menandakan lock menumpuk atau worker terpaku.
  • Gunakan TTL singkat untuk status pending agar cache tidak stale; tapi jangan terlalu pendek hingga status hilang sebelum job sempat aktif.

Tambahkan routine pembersihan: jika ada key queue:job-status lebih lama dari threshold tanpa job aktif, nilai bisa dianggap stale dan dihapus manual.

Kesimpulan

Dengan pola cache status job, locking terkoordinasi, konsistensi retry, dan monitoring worker, antrean Laravel bisa lebih tahan gangguan. Implementasi yang jelas, logging lengkap, dan pengecekan operasional rutin mengurangi risiko duplikasi ataupun deadlock.

Selalu dokumentasikan asumsi TTL, waktu eksekusi, dan konfigurasi driver agar tim bisa bereaksi cepat saat kondisi abnormal terjadi.