API Index — Namespaces, Packages, Reports, Indices
TableSessionHandler
in package
implements
SessionHandlerInterface
Table-as-store + file-as-backing session handler.
Closes the file handler's key-level merge granularity hole without requiring Redis. Three layers:
- OpenSwoole\Table — hot, in-memory, cross-worker, atomic ops
- File backing — cold, persistent across restarts, overflow
- Sharded spinlock — Atomic cmpset, one of N shards per session id
(
crc32($id) % WRITE_LOCK_SLOTS), so writes to different sessions don't serialise against each other; only same-session writes (same shard) do.
Writes use optimistic versioning (CAS): each session row has a
version column. On read, the handler snapshots both the data
(the "base") and the version. On write, it checks the current
version — if unchanged, it sets the new data + version+1 atomically.
If the version changed (a concurrent coroutine got there first), it
performs a recursive 3-way merge between the base, the local
mutation, and the current remote state, then retries with the new
version. Up to 3 retries before giving up.
The 3-way merge gives LEAF-LEVEL granularity:
- Coroutine A sets $_SESSION['cart']['item2'] = 3
- Coroutine B sets $_SESSION['cart']['item3'] = 5
- Result: BOTH item2 and item3 survive (vs file handler's last-writer-wins at the 'cart' key).
File backing handles two cases:
- Cold start: on Table miss, read from file and promote to Table.
- Persistence: write-through to file so server restart doesn't drop the entire session pool.
Use when:
- You want concurrent-coroutine-safe sessions WITHOUT Redis.
- Single-host deployment (Table is per-OpenSwoole-server).
For multi-host or cross-restart durability with cross-node sharing, use Redis WATCH/MULTI via RedisSessionHandler instead.
Table of Contents
Interfaces
- SessionHandlerInterface
Constants
- COL_DATA : mixed = 'data'
- COL_EXPIRES : mixed = 'expires'
- COL_VERSION : mixed = 'version'
- WRITE_LOCK_SLOTS : mixed = 1024
- Number of sharded write-lock slots. Session ids hash to a slot
(
crc32($id) % WRITE_LOCK_SLOTS), so writes to DIFFERENT sessions almost always take DIFFERENT locks (proceed in parallel) while writes to the SAME session always take the same lock (serialised — required for the read-merge-CAS to be atomic). Bounded at boot (N small Atomics in shared memory), so there's no per-session unbounded allocation. Replaces the single global Atomic that serialised EVERY session write cluster-wide.
Properties
- $context : array<string|int, mixed>
- Read snapshot, keyed by coroutine id THEN session id. This is a per-worker singleton, so keying by coroutine id keeps concurrent requests for the SAME session id from clobbering each other's base/version snapshot (#182).
- $dataSize : int
- $instance : self|null
- $maxRows : int
- $savePath : string
- $table : Table|null
- $ttl : int
- $writeLocks : array<int, Atomic>
Methods
- close() : bool
- destroy() : bool
- gc() : int
- instance() : self
- merge3() : array<string|int, mixed>
- Recursive 3-way merge: - base: what we read - local: our mutation - remote: current state (a concurrent coroutine wrote it)
- open() : bool
- read() : string
- register() : self
- Idempotent setup. Call BEFORE App::run() so the Table is allocated in the master process and inherited by all workers on fork.
- write() : bool
- __construct() : mixed
- decode() : array<string|int, mixed>
- encode() : string
- lockFor() : Atomic
- The write lock for a session id — its hashed shard. Same id → same lock (serialised, required); different ids → almost always different locks (parallel).
- readFile() : string
- serializedLength() : int
- Compute the length of a serialize()d value starting at $offset.
- table() : Table
- writeFile() : bool
Constants
COL_DATA
private
mixed
COL_DATA
= 'data'
COL_EXPIRES
private
mixed
COL_EXPIRES
= 'expires'
COL_VERSION
private
mixed
COL_VERSION
= 'version'
WRITE_LOCK_SLOTS
Number of sharded write-lock slots. Session ids hash to a slot
(crc32($id) % WRITE_LOCK_SLOTS), so writes to DIFFERENT sessions almost
always take DIFFERENT locks (proceed in parallel) while writes to the
SAME session always take the same lock (serialised — required for the
read-merge-CAS to be atomic). Bounded at boot (N small Atomics in shared
memory), so there's no per-session unbounded allocation. Replaces the
single global Atomic that serialised EVERY session write cluster-wide.
private
mixed
WRITE_LOCK_SLOTS
= 1024
Properties
$context
Read snapshot, keyed by coroutine id THEN session id. This is a per-worker singleton, so keying by coroutine id keeps concurrent requests for the SAME session id from clobbering each other's base/version snapshot (#182).
private
array<string|int, mixed>
$context
= []
$dataSize
private
static int
$dataSize
= 16384
$instance
private
static self|null
$instance
= null
$maxRows
private
static int
$maxRows
= 65536
$savePath
private
static string
$savePath
= '/var/lib/php/sessions'
$table
private
static Table|null
$table
= null
$ttl
private
static int
$ttl
= 7200
$writeLocks
private
static array<int, Atomic>
$writeLocks
= []
Sharded write locks, indexed crc32($id) % WRITE_LOCK_SLOTS.
Methods
close()
public
close() : bool
Return values
booldestroy()
public
destroy(mixed $sessionId) : bool
Parameters
- $sessionId : mixed
Return values
boolgc()
public
gc(mixed $maxlifetime) : int
Parameters
- $maxlifetime : mixed
Return values
intinstance()
public
static instance() : self
Return values
selfmerge3()
Recursive 3-way merge: - base: what we read - local: our mutation - remote: current state (a concurrent coroutine wrote it)
public
merge3(array<string|int, mixed> $base, array<string|int, mixed> $local, array<string|int, mixed> $remote) : array<string|int, mixed>
Returns the merged array where:
- Keys present in local but not base → added (new keys we want)
- Keys whose value differs in local vs base → use local (we changed it)
- Keys unchanged in local → use remote (someone else may have changed)
- Nested arrays → recurse for leaf-level granularity
Parameters
- $base : array<string|int, mixed>
- $local : array<string|int, mixed>
- $remote : array<string|int, mixed>
Return values
array<string|int, mixed>open()
public
open(mixed $savePath, mixed $sessionName) : bool
Parameters
- $savePath : mixed
- $sessionName : mixed
Return values
boolread()
public
read(mixed $sessionId) : string
Parameters
- $sessionId : mixed
Return values
stringregister()
Idempotent setup. Call BEFORE App::run() so the Table is allocated in the master process and inherited by all workers on fork.
public
static register([int|null $ttl = null ][, int|null $maxRows = null ][, int|null $dataSize = null ][, string|null $savePath = null ]) : self
All four params fall back to App config (App::sessionTtl(), etc.)
when null. Pass explicit values to override per-handler. Defaults
resolved here ONLY apply if App config is also unset (rare).
Parameters
- $ttl : int|null = null
-
Session TTL in seconds. App default: 7200 (2 hours)
- $maxRows : int|null = null
-
Max concurrent sessions in Table; overflow goes to file backing. App default: 65536 (64K). Memory: ~maxRows × (dataSize + 64) bytes shared.
- $dataSize : int|null = null
-
Max serialized session size in bytes. App default: 16384 (16 KB) — fits OAuth tokens
- cart state + user prefs. Overflow → file only.
- $savePath : string|null = null
-
File backing dir. App default: /var/lib/php/sessions.
Return values
selfwrite()
public
write(mixed $sessionId, mixed $data) : bool
Parameters
- $sessionId : mixed
- $data : mixed
Return values
bool__construct()
private
__construct() : mixed
decode()
private
decode(string $data) : array<string|int, mixed>
Parameters
- $data : string
Return values
array<string|int, mixed>encode()
private
encode(array<string|int, mixed> $data) : string
Parameters
- $data : array<string|int, mixed>
Return values
stringlockFor()
The write lock for a session id — its hashed shard. Same id → same lock (serialised, required); different ids → almost always different locks (parallel).
private
lockFor(string $sessionId) : Atomic
Parameters
- $sessionId : string
Return values
AtomicreadFile()
private
readFile(string $sessionId) : string
Parameters
- $sessionId : string
Return values
stringserializedLength()
Compute the length of a serialize()d value starting at $offset.
private
serializedLength(string $data, int $offset) : int
Parameters
- $data : string
- $offset : int
Return values
inttable()
private
table() : Table
Return values
TablewriteFile()
private
writeFile(string $sessionId, string $data[, array<int, string> $deletedKeys = [] ]) : bool
Parameters
- $sessionId : string
- $data : string
- $deletedKeys : array<int, string> = []
-
Top-level keys the writer deleted this request (in its read snapshot, absent from the final state) — excluded from the disk merge so they don't resurrect.