Poison pill adalah job rusak atau malformed payload yang secara konsisten memicu fatal error (seperti unhandled exception, segfault ekstensi PHP, atau Out-of-Memory) setiap kali dieksekusi oleh queue worker. Worker mati seketika, job dikembalikan ke antrian secara otomatis oleh driver (seperti Redis atau database), lalu diambil kembali oleh worker berikutnya. Siklus crash berulang ini memblokir pemrosesan antrian secara keseluruhan dan menyebabkan frontend Inertia.js terjebak pada status pending tanpa kejelasan status.
Anatomi Masalah: Dari Request Inertia ke Worker Crash
Form submission di Inertia.js umumnya mengeksekusi request HTTP POST/PUT. Jika backend langsung mendispatch job asinkron tanpa validasi payload yang memadai, atau terdapat bug parsing binary pada backend worker, crash akan terjadi pada background process.
Dampaknya terbagi menjadi dua ranah:
- Backend Worker Exhaustion: Satu payload poison pill menghabiskan resource CPU dan slots worker melalui infinite restart loop. Job legitimate lain di antrian yang sama mengalami starvation.
- Frontend Zombie State: Inertia menerima respons 200/202 bahwa job telah diterima, lalu client masuk ke mode polling atau menunggu push notification. Karena worker crash tanpa sempat mengeksekusi block
catchstandar, event kegagalan tidak pernah terkirim, membuat spinner UI berputar tanpa henti.
Alternatif paling sederhana: Eksekusi sinkron jika beban komputasi < 500ms. Gunakan queue hanya saat operasi I/O atau komputasi mutlak membutuhkan pemisahan proses.
1. Konfigurasi Isolasi: Max Retries dan Dead Letter Queue (DLQ)
Untuk menghentikan infinite retry loop, worker harus membatasi batas percobaan eksekusi dan memindahkan payload bermasalah ke antrian khusus (Dead Letter Queue atau tabel failed_jobs).
Jalankan worker dengan batas retry ketat dan timeout eksplisit:
php artisan queue:work redis --queue=default,dlq --tries=3 --timeout=60 --max-time=3600Implementasikan isolasi kegagalan pada class job Laravel untuk mencegah retry pada kondisi fatal error yang teridentifikasi:
<?php
namespace App\Jobs;
use App\Models\Task;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Throwable;
class ProcessDocumentJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public int $tries = 3;
public int $maxExceptions = 1; // Langsung gagalkan jika runtime exception tidak tertangani
public int $timeout = 45;
public function __construct(
public string $taskId,
public array $payload
) {}
public function handle(): void
{
$task = Task::findOrFail($this->taskId);
$task->update(['status' => 'processing']);
// ponytail: parser tanpa memory-guard. Upgrade ke chunked stream jika file > 50MB.
$this->executeComplexParsing($this->payload);
$task->update(['status' => 'completed']);
}
public function failed(Throwable $exception): void
{
// Terminal failure: Update status ke DB/Redis agar frontend dapat membaca status
Task::where('id', $this->taskId)->update([
'status' => 'failed',
'error_code' => 'POISON_PILL_DETECTED',
'error_message' => substr($exception->getMessage(), 0, 255),
]);
}
private function executeComplexParsing(array $data): void
{
if (isset($data['corrupt_byte_sequence'])) {
throw new \InvalidArgumentException('Payload parsing fatal failure.');
}
}
}
2. Persistensi Status Terminal di Database
Ketika job masuk ke status terminal failure melalui hook failed(), catat perubahan state secara atomik. Status ini menjadi sumber kebenaran (source of truth) tunggal yang dapat dikueri oleh controller endpoint Inertia.
<?php
namespace App\Http\Controllers;
use App\Models\Task;
use Inertia\Inertia;
use Inertia\Response;
class TaskController extends Controller
{
public function show(string $id): Response
{
$task = Task::select(['id', 'status', 'error_code', 'error_message', 'updated_at'])
->findOrFail($id);
return Inertia::render('Tasks/Show', [
'task' => $task
]);
}
}
3. Rekonsiliasi State UI di Frontend Inertia.js
Daripada bergantung pada WebSocket yang koneksinya rentan terputus saat worker crash, gunakan short-interval polling terukur dengan router.reload() berbasis partial reloads. Ini menghemat bandwidth dan secara otomatis menghentikan request saat status terminal (failed/completed) tercapai.
<script setup>
import { ref, onMounted, onUnmounted } from 'vue';
import { router } from '@inertiajs/vue3';
const props = defineProps({
task: {
type: Object,
required: true,
}
});
const isPolling = ref(false);
let pollInterval = null;
const checkStatus = () => {
if (props.task.status === 'completed' || props.task.status === 'failed') {
stopPolling();
return;
}
router.reload({
only: ['task'],
preserveScroll: true,
preserveState: true,
onError: () => stopPolling(),
});
};
const startPolling = () => {
if (props.task.status === 'processing' || props.task.status === 'queued') {
isPolling.value = true;
pollInterval = setInterval(checkStatus, 3000);
}
};
const stopPolling = () => {
isPolling.value = false;
if (pollInterval) {
clearInterval(pollInterval);
pollInterval = null;
}
};
onMounted(() => startPolling());
onUnmounted(() => stopPolling());
</script>
<template>
<div class="task-container">
<h3>Status Proses: {{ task.status }}</h3>
<div v-if="task.status === 'processing'" class="alert alert-info">
Memproses dokumen... Mohon tunggu.
</div>
<div v-else-if="task.status === 'failed'" class="alert alert-danger">
<p><strong>Pemrosesan Gagal:</strong> {{ task.error_message }}</p>
<p>Kode: <code>{{ task.error_code }}</code></p>
<button @click="router.visit('/tasks/create')" class="btn btn-secondary">
Unggah Ulang dengan File Berbeda
</button>
</div>
<div v-else-if="task.status === 'completed'" class="alert alert-success">
Dokumen berhasil diproses.
</div>
</div>
</template>
4. Workflow Triage Operasional dan Safe Redrive
Saat poison pill masuk ke DLQ (tabel failed_jobs), worker kembali beroperasi normal untuk payload yang valid. Tim engineering harus menangani poison pill melalui protokol triage terisolasi.
- Inspeksi Payload Rusak: Ambil ID job yang gagal dari antrian tanpa langsung mengeksekusinya kembali:
Periksa detail baris tertentu untuk membedah call stack:php artisan queue:failed
Ekstrak kolom payload JSON dari database untuk mereproduksi error secara lokal melalui unit test.php artisan queue:failed-table - Validasi Bugfix: Perbaiki kode backend parser atau perketat FormRequest validation rule agar payload rusak terfilter sebelum mencapai queue dispatch.
- Selective Redrive: Jangan jalankan
queue:retry alltanpa mitigasi. Redrive job secara spesifik menggunakan task ID setelah deployment fix:
Jika payload invalid secara fundamental (bukan recoverable data), hapus job secara eksplisit dari DLQ agar tidak mengotori metrics monitoring:php artisan queue:retry 42c8d2d6-4444-48f1-9430-bf8cb1946029php artisan queue:forget 42c8d2d6-4444-48f1-9430-bf8cb1946029
Pemisahan antara job failure yang bersifat transien (koneksi network terputus) dengan poison pill (payload fatal) menjaga kestabilan queue throughput. Frontend Inertia menerima update status terminal secara deterministik, mencegah pengalaman pengguna menggantung tanpa feedback sistem.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!