kinetis / queue-redis
A Redis-backed queue implementation for kinetis/queue's QueueInterface — finite leases over pending/leased/delayed keys, so a crashed worker's job is redelivered rather than stranded.
Requires
- php: ^8.4
- amphp/redis: ^2.0.4
- amphp/socket: ^2.4.0
- kinetis/framework: ^1.20.1
- kinetis/queue: ^1.7.0
- kinetis/redis: ^1.0.1
Requires (Dev)
- infection/infection: ^0.35.0
- phpstan/phpstan: ^2.2.8
- phpunit/phpunit: ^13.3.3
- vimeo/psalm: ^6.17
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
kinetis/queue-redis
A Redis-backed queue implementation for kinetis/queue's QueueInterface
Part of Kinetis, a non-blocking PHP framework for API-first applications, developed in the kinetis-dev/kinetis monorepo.
Adds Redis as a queue backend. push()/pop()/ack()/release()/fail()
work exactly like any other backend — only your configuration changes. A
reservation is a finite lease: pop() moves a job from the queue's
pending list into a leased sorted set scored with an expiry, and any
worker's next pop() returns a lease past its expiry to pending with
attempts incremented. A job whose worker died mid-execution is
redelivered rather than stranded.
use Kinetis\Config\Config; use Kinetis\QueueRedis\RedisQueueFactory; $queue = RedisQueueFactory::fromConfig($config); $queue->push(new SendWelcomeEmail($email, $name), queue: 'default');
The factory opens the queue's own Kinetis\Redis\Client and hands the
queue that client's close(), so RedisQueue declares
Kinetis\Queue\DisposableQueueInterface and dispose() ends the
connection when the worker does. Building one yourself means registering
that — $app->onDispose($queue->dispose(...)); the bootstrap behind
QUEUE_CONNECTION=redis already does. A RedisQueue constructed
directly around an Amp\Redis\RedisClient you built closes nothing:
that facade exposes no close, and the transport under it stays yours.
RedisQueue declares Kinetis\Queue\ClearableQueueInterface.
Clearing counts and removes the queue's pending and delayed entries in
one Lua script, so the number it reports is what it removed; live leases
are untouched, since they are work a running worker still owns.
size() counts pending, delayed and expired leases, not live ones.
The leased member is the exact envelope string handed back as the job's
handle, and reclaiming rewrites it with the incremented attempt count.
That makes the handle a fence: ack(), release() and fail() act only
on that exact member, so a settlement for a delivery that has been
reclaimed or already settled raises
Kinetis\Queue\Exception\StaleJobHandleException and writes nothing.
RedisQueue also declares Kinetis\Queue\RenewableQueueInterface, so
queue:work extends a running job's lease at half
QUEUE_VISIBILITY_TIMEOUT_SECONDS: one Lua script resetting that exact
leased member's expiry to Redis TIME plus the window, with
ZADD ... XX as the fence. The setting therefore sizes crash recovery,
not job duration.
Delivery is still at least once, so keep handlers idempotent. A lease
still expires under a job whose worker died and under a handler that
never yields to the event loop, and such a job can execute alongside its
replacement. maxAttempts bounds handlers that throw; it cannot bound a
succession of processes that each die mid-execution.
There is no reaper process. Every pop() promotes due delayed jobs and
reclaims expired leases for each queue it is given, in priority order,
before it waits.
Configuration
QUEUE_CONNECTION=redis
REDIS_HOST=127.0.0.1
QUEUE_VISIBILITY_TIMEOUT_SECONDS=300
QUEUE_VISIBILITY_TIMEOUT_SECONDS is how long a reservation is leased
before any worker may reclaim it. It defaults to 300 and must be a
positive integer. Every other key this backend reads — REDIS_HOST/
REDIS_URL/REDIS_TLS/... — is the exact one kinetis/cache-redis's
RedisSimpleCache already reads, scoped by the queue connection's name
the same way every other backend is (see
kinetis.dev/docs/queue.html). The connection's own REDIS_CLUSTER
is read and rejected: this backend supports standalone Redis only, so
true throws an InvalidArgumentException naming that key. A named
connection reads only its scoped key, so a cluster on the unscoped keys
leaves it alone. It opens its own connection over
kinetis/redis rather than
sharing the cache's, so its connection lifetime is its own. kinetis/queue's own keys
(QUEUE_CONNECTION, QUEUE_MAX_ATTEMPTS, ...) are documented in that
package; full reference:
kinetis.dev/docs/config.html.
Installation
composer require kinetis/queue-redis
Requires PHP 8.4+, kinetis/framework, kinetis/queue, and
kinetis/redis. Full documentation:
kinetis.dev/docs/queue-redis.html.
License
MIT — see LICENSE.