API Index — Namespaces, Packages, Reports, Indices
CircuitBreakerBackend
in package
implements
StoreBackend
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
$colof row$keyin table$nameby$by. - del() : bool
- Delete the row at
$keyfrom table$name. Returnstrueon success,falseif the key did not exist. - exists() : bool
- Return
truewhen table$namecontains a row with key$key. - get() : mixed
- Read a row OR a single field.
- incr() : int|float
- Atomically increment column
$colof row$keyin table$nameby$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 arenullfor 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
$nameunder$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 getstrueand proceeds to the primary; every concurrent loser getsfalseand 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 duringmake(). 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
CLOSED
private
mixed
CLOSED
= 0
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
OPEN
private
mixed
OPEN
= 1
Properties
$failureCount
private
Atomic
$failureCount
$failureThreshold
private
int
$failureThreshold
= 5
$failureWindowSec
private
int
$failureWindowSec
= 10
$fallback
private
StoreBackend|null
$fallback
= null
$firstFailureAt
private
Atomic
$firstFailureAt
$openDurationSec
private
int
$openDurationSec
= 30
$openedAt
private
Atomic
$openedAt
$primary
private
StoreBackend
$primary
$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
= []
$state
private
Atomic
$state
$stats
private
Stats
$stats
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
clear()
public
clear(string $name) : void
Parameters
- $name : string
count()
Return the number of rows currently stored in table $name.
public
count(string $name) : int
Parameters
- $name : string
Return values
intdecr()
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|floatdel()
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
boolexists()
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
boolget()
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|floatiterate()
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
cursorfield is'0'the scan is exhausted. $countis a HINT (Redis SCAN convention) — the batch returned may contain fewer rows on tracked-mode SETs with sparse hits, or more than$counton 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: arraymake()
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
boolnames()
public
names() : array<int, string>
Return values
array<int, string>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
boolstate()
Returns 'closed', 'open', or 'half-open' — for introspection + tests.
public
state() : string
Return values
stringstats()
Per-worker breaker stats — breaker_opened_total, breaker_closed_total, breaker_short_circuited_total.
public
stats() : Stats
Return values
StatsadmitHalfOpenProbe()
#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
boolcallRead()
private
callRead(callable(): T $primary, callable(): T $fallback) : T
Parameters
- $primary : callable(): T
- $fallback : callable(): T
Tags
Return values
TcallWrite()
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
Return values
TisHalfOpen()
True while the breaker is in either half-open sub-state (ready or probing).
private
isHalfOpen() : bool
Return values
boolprobeStateMaybe()
Maybe transition OPEN → HALF_OPEN_READY if the cooldown has elapsed.
private
probeStateMaybe() : void
recordFailure()
private
recordFailure() : void
recordSuccess()
private
recordSuccess() : 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