Alpha ZealPHP is early-stage and under active development. APIs may change between minor versions until v1.0. Feedback and bug reports welcome on GitHub.
API Index — Namespaces, Packages, Reports, Indices

TableSessionHandler
in package
implements SessionHandlerInterface

FinalYes

Table-as-store + file-as-backing session handler.

Closes the file handler's key-level merge granularity hole without requiring Redis. Three layers:

  1. OpenSwoole\Table — hot, in-memory, cross-worker, atomic ops
  2. File backing — cold, persistent across restarts, overflow
  3. 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

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 = []

$writeLocks

private static array<int, Atomic> $writeLocks = []

Sharded write locks, indexed crc32($id) % WRITE_LOCK_SLOTS.

Methods

destroy()

public destroy(mixed $sessionId) : bool
Parameters
$sessionId : mixed
Return values
bool

gc()

public gc(mixed $maxlifetime) : int
Parameters
$maxlifetime : mixed
Return values
int

merge3()

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
bool

read()

public read(mixed $sessionId) : string
Parameters
$sessionId : mixed
Return values
string

register()

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
self

write()

public write(mixed $sessionId, mixed $data) : bool
Parameters
$sessionId : mixed
$data : mixed
Return values
bool

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
string

lockFor()

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
Atomic

readFile()

private readFile(string $sessionId) : string
Parameters
$sessionId : string
Return values
string

serializedLength()

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
int

writeFile()

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.

Return values
bool
On this page