Berbagi state antar worker proses dengan aman: Swoole\Table sebagai key-value store in-memory berperforma tinggi, Swoole\Atomic untuk counter atomik, dan Swoole\Lock untuk sinkronisasi, plus praktik membangun rate limiter dan online user counter.

Setelah di episode 11 kita memindahkan pekerjaan berat ke task worker — pada episode ini kita menghadapi salah satu batasan terbesar Swoole yang sudah kita sebut sejak episode 2: memory terisolasi per worker. Variabel PHP di worker A tidak terlihat di worker B. Untuk aplikasi nyata — menghitung user online, rate limiting, caching kecil — kita butuh memory yang dibagi.
Mengapa episode ini penting? Karena inilah perbedaan antara aplikasi Swoole yang "berjalan" dan yang "berjalan dengan benar". Counter yang hidup di variabel biasa akan salah total begitu memakai 4 worker. Swoole menyediakan tiga senjata untuk ini: Swoole\Table, Swoole\Atomic, dan Swoole\Lock.
$counter = 0;
$server->on('Request', function ($req, $res) use (&$counter) {
$counter++; // hanya bertambah di worker ini!
$res->end("Count: $counter");
});Dengan 4 worker, angka yang tampil acak bergantung worker mana yang melayani request. Ini bukan bug kode — ini konsekuensi arsitektur multi-process.
Swoole\Table adalah hash table yang tinggal di shared memory — semua worker membaca/menulis data yang sama. Ia didesain untuk performa ekstrem: operasi seperti incr bersifat atomik dan jauh lebih cepat daripada Redis (tanpa jaringan).
use Swoole\Table;
$users = new Table(65536); // 65536 baris maksimum
$users->column('fd', Table::TYPE_INT);
$users->column('name', Table::TYPE_STRING, 64);
$users->column('login_at', Table::TYPE_INT);
$users->create(); // wajib: alokasikan memoryTiga aturan penting Table:
create() — setelah create(), skema tidak bisa diubah.create() harus dipanggil sebelum $server->start() — memory dialokasikan saat itu juga.Operasi dasar:
$users->set(42, ['fd' => 5, 'name' => 'Andi', 'login_at' => time()]);
$row = $users->get(42);
$users->incr(42, 'login_at'); // atomik
$users->decr(42, 'login_at');
$users->del(42);
$users->exists(42);Pola on('Open')/on('Close') WebSocket yang kita bangun di episode 6 bisa dijadikan global dengan Table:
<?php
use Swoole\WebSocket\Server;
use Swoole\Http\Request;
use Swoole\WebSocket\Frame;
use Swoole\Table;
$online = new Table(65536);
$online->column('fd', Table::TYPE_INT);
$online->column('name', Table::TYPE_STRING, 64);
$online->create();
$server = new Server('0.0.0.0', 9502);
$server->on('Open', function (Server $server, Request $req) use ($online) {
$online->set($req->fd, ['fd' => $req->fd, 'name' => "user-{$req->fd}"]);
$server->push($req->fd, "Online: " . count($online));
});
$server->on('Message', function (Server $server, Frame $frame) use ($online) {
foreach ($online as $row) {
if ($row['fd'] !== $frame->fd) {
$server->push($row['fd'], "{$frame->fd}: {$frame->data}");
}
}
});
$server->on('Close', function (Server $server, int $fd) use ($online) {
$online->del($fd);
});
$server->start();count($online) memberi jumlah user online di seluruh worker secara konsisten — hal mustahil dengan variabel biasa. Iterasi foreach ($online as $row) membaca semua baris dari shared memory.
Untuk kebutuhan sesederhana angka, Swoole\Atomic jauh lebih ringan daripada Table:
use Swoole\Atomic;
$hits = new Atomic(0); // mulai dari 0
$server->on('Request', function ($req, $res) use ($hits) {
$hits->add(1);
$res->end("Total hits seluruh worker: " . $hits->get());
});add, sub, get, set, cmpset (compare-and-swap) semuanya atomik — aman dipakai banyak worker bersamaan tanpa race condition. Sempurna untuk metrik sederhana, sequence ID, dan rate limiter.
Ada kalanya operasi kompleks (bukan sekadar counter) butuh akses eksklusif. Swoole\Lock menyediakan mutex di shared memory:
use Swoole\Lock;
$lock = new Lock(SWOOLE_MUTEX);
$server->on('Request', function ($req, $res) use ($lock) {
$lock->lock();
try {
// baca + tulis struktur kompleks secara eksklusif
$result = $heavy->process($req);
} finally {
$lock->unlock();
}
$res->end($result);
});Warning
Jangan pernah menahan lock sambil menunggu I/O (query DB, HTTP call). Worker yang memegang lock akan membuat worker lain antre di situ — dan bila lock dipegang dalam coroutine yang tertunda, bisa terjadi deadlock. Pola aman: lock hanya untuk kritikal section yang cepat dan murni in-memory.
Kombinasi Table + Atomic memberi kita rate limiter yang berjalan di seluruh worker tanpa Redis:
<?php
use Swoole\Http\Server;
use Swoole\Http\Request;
use Swoole\Http\Response;
use Swoole\Table;
$limit = new Table(65536);
$limit->column('count', Table::TYPE_INT);
$limit->column('reset_at', Table::TYPE_INT);
$limit->create();
$window = 60; // 60 detik
$max = 100; // maksimal 100 request
$server = new Server('0.0.0.0', 9501);
$server->on('Request', function (Request $req, Response $res) use ($limit, $window, $max) {
$ip = $req->server['remote_addr'] ?? 'unknown';
$now = time();
$row = $limit->get($ip);
if ($row === false || $row['reset_at'] < $now) {
$limit->set($ip, ['count' => 1, 'reset_at' => $now + $window]);
$remaining = $max - 1;
} else {
$count = $limit->incr($ip, 'count') ?: 1;
$remaining = max(0, $max - $count);
}
if ($remaining === 0) {
$res->status(429);
$res->end('Too Many Requests');
return;
}
$res->header('X-RateLimit-Remaining', (string) $remaining);
$res->end('OK');
});
$server->start();Rate limiter ini bekerja di semua worker, konsisten, dan tidak menambah beban jaringan karena murni in-memory.
| Masalah | Penyebab | Solusi |
|---|---|---|
unable to allocate memory for table | Ukuran Table terlalu besar vs memory | Turunkan jumlah baris / ukuran string |
| Data lenyap saat server restart | Table hidup di memory, bukan disk | Backup/restore, atau pakai Redis untuk persisten |
Error saat set setelah create | Skema diubah setelah alokasi | Definisikan column sekali sebelum create() |
| Deadlock | Lock ditahan sambil I/O | Persingkat kritikal section, pindahkan I/O keluar lock |
Pada episode 12 ini, kalian telah berbagi state antar worker dengan aman.
Inti yang harus dibawa pulang:
Swoole\Table: key-value di shared memory, operasi incr atomik, baris tetap.create() sebelum server->start().Swoole\Atomic: counter atomik ringan untuk metrik/sequence/rate limit.Swoole\Lock: mutex untuk kritikal section — jangan ditahan saat I/O.Di episode 13 selanjutnya, kita memanajemen koneksi database: coroutine MySQL & connection pools — PDOPool, Redis pool, dan query di bawah concurrency tinggi. Sampai jumpa di episode 13!