Masalah transaksi ganda di CodeIgniter 4 umumnya muncul saat client mengirim ulang permintaan pembayaran yang sempat gagal, dan sistem backend tidak membedakan permintaan baru dari retry. Untuk kasus ini, observasi log awal langsung menunjukkan pola: entri transaksi duplikat, status HTTP 409 pada response kedua, dan header retry seperti X-Retry-Reason. Artikel ini membahas bagaimana akar masalah hilangnya idempotensi atau mekanisme locking sederhana, lalu menyiapkan debugging dan perbaikan menyeluruh.
Gejala yang Terlihat di Log dan Metrik
Tim monitoring menemukan pola berikut di development logging dan dashboard API gateway:
- Request pembayaran dari client yang sama muncul dua kali dalam selang detik, ditandai dengan
trace_idyang berbeda tetapi payload identik. - Entitas
transactionsmendapatkan dua baris hampir bersamaan, walau skema meja seharusnya validasiinvoice_idunik. - Response pertama berhasil (HTTP 200), response kedua dikastrasi jadi HTTP 409 karena constraint jelas, tetapi client tetap mencatat pembayaran selesai karena response 200 pertama.
- Header
X-Retry-Reason: timeoutatauRetry-Afterhadir pada pengiriman ulang, menandakan client melakukan retry otomatis.
Gejala ini membentuk hipotesis bahwa backend memproses retry tanpa memeriksa idempotensi atau eksklusivitas pada row yang sama.
Analisis Root Cause: Idempotensi dan Lock yang Hilang
Permintaan pembayaran harus idempotent: menerima beberapa kali request yang sama tidak boleh menambah jumlah transaksi. Di kasus ini ada dua kekurangan:
- Tidak ada idempotent key: API hanya menerima
invoice_idtapi tidak menyimpan kunci unik untuk permintaan (misal headerX-Idempotency-Key), sehingga setiap retry dianggap transaksi baru. - Locking transaksi tidak ada: Transaksi dibuat di tabel
transactionstanpaFOR UPDATEdan tanpa flag ongoing, sehingga kedua proses bisa baca lalu tulis secara almost-simultan.
Tanpa observabilitas tambahan pun, sulit membedakan apakah response pertama benar-benar selesai sebelum retry diproses.
Langkah Perbaikan Praktis
1. Request Tracing dengan Middleware
Tambahkan filter HTTP untuk menangkap header retry dan menetapkan trace_id supaya semua log bisa dilacak ke satu permintaan logis. Contoh filter sederhana:
namespace Appilters;
use CodeIgniteriltersilterInterface;
use CodeIgniteriltersilterTrait;
use CodeIgniter
outer
outers
outer;
use CodeIgniter
eactor
equest;
use CodeIgniter
eactor
esponse;
class RequestTrace implements FilterInterface
{
use FilterTrait;
public function before(RequestInterface $request, $arguments = null)
{
$traceId = $request->getHeaderLine('X-Trace-Id');
if (empty($traceId)) {
$traceId = bin2hex(random_bytes(16));
}
Services::request()->setGlobal('trace_id', $traceId);
log_message('info', "trace_id={$traceId} method={$request->getMethod()} uri={$request->getURI()}");
}
public function after(RequestInterface $request, ResponseInterface $response, $arguments = null)
{
// optional response header
$response->setHeader('X-Trace-Id', Services::request()->getGlobal('trace_id'));
}
}
Daftarkan filter ini di app/Config/Filters.php agar setiap permintaan pembayaran ter-tag.
2. Menetapkan Kunci Idempotent
Tambahkan kolom idempotency_key pada tabel transactions dan buat logika yang memeriksa kunci tersebut sebelum membuat transaksi baru.
$idempotencyKey = $this->request->getHeaderLine('X-Idempotency-Key');
if (empty($idempotencyKey)) {
throw new
untimeException('Idempotency key wajib.');
}
$existing = $this->transactionModel
->where('idempotency_key', $idempotencyKey)
->where('status', 'completed')
->first();
if ($existing) {
return $existing;
}
$this->db->transStart();
$transactionId = $this->transactionModel->insert([... 'idempotency_key' => $idempotencyKey]);
$this->db->transComplete();
Dengan pola ini, retry akan segera mengembalikan hasil transaksi yang sebelumnya sudah ada tanpa membuat baris baru.
3. Locking Sederhana untuk Memproses Transaksi
Untuk memastikan request paralel tidak melewati validasi, gunakan locking baris sebelum melakukan insert/update:
START TRANSACTION;
SELECT id FROM transactions WHERE invoice_id = ? FOR UPDATE;
-- lakukan insert/update setelah kunci diperoleh
COMMIT;
Implementasi di CodeIgniter bisa menggunakan query builder dengan FOR UPDATE dan block transaction, lalu apabila tidak ada row, baru insert. Ini mencegah dua thread membuat record dengan invoice yang sama.
4. Observabilitas dan Monitoring
Sertakan metrik tambahan seperti counter idempotent_hit dan log HTTP 409. Jika terjadi HTTP 409 karena duplicate, tambahkan detail trace_id dan idempotency_key pada log agar mudah dikorelasi. Pastikan dashboard menampilkan latency distribusi saat lock diambil.
Regresi dan Validasi
Buat skenario pengujian regresi berikut:
- Simulasikan client mengirim
POST /paymentdengan payload yang sama, headerX-Idempotency-Key: 12345, dan ulangi permintaan kedua setelah 200 ms. - Pastikan response pertama success (200) dan kedua juga mengembalikan data yang identik tanpa status 500/409.
- Verifikasi hanya satu baris tersimpan pada tabel
transactionsdenganidempotency_key=12345dan statuscompleted. - Periksa log trace: ada dua record request tetapi nilai
trace_idsama bila middleware mengikat, dan tidak ada entri duplicate transaction insert.
Dengan skenario ini tim QA bisa memastikan perbaikan tidak merusak flow biasa dan memotong retry loop.
Kesimpulan
Bug transaksi ganda akibat retry API mudah dikenali dari log duplicate dan HTTP 409, dan paling efektif diatasi dengan kombinasi request tracing, idempotent key, locking database, dan observabilitas yang baik. Pendekatan ini memastikan backend CodeIgniter 4 tetap aman walau client melakukan retry otomatis.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!