Dead Letter Queue dan Redelivery: Menangani Pesan Macet di Queue

Pesan yang “macet” di queue hampir selalu berasal dari satu pola: consumer gagal memproses, broker mengirimkannya ulang (redelivery), gagal lagi, dan begitu seterusnya. Solusinya adalah membatasi jumlah percobaan, memberi jeda di antara percobaan, lalu memindahkan pesan yang tetap gagal ke dead letter queue (DLQ), yaitu antrean khusus untuk pesan yang tidak bisa diproses. Setelah itu tugas Anda adalah mencari penyebabnya, memperbaikinya, dan memproses ulang pesan dari DLQ dengan aman.

Tanpa batas ini, satu pesan rusak bisa membuat consumer sibuk tanpa hasil, menghabiskan CPU, memenuhi log, dan dalam kasus tertentu menahan pesan lain di belakangnya.

Istilah yang perlu dibedakan

  • Redelivery: broker mengirim ulang pesan yang belum diakui, misalnya karena consumer melakukan rollback, melempar exception, atau koneksinya putus.
  • Poison message: pesan yang akan selalu gagal, berapa kali pun dicoba. Contohnya JSON rusak, field wajib kosong, atau ID produk yang tidak ada. Mengulang tidak ada gunanya.
  • Kegagalan sementara: gagal karena database sedang sibuk atau API pihak ketiga timeout. Mencoba lagi beberapa saat kemudian biasanya berhasil.
  • Dead letter queue: tempat parkir pesan yang melewati batas percobaan, agar tidak hilang dan bisa diselidiki.

Mengonfigurasi redelivery dan DLQ

ActiveMQ Artemis

Di Artemis, aturan redelivery diatur per address lewat address-setting di broker.xml. Contoh berikut berlaku untuk semua address yang diawali pesanan.:

<address-setting match="pesanan.#">
   <dead-letter-address>DLQ.pesanan</dead-letter-address>
   <max-delivery-attempts>5</max-delivery-attempts>
   <redelivery-delay>2000</redelivery-delay>
   <redelivery-delay-multiplier>2</redelivery-delay-multiplier>
   <max-redelivery-delay>60000</max-redelivery-delay>
</address-setting>

Artinya: pesan dicoba paling banyak lima kali, jeda awalnya dua detik dan berlipat dua setiap percobaan dengan batas satu menit, lalu dipindah ke DLQ.pesanan. Pastikan address DLQ itu benar-benar memiliki queue yang terikat padanya (atau aktifkan pembuatan otomatis resource dead letter bila versi broker Anda mendukungnya), karena pesan yang dikirim ke address tanpa queue tidak tersimpan di mana pun. Secara bawaan jeda redelivery Artemis adalah nol, artinya pesan langsung dicoba lagi, jadi sebaiknya atur nilainya secara eksplisit.

RabbitMQ

RabbitMQ memakai dead letter exchange. Pesan yang ditolak consumer tanpa diantrekan ulang, kedaluwarsa, atau melebihi batas panjang queue akan diteruskan ke exchange yang ditentukan lewat policy atau argumen queue x-dead-letter-exchange. Untuk quorum queue ada pengaturan delivery-limit yang membatasi berapa kali pesan boleh dikirim ulang. Jeda di antara percobaan tidak tersedia secara bawaan seperti di Artemis; biasanya disusun dengan queue tunda ber-TTL atau ditangani di aplikasi.

Kafka

Kafka tidak memiliki DLQ bawaan karena tidak ada pengakuan per pesan. Pola umumnya: consumer menangkap error, menulis record gagal ke topic terpisah (misalnya pesanan.dlt) beserta informasi error di header, lalu melanjutkan offset. Spring for Apache Kafka menyediakan DeadLetterPublishingRecoverer untuk pola ini.

Consumer yang membedakan jenis kegagalan

Broker tidak tahu apakah sebuah kegagalan bersifat sementara atau permanen. Consumer yang tahu. Jangan buang waktu mengulang poison message; kirim langsung ke DLQ dengan alasan yang jelas, dan biarkan broker mengulang hanya kegagalan sementara. Contoh berikut memakai JMSContext bertransaksi; FormatPesanTidakValidException adalah exception milik aplikasi Anda sendiri.

public void onMessage(Message message) {
    String messageId = null;
    try {
        messageId = message.getJMSMessageID();
        int percobaan = message.getIntProperty("JMSXDeliveryCount");
        if (percobaan > 1) {
            log.info("Percobaan ke-{} untuk pesan {}", percobaan, messageId);
        }
        PesananBaru pesanan = parser.parse(message.getBody(String.class));
        layananPesanan.proses(pesanan);
        context.commit();
    } catch (FormatPesanTidakValidException e) {
        // permanen: salin ke DLQ beserta alasan, lalu commit agar tidak diulang
        kirimKeDlq(message, e.getMessage());
        context.commit();
    } catch (Exception e) {
        // sementara: rollback agar broker mengirim ulang dengan jeda
        log.warn("Gagal memproses pesan {}, akan dicoba ulang", messageId, e);
        context.rollback();
    }
}

JMSXDeliveryCount adalah property standar yang wajib didukung provider sejak JMS 2.0. Nilainya dimulai dari 1 dan bertambah setiap kali pesan dikirim ulang, sehingga berguna untuk logging dan keputusan di sisi consumer.

Langkah menyelidiki pesan di DLQ

  1. Lihat jumlah dan trennya. Satu-dua pesan per hari berbeda artinya dengan ratusan pesan dalam sepuluh menit. Lonjakan mendadak biasanya menandakan deploy yang bermasalah atau sistem hilir yang mati.
  2. Ambil beberapa contoh pesan. Gunakan konsol broker atau alat JMS browser untuk membaca header, property, dan body tanpa mengambil pesannya. Di Artemis, pesan di DLQ membawa property seperti _AMQ_ORIG_ADDRESS dan _AMQ_ORIG_QUEUE yang menunjukkan asalnya.
  3. Cocokkan dengan log consumer. Cari message ID atau correlation ID di log untuk menemukan stack trace pada percobaan terakhir.
  4. Kelompokkan penyebab. Biasanya hanya ada beberapa: format berubah dari pengirim, data referensi belum ada, bug di versi consumer baru, atau sistem hilir menolak permintaan.
  5. Perbaiki akar masalah dulu. Memproses ulang sebelum perbaikan terpasang hanya akan mengirim pesan kembali ke DLQ.

Memproses ulang dengan aman

Setelah perbaikan dipasang, pindahkan pesan dari DLQ kembali ke queue asal. Konsol Artemis menyediakan operasi untuk memindahkan pesan, RabbitMQ punya plugin shovel, atau Anda bisa menulis skrip kecil. Beberapa hal yang perlu dijaga:

  • Pastikan consumer idempoten. Sebagian pesan di DLQ mungkin sudah setengah diproses sebelum gagal. Tanpa perlindungan, pelanggan bisa mendapat dua email atau stok berkurang dua kali. Polanya dibahas di idempotent consumer.
  • Pindahkan bertahap. Mulai dari beberapa pesan, pastikan berhasil, baru sisanya. Memindahkan ribuan pesan sekaligus bisa membanjiri sistem hilir yang baru pulih.
  • Periksa umur pesan. Pesan perubahan status yang sudah berhari-hari mungkin sudah tidak relevan atau malah menimpa data yang lebih baru.
  • Catat apa yang dilakukan. Simpan daftar message ID yang diproses ulang dan yang dibuang beserta alasannya.

Kesalahan konfigurasi yang sering ditemui

  1. Batas percobaan tinggi tanpa jeda. Pesan dicoba berkali-kali dalam satu detik, log membengkak, dan sistem hilir yang sedang bermasalah makin tertekan.
  2. Satu DLQ untuk semua hal. Pesan pembayaran, notifikasi, dan sinkronisasi stok bercampur sehingga sulit diprioritaskan. Pisahkan DLQ per domain atau per queue yang penting.
  3. Kedaluwarsa tanpa tujuan. Pesan dengan time-to-live yang habis bisa dibuang diam-diam bila tidak ada expiry address. Untuk data penting, arahkan ke queue khusus agar tetap bisa diperiksa.

Pemantauan

DLQ yang tidak dipantau sama dengan tempat sampah yang tidak pernah dilihat. Pasang alert untuk jumlah pesan di DLQ di atas nol, atau di atas ambang tertentu untuk sistem yang ramai, dan untuk laju pertumbuhannya. Tambahkan juga metrik jumlah redelivery di queue utama: kenaikan redelivery sering menjadi tanda awal masalah sebelum pesan sampai ke DLQ.

Hendra Kurniawan

Oleh Hendra Kurniawan

Pengembang backend Java yang lama menangani integrasi sistem dan message broker. Menulis catatan praktis tentang JMS, API, dan arsitektur aplikasi.