Kodeloka
  1. Kodeloka
  2. Kelas RabbitMQ
  3. Materi

Modul 4 · Modul 4 dari 15

Producer & Consumer: Properti Pesan dan Siklus Hidupnya

Menulis producer dan consumer yang benar sejak awal, termasuk properti pesan yang menentukan keandalan dan kemudahan debugging.

Anatomi Sebuah Pesan

Pesan AMQP terdiri dari tiga bagian: properti (metadata baku), headers (metadata bebas), dan body (byte mentah).

struktur pesantext
┌─ Properties (baku AMQP) ────────────────────────┐
│ content_type   : application/json               │
│ delivery_mode  : 2  (persistent)                │
│ message_id     : 8f2c-4a1b-…                    │
│ correlation_id : untuk RPC / pelacakan          │
│ reply_to       : nama queue balasan             │
│ timestamp      : 1785312000                     │
│ expiration     : "60000"  (TTL per pesan, ms)   │
│ priority       : 0–9                            │
│ app_id         : layanan-pesanan                │
├─ Headers (bebas) ───────────────────────────────┤
│ x-trace-id     : 4bf92f3577b34da6               │
│ x-versi-skema  : 2                              │
│ x-percobaan    : 0                              │
├─ Body ──────────────────────────────────────────┤
│ {"pesanan_id":"P-1042","total":150000}          │
└─────────────────────────────────────────────────┘
⛔ delivery_mode = 2 saja tidak cukup

Agar pesan benar-benar bertahan saat broker restart, ketiganya harus terpenuhi:

text
1. queue durable    = true    (queue ada lagi setelah restart)
2. delivery_mode    = 2       (pesan ditulis ke disk)
3. publisher confirm            (Anda tahu broker benar-benar menerimanya)

Melewatkan salah satunya membuat dua lainnya sia-sia. Ini dibahas tuntas di bab 6.

Producer yang Benar

producer.jsjavascript
const amqp = require('amqplib');
const { randomUUID } = require('crypto');

let koneksi = null;
let channel = null;

async function siapkan() {
  koneksi = await amqp.connect({
    protocol: 'amqp',
    hostname: process.env.RMQ_HOST || 'localhost',
    port: 5672,
    username: process.env.RMQ_USER,
    password: process.env.RMQ_PASS,
    vhost: '/',
    heartbeat: 60,                       // deteksi koneksi mati
    clientProperties: {
      connection_name: 'layanan-pesanan-producer'   // terlihat di UI
    }
  });

  koneksi.on('error', (e) => console.error('koneksi error:', e.message));
  koneksi.on('close', () => { channel = null; jadwalkanSambungUlang(); });

  // ConfirmChannel: broker mengonfirmasi tiap publish
  channel = await koneksi.createConfirmChannel();

  await channel.assertExchange('pesanan', 'topic', {
    durable: true,
    arguments: { 'alternate-exchange': 'pesanan.tak-terkirim' }
  });

  // pesan yang tak bisa dirutekan dikembalikan ke sini
  channel.on('return', (msg) => {
    console.error('TIDAK TERKIRIM:', msg.fields.routingKey,
                  msg.content.toString());
  });
}

async function kirimPeristiwa(peristiwa, wilayah, data) {
  const routingKey = 'pesanan.' + wilayah + '.' + peristiwa;
  const body = Buffer.from(JSON.stringify(data));

  return new Promise((resolve, reject) => {
    channel.publish('pesanan', routingKey, body, {
      persistent: true,                  // = delivery_mode 2
      mandatory: true,                   // kembalikan bila tak terkirim
      contentType: 'application/json',
      contentEncoding: 'utf-8',
      messageId: randomUUID(),           // dasar idempotensi konsumer
      timestamp: Math.floor(Date.now() / 1000),
      appId: 'layanan-pesanan',
      headers: {
        'x-versi-skema': 2,
        'x-trace-id': ambilTraceId()
      }
    }, (err) => err ? reject(err) : resolve());   // publisher confirm
  });
}

Consumer yang Benar

consumer.jsjavascript
async function mulaiKonsumsi() {
  const channel = await koneksi.createChannel();

  await channel.assertQueue('pesanan.email', {
    durable: true,
    arguments: {
      'x-queue-type': 'quorum',
      'x-dead-letter-exchange': 'pesanan.dlx',
      'x-delivery-limit': 5
    }
  });
  await channel.bindQueue('pesanan.email', 'pesanan', 'pesanan.*.dibuat');

  // WAJIB: batasi berapa pesan boleh "in-flight" tanpa ack
  await channel.prefetch(20);

  await channel.consume('pesanan.email', async (msg) => {
    if (msg === null) return;            // konsumer dibatalkan broker

    try {
      const data = JSON.parse(msg.content.toString());
      await prosesKirimEmail(data, msg.properties.messageId);
      channel.ack(msg);                  // baru ack SETELAH berhasil
    } catch (e) {
      const bisaDiulang = e.sementara === true;
      // requeue=false → jatuh ke DLX, bukan berputar tanpa henti
      channel.nack(msg, false, bisaDiulang);
      console.error('gagal memproses:', e.message);
    }
  }, {
    noAck: false,                        // ack manual — WAJIB
    consumerTag: 'email-worker-' + process.pid
  });
}
⚠️ noAck: true berarti kehilangan pesan

Dengan noAck: true (disebut juga auto-ack), broker menganggap pesan selesai begitu terkirim ke soket — sebelum konsumer Anda sempat memprosesnya.

Bila proses konsumer mati di tengah jalan, pesan itu sudah hilang dari broker dan tidak akan pernah kembali. Pakai noAck: true hanya untuk data yang memang boleh hilang, seperti metrik sampling.

Serialisasi Payload

FormatKelebihanKekurangan
JSONMudah dibaca manusia, universalBoros ukuran, tanpa skema
Protobuf / AvroRingkas, ada skema, evolusi terkendaliPerlu registry, sulit dibaca saat debug
MessagePackRingkas, mirip JSONKurang umum

Untuk sebagian besar tim, JSON dengan header x-versi-skema adalah titik awal yang tepat. Yang penting bukan formatnya, melainkan aturan evolusinya: hanya menambah field opsional, tidak pernah mengubah arti field yang sudah ada, dan tidak menghapus field sebelum semua konsumer diperbarui.

💡 Beri nama setiap connection

connection_name muncul di UI manajemen dan di rabbitmqctl list_connections. Saat sebuah broker melayani 400 koneksi dan salah satunya berperilaku aneh, nama seperti layanan-pesanan-producer mengubah investigasi setengah jam menjadi setengah menit.

Push vs Pull

basic.consume (push)basic.get (pull)
Cara kerjaBroker mendorong pesanKlien meminta satu per satu
ThroughputTinggiSangat rendah
Pakai untukHampir semua kasusSkrip administratif, mengintip isi queue

basic.get memerlukan satu perjalanan bolak-balik jaringan per pesan. Memakainya di dalam loop adalah kesalahan performa klasik — throughput bisa 100 kali lebih rendah dibanding basic.consume.