Jebakan state di worker mode
Ini materi terpenting di seluruh fase ini. Worker mode cepat karena aplikasimu tetap hidup di memori — dan setiap bug di halaman ini berasal dari kalimat yang sama.
Intisari
- Provider hanya di-
bootsekali. Apa pun yang dibaca dari$requestdi sana akan beku selamanya. singletonyang menyimpan data permintaan = kebocoran antar pengguna. Pakaiscoped.- Properti statis dan variabel global bertahan antar permintaan; array statis yang terus diisi = kebocoran memori.
- Menyuntikkan
Requestke konstruktor singleton membekukan permintaan pertama. - Pengaman berlapis:
--max-requests, batas memori, dan tes yang menjalankan dua permintaan berurutan.
Jebakan 1 — singleton yang menyimpan data pengguna
// BERBAHAYA
class KonteksTenant
{
public ?Tenant $tenant = null;
}
$this->app->singleton(KonteksTenant::class); // ← satu objek, seumur proses
// Middleware mengisinya per permintaan
app(KonteksTenant::class)->tenant = $request->user()->tenant;
Di PHP-FPM ini bekerja sempurna: prosesnya mati setelah respons. Di worker mode, objek itu bertahan. Permintaan
berikutnya — dari pengguna lain — mendapati tenant masih terisi milik pengguna sebelumnya.
Kalau permintaan itu kebetulan lolos dari middleware yang menimpanya, pengguna B melihat data tenant A.
// BENAR — scoped: dibuat ulang untuk setiap permintaan
$this->app->scoped(KonteksTenant::class);
Ini bug paling berbahaya di worker mode karena tidak terlihat saat pengujian. Di laptop, kamu satu
orang; setiap permintaanmu menimpa state dengan datamu sendiri. Ia baru muncul di produksi, hanya sesekali,
dan hanya saat dua pengguna berbeda kebetulan dilayani worker yang sama secara berurutan. Aturannya:
kalau isinya bergantung pada siapa yang meminta, ia tidak boleh singleton.
Jebakan 2 — Request di konstruktor
// BERBAHAYA
class PenyusunLaporan
{
public function __construct(private Request $request) {} // beku pada permintaan pertama
}
$this->app->singleton(PenyusunLaporan::class);
// BENAR — terima Request sebagai argumen method
class PenyusunLaporan
{
public function untuk(Request $request): Laporan { /* ... */ }
}
// Atau ambil nilainya saat dibutuhkan, bukan saat objek dibangun
$this->app->singleton(PenyusunLaporan::class, fn ($app) => new PenyusunLaporan(
fn () => $app->make('request')->user(), // closure, dievaluasi belakangan
));
Jebakan 3 — provider yang membaca permintaan
// BERBAHAYA — boot() hanya berjalan sekali, saat worker menyala
public function boot(): void
{
$tenant = request()->getHost(); // host permintaan PERTAMA, selamanya
config(['app.tenant' => $tenant]);
}
// BENAR — putuskan per permintaan, di middleware
class TentukanTenant
{
public function handle(Request $request, Closure $next): Response
{
app(KonteksTenant::class)->tenant = Tenant::dariHost($request->getHost());
return $next($request);
}
}
Provider tidak boleh tahu apa-apa tentang permintaan. Aturan ini sudah benar di PHP-FPM juga — di sana ia cuma tidak terasa, karena provider kebetulan di-boot pada permintaan yang sama. Worker mode mengubah kebiasaan buruk yang tidak berbahaya menjadi bug yang nyata.
Jebakan 4 — kebocoran memori
// BERBAHAYA — array ini tidak pernah dikosongkan
class Metrik
{
public static array $peristiwa = [];
public static function catat(string $nama): void
{
self::$peristiwa[] = ['nama' => $nama, 'waktu' => microtime(true)];
}
}
Setiap permintaan menambah baris. Setelah sepuluh ribu permintaan, array itu berisi sepuluh ribu entri; setelah sejuta, prosesnya mati kehabisan memori. Di PHP-FPM masalah ini tidak pernah ada karena array-nya lenyap tiap permintaan.
| Sumber kebocoran | Perbaikan |
|---|---|
| Array statis yang terus diisi | Kosongkan di akhir permintaan, atau jangan statis |
| Pendengar event yang didaftarkan tiap permintaan | Daftarkan di provider, bukan di controller |
| Cache dalam memori tanpa batas | Beri batas ukuran, atau pakai Redis |
| Berkas atau soket yang tidak ditutup | Tutup di blok finally |
| Referensi melingkar | gc_collect_cycles() berkala |
Mendeteksi sebelum produksi
// Rute diagnostik sementara
Route::get('/memori', fn () => [
'sekarang_mb' => round(memory_get_usage(true) / 1048576, 1),
'puncak_mb' => round(memory_get_peak_usage(true) / 1048576, 1),
]);
# Panggil seribu kali dan lihat apakah angkanya naik terus
for i in $(seq 1 1000); do curl -s localhost:8000/memori | tail -c 40; echo; done
Angka yang naik lalu mendatar adalah normal — itu memori yang dipakai ulang. Angka yang naik terus tanpa henti
adalah kebocoran. Perhatikan juga apakah ia turun setelah --max-requests tercapai; kalau ya, jaring
pengamannya bekerja, tapi penyebabnya tetap perlu dicari.
Menguji kebocoran state antar permintaan
it('tidak membocorkan tenant antar permintaan', function () {
$a = User::factory()->create();
$b = User::factory()->create(); // tenant berbeda
$this->actingAs($a)->get('/dasbor')->assertSee($a->tenant->nama);
$this->actingAs($b)->get('/dasbor')->assertDontSee($a->tenant->nama);
});
Tes seperti ini tidak sepenuhnya membuktikan keamanannya di worker mode, karena suite tes juga
mereset container antar tes. Cara yang benar-benar meyakinkan: jalankan aplikasi dengan
octane:start --workers=1, lalu kirim permintaan dua pengguna berbeda secara bergantian dengan
curl. Satu worker memaksa keduanya berbagi proses — persis kondisi yang memunculkan bugnya.
Daftar periksa sebelum menyalakan worker mode
- Cari semua
singleton; ubah jadiscopedkalau isinya bergantung pengguna. - Cari semua
staticdiapp/; pastikan tidak ada yang bertumbuh. - Cari
request()di dalam service provider; pindahkan ke middleware. - Periksa paket pihak ketiga — cari yang menyatakan dukungan Octane secara eksplisit.
- Setel
--max-requestssebagai jaring pengaman, bukan sebagai solusi. - Pasang alarm pemakaian memori per kontainer.
- Jalankan uji beban yang panjang, bukan cuma beberapa ratus permintaan.
grep -rn "singleton(" app/ | grep -v scoped
grep -rn "static \$" app/
grep -rn "request()" app/Providers/
Latihan: buat kelas KonteksTenant sebagai singleton, isi dari middleware,
jalankan octane:start --workers=1, lalu panggil sebagai dua pengguna berbeda secara bergantian.
Amati kebocorannya terjadi. Ubah jadi scoped dan buktikan bahwa ia berhenti.
Rangkuman ini sengaja dipangkas ke bagian yang dipakai di roadmap. Buka sumber aslinya saat kamu butuh detail lengkap atau referensi parameter.