Laravel menghadapi kondisi saat beberapa worker menarik job dari queue bersamaan dengan memadukan cache lock Redis/Memcached, sehingga hanya satu worker yang dapat mengeksekusi job tertentu bersamaan. Selain itu, sistem retry idempotent memastikan bahwa job boleh dijalankan ulang tanpa merusak state, sedangkan monitoring dan fallback lock membuat operator cepat mengenali gangguan queue.

Memahami cache lock sebagai koordinator queue Laravel

Ketika worker menarik job dari queue, ia belum tentu menjadi satu-satunya yang mendapatkan payload tersebut—terutama jika queue driver seperti Redis atau Beanstalkd memproses visibilitas timeout. Laravel menempatkan cache lock sebagai gatekeeper: sebelum menjalankan job yang sensitif, worker memegang lock berdasarkan identifier job atau resource yang diakses. Lock ini mencegah worker lain masuk ke blok kode yang sama, menjaga konsistensi perubahan data.

Contoh implementasi cache lock pada worker pipeline

Penempatan lock bisa dilakukan langsung di dalam job handle() atau middleware pipeline. Contoh penggunaan:

public function handle(ProcessInvoice $job)
{
    $lock = Cache::lock('invoice:'.$job->invoice_id, 25);

    if (! $lock->get()) {
        return $this->release(5);
    }

    try {
        $this->processInvoice($job);
    } finally {
        $lock->release();
    }
}

Dengan menggunakan Cache::lock, Laravel memanfaatkan Redis atau Memcached sesuai konfigurasi cache.default. Pastikan lock tersebut membawa timeout (di atas visibilitas queue) agar lock tidak kadaluarsa sebelum job selesai. Jika lock didapatkan, worker langsung mengeksekusi job; bila tidak, ia bisa merilis job untuk dicoba ulang dengan jeda.

Timeout, fallback, dan retry

Timeout lock harus lebih panjang dari waktu eksekusi normal supaya tidak ada kerja setengah jalan yang menyebabkan race condition saat lock otomatis dilepas. Namun lock tidak boleh terlalu lama karena worker lain bisa menunggu tanpa batas. Aturan praktis: set timeout sekitar 25–30 detik saat job rata-rata 10–15 detik. Jika job memang bisa menunggu lebih lama, gunakan block() untuk menunggu mendapatkan lock:

Cache::lock('invoice:'.$job->invoice_id, 40)->block(10, function () use ($job) {
    $this->processInvoice($job);
});

Jika lock kadaluarsa sementara job masih berjalan, harus ada fallback error handling dengan logging dan flag requeue agar operator dapat mendeteksi dan menghindari duplikasi. Gunakan retryUntil() untuk menentukan aturan retry, serta log informasi lock yang gagal agar bisa dianalisis.

Retry idempotent dan konsistensi state

Laravel menyarankan agar job idempotent; artinya apabila job dijalankan ulang karena retry, hasilnya tetap konsisten. Strategi yang sering dipakai:

  • Menggunakan identifier unik (misalnya job_request_id) yang disimpan dalam tabel atau cache. Sebelum memproses, periksa apakah sudah ada hasil untuk identifier tersebut.
  • Membuat status pada database (misalnya kolom processing_status) dan menggunakan lock cache sebagai pengontrol akses. Update status via transaction agar tidak terjadi race condition antara cache dan database.
  • Menghindari operasi non-idempotent—seperti debiting akun—tanpa verifikasi lock dan status.

Contoh sederhana untuk menghindari duplikasi:

public function handle(ProcessPayment $job)
{
    return Cache::lock('payment:'.$job->payment_id, 30)->get(function () use ($job) {
        if ($job->payment->status === 'paid') {
            return;
        }

        $this->attemptCharge($job);
        $job->payment->update(['status' => 'paid']);
    });
}

Dengan cara ini, walau job di-release ulang karena lock belum tersedia, update status tidak akan berulang selama logika memeriksa status terlebih dahulu.

Operasional queue, observability, dan monitoring

Laravel tim mengandalkan observability untuk mendeteksi masalah queue. Praktik utama:

  • Metrics: Pantau jumlah job dalam queue:work, job gagal, job menunggu lebih lama dari visibilitas timeout. Export metric ini ke Prometheus atau StatsD agar pergi ke dashboard (misalnya Grafana Alert jika backlog > threshold).
  • Logging: Setiap lock acquire/release, timeout, dan error dicatat. Sertakan context seperti job_id, lock_key, dan stack trace jika ada exception.
  • Alert stuck job: Buat script/command yang memeriksa job yang sudah dijalankan lebih lama dari 2x visibilitas timeout. Laravel Horizon sudah menyertakan indikator recent_jobs, tetapi sistem kustom bisa membaca tabel job (database queue) dan memeriksa attempts serta reserved_at.

Jika lock kadaluarsa sebelum job selesai (misal worker nge-hang), sistem harus mampu mendeteksi job yang sedang memegang lock lama tadi. Salah satu pendekatan adalah memperbarui timestamp heartbeat di cache setiap beberapa detik selama job berjalan, lalu worker lain dapat mengambil lock jika heartbeat hilang, dengan catatan masih memastikan idempotensi.

Trade-off: granular vs coarse lock

Desain lock harus mempertimbangkan keseimbangan antara granularitas dan throughput. Lock granular (per baris/tabel/ID) meminimalkan blocking, tapi meningkatkan kompleksitas dan kemungkinan deadlock; contoh: locking per invoice_id agar hanya job terkait invoice tertentu yang menunggu. Lock coarse (per operasi/batch) lebih mudah dikelola namun bisa menurunkan paralelisme—jika satu job memegang lock yang mencakup banyak resource, worker lain harus menunggu.

Untuk memilih:

  • Gunakan lock granular saat job memodifikasi data yang bisa diisolasi, agar worker lain tetap bisa memproses job lain.
  • Gunakan lock coarse ketika operasi memerlukan konsistensi global atau saat ada dependency yang sulit dijaga (misal batch sync ke API eksternal).

Race condition antara cache dan database biasanya muncul saat lock dilepas tapi operasi database belum selesai di-commit. Solusi: lakukan update database di dalam transaction dan release lock di finally. Cara lainnya adalah menyimpan status di cache terlebih dahulu, lalu commit DB; bila commit gagal, pastikan lock tidak dilepas supaya job bisa dicoba ulang dengan penanganan kesalahan.

Kesimpulan

Memanfaatkan cache lock Redis/Memcached di Laravel memungkinkan worker queue bersaing tanpa merusak konsistensi job, terutama jika digabungkan dengan retry idempotent dan observability yang baik. Konfigurasi timeout, retry, logging, dan pemilihan granularitas lock menjadi kunci agar sistem queue tetap dapat diskalakan dan cepat ditangani saat terjadi stuck job.