Visibility timeout queue adalah mekanisme yang mencegah satu job diambil ulang oleh worker lain selama jangka waktu tertentu setelah job tersebut diterima untuk diproses. Tujuannya sederhana: memberi kesempatan kepada worker pertama untuk menyelesaikan pekerjaan tanpa memicu proses ganda.
Masalahnya, banyak insiden queue terjadi bukan karena sistem antreannya rusak, melainkan karena visibility timeout terlalu pendek, terlalu panjang, atau tidak selaras dengan durasi job nyata. Akibatnya, job yang masih berjalan dianggap hilang, muncul lagi ke antrean, lalu diproses oleh worker lain. Di sisi lain, jika timeout terlalu panjang, kegagalan worker baru terlihat terlambat dan backlog ikut membesar.
Apa itu visibility timeout dan mengapa penting
Saat worker mengambil job dari queue, job tersebut biasanya tidak langsung dihapus. Job hanya dibuat invisible atau tidak terlihat oleh worker lain untuk sementara. Jika worker selesai dengan sukses, ia mengirim sinyal selesai seperti acknowledge, delete, atau status sejenis agar job benar-benar keluar dari queue.
Jika worker crash, kehilangan koneksi, mati karena OOM, atau prosesnya lebih lama dari durasi yang diizinkan, maka setelah visibility timeout habis job akan terlihat lagi dan bisa diambil ulang. Inilah alasan queue modern umumnya bersifat at-least-once delivery, bukan exactly-once.
Intinya: visibility timeout bukan jaminan job hanya diproses sekali. Ia hanya mengurangi kemungkinan job diambil paralel oleh banyak worker dalam satu jendela waktu.
Alur kerja queue: dari ambil job sampai sukses atau gagal
Alur normal
- Producer mengirim job ke queue.
- Worker melakukan consume atau receive job.
- Sistem queue menandai job sebagai tidak terlihat selama visibility timeout.
- Worker memproses job.
- Jika berhasil, worker mengirim ack/delete.
- Job dihapus permanen dari queue.
Alur saat worker gagal
- Worker mengambil job.
- Job menjadi tidak terlihat selama, misalnya, 60 detik.
- Worker crash di detik ke-35 atau macet lebih lama dari 60 detik.
- Tidak ada ack/delete yang diterima queue.
- Saat 60 detik habis, job muncul lagi.
- Worker lain mengambil job yang sama.
Di sinilah proses ganda muncul. Jika worker pertama ternyata belum benar-benar berhenti dan masih sempat menulis hasil, maka dua worker bisa sama-sama melakukan efek samping ke database, cache, atau API eksternal.
Timeline kegagalan yang sering memicu proses ganda
Kasus 1: job berjalan lebih lama dari visibility timeout
Misalnya durasi rata-rata job adalah 20 detik, tetapi sesekali ada job 2 menit karena memanggil layanan eksternal yang lambat.
- T+0: Worker A mengambil job, visibility timeout 30 detik.
- T+25: Worker A masih memproses.
- T+30: Timeout habis, job muncul lagi.
- T+31: Worker B mengambil job yang sama.
- T+50: Worker A selesai dan menulis hasil.
- T+80: Worker B juga selesai dan menulis hasil kedua.
Efeknya bisa berupa pengiriman email dobel, stok berkurang dua kali, saldo dipotong ulang, atau cache tertimpa nilai lama.
Kasus 2: worker crash setelah efek samping terjadi
- Worker berhasil menulis data ke database.
- Sebelum mengirim ack, proses mati.
- Job kembali ke queue setelah timeout habis.
- Job dijalankan ulang, efek samping terulang.
Kasus ini menunjukkan bahwa masalah tidak hanya muncul saat job lama, tetapi juga saat ada jeda antara side effect dan ack.
Kasus 3: retry terlalu cepat memperparah antrean
Jika job gagal karena dependency sementara lambat, dan sistem segera me-retry tanpa jeda yang tepat, worker akan terus mengambil ulang job yang sama. Gejalanya backlog naik, CPU worker tinggi, dan antrean didominasi job yang belum mungkin sukses.
Bedanya visibility timeout, retry delay, lock, dan ack/nack
Visibility timeout
Durasi saat job yang sudah diambil worker disembunyikan dari worker lain. Fokusnya adalah lease pemrosesan.
Retry delay
Jeda sebelum job yang gagal dicoba lagi. Fokusnya adalah kapan percobaan berikutnya dimulai, bukan menyembunyikan job yang sedang diproses.
Contoh: job gagal karena API pihak ketiga mengembalikan 503. Memberi retry delay 30 detik berguna agar sistem tidak langsung menembak ulang API yang masih bermasalah. Ini berbeda dari visibility timeout yang bekerja ketika job sudah sedang dipegang worker.
Lock
Lock digunakan untuk mencegah eksekusi bersamaan pada resource atau kunci tertentu, misalnya satu order_id hanya boleh diproses satu kali dalam satu waktu. Lock dapat berada di Redis, database, atau sistem koordinasi lain.
Perbedaannya: visibility timeout bekerja pada level job di queue, sedangkan lock bekerja pada level resource atau identitas bisnis. Dalam sistem yang sensitif, keduanya sering dipakai bersama.
Ack/Nack
Ack berarti worker menyatakan job selesai diproses dengan sukses sehingga job boleh dihapus dari antrean. Nack atau sinyal gagal berarti job tidak sukses dan dapat dikembalikan ke queue atau diarahkan ke jalur retry, tergantung implementasi broker.
Visibility timeout mengatur masa pinjam job; ack/nack mengatur hasil akhir pemrosesannya.
Ringkasnya: visibility timeout menjawab "berapa lama job disembunyikan saat sedang diproses?", retry delay menjawab "kapan job gagal dicoba lagi?", lock menjawab "siapa yang boleh menyentuh resource ini?", dan ack/nack menjawab "apakah job selesai atau gagal?".
Gejala umum saat konfigurasi visibility timeout salah
1. Job diproses ganda
Gejala paling jelas adalah side effect yang muncul lebih dari sekali: email dobel, invoice terbit dua kali, stok berubah dua kali, atau webhook terkirim berulang.
2. Backlog naik meski jumlah worker cukup
Jika banyak job timeout lalu diambil ulang, kapasitas worker habis untuk mengerjakan pekerjaan yang sama berulang kali. Secara metrik, antrean terlihat sibuk, tetapi throughput bisnis rendah.
3. Timeout palsu
Job sebenarnya masih hidup, hanya memang lambat. Namun karena lease terlalu pendek, sistem menganggap job hilang. Ini sering terjadi pada job yang bergantung pada API eksternal, file besar, query database berat, atau burst trafik.
4. Cache menjadi tidak konsisten
Misalnya worker A menghitung agregat lalu menyimpan ke cache. Worker B yang memproses job duplikat selesai lebih lambat dengan data yang diambil lebih awal, lalu menimpa cache dengan nilai lama. Hasilnya pengguna melihat data mundur.
5. Konsistensi data terganggu
Pada operasi non-idempoten, proses ganda dapat memicu duplikasi row, perubahan status yang melompat, atau transaksi finansial yang terulang. Jika ada interaksi lintas sistem, pemulihannya jauh lebih sulit karena sebagian efek samping mungkin sudah keluar ke sistem lain.
Penyebab salah konfigurasi yang paling sering
- Menebak timeout dari rata-rata, bukan ekor distribusi. Job P50 mungkin 3 detik, tetapi P99 bisa 90 detik.
- Tidak membedakan tipe job. Satu queue berisi pekerjaan cepat dan sangat lambat dengan timeout sama.
- Tidak ada heartbeat atau extend lease. Job panjang tidak memperpanjang masa invisibility.
- Shutdown worker tidak graceful. Proses mati saat masih memegang job.
- Retry policy terlalu agresif. Job gagal langsung masuk lagi tanpa jeda yang sehat.
- Timeout aplikasi lebih besar dari visibility timeout. Kode masih berjalan, tetapi queue sudah menganggap lease habis.
- Tidak ada idempotency. Sekali job terambil ulang, efek samping pasti terduplikasi.
Strategi tuning visibility timeout berdasarkan durasi job
1. Ukur durasi nyata, jangan pakai asumsi
Kumpulkan metrik durasi proses per jenis job. Minimal lihat distribusi seperti median, P95, dan P99. Visibility timeout sebaiknya lebih besar dari durasi normal job, tetapi tidak terlalu panjang hingga kegagalan baru terdeteksi sangat lambat.
Prinsip praktisnya:
- Untuk job singkat dan stabil, timeout dapat relatif ketat.
- Untuk job dengan variasi tinggi, gunakan timeout lebih longgar atau mekanisme perpanjangan lease.
- Jika ada beberapa kelas job, pisahkan queue berdasarkan karakteristik durasi.
2. Selaraskan dengan timeout di level aplikasi
Jangan sampai worker masih memproses karena timeout HTTP, query, atau proses internal lebih panjang daripada visibility timeout. Jika kode Anda bisa hidup 2 menit, tetapi lease hanya 30 detik, proses ganda hampir pasti terjadi pada beban tertentu.
3. Gunakan heartbeat atau extend lease untuk job panjang
Untuk pekerjaan yang sah berjalan lama, worker dapat memperpanjang visibility timeout secara periodik selama masih sehat. Pola ini sering disebut heartbeat, lease renewal, atau extend visibility.
Dengan begitu, queue tahu bahwa worker masih aktif dan job belum perlu dilepas ke worker lain.
// pseudocode generik untuk job panjang
function processJob(job) {
const lease = queue.receive({ visibilityTimeout: 60 })
const heartbeat = setInterval(() => {
queue.extendLease(job.receiptHandle, 60)
}, 30000)
try {
doLongRunningWork(job)
queue.ack(job.receiptHandle)
} catch (err) {
queue.nack(job.receiptHandle)
throw err
} finally {
clearInterval(heartbeat)
}
}Poin pentingnya bukan nama API, melainkan polanya: perpanjang lease hanya selama worker benar-benar hidup dan masih membuat progres.
4. Pisahkan queue berdasarkan profil kerja
Menggabungkan job kirim email 2 detik dengan job sinkronisasi data 10 menit ke satu queue sering menghasilkan kompromi buruk. Lebih aman memisahkan queue cepat dan queue lambat, lalu mengatur timeout, concurrency, dan retry policy masing-masing.
5. Hindari timeout terlalu panjang
Timeout panjang memang mengurangi duplikasi karena job tidak cepat muncul lagi. Namun ada trade-off:
- Job yang benar-benar gagal baru bisa diproses ulang setelah lama menunggu.
- Backlog tampak tertahan karena banyak job “menghilang” lama.
- Recovery dari crash worker menjadi lambat.
Karena itu, timeout besar bukan solusi universal. Untuk job panjang, heartbeat biasanya lebih sehat daripada langsung menetapkan lease sangat besar untuk semua job.
Mitigasi wajib: jangan hanya mengandalkan visibility timeout
Idempotency key
Karena sistem queue umumnya at-least-once, desain job sebaiknya idempoten. Artinya, jika job yang sama dijalankan lagi, hasil akhirnya tetap benar dan tidak menggandakan efek samping.
Contoh strategi:
- Simpan idempotency key berdasarkan identitas bisnis, misalnya
payment_idatauorder_id:event_type. - Sebelum mengeksekusi side effect, cek apakah key sudah pernah diproses.
- Gunakan constraint unik di database bila memungkinkan.
// pseudocode idempotency sederhana
function handlePaymentCaptured(event) {
const key = `payment-captured:${event.paymentId}`
if (db.idempotencyKeys.exists(key)) {
return
}
db.transaction(() => {
db.idempotencyKeys.insert(key)
db.ledger.insert({ paymentId: event.paymentId, type: 'capture' })
db.orders.markPaid(event.orderId)
})
}Jika memungkinkan, simpan key dan efek bisnis utama dalam satu transaksi atomik agar tidak ada celah antara “belum tercatat” dan “sudah berefek”.
Lock pada resource sensitif
Untuk operasi yang sangat rentan konflik, gunakan lock berbasis identitas bisnis. Contohnya, hanya satu worker boleh memproses settlement untuk satu akun tertentu dalam satu waktu. Lock tidak menggantikan idempotency, tetapi menurunkan peluang eksekusi paralel yang merusak.
Dead Letter Queue (DLQ)
Jika job terus gagal atau terlalu sering timeout, jangan biarkan ia berputar tanpa akhir. Pindahkan ke DLQ setelah ambang percobaan tertentu agar worker utama tidak tersedot oleh job bermasalah yang sama.
DLQ berguna untuk:
- mencegah antrean utama tersumbat,
- mempermudah investigasi payload rusak,
- membedakan kegagalan permanen vs sementara.
Backoff untuk retry
Jangan me-retry semua kegagalan dengan interval tetap yang pendek. Gunakan backoff bertahap, terutama jika penyebabnya adalah dependency eksternal, rate limit, atau database yang sedang lambat. Dengan begitu Anda mengurangi tekanan pada sistem saat sedang tidak sehat.
Observability: metrik yang perlu dipantau
Tanpa metrik, masalah visibility timeout sering terlihat sebagai gejala acak. Pantau minimal hal berikut:
- Durasi proses job per jenis job.
- Jumlah retry dan distribusinya.
- Age of oldest message atau umur job tertua di queue.
- Backlog depth atau panjang antrean.
- Tingkat timeout/lease expiration.
- Jumlah job yang masuk DLQ.
- Success rate vs duplicate side effects berdasarkan idempotency hit.
- Worker health: crash, restart, memory, CPU, dan latency dependency.
Beberapa sinyal yang patut dicurigai:
- Durasi P99 mendekati atau melewati visibility timeout.
- Retry naik bersamaan dengan backlog.
- DLQ bertambah setelah deploy baru.
- Idempotency hit meningkat tajam, menandakan duplikasi mulai sering terjadi.
Contoh desain alur yang lebih aman
Berikut pola generik yang bisa dipakai lintas broker seperti Redis-backed queue, SQS-like queue, atau broker berbasis ack manual:
- Worker mengambil job dan memperoleh lease awal.
- Worker membuat context observability: trace ID, job ID, idempotency key.
- Jika job panjang, worker mengirim heartbeat atau memperpanjang lease secara periodik.
- Sebelum side effect utama, worker cek idempotency key.
- Worker mengeksekusi perubahan data inti secara atomik jika memungkinkan.
- Jika sukses, worker mengirim ack/delete.
- Jika gagal sementara, worker melepaskan job ke retry dengan backoff.
- Jika gagal permanen atau melebihi ambang retry, worker kirim ke DLQ.
Contoh pemisahan kebijakan berdasarkan jenis job
Job cepat dan deterministik
- Contoh: kirim notifikasi internal, update indeks kecil.
- Visibility timeout relatif pendek.
- Retry boleh cepat tetapi tetap memakai backoff ringan.
- Idempotency tetap disarankan jika ada side effect keluar.
Job lambat atau bergantung layanan eksternal
- Contoh: sinkronisasi data, generate laporan besar, panggilan API pihak ketiga.
- Visibility timeout lebih longgar atau pakai heartbeat.
- Retry harus dibatasi dan memakai backoff lebih hati-hati.
- DLQ penting untuk mencegah antrean macet.
Job sensitif secara finansial atau inventori
- Contoh: charge pembayaran, update stok, settlement.
- Idempotency wajib.
- Lock per resource sering diperlukan.
- Observability dan audit log harus jelas.
Kesalahan implementasi yang sering terjadi
- Menganggap ack selalu terjadi setelah semua efek samping aman. Dalam praktiknya, crash bisa terjadi di tengah-tengah.
- Mengandalkan cache sebagai sumber kebenaran. Saat job duplikat terjadi, cache mudah tertimpa nilai yang tidak lagi valid.
- Menggunakan satu timeout untuk semua job. Hasilnya hampir pasti buruk untuk salah satu kelas pekerjaan.
- Tidak menguji crash di tengah proses. Banyak tim hanya menguji jalur sukses.
- Tidak ada alarm untuk lease expiration. Akhirnya duplikasi baru terlihat dari keluhan pengguna.
Checklist operasional
- Apakah setiap jenis job punya distribusi durasi yang terukur?
- Apakah visibility timeout lebih besar dari durasi normal job, bukan sekadar rata-rata?
- Apakah timeout aplikasi, timeout dependency, dan visibility timeout sudah selaras?
- Apakah job panjang memakai heartbeat atau extend lease?
- Apakah job sensitif memiliki idempotency key?
- Apakah resource kritis dilindungi lock bila perlu?
- Apakah retry memakai backoff, bukan loop cepat tanpa jeda?
- Apakah ada DLQ dan prosedur penanganannya?
- Apakah ada dashboard untuk backlog, age of oldest message, retry, dan timeout?
- Apakah sistem diuji untuk skenario crash sebelum ack?
Penutup
Visibility timeout queue adalah kontrol penting agar worker tidak memproses job ganda, tetapi ia bukan solusi tunggal. Jika nilainya terlalu pendek, job yang masih berjalan bisa diambil ulang. Jika terlalu panjang, pemulihan dari crash menjadi lambat. Karena itu, strategi yang sehat biasanya menggabungkan tuning berdasarkan durasi job nyata, heartbeat untuk pekerjaan panjang, retry dengan backoff, DLQ, observability yang baik, serta desain idempoten.
Jika Anda hanya mengambil satu prinsip dari artikel ini, gunakan yang ini: anggap job bisa diproses lebih dari sekali, lalu rancang sistem agar hasil akhirnya tetap benar. Visibility timeout membantu, tetapi ketahanan sebenarnya datang dari kombinasi konfigurasi yang tepat dan desain aplikasi yang aman terhadap duplikasi.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!