Mempelajari bahaya state di worker persistent: mengapa variabel global dan singleton request-scoped bocor antar request, cara mengelola session secara aman dengan store eksternal seperti Redis, dan praktik refactor aplikasi agar worker-safe.

Setelah di episode 10-13 kita belajar menyimpan data dan melayani aset, pada episode kali ini kita membahas bahaya terbesar aplikasi long-running: state. Di PHP-FPM, setiap request mendapat dunia baru — variabel global, session, dan koneksi semua di-reset otomatis. Di RoadRunner, worker hidup lebih lama dari satu request; apa pun yang kalian simpan di memory worker bisa bocor ke request berikutnya.
Mengapa episode ini penting? Karena bug yang paling sulit dideteksi di app server PHP bukan crash, melainkan kebocoran state yang berperilaku acak — sesekali user melihat data user lain. Biasanya ini berawal dari satu variabel statis yang lupa di-reset. Episode ini mencegah kelas bug paling berbahaya tersebut.
Perhatikan contoh bug klasik:
class UserService
{
private static ?int $currentUserId = null;
public function setUser(int $id): void
{
self::$currentUserId = $id;
}
public function getUser(): ?int
{
return self::$currentUserId;
}
}Di PHP-FPM, self::$currentUserId selalu nol di awal request — aman. Di worker RoadRunner, nilai dari request sebelumnya masih tersimpan saat request baru datang. Request pertama set user 42, request kedua di worker yang sama membaca 42 tanpa pernah set — kebocoran data. Inilah contoh konkret mengapa aturan emasnya:
Danger
Jangan pernah menyimpan state request di property class, variabel statis, atau singleton. Setiap state yang hidup lebih dari satu request adalah risiko kebocoran antar user. State harus selalu di-reset di awal tiap request atau disimpan di luar worker.
Beberapa tempat state sering "menyelinap":
| Item | Risiko | Solusi |
|---|---|---|
Variabel static | Bocor antar request | Reset manual per-request, atau hindari |
| Property singleton | Bocor bila diisi data request | Hanya simpan dependency, bukan data |
$_SESSION, $_COOKIE | Tidak di-reset otomatis | Kelola eksplisit atau via framework |
| Koneksi DB dengan transaksi menggantung | Transaksi request sebelumnya ikut | Commit/rollback di tiap akhir request |
| Buffer output | Sisa output request sebelumnya | Bersihkan per-request |
Framework modern menangani ini secara otomatis — Octane me-reset container dan singleton antar request — tetapi kode kalian sendiri (di service, helper, library) tetap tanggung jawab kalian.
Bila butuh state per-request, bawa lewat request itu sendiri, bukan global:
// Simpan di request (aman — request adalah scope-nya)
$request = $request->withAttribute('current_user_id', 42);
// Di service yang menerima request
$userId = $request->getAttribute('current_user_id');Atau gunakan container yang mendukung request scope (Laravel/Octane sudah melakukan ini). Kuncinya: data harus lahir dan mati bersama request.
Session adalah state yang justru harus bertahan lintas request — tetapi bukan di memory worker. Simpan di store eksternal, sesuai pola episode 10:
function loadSession(Psr\Http\Message\ServerRequestInterface $request, $cache): array
{
$sessionId = $request->getCookieParams()['session'] ?? bin2hex(random_bytes(16));
$data = $cache->get('session:' . $sessionId) ?? [];
$data['_id'] = $sessionId;
return $data;
}
function saveSession(array $data, $cache): void
{
$cache->set('session:' . $data['_id'], $data, 1800);
}Keuntungan: session bisa dibaca worker mana pun di pool dan node mana pun di cluster — tidak ada ketergantungan pada worker tertentu. Ini juga persiapan natural untuk scaling horizontal (episode 22).
Framework juga menyediakan ini: Laravel SESSION_DRIVER=redis, Symfony session handler Redis. Pilih store eksternal, jangan default file yang bisa memakai path lokal worker.
Langkah praktis men-scan aplikasi kalian:
static dan global: grep static $, global $, $_SESSION langsung di kode.ob_start/output buffering.max_jobs besar: jalankan banyak request berurutan di worker yang sama, cek respons tetap independen.rg -n "static \$|global \$|\$_SESSION" app/ src/| Pertanyaan | Jawaban aman |
|---|---|
| Apakah data user disimpan di property class? | Tidak — lewat request/store |
| Apakah session memakai file lokal? | Tidak — Redis/DB |
| Apakah singleton hanya berisi dependency? | Ya |
| Apakah transaksi selalu ditutup per request? | Ya |
| Apakah ada global mutable? | Tidak |
Pada episode 14 ini, kalian telah memahami aturan paling penting app server PHP.
Inti yang harus dibawa pulang:
Di episode 15 selanjutnya, kita masuk ke TLS/HTTPS & reverse proxy — mengaktifkan http.ssl dengan sertifikat, atau menaruh Nginx/Caddy di depan RoadRunner, serta menyusun deployment HTTPS production yang benar. Sampai jumpa di episode 15!