Untuk memastikan worker berbasis Rust Tokio memproses job secara konsisten, kita membutuhkan strategi queue, cache, dan locking yang menangani retry, kegagalan, dan state bersama. Pendekatan desain ini menekankan koordinasi worker, konsistensi cache lokal vs terdistribusi, dan pola seperti dedup key serta lease lock agar job tidak diproses ganda dan data tidak usang.

Artikel ini langsung menjawab bagaimana mengatur queue di Tokio agar worker tidak saling tumpang-tindih, menjaga cache tetap akurat, serta memudahkan observability saat terjadi masalah operasional. Tidak hanya konsep, disertakan contoh implementasi dan pertimbangan trade-off.

1. Arsitektur queue dasar dengan Tokio

Di Tokio, queue biasanya diimplementasikan dengan mpsc atau channel berbasis shared state yang dibaca oleh sejumlah worker. Untuk konsistensi, setiap job perlu berisi metadata seperti dedup key dan timestamp. Dedup key mencegah job diproses ulang saat worker retry; job baru dibandingkan dengan record di cache atau storage sebelum dieksekusi.

Contoh pendekatan:

async fn worker_loop(mut rx: mpsc::Receiver) {
    while let Some(job) = rx.recv().await {
        if !is_duplicate(&job.dedup_key).await {
            reserve_lease(&job).await;
            process(job).await;
            release_lease(&job).await;
        }
    }
}

Fungsi is_duplicate memeriksa cache lokal atau terdistribusi. Idealnya cache menyimpan dedup key dengan TTL singkat agar job yang benar-benar gagal bisa diproses ulang setelah waktu tertentu.

Koordinasi worker dan konsistensi job

Untuk worker yang berjalan paralel, gunakan lease lock: worker merekam bahwa ia mengerjakan job dengan ID tertentu dan memiliki lease waktu tertentu. Jika job tidak selesai sebelum lease habis, job dianggap gagal dan ditaruh kembali ke queue. Lease lock bisa disimpan di Redis, database relasional, atau storage konsisten lainnya.

Penting untuk membedakan antara job retry dan job ganda akibat race condition. Untuk menghindari queue bolak-balik (job yang terus masuk keluar karena retry gagal), worker harus menandai status job dan memperpanjang lease saat proses memakan waktu panjang.

2. Cache lokal vs cache terdistribusi

Strategi cache menentukan kecepatan deteksi duplikasi dan konsistensi data status job.

Cache lokal

Cache lokal (misalnya DashMap atau TTL cache dalam worker) cocok untuk dedup key dan status sementara karena akses cepat tanpa latensi jaringan. Namun, cache lokal tidak sinkron antar worker, menyebabkan kemungkinan worker lain tidak melihat job yang sudah dikerjakan. Gunakan cache lokal hanya untuk optimisasi, bukan sumber kebenaran.

Cache terdistribusi

Cache terdistribusi seperti Redis memungkinkan semua worker membaca status job yang sama. Gunakan untuk lease lock dan dedup key utama. Penting untuk menambahkan TTL agar cache tidak menahan job lama secara permanen. Implementasi lease lock bisa memanfaatkan perintah SET key value NX PX timeout dan perpanjangan lease dengan PEXPIRE.

Trade-off: cache terdistribusi memberi konsistensi tinggi tapi menambah latensi dan potensi bottleneck. Monitor latensi Redis/DB dan fallback ke queue retry jika cache tidak tersedia.

3. Locking: Optimistic vs Pessimistic

Locking menentukan bagaimana worker berkolaborasi tanpa konflik.

Optimistic locking

Optimistic locking cocok ketika konflik jarang. Worker membaca status job, menjalankan proses, lalu menulis kembali dengan kondisi bahwa versi tidak berubah. Implementasinya bisa menggunakan SELECT ... WHERE version = ? atau counter di cache. Jika versi berubah, worker menganggap job sudah ditangani dan mengembalikan job ke queue dengan delay.

Kelebihan: throughput tinggi saat konflik rendah. Kekurangan: lebih banyak retry ketika konflik sering terjadi.

Pessimistic locking

Pessimistic locking berarti worker mengambil lock eksplisit sebelum memproses job. Lease lock Redis merupakan bentuk pessimistic: key dibuat untuk job, worker lain tidak dapat mengeksekusi job itu selama lock aktif. Pastikan key dilepas saat job selesai atau lewat timeout jika worker mati.

Gunakan pessimistic ketika job sensitif terhadap duplikasi atau data yang diubah secara kritis.

4. Konsistensi saat retry dan kegagalan

Retry harus mempertimbangkan apakah job meninggalkan state partial. Terapkan strategi idempotent di workflow: misalnya, worker menyimpan progress checkpoint untuk tiap job sehingga retry dapat memulai dari titik aman.

Ketika job gagal sebelum lease habis, mekanisme lease harus mendeteksi worker mati (misal, heartbeat atau lease timeout). Queue bisa menunda job dengan backoff eksponensial agar tidak menekan resource saat ada kegagalan sistemik.

Pola dedup key di cache membantu memastikan job yang sudah berhasil tidak diproses ulang jika queue bolak-balik terjadi karena delay ack.

5. Observability dan debugging operasional

Monitor metrik penting: antrean (queue depth), rata-rata waktu job, jumlah lease expired, dan cache miss. Selain itu:

  • Queue bolak-balik: cek apakah worker mem-backoff job terlalu cepat tanpa memperpanjang lease. Tambahkan trace untuk ack/NAK dan perpanjang lease saat pemrosesan memakan waktu.
  • Cache stale: pastikan TTL cache sesuai dengan waktu processing job dan ada mekanisme refresh jika data dianggap kadaluarsa. Gunakan timestamp atau versioning agar worker bisa mendeteksi cache tidak sinkron.
  • Worker starvation: lacak distribusi job per worker. Jika ada worker idle sementara lainnya overload, pertimbangkan mekanisme penyeimbangan ulang atau pembatasan concurrency per worker.

Gunakan tracing (contoh: Jaeger) untuk menelusuri job saat bergerak dari queue ke worker hingga menyimpan hasil, sehingga Anda bisa mengetahui titik kegagalan atau retry berulang.

6. Praktik debugging

Langkah-langkah praktis:

  1. Periksa dedup key dan lease lock di cache terdistribusi. Pastikan tidak ada job terkunci terlalu lama tanpa progress.
  2. Gunakan log level terperinci di worker untuk mencatat alasan retry dan status lease.
  3. Simulasikan kegagalan worker dengan mengirim sinyal atau mematikan proses untuk memastikan queue kembali mengantre ulang job.
  4. Cek latency cache dan database untuk memastikan timeout tidak menyebabkan retry berantai.

Dengan observability yang tepat, Anda bisa mengidentifikasi pola umum seperti job stuck di queue karena lease tidak dilepas atau cache stale yang membuat dedup key tidak terdeteksi.