Pertanyaan utama adalah: bagaimana mendeteksi dan memperbaiki race condition di handler webhook PHP ketika trafik melonjak? Artikel ini menjelaskan gejala khas, proses investigasi, penyebab teknis, serta langkah perbaikan praktis untuk memastikan webhook tetap konsisten di beban tinggi.

Latar Kasus

Tim backend menerima laporan request webhook ganda dikirim dari gateway pihak ketiga. Log menunjukkan beberapa handler memproses payload sama sekaligus, mengakibatkan data transaksi tercatat dua kali dan status order berubah-ubah. Webhook digunakan untuk sinkronisasi status pembayaran, sehingga inkonsistensi ini menimbulkan komplain pengguna dan kesalahan finansial.

Pada awalnya handler PHP sederhana menulis ke database dan cache langsung tanpa koordinasi. Dalam kondisi trafik normal tidak ada masalah, tapi saat gateway mengirim batch parallel saat jam sibuk, race condition muncul. Fokus utama adalah memahami pola concurrent execution pada webhook stateless tersebut.

Diagnosis

Gejala yang Terlihat

  • Log berganda: setiap request memiliki request_id unik, namun log menunjukkan beberapa worker menangani request_id sama dalam rentang waktu milidetik.
  • Request duplicate: database menerima insert ganda karena idempotensi tidak di-enforce.
  • Data inkonsisten: cache status selesai kemudian kembali ke pending karena penulisan out-of-order.

Investigasi Timeline dan Trace

Tim mengaktifkan instrumen trace sederhana dengan menambahkan request_id dan timestamp di setiap log entry. Hasilnya memperlihatkan dua handler memulai eksekusi hampir bersamaan dan keduanya mengakses resource shared tanpa locking. Timeline memperlihatkan:

  1. Handler A membaca status order dari cache.
  2. Handler B membaca status tersebut sebelum Handler A selesai update.
  3. Keduanya melakukan penulisan database yang berbeda.

Trace juga memperlihatkan bahwa lock berbasis file semacam flock pernah dicoba, tetapi hanya mengunci bagian awal handler sehingga operasi database masih rentan.

Root Cause Teknis

Analisis kode menunjukkan dua faktor utama:

  • Mismanagement concurrency: handler mengasumsikan webhook akan diproses serial karena infrastruktur awal tidak menggunakan worker pool besar. Tidak ada gate yang mencegah beberapa eksekusi bersamaan untuk payload sama.
  • Locking tidak lengkap: locking hanya dilakukan saat menulis ke cache, namun operasi paling kritis—penulisan database dengan validasi state—tidak dilindungi. Akibatnya dua worker bisa melewati validasi yang sama dan menulis bersamaan.
  • Shared resource tanpa koordinasi: cache shared dan tabel status digunakan tanpa guard. Tidak ada idempotensi untuk menolak operasi duplikat.

Perbaikan

Perbaikan Locking dan Koordinasi

Implementasi perbaikan dimulai dengan mengunci seluruh siklus kritis menggunakan Redis distributed lock, karena environment sudah memakai Redis untuk cache. Pendekatan ini mencegah dua worker menangani payload sama secara bersamaan:

$lockKey = "webhook:lock:" . $payload['event_id'];
$lock = $redis->set($lockKey, getmypid(), ['nx' => true, 'ex' => 30]);
if (!$lock) {
    // ada handler lain, cukup log lalu keluar
    return;
}
try {
    // validasi state, update database, publish event
} finally {
    $redis->del($lockKey);
}

Kunci di atas menggunakan NX untuk memastikan hanya satu worker yang mendapat lock, dan EX untuk mencegah deadlock jika proses crash. Penting untuk menangani digital timeout karena lock harus dilepas saat proses selesai.

Desain Retry Idempotent

Untuk menghindari efek request duplicate, sistem diubah agar setiap payload membawa event_id unik. Handler memeriksa tabel webhook_events sebelum melakukan update:

INSERT INTO webhook_events (event_id, status)
VALUES (:eventId, 'processing')
ON CONFLICT (event_id) DO NOTHING;

SELECT status FROM webhook_events WHERE event_id = :eventId;

Jika row sudah ada, handler bisa memeriksa apakah status sudah selesai dan langsung mengembalikan 200 OK tanpa memproses ulang. Ini membuat retry dari gateway menjadi aman.

Instrumentation dan Observability

Selanjutnya ditambahkan metric khusus menggunakan custom label di Prometheus/Grafana untuk melacak:

  • Timestamp lock acquisition dan release.
  • Count retry/idempotent hits.
  • Durasi handler sebelum dan sesudah locking diterapkan.

Log ditingkatkan dengan menyertakan request_id dan event_id agar dapat memetakan aliran eksekusi sesuai trace sebelumnya.

Regression Test dan Simulasi Trafik Tinggi

Dibangun suite regression test yang mensimulasikan 50 concurrent webhook dengan payload sama menggunakan wrk atau script PHP multi-thread. Test ini mencakup:

  1. Verifikasi hanya satu entry event_id tercatat.
  2. Validasi status order konsisten di database.
  3. Pastikan lock dilepas meski handler memanggil exit atau exception.

Suite dijadikan bagian pipeline CI untuk menangkap regresi sebelum deployment.

Takeaways

  • Gejala log ganda dan data inkonsisten biasanya mengarah ke race condition di handler stateless. Trace timeline membantu memastikan dua worker tidak melakukan update bersamaan.
  • Locking harus mencakup seluruh siklus kritis dan tidak cukup hanya di cache. Redis atau database row lock adalah solusi praktis di lingkungan PHP.
  • Desain idempotent membuat webhook tolerant terhadap retry, sedangkan instrumentation memastikan masalah serupa cepat terlihat.
  • Regression test dengan traffic tinggi menjadi pengingat bahwa race condition sering tidak muncul di lingkungan development standar.

Dengan pendekatan ini, handler webhook PHP menjadi robust terhadap triangular concurrency saat skala tinggi, menjaga konsistensi data tanpa mengorbankan throughput.