Requeue vs Drop #

Dalam arsitektur sistem berbasis pesan (message-driven architecture), kegagalan adalah sebuah keniscayaan yang harus kita antisipasi sejak awal. Ketika aplikasi konsumen (consumer) menerima pesan dari antrean RabbitMQ, tidak ada jaminan bahwa proses eksekusi logika bisnis akan selalu berjalan mulus. Koneksi ke database eksternal bisa terputus secara tiba-tiba, muatan data (payload) bisa saja rusak karena kegagalan serialisasi di sisi produsen, atau sistem pihak ketiga yang kita hubungi mungkin sedang mengalami kelumpuhan sementara. Pada momen kritis inilah, aplikasi konsumen kita harus membuat keputusan arsitektural yang sangat krusial: apakah kita harus mengirimkan kembali pesan tersebut ke dalam antrean agar dicoba lagi di lain waktu (Requeue), ataukah kita harus membuang pesan tersebut secara permanen dari antrean utama (Drop)?

Keputusan antara melakukan requeue atau drop bukan sekadar masalah teknis penulisan satu baris kode. Pilihan ini akan secara langsung menentukan stabilitas sistem, konsistensi data bisnis, latensi pemrosesan, hingga efisiensi penggunaan sumber daya komputasi broker kita. Kesalahan dalam menerapkan strategi penanganan kegagalan ini dapat menyebabkan masalah fatal, mulai dari kebocoran memori, antrean yang membeku (stuck queue), hingga penumpukan beban pemrosesan tiada akhir yang mampu melumpuhkan seluruh klaster RabbitMQ kita.

Anatomi Kegagalan Pemrosesan Pesan #

Sebelum kita membandingkan aksi teknis untuk menangani pesan yang gagal, kita perlu memahami mengapa sebuah pesan bisa gagal diproses di lingkungan produksi. Secara garis besar, kegagalan pemrosesan pesan diklasifikasikan berdasarkan sumber masalahnya. Pemahaman ini sangat penting karena jenis kegagalan yang berbeda menuntut respon penanganan yang berbeda pula dari aplikasi konsumen kita.

flowchart TD
    Receive["Pesan Diterima oleh Aplikasi Konsumen"] --> Execute["Eksekusi Logika Bisnis"]
    
    Execute --> TempErr["Kesalahan Sementara<br>- Database Timeout<br>- API Pihak Ketiga Down<br>- Network Glitch"]
    Execute --> PermErr["Kesalahan Permanen<br/>- Payload Rusak / Bad JSON<br/>- Validasi Skema Gagal<br/>- Pelanggaran Aturan Bisnis"]
    
    TempErr --> Requeue["REQUEUE (Kirim Ulang ke Antrean)"]
    PermErr --> Drop["DROP (Dead Letter Exchange)"]

Protokol AMQP 0-9-1 memberikan kontrol penuh kepada konsumen untuk mengelola status pesan setelah diterima. Melalui mekanisme acknowledgement (konfirmasi penerimaan), konsumen dapat secara aktif memberi tahu broker tentang apa yang harus dilakukan terhadap pesan yang sedang dipegangnya. Ketika pemrosesan gagal, konsumen memiliki opsi untuk menolak pesan tersebut menggunakan perintah basic.reject (untuk satu pesan) atau basic.nack (untuk satu atau banyak pesan secara massal). Di dalam perintah penolakan tersebut, terdapat satu parameter boolean vital bernama requeue yang akan menentukan nasib pesan selanjutnya di dalam broker.


Mekanisme Requeue (Kirim Ulang Pesan) #

Mekanisme Requeue terjadi saat aplikasi konsumen menolak pesan dengan memanggil basic.reject atau basic.nack dengan parameter requeue = true. Ketika instruksi ini diterima oleh broker RabbitMQ, broker tidak akan menghapus pesan dari memori ataupun disk. Sebaliknya, broker akan mengembalikan status pesan dari yang sebelumnya Unacknowledged (sedang diproses) kembali menjadi Ready (siap dikirimkan kembali ke konsumen).

Secara internal, RabbitMQ akan berusaha menjaga urutan pesan sebisa mungkin. Jika konsumen yang melakukan penolakan adalah satu-satunya konsumen yang terhubung ke antrean tersebut, pesan yang di-requeue biasanya akan diletakkan kembali di posisi terdepan antrean (head of the queue). Hal ini bertujuan agar pesan tersebut dapat langsung diproses ulang sesegera mungkin begitu konsumen siap. Namun, jika terdapat banyak konsumen yang terhubung (competing consumers), broker mungkin akan mengirimkan pesan yang dideklarasikan requeue tersebut ke konsumen lain yang sedang menganggur (idle).

Bahaya Besar: Loop Pengiriman Ulang Tanpa Batas (Infinite Redelivery Loop) #

Meskipun terdengar sangat praktis untuk menjamin keandalan data, melakukan requeue secara mentah-mentah menyimpan bahaya laten yang sangat besar di lingkungan produksi. Jika penyebab kegagalan pemrosesan pesan bersifat permanen (misalnya, format JSON pesan rusak sehingga tidak bisa di-unmarshal oleh konsumen), maka setiap kali konsumen mencoba memproses pesan tersebut, prosesnya akan selalu gagal.

Jika konsumen selalu merespon kegagalan tersebut dengan mengirimkan requeue = true, maka skenario mengerikan berikut akan terjadi:

  1. Konsumen menerima pesan rusak dari antrean.
  2. Proses parsing gagal karena format data salah.
  3. Konsumen mengirimkan basic.nack(requeue=true).
  4. RabbitMQ menerima penolakan dan meletakkan pesan kembali di kepala antrean.
  5. RabbitMQ langsung mengirimkan kembali pesan rusak yang sama ke konsumen yang sama (atau konsumen tetangga).
  6. Konsumen menerima kembali pesan tersebut, gagal lagi, dan mengirimkan requeue lagi.

Siklus ini akan berulang ribuan kali per detik tanpa henti. Fenomena ini dikenal sebagai Poison Message Problem atau Infinite Redelivery Loop. Dampak instan dari kondisi ini adalah lonjakan penggunaan CPU hingga 100% baik pada sisi broker maupun aplikasi konsumen, log aplikasi yang dipenuhi dengan pesan error yang sama hingga memenuhi ruang penyimpanan disk, dan konsumsi memori yang terus meningkat. Yang lebih parah, karena pesan rusak tersebut terus menempati posisi terdepan antrean, pesan-pesan sehat lainnya di belakangnya tidak akan pernah mendapatkan kesempatan untuk diproses (Head-of-Line Blocking). Seluruh sistem pemrosesan pesan kita akan mengalami kemacetan total.

Untuk mendeteksi apakah suatu pesan sudah pernah dikirim ulang sebelumnya, kita bisa memeriksa properti flag boolean Redelivered yang disertakan di dalam setiap metadata pengiriman pesan. Berikut adalah contoh logika sederhana di sisi konsumen untuk mendeteksi pesan yang dikirim ulang:

// Pemeriksaan flag Redelivered secara mendasar
if d.Redelivered {
    log.Printf("[WARN] Pesan dengan ID %s terdeteksi sebagai redelivered. Waspadai poison message!", d.MessageId)
    // Di sini kita bisa menerapkan logika pengalihan khusus, misalnya drop ke DLQ
}

Meskipun flag Redelivered membantu kita mendeteksi bahwa pesan tersebut adalah kiriman ulang, kelemahan utamanya adalah flag ini hanya berupa nilai boolean (true atau false). Kita tidak bisa mengetahui apakah ini adalah pengiriman ulang yang ke-2, ke-10, atau ke-10.000 kali. Oleh karena itu, requeue langsung ke antrean asal tanpa filter batasan jumlah percobaan adalah tindakan yang sangat berbahaya untuk sistem produksi.


Mekanisme Drop (Membuang atau Mengalihkan Pesan) #

Mekanisme Drop terjadi ketika aplikasi konsumen menolak pesan dengan memanggil basic.reject atau basic.nack dengan parameter requeue = false. Dengan mengirimkan sinyal ini, konsumen secara eksplisit menyatakan kepada broker RabbitMQ: “Saya tidak bisa memproses pesan ini, dan jangan pernah mencoba mengirimkannya kembali ke antrean utama ini.”

Ketika broker menerima instruksi requeue = false, broker akan langsung menghapus pesan tersebut dari antrean utama tempat ia berada. Namun, tindakan penghapusan ini tidak selalu berarti pesan tersebut hilang selamanya tanpa jejak. Nasib akhir pesan yang di-drop sepenuhnya bergantung pada konfigurasi antrean yang telah kita tetapkan sebelumnya:

  1. Jika Antrean Memiliki Konfigurasi Dead Letter Exchange (DLX): RabbitMQ tidak akan membuang pesan tersebut ke tempat sampah. Sebaliknya, broker akan merutekan ulang (routing) pesan tersebut ke Exchange khusus yang didefinisikan sebagai Dead Letter Exchange. Dari Exchange tersebut, pesan akan dialirkan ke antrean penampung pesan gagal yang disebut Dead Letter Queue (DLQ). Di dalam DLQ, pesan rusak tersebut akan disimpan dengan aman untuk dianalisis lebih lanjut secara manual oleh tim developer, atau diproses ulang secara otomatis oleh sistem rekonsiliasi sekunder setelah akar masalah diperbaiki.
  2. Jika Antrean Tidak Memiliki Konfigurasi Dead Letter Exchange (DLX): RabbitMQ akan menghapus pesan tersebut secara permanen dari sistem penyimpanan. Pesan akan hilang selamanya dari memori dan disk broker. Skenario ini disebut sebagai Silent Drop dan sangat berbahaya untuk data bisnis penting, karena kita kehilangan jejak data transaksi yang gagal diproses tanpa ada kesempatan untuk melakukan audit atau pemulihan data.
flowchart TD
    Start["requeue = false"] --> Cond{"Apakah Queue memiliki DLX?"}
    Cond -->|"Ya"| DLX["Dialihkan ke DLX/DLQ (Isolasi & Audit)"]
    Cond -->|"Tidak"| Delete["Dihapus Permanen (Silent Drop)"]

Dengan mengaktifkan konfigurasi DLX, aksi drop (requeue = false) berubah menjadi mekanisme penyelamatan data yang sangat elegan. Kita dapat menyingkirkan pesan-pesan bermasalah yang dapat menyumbat aliran antrean utama tanpa takut kehilangan data berharga tersebut. Throughput sistem utama tetap terjaga tinggi, dan tim operasional kita mendapatkan visibilitas penuh terhadap kegagalan sistem melalui pemantauan metrik pada DLQ.


Perbandingan Mendalam Requeue vs Drop #

Untuk mempermudah kita dalam memilih strategi yang tepat saat mendesain penanganan error pada konsumen RabbitMQ, berikut adalah tabel perbandingan mendalam antara mekanisme Requeue dan Drop:

Dimensi PerbandinganRequeue (requeue = true)Drop (requeue = false)
Definisi KonsepMengembalikan pesan ke antrean asal agar segera dicoba lagi.Mengeluarkan pesan dari antrean asal secara permanen.
Urutan Pesan (FIFO)Mengganggu urutan asli jika ada konsumen paralel atau jika pesan masuk antrean delay.Urutan asli terputus untuk pesan yang gagal, namun pesan lain tetap mengalir.
Throughput SistemBerpotensi menurun drastis jika terjadi loop kegagalan berulang.Tetap tinggi karena pesan bermasalah langsung disingkirkan dari antrean.
Beban CPU & MemoriTinggi jika terjadi infinite retry karena siklus pemrosesan yang konstan.Rendah dan stabil karena pesan gagal hanya ditangani satu kali di antrean utama.
Penyimpanan PesanPesan tetap berada di antrean utama.Pesan dihapus, atau dialihkan ke antrean khusus (DLQ) melalui DLX.
Kasus Penggunaan TerbaikKegagalan transient yang sangat singkat (misal network glitch milidetik).Kegagalan permanen (data rusak) atau kegagalan yang butuh jeda waktu lama.
Risiko UtamaMenyebabkan kemacetan total antrean (stuck) dan crash pada konsumen/broker.Kehilangan data selamanya jika tidak dikombinasikan dengan DLX/DLQ.

Pilihan terbaik di lingkungan produksi bukanlah memilih salah satu secara ekstrem, melainkan mengkombinasikan keduanya secara cerdas berdasarkan klasifikasi jenis error yang terjadi secara dinamis di dalam kode aplikasi konsumen kita.


Klasifikasi Kegagalan: Transient vs Permanent Error #

Kunci dari sistem penanganan kesalahan yang tangguh terletak pada kemampuan aplikasi konsumen kita untuk membedakan antara Transient Error (Kesalahan Sementara) dan Permanent Error (Kesalahan Permanen). Ketika kode konsumen mendeteksi sebuah error, ia harus mengevaluasi sifat dari error tersebut sebelum memutuskan untuk memanggil fungsi Ack, Nack dengan requeue, atau Nack tanpa requeue.

1. Transient Errors (Kesalahan Sementara) #

Kesalahan sementara adalah jenis gangguan yang terjadi karena faktor eksternal yang bersifat dinamis dan biasanya akan pulih dengan sendirinya dalam hitungan detik atau menit. Ketika kesalahan ini terjadi, payload pesan sebenarnya sangat valid dan tidak ada masalah pada kode aplikasi kita. Masalahnya murni terletak pada lingkungan sekitar yang sedang tidak siap.

Contoh umum kesalahan sementara meliputi:

  • Database Timeout / Deadlock: Database sedang sibuk memproses query lain sehingga koneksi konsumen mengalami timeout, atau terjadi deadlock transaksi database yang dapat diselesaikan dengan mencoba ulang (retry).
  • Network Partition / Glitch: Terjadi gangguan jaringan sesaat yang memutus koneksi HTTP ke API eksternal atau service dependency.
  • Rate Limiting: Layanan pihak ketiga membatasi request kita dengan mengembalikan response status 429 Too Many Requests.

Strategi Penanganan: Untuk kesalahan sementara, kita tidak boleh langsung membuang pesan (drop), karena data tersebut sah dan harus diproses. Namun, kita juga tidak disarankan melakukan requeue langsung secara instan (requeue = true tanpa jeda), karena jika database sedang down selama 10 menit, requeue instan hanya akan memperparah beban database yang sedang berjuang untuk pulih.

Solusi terbaiknya adalah dengan melakukan requeue yang disertai dengan jeda waktu penundaan (delay retry) menggunakan kombinasi TTL dan DLX, atau menggunakan algoritma Exponential Backoff yang akan kita bahas pada artikel-artikel berikutnya di section ini.


2. Permanent Errors (Kesalahan Permanen) #

Kesalahan permanen adalah jenis kegagalan yang disebabkan oleh faktor internal data atau logika sistem yang bersifat statis. Mencoba memproses ulang pesan ini sejuta kali pun tidak akan pernah membuahkan hasil sukses selama tidak ada perubahan kode aplikasi atau perbaikan manual pada payload pesan tersebut.

Contoh kesalahan permanen meliputi:

  • Malformed Payload (Data Rusak): Format data pesan tidak sesuai standar, misalnya payload kosong, atau data yang seharusnya berformat JSON dikirim dalam bentuk teks biasa sehingga memicu error unmarshal.
  • Validation Failure: Payload data melanggar aturan skema atau bisnis penting, misalnya kolom email tidak valid, atau nilai transaksi amount bernilai negatif yang tidak diperbolehkan oleh sistem akuntansi.
  • Resource Not Found: Pesan meminta untuk memperbarui data pengguna dengan user_id = 999, tetapi setelah diperiksa ke database, data pengguna dengan ID tersebut memang tidak pernah ada di sistem.

Strategi Penanganan: Untuk kesalahan permanen, kita wajib melakukan drop terhadap pesan tersebut dari antrean utama menggunakan basic.nack(requeue=false). Menyimpan pesan ini di antrean utama hanya akan menyia-nyiakan sumber daya sistem. Namun, pastikan antrean utama kita telah terikat dengan Dead Letter Exchange (DLX) agar pesan yang di-drop ini masuk ke Dead Letter Queue (DLQ). Dengan begitu, tim developer dapat memeriksa isi payload pesan rusak tersebut, memperbaiki bug kode di aplikasi jika ada, atau menghubungi tim produsen pesan untuk memperbaiki data mereka, tanpa mengganggu operasional sistem utama kita.


Dampak Terhadap Urutan Pesan (FIFO Ordering) #

Salah satu karakteristik penting dari antrean RabbitMQ adalah jaminan urutan pengiriman pesan berbasis FIFO (First-In, First-Out). Pesan yang pertama kali masuk ke antrean dijamin akan dikirimkan pertama kali pula ke konsumen yang terhubung. Namun, begitu kita memperkenalkan mekanisme penanganan error seperti requeue, jaminan strict FIFO ini akan terganggu.

Mari kita pelajari skenario berikut untuk memahami bagaimana requeue merusak urutan pesan:

  1. Produsen mengirimkan tiga pesan berurutan: Pesan A (urutan 1), Pesan B (urutan 2), dan Pesan C (urutan 3).
  2. Konsumen menerima Pesan A terlebih dahulu. Namun, saat memproses Pesan A, terjadi kegagalan koneksi database sementara.
  3. Sementara itu, konsumen kedua menerima Pesan B dan sukses memprosesnya karena koneksi database sudah pulih.
  4. Konsumen pertama menolak Pesan A dengan memanggil requeue = true.
  5. Broker meletakkan kembali Pesan A ke dalam antrean. Karena ada jeda waktu pemrosesan dan penolakan, Pesan A kini diproses setelah Pesan B selesai dieksekusi. Urutan pemrosesan akhir yang terjadi di sistem kita menjadi: B -> A -> C.
flowchart TD
    subgraph UrutanAsli["Urutan Asli"]
        direction LR
        A["Pesan A"] --> B["Pesan B"] --> C["Pesan C"]
    end

    A -->|"Gagal & Requeue"| AR["Pesan A dikembalikan ke Antrean"]
    B -->|"Sukses"| BR["Hasil: Pesan B Selesai"]

    AR -->|"Diproses Ulang"| AS["Pesan A Selesai"]
    AS --> CS["Pesan C Selesai"]
    
    BR --> Result["Hasil Akhir Pemrosesan: B -> A -> C (Urutan FIFO Rusak!)"]
    CS --> Result

Jika aplikasi kita sangat bergantung pada urutan pesan yang mutlak (misalnya antrean riwayat transaksi perbankan di mana saldo didebit harus terjadi sebelum saldo dikredit), rusaknya urutan FIFO akibat requeue dapat memicu kekacauan data keuangan.

Solusi untuk Menjaga Urutan Pesan #

Jika sistem kita mewajibkan jaminan urutan pesan yang sangat ketat (Strict FIFO), kita tidak bisa menggunakan pola requeue asinkron biasa. Beberapa solusi arsitektur yang bisa kita terapkan antara lain:

  • Single Active Consumer (SAC): Batasi antrean agar hanya memiliki satu konsumen aktif yang memproses pesan secara serial. Jika pesan gagal, konsumen harus menghentikan pemrosesan (block), mematikan dirinya sendiri (crash), atau mengirimkan alarm darurat ke tim operasional, tanpa mengirimkan ACK ataupun NACK requeue. Antrean akan membeku hingga masalah diselesaikan secara manual. Ini adalah harga mahal yang harus dibayar demi menjaga konsistensi urutan data yang mutlak.
  • Idempotensi di Sisi Konsumen: Desain konsumen agar bersifat idempotent. Artinya, jika pesan diproses tidak berurutan atau terjadi duplikasi pemrosesan, efek bisnis akhir di database tetap konsisten dan tidak merusak data. Dengan idempotensi, kita tidak perlu khawatir tentang rusaknya urutan FIFO akibat requeue.

Perilaku Requeue pada Quorum Queue #

Saat kita bermigrasi dari Classic Queue ke Quorum Queue untuk mendapatkan keandalan tingkat tinggi berbasis replikasi konsensus Raft, penting bagi kita untuk memahami bahwa perilaku internal requeue mengalami perubahan signifikan.

Pada Classic Queue, proses requeue sangatlah ringan di sisi broker karena RabbitMQ hanya memanipulasi pointer status pesan di memori RAM lokal node tersebut. Namun, pada Quorum Queue, setiap perubahan status pesan (termasuk penolakan pesan dan pengembaliannya ke antrean) harus dicatat secara konsisten di dalam log Raft dan direplikasikan ke mayoritas node pengikut (followers) di dalam klaster sebelum broker mengirimkan konfirmasi. Hal ini memicu overhead I/O disk yang cukup besar pada klaster broker kita.

Fitur Native Pelacakan Pengiriman Ulang pada Quorum Queue #

Untuk mengatasi masalah overhead dan bahaya infinite retry loop yang sangat boros sumber daya, Quorum Queue menyediakan fitur pelacakan pengiriman ulang yang sangat canggih secara bawaan. Setiap kali pesan dikirimkan ulang ke konsumen setelah ditolak (nack/requeue), broker Quorum Queue secara otomatis melacak jumlah pengiriman tersebut dan menyimpannya di dalam metadata pesan.

RabbitMQ menyediakan argumen khusus saat deklarasi Quorum Queue bernama x-delivery-limit. Kita dapat memanfaatkan parameter ini untuk membatasi jumlah maksimum pengiriman ulang pesan secara otomatis di sisi broker:

// Ilustrasi argumen deklarasi Quorum Queue dengan batas pengiriman ulang
args := amqp.Table{
    "x-queue-type":       "quorum",
    "x-delivery-limit":   int32(5), // Pesan otomatis di-drop/dead-letter setelah gagal 5 kali
}

Jika sebuah pesan gagal diproses dan di-requeue berulang kali hingga melampaui nilai x-delivery-limit yang telah ditentukan (dalam contoh di atas adalah 5 kali), broker RabbitMQ secara otomatis akan menyingkirkan pesan tersebut dari Quorum Queue utama dan merutekannya ke Dead Letter Exchange (DLX) yang terkonfigurasi. Mekanisme ini berjalan sepenuhnya di sisi broker, sehingga menghindarkan sistem kita dari bahaya infinite loop akibat kelalaian penulisan kode di sisi konsumen.


Implementasi Kode Go (Golang) #

Untuk memberikan pemahaman praktis, berikut adalah contoh implementasi lengkap kode konsumen di bahasa Go menggunakan library resmi github.com/rabbitmq/amqp091-go. Kode ini mendemonstrasikan bagaimana konsumen membedakan antara kesalahan transient (sementara) dan kesalahan permanen saat memproses payload JSON transaksi pembayaran.

package main

import (
	"context"
	"encoding/json"
	"errors"
	"log"
	"os"
	"os/signal"
	"syscall"
	"time"

	amqp "github.com/rabbitmq/amqp091-go"
)

// PaymentPayload merepresentasikan struktur data pesan bisnis kita
type PaymentPayload struct {
	TransactionID string  `json:"transaction_id"`
	UserID        string  `json:"user_id"`
	Amount        float64 `json:"amount"`
}

// Definisikan tipe error khusus untuk kebutuhan klasifikasi penanganan error
var (
	ErrDatabaseTimeout = errors.New("temporary database connection timeout")
	ErrInvalidData     = errors.New("permanent invalid business data")
)

func main() {
	// 1. Inisialisasi koneksi fisik ke broker RabbitMQ
	conn, err := amqp.Dial("amqp://guest:guest@localhost:5672/")
	if err != nil {
		log.Fatalf("Gagal membuka koneksi ke RabbitMQ: %v", err)
	}
	defer conn.Close()

	// 2. Membuat channel komunikasi AMQP
	ch, err := conn.Channel()
	if err != nil {
		log.Fatalf("Gagal membuka channel AMQP: %v", err)
	}
	defer ch.Close()

	// 3. Menetapkan nilai Prefetch QoS untuk membatasi penumpukan pesan di memori konsumen
	err = ch.Qos(
		10,    // prefetch count: batasi maksimal 10 pesan in-flight per consumer thread
		0,     // prefetch size: tanpa batasan ukuran bytes
		false, // global: setelan berlaku per channel aktif
	)
	if err != nil {
		log.Fatalf("Gagal menetapkan Qos Prefetch: %v", err)
	}

	// 4. Mendaftarkan konsumen ke antrean utama dengan manual ACK aktif (autoAck = false)
	msgs, err := ch.Consume(
		"payment-processing-queue", // nama antrean utama
		"payment-consumer-service", // tag identitas unik konsumen
		false,                      // auto-ack: false (kita wajib mengelola ACK/NACK secara manual)
		false,                      // exclusive: antrean bisa diakses banyak konsumen paralel
		false,                      // no-local: tidak didukung oleh RabbitMQ
		false,                      // no-wait: tunggu verifikasi deklarasi dari broker
		nil,                        // argumen tambahan
	)
	if err != nil {
		log.Fatalf("Gagal mendaftarkan konsumen pada antrean: %v", err)
	}

	log.Println("[INFO] Konsumen pembayaran aktif. Menunggu pesan masuk...")

	// Gunakan Go context untuk koordinasi shutdown yang aman
	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()

	// Goroutine utama untuk membaca stream pesan dari channel RabbitMQ
	go func() {
		for d := range msgs {
			log.Printf("[RECEIVED] Memproses pesan ID: %s", d.MessageId)

			// Eksekusi fungsi bisnis pemrosesan pembayaran
			err := processPayment(ctx, d.Body)
			if err != nil {
				// 5. Klasifikasi Error: Evaluasi jenis kegagalan yang terjadi
				if errors.Is(err, ErrDatabaseTimeout) {
					// KASUS A: Kesalahan Sementara (Transient Error)
					log.Printf("[WARN] Kegagalan sementara pada pesan %s: %v. Mengirimkan NACK dengan Requeue...", d.MessageId, err)
					
					// Kirimkan NACK dengan requeue = true agar dicoba kembali nanti
					nackErr := d.Nack(
						false, // multiple: hanya menolak pesan aktif ini saja
						true,  // requeue: true (pesan dikembalikan ke antrean utama)
					)
					if nackErr != nil {
						log.Printf("[ERROR] Gagal mengirimkan NACK Requeue ke broker: %v", nackErr)
					}
				} else {
					// KASUS B: Kesalahan Permanen (Permanent Error) atau Kesalahan Parsing Data
					log.Printf("[ERROR] Kegagalan permanen pada pesan %s: %v. Mengirimkan NACK tanpa Requeue...", d.MessageId, err)
					
					// Kirimkan NACK dengan requeue = false agar pesan dibuang dari antrean utama.
					// Jika antrean payment-processing-queue dikonfigurasi dengan DLX, pesan ini akan masuk ke DLQ.
					nackErr := d.Nack(
						false, // multiple: hanya menolak pesan aktif ini saja
						false, // requeue: false (pesan di-drop/dialihkan ke DLX)
					)
					if nackErr != nil {
						log.Printf("[ERROR] Gagal mengirimkan NACK Drop ke broker: %v", nackErr)
					}
				}
				continue
			}

			// KASUS C: Pemrosesan Sukses Tanpa Hambatan
			log.Printf("[SUCCESS] Sukses memproses pesan ID: %s. Mengirimkan manual ACK...", d.MessageId)
			ackErr := d.Ack(
				false, // multiple: hanya mengonfirmasi pesan aktif ini saja
			)
			if ackErr != nil {
				log.Printf("[ERROR] Gagal mengirimkan ACK ke broker: %v", ackErr)
			}
		}
	}()

	// Menangani gracefully shutdown saat menerima sinyal interupsi OS (Ctrl+C)
	sigChan := make(chan os.Signal, 1)
	signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
	<-sigChan

	log.Println("[INFO] Shutdown sinyal diterima. Menghentikan konsumen secara aman...")
}

// processPayment adalah fungsi simulasi logika bisnis pemrosesan data pembayaran
func processPayment(ctx context.Context, payloadBytes []byte) error {
	var payment PaymentPayload

	// 1. Validasi Sintaksis Payload JSON (Kesalahan Permanen)
	err := json.Unmarshal(payloadBytes, &payment)
	if err != nil {
		return errors.New("malformed payload: failed to parse JSON structure")
	}

	// 2. Validasi Aturan Logika Bisnis (Kesalahan Permanen)
	if payment.Amount <= 0 {
		return ErrInvalidData
	}

	// 3. Simulasi Eksekusi Transaksi Relasional (Database)
	dbErr := simulateDatabaseQuery(payment.TransactionID)
	if dbErr != nil {
		return dbErr // Mengembalikan ErrDatabaseTimeout (Kesalahan Sementara)
	}

	return nil
}

// simulateDatabaseQuery mensimulasikan query database yang berpotensi timeout
func simulateDatabaseQuery(txID string) error {
	// e.g: Untuk tujuan demonstrasi, jika ID transaksi diakhiri dengan huruf 'E', simulasikan db timeout
	if len(txID) > 0 && txID[len(txID)-1] == 'E' {
		return ErrDatabaseTimeout
	}
	return nil
}

Anti-Pattern vs Solusi Praktis #

Agar sistem produksi kita terhindar dari pemadaman mendadak dan kebocoran data, hindari beberapa kesalahan umum (anti-pattern) berikut saat merancang alur requeue vs drop:

Anti-Pattern 1: Selalu Requeue pada Blok Catch/Error Tanpa Pengecualian #

Menulis kode konsumen di mana seluruh jenis error yang ditangkap secara otomatis merespon dengan mengirimkan instruksi requeue = true ke broker.

// ANTI-PATTERN: Menyetel requeue = true secara membuta tanpa memeriksa penyebab error
go func() {
    for d := range msgs {
        err := process(d.Body)
        if err != nil {
            // JANGAN LAKUKAN INI: Pesan dengan error unmarshal JSON akan menyumbat antrean selamanya
            d.Nack(false, true) 
            continue
        }
        d.Ack(false)
    }
}()

Mengapa ini salah? #

Kode di atas adalah pemicu utama terjadinya infinite redelivery loop yang dapat mengakibatkan CPU spike 100% pada klaster RabbitMQ kita. Pesan dengan format salah akan terus memutar di antrean, memblokir pesan valid lainnya, dan membanjiri penyimpanan disk dengan baris log error yang sama.

Solusi Praktis #

Pisahkan penanganan kesalahan secara ketat. Validasi payload data di awal proses. Jika data tidak lolos validasi format, langsung panggil d.Nack(false, false) agar pesan dibuang ke Dead Letter Exchange (DLX). Gunakan requeue = true hanya jika kita yakin kesalahan disebabkan oleh faktor transient jaringan/database sementara yang dapat pulih dengan sendirinya.


Anti-Pattern 2: Melakukan Drop Pesan Tanpa Mengonfigurasi DLX (Silent Drop) #

Mengirimkan instruksi penolakan pesan dengan requeue = false pada antrean yang tidak memiliki binding Dead Letter Exchange atau Dead Letter Queue.

// ANTI-PATTERN: Melakukan drop pesan bisnis tanpa memiliki jaring pengaman (DLQ)
// Broker akan menghapus pesan secara permanen dari disk/RAM
d.Nack(false, false) 

Mengapa ini salah? #

Pola ini mengakibatkan hilangnya data transaksi secara misterius (silent data loss). Kita tidak memiliki bukti atau riwayat tentang data transaksi apa saja yang gagal, apa penyebab kegagalannya, dan tidak ada cara untuk memulihkan atau memproses ulang data tersebut setelah masalah sistem diperbaiki.

Solusi Praktis #

Jangan pernah menggunakan requeue = false kecuali kita sudah memastikan bahwa antrean utama tersebut telah terkonfigurasi secara eksplisit dengan argumen x-dead-letter-exchange ke sebuah Dead Letter Queue yang aktif dipantau. DLQ bertindak sebagai jaring pengaman utama yang memastikan tidak ada satu pun byte data penting yang hilang tanpa jejak audit.


Ringkasan #

  • Keputusan Penolakan Pesan — Saat pemrosesan pesan gagal, aplikasi konsumen harus memutuskan untuk melakukan Requeue (requeue = true) untuk mencoba kembali, atau Drop (requeue = false) untuk menyingkirkan pesan.
  • Risiko Infinite Loop — Melakukan requeue langsung tanpa batasan pada pesan rusak (poison message) akan memicu lingkaran setan pengiriman ulang yang memicu CPU 100% dan membekukan aliran antrean (stuck queue).
  • Fungsi Dead Letter Exchange (DLX) — Menggunakan aksi drop (requeue = false) yang dikombinasikan dengan DLX memungkinkan kita mengisolasi pesan gagal ke dalam Dead Letter Queue (DLQ) untuk keperluan investigasi, tanpa takut kehilangan data bisnis.
  • Klasifikasi Error Wajib — Bagi penanganan error menjadi Transient Error (seperti db timeout, gunakan delay retry) dan Permanent Error (seperti data parsing error, lakukan drop ke DLQ secara instan).
  • Dampak pada FIFO — Requeue akan merusak jaminan urutan pesan FIFO RabbitMQ. Jika urutan strict mutlak diperlukan, gunakan Single Active Consumer atau pastikan penanganan di tingkat aplikasi bersifat idempotent.
  • Kelebihan Quorum Queue — Menyediakan fitur native x-delivery-limit di sisi broker untuk secara otomatis menghentikan siklus requeue dan mengalihkan pesan ke DLX jika batas percobaan terlampaui.

← Sebelumnya: Consumer Acknowledge   Berikutnya: Dead Letter Exchange (DLX) →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact