Checklist Anti-"LGTM" bukan soal memperlambat review, tetapi memastikan sistem produksi tidak lolos hanya karena “kelihatan jalan”. Untuk queue, cache, dan worker, bug yang paling mahal biasanya bukan bug sintaks, melainkan asumsi operasional yang tidak pernah diaudit: job diproses dua kali, retry tak terkendali, cache basi, lock bocor, atau event datang tidak berurutan.

Artikel ini memakai konteks satire “Incident Report: CVE-2026-LGTM” sebagai pengingat bahaya approval dangkal, lalu mengubahnya menjadi panduan evergreen yang bisa dipakai untuk audit sistem produksi. Fokusnya praktis: gejala, root cause, langkah audit, mitigasi, pseudocode, metrik penting, dan runbook singkat.

Prinsip inti: bila komponen bekerja secara asynchronous, distributed, atau cache-heavy, maka “tes lokal berhasil” tidak cukup. Audit harus membuktikan apa yang terjadi saat duplicate delivery, partial failure, timeout, restart worker, lonjakan traffic, dan dependency melambat.

Mental model: mengapa queue, cache, dan worker sering lolos review tapi gagal di produksi

Tiga komponen ini terlihat sederhana di level kode aplikasi:

  • producer mengirim job ke queue,
  • worker mengambil dan memproses,
  • hasil disimpan ke database atau cache.

Masalahnya, sistem nyata jarang memberi jaminan ideal. Queue bisa memberi at-least-once delivery, artinya job yang sama dapat diterima lebih dari sekali. Worker bisa crash setelah menulis ke database tetapi sebelum mengirim acknowledgment. Cache bisa kedaluwarsa serentak dan menimbulkan stampede. Lock distributed bisa habis masa berlakunya saat proses masih berjalan. Event bisa tiba tidak berurutan karena retry atau partisi jaringan.

Kalau review hanya berhenti di “LGTM, logic-nya benar”, maka yang terlewat biasanya bukan logika bisnis, melainkan kontrak operasional:

  • Apakah handler aman bila dipanggil dua kali?
  • Apakah retry punya batas, backoff, dan klasifikasi error?
  • Apakah poison message dipisahkan dari trafik normal?
  • Apakah cache invalidation punya strategi yang konsisten?
  • Apakah ada metrik untuk membedakan backlog normal vs worker macet?

Checklist audit produksi: queue, cache, worker, locking, dan consistency

1) Job dobel dan idempotensi lemah

Gejala:

  • email atau notifikasi terkirim dua kali,
  • invoice terbit ganda,
  • stok berkurang lebih dari sekali,
  • job terlihat “sukses”, tetapi side effect berulang.

Root cause umum:

  • queue bersifat at-least-once,
  • worker crash setelah side effect tetapi sebelum ack,
  • producer mengirim ulang karena timeout,
  • tidak ada idempotency key atau deduplication store.

Langkah audit:

  1. Identifikasi semua handler yang menghasilkan side effect: pembayaran, email, mutasi saldo, update status eksternal.
  2. Periksa apakah ada idempotency key yang stabil per operasi bisnis, bukan per attempt.
  3. Pastikan penyimpanan idempotensi bersifat atomik dengan side effect penting, atau minimal mencegah eksekusi ulang setelah sukses.
  4. Uji skenario worker crash tepat setelah write, sebelum ack.

Mitigasi:

  • Gunakan kunci idempoten berdasarkan identitas bisnis, misalnya order_id + operation_type.
  • Simpan status pemrosesan di database dengan constraint unik.
  • Untuk side effect eksternal, simpan request/response dan status final agar retry tidak mengeksekusi ulang tanpa cek.
// Pseudocode idempotent consumer untuk charge order
function handleChargeOrder(job):
    key = "charge:" + job.orderId

    existing = idempotencyStore.find(key)
    if existing and existing.status == "done":
        return existing.result

    beginTransaction()

    // unique constraint pada order_id + operation_type membantu mencegah duplikasi
    if paymentRecord.exists(orderId=job.orderId, operation="charge"):
        commit()
        return

    result = paymentGateway.charge(job.orderId, job.amount)

    paymentRecord.insert(orderId=job.orderId, operation="charge", externalRef=result.id)
    idempotencyStore.upsert(key, status="done", result=result.summary)

    commit()

Catatan: idempotensi bukan berarti semua operasi jadi aman otomatis. Jika side effect eksternal tidak mendukung deduplication, Anda perlu strategi kompensasi atau pencatatan status yang sangat jelas.

2) Retry tak terkendali dan poison message

Gejala:

  • backlog queue naik terus,
  • CPU worker tinggi tanpa throughput berarti,
  • satu payload buruk diproses berulang kali,
  • dependency eksternal makin tertekan karena badai retry.

Root cause umum:

  • semua error diperlakukan sama,
  • tidak ada batas retry maksimum,
  • backoff terlalu agresif atau bahkan tanpa jeda,
  • poison message tidak dipindah ke dead letter queue.

Langkah audit:

  1. Kelompokkan error menjadi transient dan non-transient.
  2. Periksa apakah retry hanya diterapkan ke error yang berpotensi pulih, seperti timeout sementara atau rate limit.
  3. Pastikan ada max attempts, exponential backoff, dan jitter.
  4. Pastikan poison message masuk ke dead letter queue untuk inspeksi, bukan diputar selamanya.

Mitigasi:

  • Retry hanya untuk error sementara.
  • Untuk payload invalid, akhiri cepat, tandai gagal, dan kirim ke DLQ.
  • Tambahkan circuit breaker saat dependency eksternal sedang gagal luas.
function process(job):
    try:
        validate(job.payload)
        callDependency(job.payload)
        ack(job)
    catch ValidationError as e:
        sendToDLQ(job, reason="invalid_payload")
        ack(job)
    catch RateLimitError as e:
        retry(job, backoff=exponentialWithJitter(job.attempt))
    catch TimeoutError as e:
        if job.attempt >= MAX_ATTEMPTS:
            sendToDLQ(job, reason="timeout_exhausted")
            ack(job)
        else:
            retry(job, backoff=exponentialWithJitter(job.attempt))
    catch PermanentBusinessError as e:
        sendToDLQ(job, reason="business_rule_failed")
        ack(job)

Kesalahan umum: menganggap HTTP 500 selalu transient, atau mengulang error validasi yang tidak mungkin sembuh dengan retry.

3) Dead letter queue ada, tetapi tidak pernah dibaca

Gejala:

  • DLQ terus bertambah,
  • insiden lama berulang karena payload bermasalah tidak pernah dianalisis,
  • tim hanya tahu ada masalah dari keluhan pengguna.

Root cause umum:

  • DLQ diperlakukan sebagai tempat sampah, bukan alat diagnosis,
  • tidak ada dashboard, alert, atau prosedur replay yang aman.

Langkah audit:

  1. Pastikan setiap pesan DLQ membawa metadata: error class, timestamp, attempt count, correlation ID, versi schema.
  2. Pastikan ada prosedur replay yang eksplisit dan tidak langsung menembakkan ulang semua pesan.
  3. Tinjau apakah DLQ retention cukup untuk investigasi.

Mitigasi:

  • Dashboard top reason DLQ per service.
  • Replay bertahap setelah root cause diperbaiki.
  • Tambahkan validasi schema di sisi producer agar sampah tidak membanjiri consumer.

4) Cache stampede dan stale cache

Gejala:

  • setelah key populer expired, database mendadak spike,
  • latensi naik hanya pada key tertentu,
  • pengguna melihat data basi terlalu lama atau data berubah-ubah.

Root cause umum:

  • banyak request miss pada saat bersamaan,
  • TTL seluruh key seragam dan habis serentak,
  • tidak ada proteksi single-flight / request coalescing,
  • strategi invalidation tidak konsisten dengan sumber data.

Langkah audit:

  1. Temukan key dengan hit rate tinggi dan biaya recompute mahal.
  2. Periksa apakah TTL diberi jitter acak untuk menghindari expiry serempak.
  3. Periksa apakah ada mekanisme stale-while-revalidate atau lock singkat untuk refresh.
  4. Pastikan definisi konsistensi jelas: apakah data boleh stale 5 detik, 1 menit, atau tidak boleh sama sekali?

Mitigasi:

  • Gunakan stale-while-revalidate untuk data yang toleran terhadap sedikit kebasian.
  • Tambahkan jitter pada TTL.
  • Gunakan request coalescing agar hanya satu worker yang membangun ulang cache populer.
function getProductView(productId):
    key = "product:view:" + productId
    cached = cache.get(key)

    if cached and not cached.isExpired():
        return cached.value

    if cached and cached.isStaleButUsable():
        triggerBackgroundRefresh(productId)
        return cached.value

    lockKey = "lock:rebuild:" + productId
    if lock.tryAcquire(ttl=shortTTL):
        try:
            value = db.loadProductView(productId)
            cache.set(key, value, ttl=baseTTL + randomJitter())
            return value
        finally:
            lock.release()
    else:
        // fallback: tunggu singkat, lalu cek lagi atau kembalikan stale jika tersedia
        sleep(shortDelay)
        return cache.get(key) or db.loadProductView(productId)

Trade-off: stale-while-revalidate menukar akurasi real-time dengan stabilitas saat beban tinggi. Cocok untuk katalog, profil, atau agregasi baca; tidak cocok untuk saldo yang harus presisi setiap saat.

5) Distributed lock bocor atau salah model

Gejala:

  • job macet karena lock tidak pernah lepas,
  • dua worker tetap masuk ke critical section yang seharusnya eksklusif,
  • masalah hanya muncul saat proses lebih lama dari perkiraan.

Root cause umum:

  • TTL lock terlalu pendek,
  • release lock tidak memverifikasi ownership,
  • lock dipakai untuk menjamin hal yang seharusnya dijaga oleh database constraint,
  • proses melewati batas TTL lalu worker lain memperoleh lock yang sama.

Langkah audit:

  1. Daftar semua tempat lock digunakan dan tentukan apa invariant yang ingin dilindungi.
  2. Periksa apakah lock punya token kepemilikan unik dan hanya owner yang boleh release.
  3. Uji skenario kerja lebih lama dari TTL, restart worker, dan clock skew antar node.
  4. Tanyakan: apakah masalah ini sebenarnya lebih aman diselesaikan dengan constraint di database?

Mitigasi:

  • Gunakan lock hanya untuk koordinasi sementara, bukan sebagai satu-satunya sumber kebenaran.
  • Untuk operasi bisnis penting, tetap pakai unique constraint, compare-and-set, atau transaksi database bila memungkinkan.
  • Perpanjang lock secara eksplisit bila proses panjang dan mekanismenya aman.
function withLock(lockName, ttl, fn):
    token = randomUUID()
    acquired = lockStore.acquire(lockName, token, ttl)
    if not acquired:
        throw BusyError()

    try:
        return fn(token)
    finally:
        // release hanya jika token masih milik kita
        lockStore.releaseIfOwner(lockName, token)

Kesalahan umum: memakai lock untuk mencegah duplicate insert, padahal database unique index biasanya lebih kuat dan lebih mudah diaudit.

6) Event out-of-order dan consistency yang diasumsikan terlalu ideal

Gejala:

  • status entitas mundur dari “paid” menjadi “pending”,
  • cache menampilkan versi lama setelah event baru diproses,
  • read model tidak konsisten antar service.

Root cause umum:

  • event diproses paralel tanpa ordering key yang benar,
  • retry membuat event lama datang setelah event baru,
  • consumer tidak memeriksa versi atau urutan event.

Langkah audit:

  1. Identifikasi agregat yang membutuhkan ordering, misalnya per order_id atau account_id.
  2. Periksa apakah producer menyertakan sequence number, version, atau event time yang dapat dipakai secara aman.
  3. Pastikan consumer menolak event yang lebih lama dari versi saat ini bila model bisnis mengizinkan.

Mitigasi:

  • Partisi queue berdasarkan entity key bila ordering penting.
  • Simpan version terakhir yang diterapkan.
  • Gunakan upsert berbasis versi untuk read model atau cache invalidation yang sensitif urutan.
function applyOrderEvent(event):
    current = orderProjection.find(event.orderId)

    if current and event.version <= current.version:
        // event lama atau duplikat, abaikan
        return

    orderProjection.upsert(
        orderId=event.orderId,
        status=event.status,
        version=event.version,
        updatedAt=event.occurredAt
    )

7) Worker tidak graceful saat shutdown atau deploy

Gejala:

  • setelah deploy, ada lonjakan retry dan job dobel,
  • job panjang terputus di tengah,
  • backlog naik setiap kali autoscaling atau rolling restart.

Root cause umum:

  • worker dihentikan paksa tanpa drain,
  • visibility timeout lebih pendek dari durasi job,
  • tidak ada sinyal shutdown yang memberi kesempatan menyelesaikan pekerjaan.

Langkah audit:

  1. Periksa durasi job terpanjang dibanding timeout queue dan timeout proses.
  2. Uji shutdown saat worker sedang memproses job kritis.
  3. Pastikan deployment pipeline mendukung drain dan health check yang benar.

Mitigasi:

  • Implementasikan graceful shutdown.
  • Sesuaikan visibility timeout dengan durasi kerja nyata plus margin.
  • Pecah job panjang menjadi unit lebih kecil jika memungkinkan.

8) Observabilitas minim: tahu gagal, tapi tidak tahu di mana

Gejala:

  • alert berbunyi tetapi tim tidak tahu producer, broker, worker, atau dependency mana yang rusak,
  • investigasi perlu grep log manual di banyak service,
  • tidak bisa menghubungkan satu request pengguna ke serangkaian job asynchronous.

Root cause umum:

  • tidak ada correlation ID,
  • metrik hanya total error tanpa cardinality yang berguna,
  • log tidak membawa konteks attempt, queue name, latency, atau dependency result.

Langkah audit:

  1. Pastikan setiap job membawa correlation ID atau trace context dari request awal bila relevan.
  2. Pastikan log terstruktur memuat: job type, attempt, queue, duration, result, reason, dependency target.
  3. Pastikan ada metrik backlog, age of oldest message, success rate, retry rate, DLQ rate, cache hit rate, lock contention, dan processing latency.

Mitigasi:

  • Buat dashboard per antrian dan per jenis job.
  • Tambahkan tracing untuk alur request → publish → consume → dependency → write.
  • Bedakan error bisnis, error validasi, timeout, dan infra failure.

Metrik dan alert yang wajib ada

Berikut metrik yang paling sering membantu membedakan bug aplikasi dari gangguan operasional:

Metrik queue dan worker

  • Queue depth/backlog: jumlah pesan yang menunggu.
  • Age of oldest message: indikator backlog yang benar-benar menyakitkan pengguna.
  • Processing latency: waktu dari mulai diproses sampai selesai.
  • End-to-end latency: waktu dari job dipublish sampai side effect selesai.
  • Success, failure, retry, dan DLQ rate.
  • Attempt distribution: berapa banyak job sukses di attempt pertama vs ketiga.
  • Worker concurrency dan saturation.

Metrik cache

  • Cache hit ratio per key pattern atau use case.
  • Miss burst setelah expiry massal.
  • Rebuild latency untuk key mahal.
  • Stale serve count bila memakai stale-while-revalidate.

Metrik lock dan consistency

  • Lock acquisition failure rate.
  • Lock hold duration dan lock timeout.
  • Duplicate suppression count dari mekanisme idempotensi.
  • Out-of-order event discard count bila consumer mengabaikan event lama.

Alert yang praktis

  • Backlog naik terus selama beberapa interval, bukan hanya spike singkat.
  • Age of oldest message melewati SLO.
  • Retry rate melonjak tanpa kenaikan throughput.
  • DLQ bertambah cepat atau reason tertentu mendominasi.
  • Cache hit ratio turun tajam pada key penting.
  • Lock contention naik dan p95 processing time ikut naik.

Tip: alert terbaik biasanya menggabungkan dua sinyal. Contoh: backlog naik dan throughput turun, atau retry naik dan dependency timeout naik. Ini mengurangi alarm palsu.

Checklist audit teknis yang bisa langsung dipakai

A. Queue dan consumer

  • Apakah setiap jenis job punya kontrak retry yang jelas?
  • Apakah error transient dibedakan dari error permanen?
  • Apakah ada max attempts dan backoff dengan jitter?
  • Apakah poison message masuk ke DLQ?
  • Apakah payload tervalidasi sebelum kerja mahal dilakukan?
  • Apakah ack hanya dilakukan setelah state penting aman?
  • Apakah durasi job sesuai dengan timeout queue?
  • Apakah worker mendukung graceful shutdown?

B. Idempotensi dan consistency

  • Apakah handler aman bila dijalankan dua kali?
  • Apakah ada unique constraint atau compare-and-set yang mendukung invariant bisnis?
  • Apakah side effect eksternal punya idempotency key atau dedupe record?
  • Apakah consumer tahan terhadap event lama atau event duplikat?
  • Apakah ordering dibutuhkan per entity? Jika ya, bagaimana dijaga?

C. Cache

  • Apakah setiap cache punya owner yang jelas: siapa mengisi, siapa invalidasi, kapan boleh stale?
  • Apakah TTL diberi jitter?
  • Apakah ada proteksi stampede untuk key populer?
  • Apakah stale cache diterima secara sadar, bukan kebetulan?
  • Apakah ada fallback jika cache miss saat dependency lambat?

D. Locking

  • Apakah lock benar-benar dibutuhkan, atau database invariant sudah cukup?
  • Apakah lock memakai token ownership?
  • Apakah TTL lock realistis terhadap durasi kerja?
  • Apakah ada strategi bila lock bocor atau habis saat proses belum selesai?

E. Observabilitas

  • Apakah job punya correlation ID?
  • Apakah log terstruktur memuat attempt, queue, duration, dan error class?
  • Apakah ada dashboard backlog, oldest age, retry, DLQ, hit ratio cache, dan lock contention?
  • Apakah runbook tersedia dan pernah diuji?

Runbook singkat saat insiden terjadi

Kasus 1: backlog queue naik cepat

  1. Cek age of oldest message. Jika rendah, mungkin hanya spike sementara; jika terus naik, ada bottleneck nyata.
  2. Bandingkan publish rate vs consume rate.
  3. Cek apakah worker sehat, concurrency berubah, atau deployment baru saja terjadi.
  4. Lihat retry rate dan top error reason. Jika retry mendominasi, fokus ke dependency atau poison message.
  5. Bila ada poison message, isolasi ke DLQ agar trafik sehat bisa lewat.

Kasus 2: job dobel terdeteksi

  1. Cari correlation ID atau idempotency key dari kasus terdampak.
  2. Periksa apakah side effect terjadi sebelum ack dan worker crash sesudahnya.
  3. Verifikasi apakah constraint unik/idempotency store benar-benar aktif.
  4. Jika perlu, hentikan replay otomatis sementara dan aktifkan dedupe darurat di jalur kritis.

Kasus 3: cache stampede

  1. Identifikasi key pattern yang miss bersamaan.
  2. Aktifkan atau perketat jitter TTL.
  3. Jika tersedia, nyalakan stale-while-revalidate atau single-flight.
  4. Naikkan proteksi di dependency downstream bila database atau API sudah tercekik.

Kasus 4: lock contention atau lock bocor

  1. Cek lock hold duration dan siapa owner terakhir jika metadata tersedia.
  2. Verifikasi apakah proses berjalan lebih lama dari TTL lock.
  3. Jangan menghapus lock secara buta tanpa memastikan side effect yang mungkin masih berjalan.
  4. Setelah insiden, evaluasi apakah lock perlu diganti dengan kontrol berbasis database.

Contoh review anti-"LGTM" untuk perubahan kecil yang dampaknya besar

Saat ada pull request yang “hanya menambah worker baru” atau “hanya menambah cache”, gunakan pertanyaan ini sebelum approve:

  • Bagaimana perilakunya jika pesan dikirim dua kali?
  • Apa yang terjadi jika dependency timeout setelah side effect parsial?
  • Jika worker restart di tengah proses, state mana yang sudah permanen?
  • Jika cache invalidation gagal, berapa lama data bisa stale dan apa dampaknya?
  • Jika lock expired saat proses masih jalan, apakah worker kedua bisa merusak invariant?
  • Apa metrik baru yang perlu ditambahkan agar perubahan ini bisa dioperasikan?

Kalau pertanyaan-pertanyaan ini belum punya jawaban yang jelas, maka “LGTM” masih prematur.

Penutup

Checklist Anti-"LGTM": Audit Queue, Cache, dan Worker Produksi pada dasarnya adalah disiplin untuk memeriksa perilaku sistem saat kondisi tidak ideal. Queue mengirim ulang, worker crash, cache basi, lock bocor, event telat, dan observabilitas kurang adalah kejadian normal di sistem produksi, bukan edge case eksotis.

Audit yang baik tidak berhenti pada “apakah fitur bekerja”, tetapi lanjut ke “apakah fitur tetap aman saat duplicate delivery, retry, partial failure, dan traffic spike”. Jika tim Anda menjadikan checklist ini bagian dari review dan readiness produksi, banyak insiden yang biasanya lolos dengan cap “LGTM” bisa dihentikan sebelum benar-benar menjadi incident report.