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

CircuitBreakerBackend
in package
implements StoreBackend

FinalYes

3-state circuit breaker decorator for StoreBackend. Opt-in only: applications that want "Redis down → degrade to Table cache" wrap their RedisBackend in this decorator at boot. Default behaviour is unchanged — apps that don't wrap see the same throw-on-Redis-failure semantics they had before.

State machine: closed — every request reaches the primary; failures incrementally tracked within $failureWindowSec. Exceeding $failureThreshold consecutive failures trips → OPEN. open — every request fails fast for $openDurationSec: reads → return from the fallback backend (if configured) writes → throw immediately (no fallback semantics for writes) After the cooldown elapses, the next call transitions to HALF_OPEN and acts as a probe. half-open — exactly one probe is sent to the primary, enforced by an atomic cmpset(HALF_OPEN_READY → HALF_OPEN_PROBING): the lone CAS winner probes, every concurrent caller is turned away (fallback for reads, fail-fast for writes — no thundering herd). Probe success → CLOSED; failure → OPEN (timer resets).

Reads vs writes: READS (get, exists, count, iterate, mget, names) — can fall back to the optional secondary backend. The fallback's contents may be stale or empty; that's strictly better than failing the request when caching is acceptable. WRITES (set, del, incr, decr, mset, clear) — NEVER fall back. A Table fallback write would diverge from the cluster-wide truth; when the primary recovers, the writes during open would be lost. Better to surface the failure to the caller via StoreException.

Control (make/clear) is idempotent and propagates to BOTH backends:

  • make: schemas land in both so reads can serve from fallback when primary is open. Failure on primary increments the breaker's failure counter but doesn't throw to the caller — the schema still exists on the fallback. The failed primary make() is REMEMBERED and retried on the next write to that table (#241), so writes recover once the primary is reachable instead of throwing "table not registered" forever.
  • clear: a "destroy table" write; throws on open (drop semantics).

Concurrency: all state lives in OpenSwoole\Atomic slots so transitions are visible across coroutines AND across workers (Atomic is shared memory). Two cors crossing the threshold simultaneously may BOTH write OPEN to the same value — idempotent, harmless.

Caveat — failure classification: this version treats every StoreException from the primary as a "Redis unavailable" signal. That includes schema-violation errors that aren't transport failures. Recommend setting $failureThreshold high enough that legitimate schema errors don't cumulatively trip the breaker. A typed StoreUnavailableException for finer-grained classification is on the v0.2.42 roadmap.

Table of Contents

Interfaces

StoreBackend
Behavioural contract every Store backend implements.

Constants

CLOSED  : mixed = 0
HALF_OPEN_PROBING  : mixed = 3
Half-open, a probe is in flight. Concurrent callers are turned away.
HALF_OPEN_READY  : mixed = 2
Half-open, awaiting a probe. The FIRST caller to win the atomic cmpset(HALF_OPEN_READY, HALF_OPEN_PROBING) is admitted as the lone probe (#255); concurrent callers lose the CAS and fall back / fail-fast exactly like OPEN, so the primary sees a single probe — not a herd.
OPEN  : mixed = 1

Properties

$failureCount  : Atomic
$failureThreshold  : int
$failureWindowSec  : int
$fallback  : StoreBackend|null
$firstFailureAt  : Atomic
$openDurationSec  : int
$openedAt  : Atomic
$primary  : StoreBackend
$primaryMakePending  : array<string, array{0: int, 1: array, 2: array}>
Per-table args of a primary make() that FAILED — keyed by table name, value [maxRows, columns, opts]. #241: the breaker stays CLOSED after a single make() failure (threshold default 5), but the primary never had the table registered, so every later write would throw "table not registered" forever. The write entrypoints retry the pending make() (when not OPEN) and clear the entry on success.
$state  : Atomic
$stats  : Stats

Methods

__construct()  : mixed
clear()  : void
count()  : int
Return the number of rows currently stored in table $name.
decr()  : int|float
Atomically decrement column $col of row $key in table $name by $by.
del()  : bool
Delete the row at $key from table $name. Returns true on success, false if the key did not exist.
exists()  : bool
Return true when table $name contains a row with key $key.
get()  : mixed
Read a row OR a single field.
incr()  : int|float
Atomically increment column $col of row $key in table $name by $by.
iterate()  : Generator<string, array<string, scalar>>
iteratePaged()  : array{cursor: string, rows: array>}
Paginated iteration (S-3). Returns one batch + an opaque cursor for the next batch. Use for large tables where a full iterate() drain is impractical (e.g. rendering a single page of a 100k-member room roster, exposing cursors over HTTP).
make()  : void
Register a named table with a column schema. For Table this allocates shared memory immediately; for Redis it stores the schema and defers connection until first use.
mget()  : array<string, array<string, scalar>|null>
Bulk read. Returns [key => row]; rows are null for missing keys (NOT omitted) so callers can distinguish "not found" from "empty row". RedisBackend pipelines this into one round-trip; TableBackend loops.
mset()  : bool
Bulk write. Returns true on full success, false if any individual row failed (TableBackend overflow). The Redis impl is pipelined.
names()  : array<int, string>
reset()  : void
Force the breaker back to CLOSED — operational override (e.g. after ops fixes Redis and wants to skip the cooldown).
set()  : bool
Write a row into table $name under $key. Overwrites any existing row.
state()  : string
Returns 'closed', 'open', or 'half-open' — for introspection + tests.
stats()  : Stats
Per-worker breaker stats — breaker_opened_total, breaker_closed_total, breaker_short_circuited_total.
admitHalfOpenProbe()  : bool
#255 — admit exactly one half-open probe. Atomically claims the probe slot via cmpset(HALF_OPEN_READY → HALF_OPEN_PROBING): the single winner gets true and proceeds to the primary; every concurrent loser gets false and is turned away (fallback for reads, fail-fast for writes).
callRead()  : T
callWrite()  : T
isHalfOpen()  : bool
True while the breaker is in either half-open sub-state (ready or probing).
probeStateMaybe()  : void
Maybe transition OPEN → HALF_OPEN_READY if the cooldown has elapsed.
recordFailure()  : void
recordSuccess()  : void
retryPrimaryMake()  : void
#241 — retry a primary make() that failed during make(). Re-runs the original args; on success the pending entry is cleared so subsequent writes take the fast path. On failure the entry is kept (so the next write retries again) and the exception propagates — the caller's write then surfaces it through the normal failure path.

Constants

HALF_OPEN_PROBING

Half-open, a probe is in flight. Concurrent callers are turned away.

private mixed HALF_OPEN_PROBING = 3

HALF_OPEN_READY

Half-open, awaiting a probe. The FIRST caller to win the atomic cmpset(HALF_OPEN_READY, HALF_OPEN_PROBING) is admitted as the lone probe (#255); concurrent callers lose the CAS and fall back / fail-fast exactly like OPEN, so the primary sees a single probe — not a herd.

private mixed HALF_OPEN_READY = 2

Properties

$primaryMakePending

Per-table args of a primary make() that FAILED — keyed by table name, value [maxRows, columns, opts]. #241: the breaker stays CLOSED after a single make() failure (threshold default 5), but the primary never had the table registered, so every later write would throw "table not registered" forever. The write entrypoints retry the pending make() (when not OPEN) and clear the entry on success.

private array<string, array{0: int, 1: array, 2: array}> $primaryMakePending = []

Methods

__construct()

public __construct(StoreBackend $primary[, StoreBackend|null $fallback = null ][, int $failureThreshold = 5 ][, int $failureWindowSec = 10 ][, int $openDurationSec = 30 ]) : mixed
Parameters
$primary : StoreBackend
$fallback : StoreBackend|null = null
$failureThreshold : int = 5
$failureWindowSec : int = 10
$openDurationSec : int = 30

count()

Return the number of rows currently stored in table $name.

public count(string $name) : int
Parameters
$name : string
Return values
int

decr()

Atomically decrement column $col of row $key in table $name by $by.

public decr(string $name, string $key, string $col[, int|float $by = 1 ]) : int|float

Returns the new column value. Creates the row with the column set to -$by when it does not exist.

Parameters
$name : string
$key : string
$col : string
$by : int|float = 1
Return values
int|float

del()

Delete the row at $key from table $name. Returns true on success, false if the key did not exist.

public del(string $name, string $key) : bool
Parameters
$name : string
$key : string
Return values
bool

exists()

Return true when table $name contains a row with key $key.

public exists(string $name, string $key) : bool
Parameters
$name : string
$key : string
Return values
bool

get()

Read a row OR a single field.

public get(string $name, string $key[, string|null $field = null ]) : mixed

Returns array<string, scalar> when $field is null, the field's scalar value when set, or null on miss. The exact typed shape across backends is preserved by TypeCodec (Task 5).

Parameters
$name : string
$key : string
$field : string|null = null

incr()

Atomically increment column $col of row $key in table $name by $by.

public incr(string $name, string $key, string $col[, int|float $by = 1 ]) : int|float

Returns the new column value. Creates the row with the column set to $by when it does not exist.

Parameters
$name : string
$key : string
$col : string
$by : int|float = 1
Return values
int|float

iterate()

public iterate(string $name) : Generator<string, array<string, scalar>>
Parameters
$name : string
Return values
Generator<string, array<string, scalar>>

iteratePaged()

Paginated iteration (S-3). Returns one batch + an opaque cursor for the next batch. Use for large tables where a full iterate() drain is impractical (e.g. rendering a single page of a 100k-member room roster, exposing cursors over HTTP).

public iteratePaged(string $name[, string $cursor = '0' ][, int $count = 100 ]) : array{cursor: string, rows: array>}

Contract:

  • $cursor === '0' (or '') starts a fresh scan.
  • When the returned cursor field is '0' the scan is exhausted.
  • $count is a HINT (Redis SCAN convention) — the batch returned may contain fewer rows on tracked-mode SETs with sparse hits, or more than $count on small SETs (single-batch returns).
  • Cursors are NOT stable across schema/clear changes; resume only within a logically-consistent window.
Parameters
$name : string
$cursor : string = '0'
$count : int = 100
Return values
array{cursor: string, rows: array>}

make()

Register a named table with a column schema. For Table this allocates shared memory immediately; for Redis it stores the schema and defers connection until first use.

public make(string $name, int $maxRows, array<string|int, mixed> $columns[, array<string|int, mixed> $opts = [] ]) : void
Parameters
$name : string
$maxRows : int
$columns : array<string|int, mixed>
$opts : array<string|int, mixed> = []

backend-specific: mode/ttl/etc.

mget()

Bulk read. Returns [key => row]; rows are null for missing keys (NOT omitted) so callers can distinguish "not found" from "empty row". RedisBackend pipelines this into one round-trip; TableBackend loops.

public mget(string $name, array<string|int, mixed> $keys) : array<string, array<string, scalar>|null>
Parameters
$name : string
$keys : array<string|int, mixed>
Return values
array<string, array<string, scalar>|null>

mset()

Bulk write. Returns true on full success, false if any individual row failed (TableBackend overflow). The Redis impl is pipelined.

public mset(string $name, array<string|int, mixed> $rows) : bool
Parameters
$name : string
$rows : array<string|int, mixed>
Return values
bool

reset()

Force the breaker back to CLOSED — operational override (e.g. after ops fixes Redis and wants to skip the cooldown).

public reset() : void

set()

Write a row into table $name under $key. Overwrites any existing row.

public set(string $name, string $key, array<string|int, mixed> $row) : bool

Returns true on success, or false when the backend cannot store the entry (e.g. the OpenSwoole\Table is full).

Parameters
$name : string
$key : string
$row : array<string|int, mixed>

Column-name → value map. All declared columns must be present.

Return values
bool

state()

Returns 'closed', 'open', or 'half-open' — for introspection + tests.

public state() : string
Return values
string

stats()

Per-worker breaker stats — breaker_opened_total, breaker_closed_total, breaker_short_circuited_total.

public stats() : Stats
Return values
Stats

admitHalfOpenProbe()

#255 — admit exactly one half-open probe. Atomically claims the probe slot via cmpset(HALF_OPEN_READY → HALF_OPEN_PROBING): the single winner gets true and proceeds to the primary; every concurrent loser gets false and is turned away (fallback for reads, fail-fast for writes).

private admitHalfOpenProbe() : bool

Returns false when the breaker isn't half-open-ready (e.g. another coroutine already took the probe slot).

Return values
bool

callRead()

private callRead(callable(): T $primary, callable(): T $fallback) : T
Parameters
$primary : callable(): T
$fallback : callable(): T
Tags
template
Return values
T

callWrite()

private callWrite(callable(): T $primary[, string|null $table = null ]) : T
Parameters
$primary : callable(): T
$table : string|null = null

When set, a pending primary make() for this table is retried before the write (#241).

Tags
template
Return values
T

isHalfOpen()

True while the breaker is in either half-open sub-state (ready or probing).

private isHalfOpen() : bool
Return values
bool

probeStateMaybe()

Maybe transition OPEN → HALF_OPEN_READY if the cooldown has elapsed.

private probeStateMaybe() : void

retryPrimaryMake()

#241 — retry a primary make() that failed during make(). Re-runs the original args; on success the pending entry is cleared so subsequent writes take the fast path. On failure the entry is kept (so the next write retries again) and the exception propagates — the caller's write then surfaces it through the normal failure path.

private retryPrimaryMake(string $name) : void
Parameters
$name : string
On this page