feat(queue): add reliable Redis claims#73
Conversation
Greptile SummaryThis PR introduces opt-in reliable Redis queue delivery for the DAT-1898 recovery path. Messages are atomically claimed into an in-flight lease sorted set, heartbeated by the Swoole adapter, and committed or rejected via immutable
Confidence Score: 5/5The change is additive and opt-in; existing non-reliable queue paths are unmodified and the Lua scripts are atomic with layered defensive validation. All reliable-path operations use atomic Lua scripts that correctly validate state, manage stats, and handle corrupt entries via quarantine. The PHP broker resets connections on ambiguous failures without replaying mutating commands. The Swoole heartbeat and recovery coroutines manage WaitGroup and Channel lifecycles properly, including coroutine-creation failure fallbacks. packages/queue/src/Queue/Broker/Redis/Script.php for the EXPIRED/RECLAIM ordering issues from prior threads; packages/queue/src/Queue/Broker/Redis.php for the getQueueSize atomic guard inconsistency. Important Files Changed
Reviews (2): Last reviewed commit: "(fix): recover reliable Redis claims saf..." | Re-trigger Greptile |
| public const string EXPIRED = <<<'LUA' | ||
| local now = redis.call('TIME') | ||
| local micros = (tonumber(now[1]) * 1000000) + tonumber(now[2]) | ||
| local pids = redis.call('ZREVRANGEBYSCORE', KEYS[1], micros, '-inf', 'LIMIT', 0, tonumber(ARGV[1])) |
There was a problem hiding this comment.
EXPIRED scan order contradicts "oldest-first" intent
ZREVRANGEBYSCORE sorts results descending by score, so the batch returned first contains the items with the highest leaseUntil value — i.e., the messages whose leases expired most recently. Messages that have been stuck the longest (lowest leaseUntil, expired earliest) are pushed to the tail of each scan batch and will wait the longest to be reclaimed. The PR description explicitly states "recover oldest-first across batches", which requires ascending order: ZRANGEBYSCORE KEYS[1] '-inf' micros 'LIMIT' 0 N.
Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/queue/src/Queue/Broker/Redis/Script.php
Line: 187
Comment:
**EXPIRED scan order contradicts "oldest-first" intent**
`ZREVRANGEBYSCORE` sorts results descending by score, so the batch returned first contains the items with the **highest** `leaseUntil` value — i.e., the messages whose leases expired most recently. Messages that have been stuck the longest (lowest `leaseUntil`, expired earliest) are pushed to the tail of each scan batch and will wait the longest to be reclaimed. The PR description explicitly states "recover oldest-first across batches", which requires ascending order: `ZRANGEBYSCORE KEYS[1] '-inf' micros 'LIMIT' 0 N`.
How can I resolve this? If you propose a fix, please make it concise.| .. ',"queue":' .. cjson.encode(replacement.queue) | ||
| .. ',"timestamp":' .. tostring(replacement.timestamp) | ||
| .. ',"payload":' .. record.payload .. '}' | ||
| redis.call('RPUSH', KEYS[3], encoded) |
There was a problem hiding this comment.
Reclaimed messages silently inherit priority-queue position
RPUSH places the reclaimed envelope at the right end of the pending list — the same end from which RPOP (in the CLAIM script) dequeues. This makes reclaimed messages indistinguishable from explicitly priority-flagged messages, so they will be picked up before any normal-priority messages that were enqueued before the reclaim happened. If a large number of claims expire simultaneously during an outage, repeated reclaims can starve the normal-priority backlog. The RETRY script uses LPUSH (normal priority) for symmetry — consider whether RECLAIM should do the same, or at least document that reclaimed messages are implicitly elevated to priority tier.
Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/queue/src/Queue/Broker/Redis/Script.php
Line: 268
Comment:
**Reclaimed messages silently inherit priority-queue position**
`RPUSH` places the reclaimed envelope at the right end of the pending list — the same end from which `RPOP` (in the `CLAIM` script) dequeues. This makes reclaimed messages indistinguishable from explicitly priority-flagged messages, so they will be picked up before any normal-priority messages that were enqueued before the reclaim happened. If a large number of claims expire simultaneously during an outage, repeated reclaims can starve the normal-priority backlog. The `RETRY` script uses `LPUSH` (normal priority) for symmetry — consider whether `RECLAIM` should do the same, or at least document that reclaimed messages are implicitly elevated to priority tier.
How can I resolve this? If you propose a fix, please make it concise.|
Pausing this PR as draft because DAT-1898 moved to Canceled before publication was reconciled with Linear ownership. I am not reopening the ticket or making further behavioral changes without an active owner. Current exact-head state:
The first thread identifies an ambiguity in the "oldest-first" wording: The smallest paired remediation for a future active owner is:
No production deployment or mutation was performed. |
|
Closing — this was the queue-library slice of DAT-1898 (StatsResources reliable Redis claims), and that approach was cancelled by the maintainer: the stats-resources queue isn't worth the extra claim/lease machinery; if it wedges again we flush it. DAT-1898 is Canceled in Linear and the cloud-side PR (appwrite-labs/cloud#4807) was closed for the same reason. No fault with the implementation — the requirement went away. |
Summary
Adds opt-in reliable Redis queue claims for the DAT-1898 recovery path:
ClaimLostexceptionThis is the queue-library dependency slice of DAT-1898. Cloud queue wiring, package release/upgrade, separate gauge scheduling, and rollout observation remain follow-up gates; this PR does not deploy production.
Verification
The macOS test process was also investigated under LLDB: its exit 139 originates in the loaded Xdebug extension; the same suites exit cleanly with
XDEBUG_MODE=off.Related work
This is additive and intentionally avoids folding in the broader reconnect, KEDA sizing, or publisher-refactor work tracked by open PRs #34, #41, and #44.