Aplikasi monolit modern dengan arsitektur Inertia.js sering kali mengaburkan batas pemrosesan karena interaksi frontend dan backend yang terasa tanpa jeda. Ketika volume transaksi meningkat, gesekan data di database relasional (seperti MySQL/InnoDB atau PostgreSQL) tak terhindarkan. Masalah umum yang kerap muncul di lingkungan produksi adalah kegagalan mutasi dengan galat SQLSTATE[40001]: Serialization failure: 1213 Deadlock found when trying to get lock atau SQLSTATE[HY000]: Lock wait timeout exceeded.
Insiden ini terjadi saat mutasi HTTP langsung dari form Inertia bersilangan dengan background worker queue yang sedang melakukan agregasi, sinkronisasi, atau rekapitulasi data pada baris yang sama. Penanganan masalah ini membutuhkan standardisasi penguncian data di backend dan penanganan status form yang persisten di frontend.
Anatomi Masalah: Lock Contention Antara Web dan Worker
Penyebab utama deadlock dalam skenario web versus worker bukanlah performa server yang lambat, melainkan urutan penguncian baris (lock ordering) yang tidak konsisten dan lingkup transaksi (transaction boundary) yang terlalu lebar.
Perhatikan skenario mutasi pesanan dan item inventaris:
- Thread Web (Inertia HTTP Request): User mengirim form penyesuaian stok manual. Controller membuka transaksi, mengunci baris inventaris A (
FOR UPDATE), lalu mencoba mengunci pesanan B untuk memperbarui log audit. - Thread Queue Worker: Pada saat bersamaan, job rekapitulasi pesanan berjalan di worker background. Job ini membuka transaksi, mengunci pesanan B untuk menghitung nilai agregat, lalu mencoba mengunci baris inventaris A untuk memverifikasi sisa stok.
Database mendeteksi dependensi siklis: Web menunggu Worker melepaskan lock pesanan B, sedangkan Worker menunggu Web melepaskan lock inventaris A. Database engine menghentikan siklus ini dengan membunuh salah satu transaksi (biasanya yang memiliki rollback cost paling kecil) dan melempar exception deadlock.
Kondisi ini diperparah jika controller Inertia membungkus seluruh alur HTTP di dalam transaksi, termasuk proses validasi kompleks, pemanggilan API pihak ketiga, atau dispatch event/job.
Mitigasi Backend: Urutan Kunci Deterministik & Transaksi Ringkas
1. Standardisasi Urutan Penguncian (Deterministic Lock Ordering)
Deadlock yang disebabkan oleh dependensi siklis dapat dieliminasi secara total jika seluruh proses dalam aplikasi mengunci baris data dalam urutan yang identik secara global. Metode paling efektif adalah mengurutkan primary key sebelum dieksekusi.
use App\Models\InventoryItem;
use Illuminate\Support\Facades\DB;
public function updateStock(StockAdjustmentRequest $request)
{
$itemIds = collect($request->validated('items'))
->pluck('id')
->sort()
->values()
->all();
DB::transaction(function () use ($itemIds, $request) {
// Penguncian deterministik berdasarkan ascending ID
$items = InventoryItem::whereIn('id', $itemIds)
->orderBy('id', 'asc')
->lockForUpdate()
->get()
->keyBy('id');
foreach ($request->validated('items') as $data) {
$item = $items->get($data['id']);
$item->decrement('quantity', $data['quantity']);
}
}, 3); // Coba ulang otomatis hingga 3 kali jika terjadi deadlock transient
return back()->with('success', 'Stok berhasil diperbarui.');
}Aturan ini juga wajib diterapkan pada Queue Worker. Pastikan worker batch tidak memproses collection dengan urutan acak atau urutan indeks non-primary yang berbeda arah urutannya.
2. Mempersempit Transaction Boundary
Jaga agar transaksi database sesingkat mungkin. Jangan pernah menempatkan hal berikut di dalam blok DB::transaction:
- Proses otentikasi atau parsing form request yang berat.
- Panggilan HTTP external (payment gateway, webhook, API logistik).
- I/O disk lokal seperti upload gambar atau pembuatan berkas PDF.
- Pemberitahuan/Email event dispatcher yang dijalankan secara sinkron.
// SALAH: Transaksi terbuka terlalu lama
DB::transaction(function () use ($request) {
$order = Order::lockForUpdate()->find($request->order_id);
$pdfPath = $this->pdfService->generateInvoice($order); // Menahan lock saat disk I/O
$this->paymentGateway->charge($order); // Menahan lock saat network I/O
$order->update(['status' => 'paid', 'invoice_path' => $pdfPath]);
});
// BENAR: Kunci hanya saat proses mutasi atomik
$pdfPath = $this->pdfService->generateInvoice($orderData);
$paymentResult = $this->paymentGateway->charge($orderData);
DB::transaction(function () use ($orderId, $pdfPath, $paymentResult) {
$order = Order::where('id', $orderId)->lockForUpdate()->first();
$order->update([
'status' => 'paid',
'invoice_path' => $pdfPath,
'transaction_ref' => $paymentResult->id
]);
}, 3);3. Gunakan SKIP LOCKED untuk Queue Processing Berbasis Baris
Jika background worker mengambil daftar tugas langsung dari tabel database (misalnya batch job processor internal), hindari menggunakan query update langsung yang saling menunggu dengan thread web. Gunakan klausa FOR UPDATE SKIP LOCKED agar worker melewati baris yang sedang dikunci oleh permintaan web:
$pendingBatch = OrderQueueItem::where('status', 'pending')
->orderBy('id', 'asc')
->limit(100)
->lock("FOR UPDATE SKIP LOCKED")
->get();Pendekatan ini mencegah worker terhenti (stalled) ketika ada pengguna Inertia yang sedang mengedit salah satu pesanan di antrean tersebut.
Penanganan di Frontend Inertia.js
Meskipun backend telah menggunakan mekanisme otomatis retry (DB::transaction(..., 3)), kegagalan lock wait timeout atau serialisasi tetap berpotensi tembus ke client saat traffic memuncak. Frontend Inertia harus menangani error tersebut secara elegan tanpa menghilangkan input data yang telah dimasukkan pengguna.
1. Mapping Error di Backend Handler
Ubah error database deadlock/timeout menjadi response HTTP yang ramah (misalnya status 409 Conflict atau validasi khusus) di exception handler framework:
// app/Exceptions/Handler.php atau bootstrap/app.php (Laravel 11)
use Illuminate\Database\QueryException;
public function register(): void
{
$this->renderable(function (QueryException $e, $request) {
if ($request->header('X-Inertia') && in_array($e->getCode(), ['40001', '1205', '1213'])) {
return back()->withErrors([
'concurrency' => 'Data sedang diperbarui oleh sistem background. Silakan tekan tombol Simpan lagi.'
])->setStatusCode(409);
}
});
}2. Form Submission dengan State Preservation
Form helper Inertia (misalnya pada Vue 3 atau React) secara default mempertahankan state input jika server mengembalikan error validasi. Namun, Anda harus memastikan parameter preserveState dan preserveScroll selalu dipertahankan secara eksplisit jika menerima status error non-standar.
<script setup>
import { useForm } from '@inertiajs/vue3';
const props = defineProps({
item: Object,
errors: Object
});
const form = useForm({
id: props.item.id,
stock: props.item.stock,
note: ''
});
const submit = () => {
form.put(`/inventory/${props.item.id}`, {
preserveScroll: true,
preserveState: true,
onError: () => {
// Form data tidak ter-reset, user dapat langsung mengklik ulang tombol submit
}
});
};
</script>
<template>
<form @submit.prevent="submit">
<div v-if="$page.props.errors.concurrency" class="alert alert-warning">
{{ $page.props.errors.concurrency }}
<button type="button" @click="submit" :disabled="form.processing">
Coba Simpan Kembali
</button>
</div>
<input v-model.number="form.stock" type="number" />
<textarea v-model="form.note"></textarea>
<button type="submit" :disabled="form.processing">
<span v-if="form.processing">Menyimpan...</span>
<span v-else>Simpan Perubahan</span>
</button>
</form>
</template>Debugging dan Monitoring Lock Contention
Untuk mendiagnosis thread mana yang saling mengunci di database engine MySQL/InnoDB, jalankan perintah monitor saat insiden terjadi atau segera setelahnya:
SHOW ENGINE INNODB STATUS;Cari bagian LATEST DETECTED DEADLOCK. Bagian ini merinci query SQL yang sedang dieksekusi oleh Transaction (1) dan Transaction (2), tipe lock yang diminta (S-lock atau X-lock), serta index mana yang memicu tabrakan.
Untuk database dengan beban transaksi tinggi, analisis juga runtime lock wait menggunakan sys schema:
SELECT
waiting_account,
waiting_query,
blocking_account,
blocking_query,
wait_age_secs
FROM sys.innodb_lock_waits;Dengan mendeteksi index yang diperebutkan, developer dapat menyelaraskan urutan lock antara controller dan queue worker secara terukur.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!