Conversation
reap() threw `Queue delivery payload is missing` whenever a claim on the processing list had an ownership record but no payload, on the assumption that only legacy deliveries could lose one. Two ordinary situations break that assumption: a settle in another worker deletes the job key between this sweep's owner read and its payload read, and maxmemory eviction can take a claimed payload, which is stored without a TTL. The throw aborted the whole sweep -- every claim behind the broken one went unrecovered, and where the state persisted, maintenance never made progress again. A payload-less claim cannot be requeued, so it is now parked on the dead list like an exhausted one, through the same reclaim script: ownership still has to match and a live heartbeat still wins, so a worker actually holding the delivery keeps it, and a claim someone else already settled is left alone. reclaim.lua only insists on the payload key when it is replaying a payload, which is the case where its absence means another consumer got there first. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Thanks for contributing! This repository is a read-only mirror; development for this library happens in |
|
| $this->expire('.claims.*'); | ||
| $this->redis->del($this->namespace . '.jobs.recovery.' . $claimed->getPid()); |
There was a problem hiding this comment.
These tests hard-code and mutate the Redis .claims, .jobs, .owners, and .stats key layout, then assert those same storage details. This violates the repository directive to test observable behavior rather than mirror source implementation or configuration. A harmless key-layout refactor would break these tests even if broker behavior remained correct. Replace the duplicated key construction and internal-state checks with behavior-oriented fault injection and broker-visible recovery outcomes. The repository requirement must be satisfied before merging; the same pattern also appears at lines 114–115, 124–125, and 141.
Context Used: Call out and harshly judge implementation-coupled tests. We don't mirror source code, configuration, or version pins in assertions. We test observable behavior; use linters for syntax and schema checks. (source)
Prompt To Fix With AI
This is a comment left during a code review.
Path: tests/Queue/E2E/Adapter/RedisBrokerRecoveryTest.php
Line: 105-106
Comment:
**Tests Mirror Redis Internals**
These tests hard-code and mutate the Redis `.claims`, `.jobs`, `.owners`, and `.stats` key layout, then assert those same storage details. This violates the repository directive to test observable behavior rather than mirror source implementation or configuration. A harmless key-layout refactor would break these tests even if broker behavior remained correct. Replace the duplicated key construction and internal-state checks with behavior-oriented fault injection and broker-visible recovery outcomes. The repository requirement must be satisfied before merging; the same pattern also appears at lines 114–115, 124–125, and 141.
**Context Used:** Call out and harshly judge implementation-coupled tests. We don't mirror source code, configuration, or version pins in assertions. We test observable behavior; use linters for syntax and schema checks. ([source](https://app.greptile.com/review/custom-context?memory=instruction-0))
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
The error
Sentry CLOUD-3R40 —
RuntimeException: Queue delivery payload is missing, 100 events, thrown fromBroker\Redis::reap()undermaintain()on the Swoole adapter's maintenance tick.Root cause
reap()read the ownership record and then the payload as two separate commands, and treated "owner present, payload gone" as impossible — "Only legacy payloads expire while processing" — by throwing. Two ordinary situations produce exactly that state:settle.luadeletes the owner key and the job key in one atomic call. A commit landing between this sweep'sGET ownerandGET jobleaves the sweep holding a token for a delivery that no longer exists. On a busy queue a sweep scans up to 2000 claims, two round trips each, while workers commit throughout.claim.luastores the job key without a TTL, so under anallkeys-*maxmemory policy a claimed payload can be evicted while its owner key survives.The throw did more than log noise: it aborted the sweep, so every claim behind the broken one went unrecovered, and where the state persisted, that queue's reaping never made progress again.
The fix
A payload-less claim cannot be requeued — there is no payload to put back. It is now parked on the dead list like an exhausted claim, through the same
reclaimscript the rest of the sweep uses, so the decision stays atomic:reclaim.luaonly insists on the payload key when it is replaying a payload — the case where its absence means someone else got there first. Parking needs no payload.Legacy claims (no ownership record) keep their existing behaviour: dropped from the processing list.
Tests
Written first, against real Redis; all three reproduced the exact exception at
Redis.php:457before the fix.testReapClearsAnOwnedClaimWhosePayloadIsGone— the claim is cleared, parked on the dead list, its ownership record removed and the processing counter settled.testReapKeepsSweepingPastAClaimWhosePayloadIsGone— the stranded claim behind the broken one is still recovered.testAHeartbeatedClaimWithoutItsPayloadIsLeftAlone— ownership plus a live heartbeat means the sweep does not take the delivery from its worker.Full
unitsuite (143) ande2esuite (149, KEDA excluded — no kind cluster locally) pass against the compose services. PHPStan reports no new errors.🤖 Generated with Claude Code