Tentukan perilaku pengiriman
Acknowledgment, pengiriman ulang, efek ganda, pesan bermasalah, dan dead-letter. Kenapa at-least-once plus consumer idempoten adalah default yang masuk akal.
Pertanyaan
Kalau worker crash di tengah pesan, apa yang terjadi pada pesan itu dan pada efek yang sudah terlanjur dikerjakan?
Setelah baca ini, kamu bisa menelusuri satu pesan dari diterima sampai di-ack, menunjuk titik crash yang menghasilkan efek ganda, dan merancang penanganan pesan bermasalah.
Prasyarat dan contoh
Kamu sudah paham antrean tugas dan pub/sub (M10.01), serta kunci idempotensi dan retry (M03).
Worker menerima pesan "kirim struk order 8812". Ia memanggil penyedia email, email terkirim, lalu proses worker di-kill oleh deploy sebelum sempat bilang ke broker bahwa pesan beres. Broker menunggu, timeout, lalu mengirim pesan yang sama ke worker lain. Pelanggan menerima dua struk. Tidak ada bug di satu pun komponen. Itu perilaku yang diharapkan dari desain ini.
Mekanisme
Acknowledgment (ack). Broker menyimpan pesan sampai consumer bilang selesai. Ada dua waktu ack, dan masing-masing menukar satu risiko:
- Ack sebelum kerja (at-most-once): crash setelah ack berarti pesan hilang. Efek paling banyak sekali, bisa nol.
- Ack setelah kerja (at-least-once): crash sebelum ack berarti pesan dikirim ulang. Efek minimal sekali, bisa dua atau lebih.
Hampir semua sistem produksi memilih at-least-once karena kehilangan pekerjaan lebih mahal daripada mengulangnya, lalu menjadikan consumer idempoten.
Visibility timeout / lease. Pesan yang sedang diproses disembunyikan dari consumer lain selama T detik. Kalau ack nggak datang dalam T, pesan muncul lagi. Aturan praktis: T harus lebih besar dari p99 waktu proses, misalnya p99 = 20 detik berarti T = 60 detik. Terlalu kecil membuat pesan diproses dua worker sekaligus padahal nggak ada yang crash. Terlalu besar membuat pesan yang worker-nya mati menunggu lama. Untuk pekerjaan panjang, perpanjang lease secara berkala.
Consumer idempoten. Pemrosesan kedua dengan pesan yang sama harus menghasilkan keadaan akhir yang sama. Dua teknik umum:
1. Catat id pesan dalam transaksi yang sama dengan efeknya:
BEGIN; INSERT INTO diproses(id) ...; UPDATE saldo ...; COMMIT;
(konflik pada id berarti sudah pernah, lewati)
2. Beri kunci idempotensi ke pihak luar:
POST /email Idempotency-Key: struk-8812
Teknik pertama bekerja kalau efeknya ada di penyimpanan yang sama dengan catatan id. Kalau efek ada di pihak luar (email, payment gateway), kamu bergantung pada dukungan kunci idempotensi dari mereka. Kalau mereka nggak punya, sisa risikonya harus kamu nyatakan, bukan kamu abaikan.
Pesan bermasalah (poison message). Pesan yang selalu gagal, misalnya JSON rusak, akan diulang tanpa henti dan memblokir antrean. Penanganan standar:
- Batasi percobaan, misalnya 5 kali dengan backoff 10 detik, 1 menit, 5 menit.
- Setelah batas, pindahkan ke dead-letter queue (DLQ) beserta alasan gagal dan jumlah percobaan.
- Beri alarm pada kedalaman DLQ, dan siapkan prosedur redrive: perbaiki penyebab, lalu kirim ulang pesan secara terkendali.
Bedakan gagal sementara (timeout, 503) yang pantas diulang, dari gagal permanen (validasi, data tidak ada) yang diulang pun sia-sia dan sebaiknya langsung ke DLQ.
Saat rancangan ini gagal
- Ack di awal "biar cepat". Worker ack, lalu OOM. Pesanan 8812 tidak pernah dapat struk dan tidak ada catatan yang menunjukkannya.
- Lease lebih pendek dari waktu proses. Laporan butuh 90 detik, T = 30 detik. Pesan muncul lagi di worker kedua pada detik 30, ketiga pada detik 60. Tiga worker mengerjakan hal sama dan beban malah naik.
- Dedup tanpa kedaluwarsa. Tabel id-diproses tumbuh 40 juta baris per bulan. Tanpa retensi, indeks membesar dan insert melambat. Retensi harus lebih panjang dari jendela pengiriman ulang terpanjang.
- Retry tanpa batas. Pesan rusak diulang tiap detik selama 3 hari dan memenuhi log. Antrean dengan satu pesan racun bisa menahan yang lain kalau urutan harus dijaga.
- DLQ yang tidak pernah dilihat. 12.000 pesan menumpuk di DLQ, termasuk pembayaran. Tanpa alarm dan pemilik, DLQ hanyalah tempat penguburan yang rapi.
- Efek dan catatan di dua tempat. Worker kirim email lalu insert id-diproses. Crash di antara keduanya, email terkirim dua kali. Urutan terbalik malah membuat email tidak terkirim sama sekali.
Contoh tandingan: "pakai exactly-once dari broker, jadi nggak perlu idempoten". Jaminan broker biasanya berlaku antara topik dan topik atau di dalam transaksinya sendiri. Begitu worker memanggil API email, jaminan itu berhenti di batas broker.
Latihan
Worker memproses pesan "tagih langganan user 77, Rp 49.000". Urutannya: (a) ambil pesan, (b)
panggil payment gateway, (c) tulis status "ditagih" di database, (d) ack. Gateway mendukung
Idempotency-Key.
- Tulis apa yang terjadi kalau crash terjadi sebelum (b), antara (b) dan (c), antara (c) dan (d).
- Rancang perubahan supaya user tidak tertagih dua kali.
- Pesan dengan
user 99999(tidak ada) selalu gagal. Apa yang kamu lakukan?
Lihat pembahasan
- Sebelum (b): tidak ada efek, pesan diulang, aman. Antara (b) dan (c): user sudah tertagih tapi database belum tahu. Pesan diulang dan gateway ditagih lagi. Ini titik bahaya utama. Antara (c) dan (d): efek dan catatan sudah benar, hanya ack yang belum. Pesan diulang, jadi worker harus mendeteksi "sudah ditagih" dan langsung ack.
- Kirim
Idempotency-Key: tagih-77-2026-11(pengguna + periode) ke gateway pada tiap percobaan. Percobaan ulang menghasilkan respons asli, bukan tagihan baru. Sebelum memanggil, cek status di database. Setelah dapat respons, tulis status dengan upsert berdasarkan kunci yang sama, lalu ack. Retensi kunci di gateway harus mencakup waktu retry terpanjang kita (misalnya 24 jam), dan itu perlu dikonfirmasi di dokumentasi gateway. - Itu gagal permanen. Jangan diulang 5 kali. Tandai "user tidak ada", kirim ke DLQ dengan alasan itu, dan pastikan ada alarm. Pesan lain di antrean tetap jalan. Selidiki kenapa pesan untuk user yang tidak ada bisa terbit (mungkin bug di producer).
Terkait
- M10.01 Pilih pekerjaan asinkron menentukan model pesannya.
- M10.03 Kendalikan beban consumer menunjukkan bagaimana retry yang tidak dibatasi memperparah beban.
- M10.05 Publikasikan event dengan andal menutup celah antara database dan broker.
- M03 (idempotensi dan retry) dibahas di modul tersendiri.
Catatan
- Asumsi: broker bergaya antrean dengan ack dan visibility timeout. Nama dan detail berbeda di tiap produk.
- Angka p99 20 detik dan T 60 detik adalah ilustrasi, bukan rekomendasi universal.
- Yang nggak dibahas: transaksi lintas broker dan database dengan commit dua fase, serta protokol idempotent producer milik broker tertentu.
- Terakhir ditinjau: 26 Oktober 2026.