Bug Timeout Job Node.js di Queue MQTT produksi muncul ketika worker tidak menyelesaikan payload sebelum lock/ack timeout, sehingga job dianggap gagal padahal pekerjaan masih berjalan. Penjelasan ini langsung menunjukkan akar masalah dan perbaikan yang bisa diterapkan agar job kembali stabil.

Pada artikel ini kita membahas konteks arsitektur, gejala timeout di produksi, langkah investigasi menggunakan log, metrik, dan tracing, hingga rekomendasi tuning timeout, manajemen ack/lock, retry, serta konfigurasi queue dan alert observability.

Konteks Arsitektur dan Pemicunya

Arsitektur yang dimaksud terdiri dari satu topik MQTT yang di-subscribe oleh fleet worker Node.js. Queue tidak menggunakan broker khusus seperti Redis, namun memanfaatkan broker MQTT dengan QoS 1 agar job minimal sekali. Worker menarik pesan, menjalankan job, lalu mengirim ack via MQTT ack handler. Lock dibuat dengan semacam timestamp yang memblokir redispatch selama rentang timeout.

Pemicunya adalah spike latensi downstream (API eksternal atau database) yang menyebabkan pekerjaan melebihi lockTimeout default. Lock berakhir sementara worker masih memproses, sehingga job dianggap gagal namun tidak pernah dikembalikan secara konsisten. Hal ini memperlihatkan bug timeout job Node.js di queue MQTT.

Gejala Bug Timeout Job Node.js di Queue MQTT

Di produksi, gejala utama yang terlihat adalah:

  • Job nge-hang di status "processing" selama beberapa menit tanpa menyelesaikan callback ack.
  • Message broker mencatat ulang pesan setelah lock timeout karena tidak menerima ack, lalu dua worker memproses payload yang sama.
  • Metrik job_duration_seconds menunjukkan lonjakan bucket tertinggi (lebih dari lockTimeout) disertai log error dari worker karena proses downstream timeout.

Kesalahan ini tidak muncul di staging karena latensi lebih rendah, jadi dibutuhkan observability yang memotret perbedaan runtime di produksi.

Langkah Investigasi: Log, Metrik, Tracing

Pertama, kumpulkan log worker yang mencatat timestamp mulai dan selesai job, serta status ack. Gunakan context trace ID untuk mengaitkan job dengan permintaan downstream.

  1. Log: Cari kesalahan berupa "ack timeout" atau exception tanpa ack, dan catat durasi dari awal hingga terakhir log.
  2. Metrik: Pantau histogram job_duration dan counter job_retries. Bandingkan bucket > lockTimeout dengan volume job per worker.
  3. Tracing: Gunakan distributed tracing agar terlihat apakah dependency seperti API eksternal menunda lebih dari lockTimeout.

Kenali pola: job terblokir pada langkah tertentu, dan log ack tidak pernah dikirim karena worker sedang menunggu response, bukan karena crash.

Akar Masalah dan Perbaikan Teknis

Root cause adalah kombinasi lock timeout terlalu pendek dan ack dikirim sebelum retry logic selesai, lalu job diproses ulang. Perbaikan terbagi tiga area teknis utama.

Tuning Timeout dan Retry

Perpanjang lockTimeout agar setidaknya dua kali rata-rata latensi dependency plus margin. Gunakan retry dengan delay linear untuk menjaga agar job tidak langsung ditalangi setelah timeout.

Contoh pengaturan:

const queueConfig = {
  topic: 'jobs/sensor',
  qos: 1,
  lockTimeoutMs: 180000,
  maxRetries: 3,
  retryDelayMs: 5000,
  concurrency: 6
};

Lock timeout 180 detik memberi ruang saat downstream lambat, sementara retry terbatas agar tidak menumpuk ketika sistem masih sibuk.

Manajemen Ack dan Lock

Jangan mengirim ack sebelum semua operasi selesai. Jika worker menggunakan ack function, pastikan dipanggil di blok finally setelah commit berhasil. Gunakan mekanisme lock renewal jika job benar-benar membutuhkan waktu lebih lama, misalnya update timestamp pada broker.

async function processJob(message, ack) {
  const start = Date.now();
  try {
    await doWork(message.payload);
    await ack();
  } catch (err) {
    console.error('Job gagal', err);
    throw err;
  } finally {
    metrics.jobDuration.observe(Date.now() - start);
  }
}

Dengan struktur ini, ack hanya terkirim saat doWork selesai tanpa error. Jika terjadi exception, job akan otomatis diretry karena ack tidak terkirim.

Retry yang Deterministik dan Dead-letter

Pastikan queue menandai job yang sudah mencapai retry limit untuk masuk dead-letter agar tidak memicu timeout terus-menerus. Secara teknis, update status database dan kirim notifikasi ke tim saat retry habis.

Contoh Konfigurasi Queue dan Observability

Berikut contoh konfigurasi queue serta observability agar developer bisa langsung praktik:

const mqttQueue = new MqttJobQueue({
  topic: 'jobs/sensor',
  qos: 1,
  ackTimeoutMs: 120000,
  lockTimeoutMs: 180000,
  maxRetries: 3,
  retryDelayMs: 5000,
  concurrency: 4,
  onLockRenew: () => mqttClient.publish('jobs/lock', ...)
});

Tambahkan alert promQL untuk mendeteksi timeout job yang tidak selesai:

ALERT JobTimeoutSpike
IF job_duration_seconds_bucket{job="mqtt_worker",le="120"} > 0
FOR 2m
ANNOTATIONS {
  summary = "Job MQTT worker melebih lock timeout"
}

Alert ini memicu setelah bucket durasi job mencapai 120 detik, menandakan worker hampir keluar dari lock window.

Rekomendasi Pencegahan

  • Nilai ulang lockTimeout setiap kali dependency berganti. Jangan biarkan timeout statis selama tahun produksi.
  • Gunakan metric latency upstream untuk memicu autoscaling worker agar tidak ada job terlalu lama menunggu resource.
  • Implementasikan tracing end-to-end agar mudah melihat apakah keterlambatan berasal dari job itself atau dependency.
  • Dokumentasikan prosedur recovery: bagaimana memaksa retry manual, membersihkan lock, atau memindahkan job ke dead-letter.

Pencegahan ini menjaga stabilitas job Node.js di queue MQTT agar timeout tidak lagi menjadi bug berulang di produksi.