API Index — Namespaces, Packages, Reports, Indices
RedisPubSub
in package
Per-worker pub/sub runner.
Owns ONE dedicated Redis connection (separate from the pool — SUBSCRIBE
monopolises a connection) and one runner coroutine. Routes inbound
messages to registered handlers via go() per message so a slow
handler can't block the next read.
Wake-up + clean shutdown: a private sentinel channel is subscribed
alongside the user channels; stop() publishes a marker to it via a
separate client. The subscribe consumer detects the marker, throws
PubSubStopException, the driver catches it, the runner exits.
Reconnect: connection drop while subscribed → bounded exponential
backoff (capped at 5 s) → re-connect → re-SUBSCRIBE / re-PSUBSCRIBE
everything. Messages published during the reconnect window are lost
(Redis pub/sub has no buffering) — use Store::publishReliable for
at-least-once.
Table of Contents
Properties
- $exactHandlers : array<string, array<int, callable>>
- Exact-channel handlers keyed by channel name.
- $maxAttempts : int
- $opts : array<string|int, mixed>
- $patternHandlers : array<string, array<int, callable>>
- Pattern handlers keyed by PSUBSCRIBE pattern (contains
*). - $prefix : string
- $running : Atomic
- Atomic flag (
1= running,0= stopped). Cross-coroutine visibility viaOpenSwoole\Atomic. - $stats : Stats
- Per-worker counters exposed via
stats(). - $stopChannel : string
- Private sentinel channel used by
stop()to wake the subscriber loop cleanly. - $url : string
Methods
- __construct() : mixed
- exactChannels() : array<int, string>
- Return the exact channel names with registered handlers.
- isRunning() : bool
- Return
truewhen the runner coroutine is active. - patternChannels() : array<int, string>
- Return the PSUBSCRIBE pattern strings with registered handlers.
- register() : void
- Register a handler. Channels containing '*' are PSUBSCRIBE patterns; everything else is SUBSCRIBE exact. Multiple handlers per channel allowed; all fire on each message.
- start() : void
- Spawn the runner coroutine. Idempotent — re-calling while already running is a no-op. MUST be called from inside a coroutine context.
- stats() : Stats
- Per-worker stats — pubsub_reconnects_total, pubsub_messages_received_total, pubsub_handler_errors_total.
- stop() : void
- Signal the runner to exit cleanly. Publishes a sentinel to the private stop channel via a NEW client (the running subscriber owns its connection, can't publish to itself).
- stopChannel() : string
- Return the private stop-sentinel channel name (used by
stop()internally). - atomicIsZero() : bool
- Read
$a->get() === 0via an out-of-line method. - backoffSeconds() : float
- Compute the reconnect wait time for a given attempt number.
- dispatch() : void
- Fan out an inbound message to all matching handlers via
go(). - runner() : void
- Main subscriber loop running inside its own coroutine.
Properties
$exactHandlers
Exact-channel handlers keyed by channel name.
private
array<string, array<int, callable>>
$exactHandlers
= []
$maxAttempts
private
int
$maxAttempts
= 0
$opts
private
array<string|int, mixed>
$opts
= []
$patternHandlers
Pattern handlers keyed by PSUBSCRIBE pattern (contains *).
private
array<string, array<int, callable>>
$patternHandlers
= []
$prefix
private
string
$prefix
= 'zealstore'
$running
Atomic flag (1 = running, 0 = stopped). Cross-coroutine visibility via OpenSwoole\Atomic.
private
Atomic
$running
$stats
Per-worker counters exposed via stats().
private
Stats
$stats
$stopChannel
Private sentinel channel used by stop() to wake the subscriber loop cleanly.
private
string
$stopChannel
$url
private
string
$url
Methods
__construct()
public
__construct(string $url[, string $prefix = 'zealstore' ][, int $maxAttempts = 0 ][, array{prefer?: "auto"|"phpredis"|"predis"} $opts = [] ]) : mixed
Parameters
- $url : string
- $prefix : string = 'zealstore'
- $maxAttempts : int = 0
-
Bounded reconnect attempts (0 = unlimited, the default; preserves pre-H10 behaviour). When set to N > 0, the runner gives up after N consecutive backoff cycles and logs a final error. Use this when "keep trying forever" isn't acceptable — e.g. a CI worker that should fail loudly if its Redis disappears.
- $opts : array{prefer?: "auto"|"phpredis"|"predis"} = []
-
Driver preference for the runner's connections. Forced to
['prefer' => 'predis']by App::wirePubSubBoot() when phpredis would block the worker under HOOK_ALL=0 (H7) — predis SUBSCRIBE yields on its pure-PHP socket regardless of HOOK_ALL.
exactChannels()
Return the exact channel names with registered handlers.
public
exactChannels() : array<int, string>
Return values
array<int, string>isRunning()
Return true when the runner coroutine is active.
public
isRunning() : bool
Return values
boolpatternChannels()
Return the PSUBSCRIBE pattern strings with registered handlers.
public
patternChannels() : array<int, string>
Return values
array<int, string>register()
Register a handler. Channels containing '*' are PSUBSCRIBE patterns; everything else is SUBSCRIBE exact. Multiple handlers per channel allowed; all fire on each message.
public
register(string $channelOrPattern, callable $handler) : void
Parameters
- $channelOrPattern : string
- $handler : callable
start()
Spawn the runner coroutine. Idempotent — re-calling while already running is a no-op. MUST be called from inside a coroutine context.
public
start() : void
stats()
Per-worker stats — pubsub_reconnects_total, pubsub_messages_received_total, pubsub_handler_errors_total.
public
stats() : Stats
Return values
Statsstop()
Signal the runner to exit cleanly. Publishes a sentinel to the private stop channel via a NEW client (the running subscriber owns its connection, can't publish to itself).
public
stop() : void
stopChannel()
Return the private stop-sentinel channel name (used by stop() internally).
public
stopChannel() : string
Return values
stringatomicIsZero()
Read $a->get() === 0 via an out-of-line method.
private
static atomicIsZero(Atomic $a) : bool
An indirect read prevents PHPStan from constant-folding the
while ($this->running->get() === 1) loop condition when it can
prove running was set to 1 just above — which would eliminate
the loop body entirely under strict flow analysis.
Parameters
- $a : Atomic
Return values
boolbackoffSeconds()
Compute the reconnect wait time for a given attempt number.
private
static backoffSeconds(int $attempt) : float
Uses bounded exponential backoff: 0.1 × 2^attempt seconds, capped
at 5.0 s. Sequence: 0.1, 0.2, 0.4, 0.8, 1.6, 3.2, 5.0, 5.0, …
Parameters
- $attempt : int
Return values
floatdispatch()
Fan out an inbound message to all matching handlers via go().
private
dispatch(string $payload, string $channel, string|null $pattern) : void
Each handler runs in its own coroutine so a slow handler cannot block
the next message read. Exceptions thrown by handlers are caught,
counted in pubsub_handler_errors_total, and logged via error_log().
Parameters
- $payload : string
- $channel : string
- $pattern : string|null
runner()
Main subscriber loop running inside its own coroutine.
private
runner() : void
Connects a dedicated RedisClient, subscribes to all registered exact
channels + PSUBSCRIBE patterns + the private $stopChannel, then reads
messages in a blocking loop. On PubSubStopException (sentinel received
via stop()) the loop exits cleanly. On StoreException (connection
drop) it applies bounded exponential backoff and reconnects, up to
$maxAttempts times (0 = unlimited).