Saat volume pull request naik, tim sering terbantu oleh bot review yang cepat memberi komentar soal style, naming, atau potensi refactor. Masalahnya, bug race condition jarang terlihat dari pembacaan diff secara dangkal. Akibatnya, backend bisa lolos review dengan log yang tampak normal, tetapi menghasilkan data ganda, status job melompat, atau hasil akhir yang inkonsisten di produksi.

Artikel ini membahas studi kasus debug backend saat bot review menyembunyikan bug race condition: bagaimana gejalanya muncul, mengapa reviewer manusia dan AI sama-sama bisa melewatkannya, cara mereproduksi bug dengan uji konkurensi, apa yang harus diperiksa di database dan worker, lalu bagaimana memastikan perbaikannya benar-benar menutup celah.

Kasus yang Terjadi di Produksi

Bayangkan sebuah endpoint backend memproses permintaan pembuatan job impor data. Untuk mencegah job ganda, sistem mengecek apakah job dengan kunci tertentu sudah ada. Jika belum ada, sistem membuat job baru, menyimpan status awal, lalu worker memproses job tersebut secara asinkron.

Pada kondisi trafik rendah, alur ini terlihat aman. Namun saat volume request naik dan beberapa request identik datang hampir bersamaan, dua proses bisa membaca kondisi yang sama: job belum ada. Keduanya lalu membuat record sendiri-sendiri. Dari sisi log aplikasi, setiap request tampak berhasil. Dari sisi bisnis, hasilnya kacau:

  • Data yang seharusnya unik menjadi ganda.
  • Status job berubah dari queued ke processing, lalu tiba-tiba ada job lain yang langsung done atau failed.
  • Worker melakukan retry dan memperparah duplikasi karena operasi tidak idempoten.
  • Dashboard monitoring tidak selalu merah karena setiap komponen merasa bekerja sesuai kontraknya.

Ini tipikal race condition backend: masalah muncul bukan karena satu baris kode jelas salah, tetapi karena urutan eksekusi antar request atau worker tidak terkendali.

Mengapa Bot Review dan Reviewer Manual Bisa Melewatkannya

1. Komentar review terlalu fokus pada style

Pada saat PR menumpuk, bot review sering mendominasi percakapan dengan komentar seperti konsistensi nama variabel, duplikasi kecil, atau pemisahan fungsi. Itu berguna, tetapi tidak menjawab pertanyaan penting: apa yang terjadi jika dua request identik masuk bersamaan?

Reviewer manusia pun mudah terdorong mengikuti arah yang sama. Jika komentar AI sudah ramai, perhatian bisa habis untuk hal-hal kosmetik. Ini sejalan dengan masalah kelelahan code review akibat AI yang mulai sering dibahas di komunitas, termasuk di dev.to: semakin banyak komentar otomatis, semakin besar risiko sinyal penting tenggelam oleh noise.

2. Diff terlihat masuk akal bila dibaca secara linear

Race condition sering tersembunyi dalam pola seperti:

  • SELECT lalu INSERT tanpa pengaman atomik.
  • Validasi status dilakukan di luar transaksi yang benar.
  • Retry worker dianggap aman padahal efek sampingnya tidak idempoten.
  • State transition diasumsikan berurutan tanpa verifikasi versi atau lock.

Secara linear, kode semacam ini tampak bersih. Masalahnya baru muncul ketika dua eksekusi saling bertabrakan dalam milidetik yang sama.

3. Log tampak normal

Tim sering terkecoh karena log menunjukkan pesan sukses berurutan. Padahal tanpa correlation ID atau job key yang konsisten, kita tidak melihat bahwa dua request berbeda sedang memproses entitas yang sama pada waktu hampir bersamaan.

Gejala Nyata yang Perlu Dicurigai

Jika Anda menduga ada race condition di backend, perhatikan kombinasi gejala berikut:

  • Data ganda pada entitas yang secara bisnis seharusnya unik.
  • Status job lompat, misalnya langsung dari queued ke done pada satu alur, sementara alur lain masih processing.
  • Log terlihat normal jika dibaca per baris, tetapi hasil akhir antar tabel atau antar layanan tidak konsisten.
  • Retry worker meningkat tanpa error aplikasi yang jelas.
  • Kasus sulit direproduksi secara lokal dengan satu request tunggal.

Catatan penting: race condition tidak selalu memunculkan exception. Justru sering kali sistem terlihat sehat, tetapi menyimpan state yang salah.

Akar Masalah Teknis

Idempotency lemah

Idempotency berarti pengulangan request atau job yang sama tidak menghasilkan efek samping tambahan. Banyak sistem merasa sudah aman hanya karena ada pengecekan if exists. Itu belum cukup. Jika pengecekan dan penulisan tidak atomik, dua proses tetap bisa sama-sama lolos.

Transaksi terlalu sempit

Bug sering muncul karena transaksi hanya membungkus INSERT tertentu, sementara pembacaan kondisi, validasi status, atau enqueue job terjadi di luar area transaksi. Akibatnya, state yang diputuskan saat awal transaksi bisa sudah usang ketika operasi berikutnya berjalan.

Unique constraint tidak ada atau tidak dipakai sebagai garis pertahanan

Jika bisnis mengharuskan satu job unik per kombinasi kunci tertentu, database seharusnya menegakkan aturan itu. Tanpa unique constraint, aplikasi bergantung penuh pada asumsi bahwa tidak ada request bersamaan.

Retry worker tidak diaudit

Worker queue sering diatur untuk retry otomatis. Ini benar untuk keandalan, tetapi berbahaya bila handler job menulis data tanpa kunci idempoten. Satu kegagalan sementara bisa berubah menjadi beberapa penulisan ganda.

Asumsi review hanya menilai keterbacaan, bukan perilaku konkurensi

PR yang lolos review belum tentu aman di bawah beban. Jika checklist review tidak memaksa pertanyaan tentang konkurensi, lock, dan idempotency, bug semacam ini mudah lolos.

Langkah Investigasi yang Bisa Diikuti

1. Reproduksi dengan concurrency test

Jangan mulai dari membaca kode saja. Buat skenario yang menembakkan request identik secara paralel ke endpoint atau memicu worker yang sama secara bersamaan. Tujuannya bukan throughput, tetapi mencari interleaving yang salah.

Contoh pendekatan:

  • Kirim 20-100 request identik secara paralel dengan payload yang sama.
  • Jalankan beberapa worker sekaligus pada antrean yang sama.
  • Tambahkan jeda kecil terkontrol di titik kritis untuk memperlebar jendela race.
# Contoh pseudo-command untuk menembakkan request paralel
# Gunakan alat yang tersedia di stack Anda, fokusnya adalah request identik dan paralel
for i in $(seq 1 50); do
  curl -X POST http://localhost:8080/import \
    -H 'Content-Type: application/json' \
    -d '{"account_id":42,"source":"daily-sync","request_key":"abc-123"}' &
done
wait

Jika bug hanya muncul sesekali, itu justru indikasi kuat race condition. Ulangi pengujian beberapa kali dengan payload yang sama dan inspeksi hasil di database.

2. Korelasikan log per request dan per job

Log baris demi baris hampir tidak berguna jika tidak ada identitas bersama. Pastikan Anda bisa mengelompokkan event berdasarkan:

  • request_id
  • job_id
  • idempotency_key atau business_key
  • Timestamp dengan resolusi cukup baik

Cari pola seperti dua request berbeda dengan business_key sama yang sama-sama mencetak log job created. Itu lebih bernilai daripada sekadar melihat jumlah error.

3. Cek lock dan unique constraint di database

Periksa apakah ada pertahanan di level database, bukan hanya di aplikasi:

  • Apakah tabel target punya unique index sesuai aturan bisnis?
  • Apakah operasi kritis memakai row lock atau mekanisme atomik yang setara saat diperlukan?
  • Apakah transaksi mencakup baca-ubah-tulis yang relevan, bukan hanya sebagian?

Trade-off-nya jelas: lock yang terlalu luas bisa menurunkan paralelisme, tetapi tanpa kontrol sama sekali Anda menukar performa semu dengan korupsi data.

4. Audit retry worker dan efek sampingnya

Lihat konfigurasi dan implementasi worker:

  • Kapan job di-retry?
  • Apakah ada timeout yang menyebabkan handler berjalan ulang?
  • Apakah job aman diproses lebih dari sekali?
  • Apakah enqueue dilakukan sebelum transaksi commit, sehingga worker membaca state setengah jadi?

Kesalahan umum adalah menganggap retry hanya soal reliabilitas. Pada praktiknya, retry adalah pengali race condition jika idempotency lemah.

5. Verifikasi perbaikan dengan skenario yang sama

Setelah perbaikan diterapkan, jalankan lagi concurrency test yang sama. Jangan puas karena kode tampak lebih rapi. Bukti yang dibutuhkan adalah:

  • Tidak ada data ganda pada payload identik.
  • State transition konsisten dan tidak melompat.
  • Retry tidak menambah efek samping.
  • Constraint database menolak duplikasi yang seharusnya mustahil.

Contoh Pseudo-code Sebelum dan Sesudah

Sebelum: cek lalu buat, tanpa jaminan atomik

function createImportJob(accountId, source, requestKey) {
  existing = db.queryOne(
    "SELECT id FROM import_jobs WHERE account_id = ? AND source = ? AND request_key = ?",
    [accountId, source, requestKey]
  )

  if (existing) {
    return existing.id
  }

  jobId = db.insert(
    "INSERT INTO import_jobs(account_id, source, request_key, status) VALUES (?, ?, ?, ?)",
    [accountId, source, requestKey, "queued"]
  )

  queue.publish("process-import", { jobId: jobId })
  return jobId
}

Masalah pada kode di atas:

  • Dua request paralel bisa sama-sama tidak menemukan data lalu sama-sama INSERT.
  • queue.publish terjadi setelah insert tanpa pengaman tambahan; jika ada retry, efek samping bisa berulang.
  • Tidak ada pertahanan database yang memaksa keunikan.

Sesudah: idempotency lebih kuat, transaksi tepat, database jadi garis pertahanan

function createImportJob(accountId, source, requestKey) {
  return db.transaction(() => {
    // Database memiliki unique constraint pada:
    // (account_id, source, request_key)

    row = db.queryOne(
      "SELECT id, status FROM import_jobs WHERE account_id = ? AND source = ? AND request_key = ? FOR UPDATE",
      [accountId, source, requestKey]
    )

    if (row) {
      return row.id
    }

    jobId = db.insert(
      "INSERT INTO import_jobs(account_id, source, request_key, status) VALUES (?, ?, ?, ?)",
      [accountId, source, requestKey, "queued"]
    )

    db.insert(
      "INSERT INTO outbox_events(event_type, aggregate_id, dedupe_key) VALUES (?, ?, ?)",
      ["process-import", jobId, requestKey]
    )

    return jobId
  })
}

function workerProcessOutbox(event) {
  // publish event ke queue secara idempoten
}

function processImportJob(jobId) {
  return db.transaction(() => {
    row = db.queryOne(
      "SELECT id, status FROM import_jobs WHERE id = ? FOR UPDATE",
      [jobId]
    )

    if (row.status === "done") {
      return
    }

    if (row.status === "queued") {
      db.execute("UPDATE import_jobs SET status = ? WHERE id = ?", ["processing", jobId])
    }

    // lakukan proses bisnis yang juga harus idempoten
    // misalnya menulis hasil dengan unique key atau upsert yang tepat

    db.execute("UPDATE import_jobs SET status = ? WHERE id = ?", ["done", jobId])
  })
}

Poin penting dari perbaikan ini:

  • Unique constraint menjadi benteng terakhir jika dua proses tetap berlomba.
  • Transaksi membungkus keputusan penting, bukan hanya insert.
  • Lock dipakai pada baris yang relevan untuk mencegah perubahan status bertabrakan.
  • Outbox pattern membantu menghindari publish queue sebelum state database benar-benar commit.
  • Handler worker memeriksa state dan dibuat lebih idempoten.

Catatan trade-off: FOR UPDATE dan transaksi yang lebih lebar bisa menambah kontensi. Gunakan hanya pada jalur kritis yang memang membutuhkan konsistensi kuat. Untuk beberapa kasus, pendekatan seperti upsert, compare-and-set, atau dedupe key bisa lebih efisien daripada lock yang luas.

Memilih Perbaikan yang Tepat

Gunakan unique constraint jika aturan bisnis memang unik

Jika hanya boleh ada satu job untuk kombinasi kunci tertentu, letakkan aturan itu di database. Ini perlindungan paling kuat terhadap bug aplikasi dan deployment yang tidak sinkron.

Gunakan lock saat ada state transition kritis

Bila masalahnya bukan sekadar duplikasi insert, tetapi perubahan status yang harus berurutan, lock atau mekanisme kontrol konkurensi lain menjadi relevan.

Gunakan idempotency key untuk request dan retry

Jika klien atau worker bisa mengirim ulang operasi yang sama, pastikan ada kunci idempoten yang stabil dan tersimpan. Jangan mengandalkan timestamp acak yang berbeda tiap retry.

Gunakan outbox bila write database dan publish message harus konsisten

Jika sistem Anda menulis ke database lalu mengirim event atau job, outbox mengurangi risiko state database dan message broker saling tidak sinkron.

Kesalahan Umum Saat Memperbaiki Race Condition

  • Hanya menambah pengecekan di aplikasi tanpa unique constraint di database.
  • Menambah log lebih banyak tetapi tetap tanpa correlation key yang jelas.
  • Menganggap transaksi selalu menyelesaikan masalah, padahal cakupannya salah atau tidak mencakup semua langkah kritis.
  • Memperbaiki endpoint tetapi lupa bahwa worker retry masih menulis efek samping ganda.
  • Mengandalkan review bot untuk menemukan bug konkurensi yang sebenarnya memerlukan pengujian perilaku.

Checklist Pencegahan di Tim Backend

  • Apakah operasi ini aman jika request yang sama masuk dua kali?
  • Apakah ada unique constraint sesuai invariannya?
  • Apakah retry worker aman dan idempoten?
  • Apakah state transition divalidasi di dalam transaksi atau dengan mekanisme atomik?
  • Apakah enqueue/publish terjadi setelah state stabil, atau lewat outbox?
  • Apakah log punya request_id, job_id, dan business_key untuk korelasi?
  • Apakah PR yang menyentuh queue, status, billing, inventory, atau import otomatis mendapat review konkurensi?
  • Apakah ada concurrency test minimal untuk jalur kritis?

Mengaitkan dengan Kelelahan Code Review Akibat AI

Masalah inti di sini bukan bahwa AI review tidak berguna. Masalahnya adalah ketidakseimbangan perhatian. Saat PR membanjir dan bot memenuhi review dengan komentar yang murah secara komputasi tetapi mahal secara atensi, reviewer manusia makin sulit fokus pada pertanyaan bernilai tinggi: invariant data, konkurensi, urutan event, dan idempotency.

Dalam praktiknya, tim bisa mengambil langkah berikut agar pengalaman review tetap sehat:

  • Pisahkan komentar style otomatis dari review desain dan konkurensi.
  • Buat template PR yang mewajibkan penulis menjawab: “apa yang terjadi jika request diproses dua kali?”
  • Terapkan label khusus untuk perubahan yang menyentuh queue, transaksi, atau state machine.
  • Kurangi noise dari bot agar reviewer manusia punya ruang berpikir, bukan sekadar menyapu komentar kosmetik.

Referensi diskusi di komunitas seperti dev.to relevan secara praktis: ketika AI membuat review terasa sibuk tetapi dangkal, bug yang membutuhkan penalaran lintas request justru lebih mudah lolos.

Penutup

Jika backend Anda menampilkan data ganda, status job melompat, dan log tampak normal tetapi hasil akhir inkonsisten, jangan berhenti pada analisis diff PR. Curigai race condition. Mulailah dari reproduksi dengan concurrency test, korelasi log per request dan job, audit constraint serta lock di database, periksa retry worker, lalu buktikan perbaikan dengan skenario yang sama.

Pelajaran penting dari studi kasus ini sederhana: review bot membantu menjaga kebersihan kode, tetapi debug backend saat bot review menyembunyikan bug race condition menuntut lebih dari itu. Anda butuh desain idempoten, transaksi yang tepat, pertahanan di level database, dan kultur review yang masih memberi ruang untuk berpikir tentang perilaku sistem di bawah konkurensi nyata.