Ketika queue worker CodeIgniter 4 menjalankan job yang juga memperbarui cache Redis, refresh cache atau instance worker paralel dapat menyebabkan race condition dan data tidak konsisten. Artikel ini langsung membahas bagaimana menggabungkan arsitektur queue berbasis CLI command, cache Redis, dan lock terdistribusi (Redlock atau setnx) untuk memastikan job tetap idempotent dan cache tidak korup saat worker restart, auto-scale, atau cache invalidasi.

Setiap langkah dilengkapi contoh konfigurasi, strategi lock, dan pengawasan agar Anda bisa menerapkan solusi operasional yang nyata di lingkungan production.

Masalah Operasional Queue Worker dan Cache Redis

Queue worker yang berjalan paralel sering memanggil cache Redis untuk membaca atau menyimpan data. Jika worker A sedang memperbarui cache sementara worker B mengonsumsi job dengan data usang, atau cache di-flush secara manual/dengan TTL pendek, data bisa tertulis dua kali atau read-after-write gagal. Ini terjadi lebih sering saat job tidak idempotent dan tidak ada mekanisme locking yang mencakup cache.

Race condition lain muncul ketika lock tidak dilepas karena worker mati, atau instance baru mulai sebelum lock yang lama berakhir, sehingga job kritis dilewatkan atau dijalankan dua kali. Masalah ini mempengaruhi konsistensi cache dan kehandalan queue, terutama selama auto-scaling atau roll-out.

Arsitektur Queue, Cache, dan Lock Terdistribusi

Komponen utama

  • Queue Worker: CLI command CodeIgniter 4 yang membaca job dari sistem queue (misalnya database, Redis, atau service queue eksternal) dan menjalankan logika business.
  • Cache Redis: menyimpan data yang sering dipakai oleh job. Redis dipilih karena mendukung perintah atomik yang dibutuhkan oleh lock dan TTL.
  • Lock Terdistribusi: implementasi Redlock atau pola manual dengan SET key value NX PX ttl / SETNX untuk memastikan hanya satu worker yang memodifikasi cache tertentu saat job berjalan.

Contoh konfigurasi Redis di app/Config/Cache.php:

'redis' => [
    'handler' => '\CodeIgniter\Cache\Handlers\RedisHandler',
    'backup'  => 'file',
    'host'    => '127.0.0.1',
    'password'=> null,
    'port'    => 6379,
    'timeout' => 0,
    'database'=> 0,
]

Pastikan Redis diatur dengan persistence minimal (AOF hybrid) agar lock dan cache tetap stabil saat restart.

Desain Job Idempotent dan Mekanisme Locking

Job harus idempotent agar, walaupun dijalankan ulang karena lock kadaluarsa, kondisi akhir tetap konsisten. Strategi:

  • Simpan identifier unik job (misalnya hash dari payload) di Redis sebelum mutasi.
  • Sertakan versi data yang diperbarui agar worker bisa memutuskan apakah perlu menulis ulang cache.
  • Rollback partial jika lock gagal dilepas.

Lock dipasang sebelum operasi cache kritis dimulai. Prosesnya:

  1. Worker membaca job dari queue.
  2. Membentuk key lock berbasis konteks, contoh: cache-lock:user:123.
  3. Mencoba SET key uniqueValue NX PX 120000 (lock 2 menit). Jika gagal, worker bisa menunda retry atau enqueue ulang.
  4. Jika berhasil, worker melakukan update cache dan job logic.
  5. Setelah selesai, worker hanya menghapus lock jika value sesuai untuk mencegah melepaskan lock milik worker lain.

Penempatan lock juga harus mencakup cache invalidation: saat job mengirimkan event cache invalidasi, worker lain harus menunggu lock selesai agar tidak membaca data lama.

Implementasi Lock Terdistribusi dan Pseudocode Worker

Implementasi pada CodeIgniter 4 dapat dibangun sebagai command CLI. Berikut pseudocode yang mencakup lock dan idempotent check:

public function execute(): void
{
    $job = $this->queue->nextJob();
    if (! $job) {
        return;
    }

    $lockKey = "cache-lock:{$job['cache_context']}";
    $lockToken = bin2hex(random_bytes(16));

    if (! $this->redis->set($lockKey, $lockToken, ['nx', 'px' => 120000])) {
        $this->queue->retry($job);
        return;
    }

    try {
        if ($this->isJobStale($job)) {
            $this->queue->ack($job);
            return;
        }

        $this->processJob($job);
        $this->updateCache($job);
        $this->queue->ack($job);
    } finally {
        $this->releaseLock($lockKey, $lockToken);
    }
}

Fungsi releaseLock harus memastikan hanya token yang sama yang boleh menghapus lock, seperti pola Lua script:

local value = redis.call('GET', KEYS[1])
if value == ARGV[1] then
  return redis.call('DEL', KEYS[1])
end
return 0

Jika Redlock diinginkan, gabungkan dengan beberapa node Redis dan pastikan clock drift kecil.

Recovery Saat Lock Tertinggal dan Monitoring

Lock tidak boleh bertahan terlalu lama. TTL perlu dipasang sesuai waktu maksimum job. Namun jika worker mati setelah mengambil lock, TTL memastikan lock akhirnya hilang. Tips:

  • Gunakan watchdog untuk memeriksa job lama yang masih di-lock: jika job awalnya sedang dalam queue lebih dari TTL + margin, buat alert.
  • Gunakan proses recovery yang mencoba menghapus lock jika job sudah selesai atau tidak ada worker aktif (misalnya periksa heartbeat worker).
  • Selalu log state lock, job id, dan token untuk memudahkan audit.

Untuk monitoring konsistensi cache dan antrian:

  • Pantau queue length, retry count, dan failed jobs.
  • Gunakan dashboard Redis (misalnya RedisInsight) untuk melihat key lock yang masih aktif.
  • Jalankan health check yang mengeksekusi job dummy ringan untuk memastikan worker mendapat lock dan bisa menyelesaikan.

Debugging umum:

  • Jika worker sering gagal ambil lock: tambahkan logging lock key dan token, lalu pastikan tidak ada job parallel dengan lock sama lebih dari satu.
  • Jika lock masih ada setelah job selesai: periksa apakah finally pernah dijalankan (misalnya karena PHP fatal). Tambahkan shutdown handler untuk release lock atau gunakan supervisor untuk restart worker.
  • Jika cache invalidasi tidak konsisten: tunjukkan versi cache di job dan baca version di redis sebelum menulis hasil.

Mekanik ini menjaga agar queue worker CodeIgniter 4 tetap stabil bahkan saat cache di-refresh, job dijalankan paralel, atau worker otomatis diskalakan.