Ketika database upstream atau core service mengalami downtime, API gateway tetap harus memproses request pembayaran atau webhook dari payment gateway secara deterministik. Pola umum yang mengandalkan Redis atau database terpusat untuk menyimpan idempotency key sering kali ikut tumbang jika insiden melibatkan saturasi koneksi atau partisi jaringan. Akibatnya, retry request dari upstream penyedia webhook dieksekusi ulang secara membabi-buta saat sistem pulih, memicu eksekusi ganda (double-spend).
Solusi pragmatis untuk mengatasi masalah ini adalah menggunakan idempotency store LMDB di API Gateway. LMDB (Lightning Memory-Mapped Database) merupakan embedded transactional key-value store berbasis B+ Tree yang beroperasi langsung di proses gateway melalui memory-mapped file (mmap). LMDB meniadakan latensi round-trip jaringan (0ms I/O overhead via OS page cache) dan menjamin konsistensi ACID secara lokal.
Arsitektur State Machine Idempotensi
Idempotensi yang andal memerlukan pelacakan siklus hidup request secara ketat. Siklus request dibagi menjadi dua state utama:
- IN_FLIGHT: Request sedang diproses oleh downstream/upstream service. Request lain dengan key identik yang datang bersamaan harus ditolak dengan kode respon
409 Conflictatau ditahan untuk mencegah race condition. - COMPLETED: Request selesai diproses. Response payload dan status HTTP dicatat di store. Setiap retry dengan key yang sama langsung menerima cached response ini tanpa menyentuh core DB backend.
Jika upstream database down saat gateway mencoba mem-forward request dengan status IN_FLIGHT, gateway mencatat kegagalan, membebaskan lock atau mengubah status menjadi FAILED/dihapus, lalu mengembalikan 503 Service Unavailable secara aman tanpa meninggalkan mutasi gantung.
Atomic Check-and-Set Menggunakan C API LMDB
LMDB mengimplementasikan serialisasi penulisan (single-writer model via POSIX mutex lock). Hal ini secara alami mengeliminasi race condition pada konkurensi tinggi tanpa memerlukan distributed lock yang kompleks. Operasi cek status dan reservasi status IN_FLIGHT dilakukan dalam satu transaksi write atomik tunggal (mdb_txn_begin hingga mdb_txn_commit).
#include <stdio.h>
#include <string.h>
#include <time.h>
#include <lmdb.h>
typedef enum {
STATUS_IN_FLIGHT = 1,
STATUS_COMPLETED = 2
} ReqStatus;
typedef struct {
ReqStatus status;
uint32_t http_code;
time_t created_at;
char response_body[512];
} IdempotencyRecord;
// Mengembalikan 0: Key baru berhasil di-lock (IN_FLIGHT)
// Mengembalikan 1: Key sudah COMPLETED (replay terdeteksi)
// Mengembalikan 2: Key sedang IN_FLIGHT (konkuren conflict)
// Mengembalikan -1: Error sistem LMDB
int acquire_or_get_idempotency(
MDB_env *env,
MDB_dbi dbi,
const char *idemp_key,
IdempotencyRecord *out_record
) {
MDB_txn *txn;
MDB_val key, data;
int rc;
// Mulai write transaction secara eksklusif
rc = mdb_txn_begin(env, NULL, 0, &txn);
if (rc != MDB_SUCCESS) return -1;
key.mv_size = strlen(idemp_key);
key.mv_data = (void *)idemp_key;
rc = mdb_get(txn, dbi, &key, &data);
if (rc == MDB_SUCCESS) {
// Record ditemukan, salin data yang ada
memcpy(out_record, data.mv_data, sizeof(IdempotencyRecord));
mdb_txn_abort(txn);
if (out_record->status == STATUS_COMPLETED) {
return 1; // Return cached response
}
return 2; // Conflict, request identik sedang berjalan
} else if (rc != MDB_NOTFOUND) {
mdb_txn_abort(txn);
return -1;
}
// Key belum ada: Daftarkan sebagai IN_FLIGHT secara atomik
IdempotencyRecord new_record;
memset(&new_record, 0, sizeof(IdempotencyRecord));
new_record.status = STATUS_IN_FLIGHT;
new_record.created_at = time(NULL);
data.mv_size = sizeof(IdempotencyRecord);
data.mv_data = &new_record;
rc = mdb_put(txn, dbi, &key, &data, MDB_NOOVERWRITE);
if (rc != MDB_SUCCESS) {
mdb_txn_abort(txn);
return -1;
}
// Commit mengunci perubahan ke disk / memory map
rc = mdb_txn_commit(txn);
return (rc == MDB_SUCCESS) ? 0 : -1;
}Menangani Edge Case Kegagalan Upstream DB
Ketika API gateway menerima status sukses 0 dari fungsi di atas, thread gateway memproses payload ke upstream service/database. Terdapat tiga skenario eksekusi:
- Upstream Sukses: Gateway membuka transaksi write LMDB baru, memperbarui payload dengan status
STATUS_COMPLETED, status HTTP (misal200 OK), serta response body. Transaksi di-commit. - Upstream Database Timeout/Down (HTTP 500/503): Gateway harus membatalkan reservasi. Key yang berstatus
STATUS_IN_FLIGHTharus dihapus viamdb_del()agar client diizinkan mengirim request ulang secara sah tanpa terhalang pesan error conflict palsu. - Gateway Crash Saat Memproses: Jika instance gateway mati sebelum commit response berhasil dibuat, record akan tersisa dalam status
STATUS_IN_FLIGHT. Hal ini ditangani menggunakan timeout threshold (TTL) pada saat pembacaan record.
Pembersihan Expired Record untuk Mencegah Disk Bloat
LMDB menggunakan alokasi file berbasis sparse map (mapsize). Jika record lama tidak pernah dibersihkan, file data (data.mdb) akan terus membesar tanpa batas. Karena LMDB tidak memiliki native TTL bawaan per-key layaknya Redis, proses housekeeping harus diatur via secondary database atau background cleanup worker.
Strategi Pruning dengan Sliding-Window B-Tree
Pendekatan paling efisien tanpa scanning seisi database adalah menggunakan tabel indeks terpisah (expiry_dbi) yang menyimpan key berupa integer UNIX timestamp:
// Pseudocode Worker Pembersihan Berkala
void prune_expired_keys(MDB_env *env, MDB_dbi main_dbi, MDB_dbi exp_dbi, time_t ttl_threshold) {
MDB_txn *txn;
MDB_cursor *cursor;
MDB_val exp_key, val_key;
time_t now = time(NULL);
mdb_txn_begin(env, NULL, 0, &txn);
mdb_cursor_open(txn, exp_dbi, &cursor);
// B-Tree menyimpan integer timestamp secara terurut (ascending)
while (mdb_cursor_get(cursor, &exp_key, &val_key, MDB_NEXT) == MDB_SUCCESS) {
time_t record_ts = *(time_t *)exp_key.mv_data;
if (record_ts > (now - ttl_threshold)) {
break; // Berhenti ketika menemukan record yang belum expired
}
// Hapus record dari database utama dan database expiry
mdb_del(txn, main_dbi, &val_key, NULL);
mdb_cursor_del(cursor, 0);
}
mdb_cursor_close(cursor);
mdb_txn_commit(txn);
}Trade-off dan Batasan LMDB
Peringatan Multi-Node: LMDB terikat pada filesystem lokal host (shared-memory POSIX lock). LMDB tidak dapat disinkronisasi langsung antar server API gateway yang berbeda tanpa layer replikasi jaringan tambahan.
Untuk topologi horizontal scaling dengan banyak pod gateway, terapkan salah satu strategi arsitektur berikut:
- Sticky Hashing di Load Balancer: Arahkan request yang membawa header
X-Idempotency-Keyke node gateway yang sama berdasarkan consistent hash algoritma header tersebut. - Hybrid Storage Tier: Gunakan LMDB sebagai first-layer circuit-breaker shield lokal untuk menangkal burst duplicate delivery secara instan, dipadukan dengan database terpusat yang asinkron.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!