Inkonsistensi read-after-write sering muncul pada aplikasi berbasis Inertia.js saat developer memindahkan operasi mutasi data ke queue worker. Dalam arsitektur web sinkron tradisional, siklus request-response menjamin bahwa saat browser merender ulang tampilan, data di database dan cache telah terbarukan. Namun, ketika proses penulisan dialihkan ke background job, respons HTTP 303 (Redirect) dari Inertia sering kali tiba di browser dan memicu pengambilan props baru sebelum worker selesai memproses transaksi database dan mengosongkan cache.
Akibatnya, browser menampilkan data usang (stale data). Artikel ini membedah arsitektur solusi deterministik untuk mengeliminasi race condition tersebut menggunakan database transaction afterCommit, cache tagging di level worker, serta koordinasi WebSocket untuk memicu selective partial reload pada Inertia router.
Anatomi Masalah: Race Condition pada Inertia Lifecycle
Inertia.js mengandalkan respons redirect setelah permintaan POST, PUT, atau DELETE. Siklus yang memicu stale read berjalan sebagai berikut:
- Klien mengirim form mutasi via
router.post(). - Controller backend memvalidasi payload, mendispatch job ke antrean (Redis/DB), lalu langsung mengembalikan
to_route()atauback(). - Inertia menerima respons HTTP redirect dan secara otomatis mengirimkan request
GETuntuk rute tujuan. - Controller rute tujuan membaca data dari database atau cache layer.
- Masalah: Queue worker belum mengambil atau menyelesaikan eksekusi job. Database belum ter-update dan cache belum di-invalidasi. Klien merender data lama.
Alternatif paling hemat: Jika proses mutasi selesai di bawah 300-500ms, jangan gunakan queue. Jalankan sinkron dalam transaksi database untuk menghindari kompleksitas WebSocket dan worker synchronization.
Langkah 1: Proteksi Mutasi dengan afterCommit
Kesalahan fatal yang sering terjadi adalah mendispatch queue job di dalam blok transaksi sebelum transaksi selesai di-commit. Jika worker memproses job lebih cepat daripada proses commit database utama, worker akan membaca data transaksi yang belum selesai (dirty read) atau bahkan gagal menemukan relasi data terkait.
Gunakan callback afterCommit atau set flag $afterCommit = true pada job Laravel:
namespace App\Http\Controllers;
use App\Http\Requests\UpdateProjectRequest;
use App\Jobs\ProcessProjectMetrics;
use App\Models\Project;
use Illuminate\Http\RedirectResponse;
use Illuminate\Support\Facades\DB;
class ProjectController extends Controller
{
public function update(UpdateProjectRequest $request, Project $project): RedirectResponse
{
DB::transaction(function () use ($request, $project) {
$project->update($request->validated());
// Dispatch hanya jika transaksi database berhasil commit
ProcessProjectMetrics::dispatch($project->id, auth()->id())
->afterCommit();
});
// Mengembalikan status sementara tanpa menunggu kalkulasi berat
return back()->with('status', 'Pembaruan sedang diproses.');
}
}Langkah 2: Worker Execution, Cache Tagging, dan Broadcasting
Queue worker bertanggung jawab mengeksekusi komputasi berat, membersihkan entri cache yang relevan, dan memancarkan (broadcast) sinyal bahwa state telah konsisten. Hindari pembersihan cache di controller sebelum worker selesai.
namespace App\Jobs;
use App\Events\ProjectUpdatedEvent;
use App\Models\Project;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Cache;
class ProcessProjectMetrics implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public function __construct(
public int $projectId,
public int $userId
) {}
public function handle(): void
{
$project = Project::findOrFail($this->projectId);
// Eksekusi mutasi atau kalkulasi berat
$project->recalculateMetrics();
$project->save();
// Invalidasi cache secara terarah via tagging
Cache::tags(['projects', "project:{$this->projectId}"])->flush();
// Broadcast event completion ke private channel user
broadcast(new ProjectUpdatedEvent($this->projectId, $this->userId));
}
}Event broadcasting harus mengimplementasikan ShouldBroadcastNow jika menggunakan Redis/Reverb agar event dikirim instan tanpa masuk antrean kedua:
namespace App\Events;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcastNow;
class ProjectUpdatedEvent implements ShouldBroadcastNow
{
public function __construct(
public int $projectId,
public int $userId
) {}
public function broadcastOn(): array
{
return [
new PrivateChannel("App.Models.User.{$this->userId}"),
];
}
public function broadcastAs(): string
{
return 'project.updated';
}
}Langkah 3: Sinkronisasi Frontend dengan Selective Reload
Alih-alih melakukan full page reload atau polling berkala via HTTP yang membebani server, tangkap event penyelesaian mutasi di sisi klien menggunakan Laravel Echo. Gunakan metode router.reload() dengan opsi only untuk meminta partial props yang terdampak saja.
// Implementasi pada Vue 3 Composition API / Script Setup
import { onMounted, onUnmounted } from 'vue';
import { router } from '@inertiajs/vue3';
const props = defineProps<{
project: { id: number; name: string; metrics: object };
auth: { user: { id: number } };
}>();
let channel: any = null;
onMounted(() => {
channel = window.Echo.private(`App.Models.User.${props.auth.user.id}`)
.listen('.project.updated', (event: { projectId: number }) => {
if (event.projectId === props.project.id) {
// Tarik ulang props 'project' secara selektif dari backend
router.reload({
only: ['project'],
preserveScroll: true,
preserveState: true,
});
}
});
});
onUnmounted(() => {
if (channel) {
window.Echo.leave(`App.Models.User.${props.auth.user.id}`);
}
});Trade-offs dan Pertimbangan Sistem
- Kompleksitas Infrastruktur: Pendekatan ini membutuhkan WebSocket server aktif (seperti Laravel Reverb atau Soketi) dan worker Redis yang andal.
- Connection Leaks: Pastikan selalu memanggil
Echo.leave()pada hookonUnmounted(Vue) atau cleanup callbackuseEffect(React) agar klien tidak menumpuk subscription. - Graceful Degradation: Jika koneksi WebSocket terputus, sediakan fallback berupa tombol sinkronisasi manual atau optimistic indicator di antarmuka pengguna agar status sistem tetap transparan.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!