Job queue macet karena ack datang terlambat sering terlihat seperti masalah performa biasa: antrean mulai panjang, worker tampak aktif, tidak ada error besar, tetapi data mulai ganda dan status proses saling menimpa. Dalam banyak insiden produksi, akar masalahnya bukan sekadar worker lambat, melainkan urutan eksekusi yang salah antara processing, commit, dan acknowledgement.
Pola ini berbahaya karena sistem tampak normal dari luar. CPU mungkin rendah, broker queue tetap menerima pesan, dan dashboard hanya menunjukkan retry yang naik perlahan. Namun di balik itu, job yang seharusnya selesai dianggap belum selesai karena ack terlambat atau tidak pernah tercatat. Akibatnya, job diambil ulang, diproses ulang, memicu duplikasi, backlog, dan status yang tidak konsisten.
Kasus Produksi: Gejala yang Terlihat Sepele
Bayangkan sebuah backend memproses job untuk sinkronisasi pesanan. Setiap job melakukan beberapa langkah:
- Membaca payload dari queue
- Mengambil data terkait dari database
- Memanggil API pihak ketiga
- Menyimpan hasil ke database
- Menandai job sebagai selesai dengan ack atau commit offset
Pada jam sibuk, tim melihat gejala berikut:
- Backlog queue naik terus meskipun jumlah worker tidak berubah
- Order tertentu diproses dua atau tiga kali
- Status di database bolak-balik dari processing ke done lalu kembali diproses
- Beberapa API eksternal menerima request duplikat
- Grafik error tidak terlalu tinggi, tetapi retry meningkat tajam
Yang membuat kasus ini menipu adalah metrik dasar terlihat “normal”. Worker tetap hidup, pesan tetap dikonsumsi, dan tidak ada crash total pada layanan queue. Masalahnya tersembunyi di semantik pengantaran pesan: sistem beroperasi seperti at-least-once delivery, tetapi kode worker ditulis seolah-olah queue menjamin exactly-once processing.
Timeline Insiden
Tahap 1: Backlog mulai naik
Pada awal insiden, antrean bertambah perlahan. Tim menduga beban trafik meningkat atau API eksternal melambat. Hipotesis ini masuk akal karena durasi job memang sedikit lebih tinggi dari biasanya.
Tahap 2: Muncul data ganda
Beberapa menit kemudian, ditemukan order yang dikirim dua kali ke downstream system. Fokus investigasi bergeser ke kemungkinan bug di sisi producer, misalnya endpoint membuat job dobel.
Tahap 3: Status proses tidak konsisten
Tim kemudian melihat satu order memiliki lebih dari satu jejak pemrosesan. Satu worker sudah menulis status done, tetapi worker lain tetap menjalankan job yang sama. Ini petunjuk penting bahwa duplikasi terjadi di sisi consumer, bukan producer.
Tahap 4: Retry melonjak, worker sesekali restart
Di log infrastruktur terlihat beberapa worker restart karena deploy, autoscaling, atau timeout proses. Ini memperkuat dugaan bahwa ada jendela waktu di mana job sudah menjalankan efek samping, tetapi ack belum tercatat.
Hipotesis yang Sering Salah
Sebelum menemukan akar masalah, tim biasanya melewati beberapa dugaan yang ternyata tidak menyelesaikan sumber bug:
- “Producer mengirim pesan ganda.” Bisa terjadi, tetapi jika satu message ID diproses lebih dari sekali setelah diambil consumer, masalahnya ada di sisi ack atau retry.
- “Database lambat, jadi queue macet.” Database lambat bisa memperburuk situasi, tetapi belum tentu penyebab utama duplikasi.
- “Broker queue bermasalah.” Broker sering hanya menjalankan kontraknya: jika tidak ada ack dalam waktu tertentu, pesan dianggap belum selesai.
- “Tambahkan worker saja.” Ini sering memperparah duplikasi jika akar masalah adalah ack terlambat dan job tidak idempoten.
Masalah inti biasanya ada pada kombinasi tiga hal: ack datang terlambat, visibility timeout atau lease terlalu pendek, dan operasi bisnis tidak idempoten.
Root Cause Teknis: Ack/Commit Terlambat Membuka Jendela Duplikasi
Konsep dasarnya sederhana. Saat worker mengambil job, broker queue biasanya memberi “masa pinjam” sementara: pesan dianggap sedang diproses dan tidak langsung terlihat oleh worker lain. Jika dalam periode itu worker tidak mengirim ack, commit, atau perpanjangan lease, broker menganggap job belum selesai dan akan mengantarkannya lagi.
Masalah muncul ketika urutannya seperti ini:
- Worker mengambil job
- Worker menjalankan efek samping penting, misalnya menulis ke database atau memanggil API eksternal
- Worker belum sempat ack
- Worker melambat, timeout, crash, atau koneksi ke broker terganggu
- Visibility timeout habis atau offset belum ter-commit
- Job dikirim ulang ke worker lain
Dalam skenario ini, job yang “secara bisnis” sudah dikerjakan tetap dianggap belum selesai oleh sistem queue. Jika kode tidak idempoten, efeknya langsung terlihat sebagai duplikasi.
Interaksi retry, visibility timeout, dan crash worker
Tiga komponen ini sering saling memperkuat:
- Retry: saat worker gagal ack, broker atau framework akan menjadwalkan ulang job.
- Visibility timeout: jika durasi proses lebih lama daripada waktu pinjam pesan, job muncul lagi meski worker pertama belum benar-benar selesai.
- Crash worker: jika proses mati setelah efek samping terjadi tetapi sebelum ack, sistem masuk ke kondisi setengah selesai yang paling sulit dideteksi.
Itulah sebabnya queue tampak “macet”. Sebenarnya bukan hanya lambat, melainkan menghabiskan kapasitas untuk memproses ulang pekerjaan yang sama.
Alur Worker yang Bermasalah
Berikut pseudocode worker yang terlihat wajar tetapi berisiko tinggi:
function handleJob(message) {
payload = parse(message)
markStatus(payload.orderId, "processing")
result = callExternalAPI(payload)
saveResult(payload.orderId, result)
markStatus(payload.orderId, "done")
ack(message)
}Masalah pada alur ini:
- Efek samping bisnis terjadi sebelum ack
- Tidak ada proteksi idempotensi
- Jika worker crash setelah
saveResulttetapi sebelumack, job akan diproses ulang - Status
processingdandonebisa ditulis berkali-kali oleh worker berbeda
Pada sistem yang menerapkan at-least-once delivery, alur di atas hampir pasti akan menimbulkan bug cepat atau lambat.
Versi Perbaikan: Idempotensi Dulu, Ack Belakangan dengan Aman
Perlu ditekankan: ack tetap biasanya dilakukan setelah pekerjaan selesai, bukan sebelum. Namun agar aman, operasi bisnis harus dirancang supaya eksekusi ulang tidak merusak hasil. Artinya, perbaikannya bukan sekadar memindahkan ack, melainkan mengubah semantik pemrosesan.
function handleJob(message) {
payload = parse(message)
idempotencyKey = payload.eventId
if (hasBeenProcessed(idempotencyKey)) {
ack(message)
return
}
acquireProcessingLock(idempotencyKey)
try {
if (hasBeenProcessed(idempotencyKey)) {
ack(message)
return
}
markStatusIfAllowed(payload.orderId, "processing")
result = callExternalAPIWithIdempotencyKey(payload, idempotencyKey)
beginTransaction()
saveResult(payload.orderId, result)
markProcessed(idempotencyKey)
markStatusIfAllowed(payload.orderId, "done")
commitTransaction()
ack(message)
} catch (err) {
rollbackIfNeeded()
releaseProcessingLock(idempotencyKey)
throw err
}
releaseProcessingLock(idempotencyKey)
}Yang berubah di sini:
- Ada idempotency key untuk mendeteksi apakah job sudah pernah diselesaikan
- Ada lock atau mekanisme deduplikasi untuk mencegah dua worker mengerjakan pesan yang sama bersamaan
- Status ditulis secara defensif, bukan overwrite membabi buta
- Operasi akhir dicatat atomik sebisa mungkin, misalnya menyimpan hasil dan menandai pesan sudah diproses dalam satu transaksi database
- Ack tetap terakhir, tetapi jika job datang ulang, sistem cukup mengenali bahwa efek bisnis sudah pernah diterapkan
Ini lebih realistis daripada mengejar exactly-once dari broker queue, karena di praktiknya yang lebih penting adalah exactly-once effect di level bisnis.
Langkah Isolasi Bug di Produksi
1. Korelasikan message ID dengan efek bisnis
Jangan hanya melihat log error umum. Pastikan setiap job punya identifier yang bisa dilacak dari broker sampai database dan API eksternal. Tanpa ini, duplikasi akan terlihat seperti dua request terpisah yang kebetulan mirip.
2. Bandingkan durasi proses dengan visibility timeout
Jika banyak job selesai mendekati atau melewati batas lease, kemungkinan besar pesan dipublikasikan ulang sebelum ack sempat tiba. Ini indikator kuat bahwa masalahnya ada pada timing, bukan hanya logika bisnis.
3. Cari crash di antara side effect dan ack
Periksa apakah ada restart container, deploy, OOM, timeout proses, atau disconnect ke broker tepat setelah penulisan data atau pemanggilan API. Ini jendela kegagalan yang paling kritis.
4. Audit retry policy
Lihat apakah retry terlalu agresif, tanpa jeda yang masuk akal, atau tidak membedakan error sementara dan error permanen. Retry cepat terhadap operasi non-idempoten akan memperbesar kerusakan.
5. Uji dengan fault injection
Jika memungkinkan di staging, paksa worker mati setelah langkah tertentu, misalnya setelah update database tetapi sebelum ack. Jika hasilnya sama seperti insiden produksi, Anda sudah menemukan pola kegagalannya.
Checklist Observabilitas untuk Job Queue
Supaya kasus seperti ini cepat terlihat, metrik dan log berikut sebaiknya tersedia:
- Message ID / job ID / correlation ID di semua log
- Jumlah delivery per message atau setidaknya percobaan pemrosesan
- Durasi proses per job
- Selisih waktu antara start processing dan ack/commit
- Jumlah timeout visibility lease atau indikasi redelivery
- Retry count berdasarkan jenis error
- Status worker restart, deploy, OOM, dan kill signal
- Rasio duplicate effect, misalnya insert konflik, request API ganda, atau transaksi ganda
- Backlog queue per tipe job
Jika dashboard hanya menampilkan panjang antrean dan jumlah worker aktif, Anda akan terlambat menyadari bahwa masalah sebenarnya adalah redelivery dan duplikasi.
Mitigasi Cepat Saat Insiden Berlangsung
Saat produksi sedang bermasalah, tujuan pertama bukan desain sempurna, melainkan menghentikan ledakan duplikasi.
Naikkan visibility timeout dengan hati-hati
Jika akar masalahnya job valid yang sering belum sempat ack, memperpanjang lease bisa memberi ruang. Namun ini hanya mitigasi sementara. Jika worker benar-benar hang, backlog justru bisa tertahan lebih lama.
Turunkan concurrency untuk job yang tidak idempoten
Lebih sedikit worker kadang lebih aman daripada banyak worker yang saling menggandakan efek samping. Ini terutama berguna jika sistem downstream sensitif terhadap request ganda.
Aktifkan deduplikasi di level aplikasi
Tambahkan pengecekan sederhana berbasis idempotency key atau unique constraint untuk mencegah dampak bisnis berulang. Solusi ini sering bisa diterapkan lebih cepat daripada mengubah arsitektur queue.
Bedakan retryable dan non-retryable error
Jangan perlakukan semua kegagalan sama. Error validasi permanen sebaiknya masuk dead-letter path, bukan diulang terus.
Hentikan sementara consumer tertentu jika efek bisnis berbahaya
Dalam beberapa kasus, backlog lebih aman daripada transaksi ganda. Keputusan ini bergantung pada domain bisnis: pembayaran, inventori, dan pengiriman umumnya lebih sensitif terhadap duplikasi.
Perbaikan Jangka Panjang
1. Rancang job sebagai operasi idempoten
Ini perbaikan paling penting. Gunakan idempotency key yang stabil dari event sumber, bukan ID acak per attempt. Simpan status pemrosesan di penyimpanan yang bisa di-query cepat dan konsisten.
2. Gunakan transaksi untuk menutup celah state
Jika hasil bisnis dan penanda “sudah diproses” bisa ditulis dalam satu transaksi, peluang inkonsistensi turun drastis. Jika melibatkan sistem eksternal, Anda mungkin perlu pola seperti outbox, inbox, atau rekonsiliasi periodik.
3. Selaraskan timeout dengan karakteristik job
Job berdurasi panjang membutuhkan lease lebih lama atau mekanisme perpanjangan heartbeat. Job berdurasi pendek lebih cocok dengan timeout ketat agar kegagalan cepat dipulihkan. Satu nilai timeout untuk semua tipe job sering menjadi sumber masalah.
4. Tambahkan heartbeat atau lease extension
Jika broker atau framework mendukungnya, worker yang masih sehat bisa memperpanjang masa pinjam job selama sedang bekerja. Ini mengurangi redelivery palsu pada job panjang.
5. Pisahkan efek samping eksternal dari status internal
Menulis status done terlalu dini atau terlalu sering bisa menyesatkan observabilitas. Simpan status dengan model state yang jelas, misalnya received, processing, effect-applied, acknowledged, atau status lain yang relevan dengan domain.
6. Uji skenario crash sebagai bagian dari testing
Bug queue jarang muncul di unit test biasa. Tambahkan integration test atau chaos test yang mensimulasikan:
- Worker mati sebelum ack
- Timeout saat memanggil API eksternal
- Commit database berhasil tetapi koneksi ke broker putus
- Pesan yang sama dikirim ulang beberapa kali
Kesalahan Desain yang Sering Diulang
- Menganggap ack = bukti efek bisnis selesai dengan aman. Ack hanya berarti broker menerima sinyal, bukan bahwa seluruh sistem konsisten.
- Mengandalkan queue untuk exactly-once. Di banyak sistem nyata, yang realistis adalah at-least-once plus idempotensi aplikasi.
- Menyimpan status proses tanpa kontrol konkurensi. Update status yang bebas overwrite membuat investigasi makin sulit.
- Menyamakan semua retry. Retry tanpa klasifikasi error sering membuat backlog dan duplikasi makin besar.
- Tidak punya correlation ID lintas sistem. Tanpa jejak end-to-end, akar masalah mudah salah dituduh.
Pelajaran Desain Queue untuk Developer Backend
Kasus debug backend: job queue macet karena ack datang terlambat menunjukkan satu hal penting: queue bukan sekadar buffer kerja. Ia membawa semantik pengantaran, kegagalan parsial, dan kondisi balapan yang harus dipahami sejak desain awal.
Prinsip praktis yang patut dipegang:
- Asumsikan setiap job bisa terkirim lebih dari sekali
- Rancang efek bisnis agar aman saat diproses ulang
- Jangan menilai kesehatan queue hanya dari backlog dan CPU
- Ukur waktu antara receive, process, dan ack
- Bedakan kegagalan sementara, permanen, dan parsial
- Pastikan ada strategi rekonsiliasi jika side effect lintas sistem tidak atomik
Pada akhirnya, bug seperti ini jarang selesai hanya dengan menambah worker atau memperbesar server. Solusi yang benar biasanya datang dari kombinasi idempotensi, observabilitas, timeout yang selaras, dan alur ack yang dipahami dengan benar. Jika salah satu hilang, job queue akan terlihat normal sampai hari di mana redelivery menghabiskan kapasitas dan mulai merusak data.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!