Batch queue consumption independently of coroutines by loks0n · Pull Request #316 · utopia-php/monorepo · GitHub
Skip to content

Batch queue consumption independently of coroutines - #316

Merged
loks0n merged 10 commits into
mainfrom
codex/queue-prefetch
Sep 21, 2026
Merged

loks0n merged 10 commits into
mainfrom
codex/queue-prefetch

Conversation

@loks0n

@loks0n loks0n commented Sep 21, 2026 •

Copy link
Copy Markdown
Contributor

Workers can prefetch 100 messages while running one coroutine, reducing broker round trips without acknowledging unfinished work. The public API is job('stats-usage', coroutines: 1, prefetch: 100). Prefetch bounds waiting, running, and unconfirmed messages together; it defaults to coroutines and cannot be lower. Each message retains its own acknowledgement and error handling.

Redis keeps the existing opaque ready-list payloads. Bounded Lua operations reserve and finalize batches, combine waiting completions with independent results, and renew heartbeats. Reservations survive process death after a blocking move. Outstanding payloads and ownership persist until completion or atomic recovery; heartbeat expiry alone does not revoke ownership. Positive jobTtl applies after rejection/dead-lettering. Commit, reject, and release are explicit operations, and completion grouping is scoped to the namespace for Redis Cluster.

NATS combines independent acknowledgement requests on its command connection, retaining a server confirmation per message. The internal transport buffer reports results incrementally without duplicate aggregate results. Factory-created brokers replace disconnected command connections on the next operation without replaying ambiguous acknowledgements. Shutdown returns work whose handler never started and bounds drain time.

Removes queue priority from publisher interfaces, Redis insertion, background batching, and NATS consumption. Existing normal subjects and Redis list payloads remain unchanged. Queues that used priority must drain that work with old consumers before upgrading; new workers do not consume the priority subject. Empty legacy consumers can remain.

Release as queue 5. NATS support is published as 1.4.0; this PR requires ^1.4 and targets main. Rename named arguments and Platform job keys from maxCoroutines/batch to coroutines/prefetch; positional order is unchanged. Platform now requires queue 5. Custom Redis connections must implement execute(). Redis Cluster namespaces require a shared hash tag, so existing Cluster queues need an explicit drain/migration. Before rollback, drain and recover reservations with a new consumer still available; old consumers cannot recover those reservations. Existing third-party single-message renewal remains supported.

Validation:

  • 145 queue unit tests / 329 assertions, including worker lifecycle, unconfirmed-message prefetch bounds, and incremental confirmation failure handling. Worker tests use a Consumer fake without broker storage. The prefetch regression starts through Server::job() and measures actual outstanding messages.
  • 72 Redis, Redis Cluster, NATS, and prefetch integration tests / 340 assertions, including payload retention, ownership/recovery races, command-connection recovery, one coroutine with prefetch 100, and graceful shutdown.
  • 56 broker tests / 189 assertions before the final codec-test refinement; the refined 11 codec tests pass with 36 assertions against both real Redis and Dragonfly v1.39.0. Includes 40 tests moved out of the unit suite for receive, codecs, rejection, retry, and recovery. Deleted the Lua-emulating PHP fake; real Redis namespaces are isolated and cleaned per test.
  • 2 Platform worker tests / 8 assertions against the local queue 5 package.
  • Pint, PHPStan, Rector, package validation, documentation lint, and benchmark shell syntax pass locally.

Appwrite/Cloud call-site and package-lock updates, staging checks on both brokers, representative stats-usage failure/accounting checks, and a deployment canary follow the package releases. Existing deployment environment names can stay; map their values to the new prefetch contract. No deployment defaults change here. Local transport benchmarks are not a production prefetch recommendation.

Design, compatibility, and rollout gates: packages/queue/docs/batching.md and prior-art.md.

Tracked in https://github.com/appwrite-labs/cloud/issues/5996.

@loks0n loks0n self-assigned this Sep 21, 2026
@loks0n

loks0n commented Sep 21, 2026

Copy link
Copy Markdown
Contributor Author

@github-actions

github-actions Bot commented Sep 21, 2026 •

Copy link
Copy Markdown

Benchmark results

queue — workload shapes across both concurrency axes (4 cores, 600 messages, median of 3)

workload shape redis msg/s nats msg/s
io 1p x 1c x 1f 38 38
io 1p x 4c x 4f 151 149
io 4p x 1c x 1f 152 152
cpu 1p x 1c x 1f 178 175
cpu 1p x 4c x 4f 190 183
cpu 4p x 1c x 1f 425 420
mixed 1p x 1c x 1f 64 65
mixed 1p x 4c x 4f 172 170
mixed 4p x 1c x 1f 245 250
null 1p x 16c x 16f 8590 7556
null 1p x 16c x 64f 13895 14443
null 1p x 16c x 100f 14718 16368

Np x Mc x Ff = N consumer processes x M handler coroutines x a prefetch limit of F
unacknowledged messages. io yields and should follow coroutines; cpu does not and should follow
processes; mixed is in between. null has no handler at all, so it measures the broker
rather than the job -- the only row where prefetch can matter.

Shared CI runners — treat absolute numbers as rough, compare modes within a run. Commit de792b9.

@loks0n

loks0n commented Sep 21, 2026

Copy link
Copy Markdown
Contributor Author

Updated and rebased for the callback-only NATS API in #315; 185 queue unit tests and 53 NATS broker/prefetch integration tests pass locally. For the queue-specific review before this draft becomes ready: verify command-connection replacement after an ambiguous transport failure. NATS requests() intentionally disconnects without replay, while commandsConnection() currently retains the cached instance. Recovery of future operations needs explicit coverage in the broker; callback-abort and response-timeout connection usability are already covered in the NATS package.

@loks0n

loks0n commented Sep 21, 2026

Copy link
Copy Markdown
Contributor Author

Rebased onto the unified NATS request lifecycle in #315. The queue now passes JetStreamMessage objects to JetStream::ackBatch(); it no longer constructs acknowledgement subjects or payloads. Updated the design document. Validation passes locally: 185 queue unit tests / 442 assertions, 53 NATS broker and prefetch integration tests / 261 assertions (including batch 100 with one coroutine), Pint, PHPStan, Rector, and documentation lint. Existing package-release and command-connection recovery review gates remain open.

@loks0n
loks0n force-pushed the codex/queue-prefetch branch 2 times, most recently from aee7186 to 8ae062d Compare September 21, 2026 20:40
@loks0n

loks0n commented Sep 21, 2026

Copy link
Copy Markdown
Contributor Author

Rebased onto the NATS review fix 24cbdee: server-closing errors recycle the connection before invoking onError, so a throwing callback cannot leave a dead connection marked connected. Queue validation after rebase: 185 unit tests / 442 assertions and 53 NATS broker/prefetch integration tests / 261 assertions pass. Existing release and command-connection recovery gates remain unchanged.

@loks0n
loks0n force-pushed the codex/queue-prefetch branch from 8ae062d to e683b77 Compare September 21, 2026 20:50
@loks0n

loks0n commented Sep 21, 2026

Copy link
Copy Markdown
Contributor Author

Rebased onto a746aaf from #315. Request collection now performs keepalive checks without reconnecting/replaying an in-flight batch; ackBatch documentation explicitly requires AckPolicy::Explicit for isolation of selected messages. The queue already uses Explicit. Dependent NATS broker/prefetch integration suite passes: 53 tests / 261 assertions. Existing release and command-connection recovery gates remain unchanged.

@loks0n
loks0n force-pushed the codex/queue-prefetch branch from e683b77 to b507633 Compare September 21, 2026 20:57
@loks0n

loks0n commented Sep 21, 2026

Copy link
Copy Markdown
Contributor Author

Rebased onto NATS fix 1077972, which consumes buffered protocol data before keepalive emission or stale checks. This preserves buffered acknowledgement replies and PONGs. Dependent NATS broker/prefetch integration validation passes: 53 tests / 261 assertions. Existing release and command-connection recovery gates remain unchanged.

@loks0n
loks0n force-pushed the codex/queue-prefetch branch from b507633 to 4cae744 Compare September 21, 2026 21:08
@loks0n
loks0n changed the base branch from codex/nats-requests to main September 21, 2026 21:08
@loks0n

loks0n commented Sep 21, 2026

Copy link
Copy Markdown
Contributor Author

NATS 1.4.0 is released and available on Packagist: https://github.com/utopia-php/nats/releases/tag/1.4.0 . Rebased onto main, retargeted the PR from codex/nats-requests to main, and updated the rollout document. Existing ^1.4 constraints now resolve normally; Composer installed the published archive (mirror commit 016f48bd25f7bf7a06e5e12a9c768036945d5459), not a local path package. Validation against that archive: 185 unit tests / 442 assertions and 53 NATS broker/prefetch integration tests / 261 assertions pass. The full runner hit a port conflict when starting duplicate services, so targeted integration ran against the existing services. Board moved from Blocked to In progress. Remains draft for the separate command-connection recovery review; downstream queue/Platform/Appwrite/Cloud releases are not performed.

@loks0n

loks0n commented Sep 21, 2026

Copy link
Copy Markdown
Contributor Author

Closed the acknowledgement connection recovery gap in b6a535e. Reproduced both failure modes against released NATS 1.4.0: an acknowledgement write never delivered and a write delivered with its confirmation lost. Previously the next commit failed because the disconnected connection stayed cached. Now command() clears the disconnected factory-created connection, JetStream handle and consumer handles under the lock in finally; the next operation obtains a fresh connection. No retry of the failed operation and no reset of the receive connection or other in-flight messages. Tests verify successful later commits, working queue-depth reads through refreshed consumer handles, redelivery only when the original acknowledgement did not reach NATS, and no replay when commit is called again for the failed delivery. Live-connection sources retain the existing owner-managed replacement contract.

Validation: 185 unit tests / 442 assertions; 55 NATS broker/prefetch integration tests / 276 assertions; Pint, PHPStan, Rector and Vale pass. Updated the design document and removed this blocker. Remote CI is running.

@loks0n
loks0n marked this pull request as ready for review September 21, 2026 21:15
@greptile-apps

greptile-apps Bot commented Sep 21, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerConfidence Score: 4/5

The queue behavior appears mergeable, but the explicit repository requirement against implementation-coupled tests must be satisfied first.

Fix All in Claude CodeFindings

  1. P2 Test Mirrors Lifecycle Internals ▶
Fix with agent prompt
### Issue 1
packages/queue/tests/Queue/Unit/PrefetchTest.php:61-63
This test directly iterates Swoole's protected `$onWorkerStart` storage and reproduces its internal callback argument to enter `Server::start()`. A behavior-preserving lifecycle refactor could therefore break the test even when prefetch still works correctly. `RedisExpiryTest.php:48-50` has the same problem by identifying a Lua operation from its source prefix. This violates the repository directive to test observable behavior rather than mirror source code or configuration, so the requirement must be satisfied before merging.

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!

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Summary

This PR redesigns queue consumption around independently bounded prefetch and handler concurrency.

  • Adds recoverable Redis batch reservations, grouped settlement and renewal, durable ownership, and Redis Cluster hash-tag validation.
  • Adds incremental, independently confirmed NATS acknowledgement batching and command-connection replacement.
  • Updates Swoole buffering, renewal, and bounded shutdown behavior.
  • Renames public concurrency and batching configuration to coroutines and prefetch, removes priority publishing, and updates Platform to require queue 5.
  • Reworks broker tests around real Redis/Dragonfly and observable worker outcomes, although one new prefetch test still mirrors protected lifecycle internals.

Reviews (6) · Last reviewed commit: "test(queue): assert release and prefetch..."

Comment thread packages/queue/src/Queue/Broker/Redis/settle.lua Outdated
Comment thread packages/queue/tests/Queue/Unit/BufferTest.php Outdated
@loks0n

loks0n commented Sep 21, 2026

Copy link
Copy Markdown
Contributor Author

Benchmark interpretation from source review: the screenshot shows Redis about 1.55x NATS throughput for the null-handler batch-16 row (10,043 vs 6,465 msg/s), while I/O/mixed handlers largely hide broker overhead. Concrete differences: Redis uses ext-redis and Lua reserve/claim/settle operations with aggregate replies; the NATS client parses/dispatches messages in PHP and ackBatch still uses an independent confirmation per message (pipelined, not serial round trips). For a normal backlog with batch > 1, NATS pull() performs an empty priority poll, a one-message blocking fetch, then a no-wait top-up; each Consumer::fetch creates and removes an inbox subscription. Benchmark defaults also differ in persistence: NATS File storage with one replica, Redis compose without AOF enabled. These are candidate contributors, not measured attribution: the table is not a CPU profile or a durability-matched comparison. A 600-message null-handler sample at these rates lasts only about 60-93 ms; longer runs and component timing are needed before assigning percentages or selecting an optimization.

@loks0n

loks0n commented Sep 21, 2026

Copy link
Copy Markdown
Contributor Author

Removed priority in 94c48be, net -129 lines. Publisher APIs no longer accept the flag; Redis always uses normal FIFO insertion; background batches group by queue only; NATS has one work consumer, one depth query and no priority poll. Existing normal subject/durable names remain unchanged. Added an integration regression for existing normal messages on a legacy two-consumer topology with an unused priority consumer.

Audited current Appwrite main f2f494fd and Cloud main 72a03895 for queue priority publishing; found no usage. Cloud cutover status still mentions worker_priority, but handles its absence as 404. This does not establish that every deployed priority backlog is empty. Queue 5 migration notes require stopping priority producers and draining pending/unacknowledged legacy priority deliveries with old workers before upgrade. No live resources or messages were deleted.

Validation: 184 unit tests / 440 assertions; final NATS broker suite 50 tests / 258 assertions; 14 background/prefetch integration tests passed (including Redis and NATS). The initial combined run had a test-setup error while constructing the legacy stream config; corrected the fixture and reran the entire NATS suite successfully. Pint, PHPStan, Rector and Vale pass. No throughput gain is claimed without another benchmark run.

@loks0n

loks0n commented Sep 21, 2026

Copy link
Copy Markdown
Contributor Author

Review of 94c48be: three correctness findings to address before merge.

[P1] Do not silently drop expired prefetched payloads on release

packages/queue/src/Queue/Broker/Redis/settle.lua:17-20

With a positive Queue::jobTtl, a prefetched message can lose its jobs payload while waiting for an earlier handler. Renewal extends only the claim, not that payload. release() then deletes the claim/processing entry, skips RPUSH because GET returned false, decrements processing and reports success. Work that never entered a handler disappears. Reproduced against Redis with jobTtl=1: publish, receive, wait 2s, release; ready=0, processing=0, claim absent, no exception. This confirms Greptile's release finding. Preserve the payload for the outstanding delivery (or make release reconstruct it safely); do not report successful release after discarding the only delivery state.

[P1] Preserve long-running Redis jobs on adapters without renewal

packages/queue/src/Queue/Broker/Redis/settle.lua:13 (claim TTL established at claim.lua:20)

Settlement now requires the 90-second claim key to remain live, but only Adapter/Swoole renews deliveries. KubernetesJob and Workerman still inherit Adapter::withAckExtension(), which merely invokes the handler. Consequently a handler lasting beyond CLAIM_TTL completes its side effects and then cannot commit or reject, even when no other consumer reclaimed it and the job payload is intact. It remains processing until recovery and can run again. This is a regression from the previous commit path, which did not require a live heartbeat. Reproduced through KubernetesJob::consume(), accelerating claim expiry with PEXPIRE during its handler: handled=1, success callbacks=0, error='Queue delivery is no longer owned by this consumer'. Provide renewal for all supported execution paths or separate durable ownership from the expiring liveness signal; simply removing the ownership check would weaken fencing.

[P2] Partition grouped Cluster settlements by compatible key placement

packages/queue/src/Queue/Broker/Redis.php:191-197

The broker has one settlement Buffer for every queue/namespace, and concatenates all waiting keys into one script. Individually valid Redis Cluster queues with different namespace hash tags therefore fail once their completions coalesce. Reproduced using one broker with {review316-a-...} and {review316-b-...} namespaces: the first completion succeeds, then the grouped A/B completions both throw 'Atomic Redis queue operations require a shared hash tag'. Both namespaces satisfy the documented placement rule individually. Partition batching by hash tag/slot (or namespace conservatively), retaining independent indexed outcomes.

The consolidated worker loop and JetStream ackBatch boundary are improvements. The remaining modeling issue is that payload retention, ownership and heartbeat lifetime have different meanings, while settlement currently assumes they expire together. Likewise, a batching boundary must represent a valid atomic operation, not just all calls sharing a broker instance.

Validation on an isolated export of the exact head:

  • Full configured unit suite: 184 tests / 440 assertions pass.
  • Redis reservation integration: 8 tests / 30 assertions pass.
  • Redis/NATS prefetch + graceful shutdown integration: 4 tests / 12 assertions pass with isolated Packagist NATS ^1.4 installed. The initial run used an older local NATS dependency; the corrected run passed.
  • Additional focused reproductions above against local Redis/Redis Cluster, with uniquely named keys removed afterward. Claim expiry was accelerated, not a 90-second wall-clock run.
  • No source changes or production operations. Board item remains In review.

@loks0n

loks0n commented Sep 21, 2026

Copy link
Copy Markdown
Contributor Author

Checked the linked review and both Greptile comments against current head 94c48be. All three correctness findings are valid merge blockers: (1) claim.lua applies jobTtl to payload while extend.lua renews only the claim; settle.lua release removes delivery state and reports success even if payload is missing. (2) settle.lua fences on a 90-second expiring claim, while KubernetesJob and Workerman have no renewal override, so long handlers lose settlement rights without a takeover. (3) the broker-wide settlement Buffer combines keys across namespaces; RedisCluster::execute rejects distinct hash tags even when each queue is valid on its own.

Greptile Release Can Lose Work duplicates finding 1. Its test-quality finding is also valid: BufferTest and RedisReservationTest pin exact scheduler-dependent groupings; LockingTest mirrors the interface method list through reflection. Replace these with behavioral checks for delivery/outcomes, coalescing and serialization rather than exact partitions or a copied API inventory.

Recommended correction: separate payload retention, durable ownership and expiring liveness; retain fencing across actual takeover. Partition settlement batches by compatible placement (namespace is a conservative option). These need focused TTL, non-renewing-adapter and cross-namespace Cluster regressions. Source verification only in this pass; I did not rerun the review reproductions or modify code. The Appwrite/Cloud priority backlog is confirmed empty by the owner and is not a rollout blocker.

@loks0n

loks0n commented Sep 21, 2026

Copy link
Copy Markdown
Contributor Author

Prior-art-based consumer model proposal (design only; no changes to the correctness fixes currently being edited in the PR worktree):

Use Message, Consumer, Worker, receipt, acknowledgement, prefetch, heartbeat. No new public Delivery/Claim/Lease/Settlement objects.

  • Primary reference: Dramatiq's Consumer exposes ack/nack/requeue; its Redis consumer counts cached and outstanding messages against prefetch. Its nack semantics differ from our retrying reject, so retain our explicit outcome policy rather than copying names indiscriminately.
  • RabbitMQ distinguishes unacknowledged messages, prefetch and channel-scoped acknowledgement identity. Our worker should track unacknowledged messages through confirmation; compatible transport ownership determines what can batch together.
  • SQS distinguishes message ID from the receipt handle for a particular receive. An opaque receipt on the existing received Message is enough; a public Delivery wrapper is unnecessary.
  • BullMQ couples active-job recovery to reliable worker renewal. Copying its strict lock model without that scheduling contract would not suit our synchronous adapters.

Concrete target:

  1. Keep receive/commit/reject; document commit as acknowledgement. Keep release for returning work that never started, without incrementing attempts. Replace the internal ?bool $success and 0/1/2 protocol with explicit commit/reject/release operations.
  2. Worker-level prefetch means all unacknowledged messages: waiting, running and awaiting confirmation. Concurrency means running handlers. Batch size is only the count in one broker operation. Proposed queue-5 shape: job('stats-usage', maxCoroutines: 1, prefetch: 100); omitted prefetch defaults to concurrency, explicit prefetch below concurrency is rejected. This would require call-site/migration updates and is not implemented yet.
  3. Redis ownership and liveness are separate: heartbeat expiry permits recovery; atomic recovery invalidates ownership. Commit/recovery must have one winner. Preserve payloads until no outstanding work needs them. Document retention changes explicitly instead of silently redefining jobTtl. This is our proposed policy, not a universal broker guarantee.
  4. Keep Redis's raw reservation/finalization protocol private to the existing opaque-list compatibility constraint. Dramatiq's ID/payload layout is a separate storage migration.
  5. Namespace-scope Redis command grouping; connection-scope NATS acknowledgement grouping. Make the shared buffer internal transport machinery, with one incremental result callback instead of optional callbacks plus aggregate returns. Redis can dispatch each returned script result; NATS dispatches each arriving confirmation.

Deletion targets: nullable outcome interpretation, duplicated result accumulation, generic-looking public Buffer, and redundant state/type wrappers. Keep the consolidated worker loop. A renewal capability should replace reflection only with an explicit third-party migration plan.

@loks0n

loks0n commented Sep 21, 2026

Copy link
Copy Markdown
Contributor Author

Addressed all three findings in #316 (comment) and both Greptile comments in 20b2327.

  • Payload retention: outstanding jobs remain available through release/reclaim; jobTtl applies to retained failed/dead payloads after settlement. A missing release payload returns an error before mutating ownership or processing state.
  • Ownership/liveness: durable owner tokens are separate from expiring heartbeats. KubernetesJob/Workerman can settle after heartbeat expiry if ownership is unchanged. Recovery atomically rechecks heartbeat and token, invalidates ownership and moves the message; stale completion/renewal cannot undo a takeover. Legacy processing entries remain recoverable. The existing publish-age recovery threshold still must exceed runtime for adapters without renewal.
  • Cluster batching: settlement buffers are scoped by namespace, so completions from individually valid namespaces cannot create a cross-slot script.
  • Tests: removed exact scheduler partition assertions and interface/private-lock reflection checks; test independent outcomes, coalescing bounds and concurrent serialization instead.

The four primary regressions fail against the previous implementation and pass after the fixes. Final validation: 183 unit tests / 443 assertions; 22 Redis/Cluster/prefetch integration tests / 82 assertions; 16 reservation tests / 54 assertions against Dragonfly. Includes an actual KubernetesJob consume path with accelerated heartbeat expiry, failed-payload TTL, missing-payload release, stale owner after reclaim, and completion/renewal winning races with reclaim. Pint, PHPStan, Rector, Vale and diff checks pass. Remote CI is running. Updated retention and rollback documentation; no production changes.

Comment thread packages/queue/tests/Queue/E2E/Adapter/InMemoryConnection.php Outdated
@loks0n

loks0n commented Sep 21, 2026

Copy link
Copy Markdown
Contributor Author

Design update requested by @loks0n: rename maxCoroutines to coroutines alongside batch → prefetch.

$server->job('stats-usage', coroutines: 1, prefetch: 100);

Apply consistently to parameter names, internal queue specifications, examples and named-argument callers in queue 5; retain positional argument order. This updates the proposed model, not a claim that the implementation has already changed.

@loks0n

loks0n commented Sep 21, 2026

Copy link
Copy Markdown
Contributor Author

Implemented the domain model proposal and naming update in 3addb37.

$server->job('stats-usage', coroutines: 1, prefetch: 100);
  • Prefetch limits waiting, running, and unconfirmed messages together. Omission defaults to coroutines; an explicit lower value is rejected before registering the job. Positional argument order is unchanged. Queue specs, Platform configuration, examples, tests, and benchmark options use the same names.
  • Redis uses explicit commit / reject / release operations throughout PHP and Lua. Release still returns work that never started without counting a failure. Existing ownership/heartbeat separation, payload retention, atomic recovery, and receipt fencing remain intact.
  • The shared buffer is under Internal, with one required incremental result callback. Removed aggregate result accumulation from NATS and dispatch Redis script results individually. A transport failure only fails unresolved requests; earlier confirmations remain successful. Redis grouping remains namespace-scoped and NATS acknowledgements use the broker's command connection.
  • No new public message wrappers or renewal interfaces. Existing single-message third-party renewal remains supported.

Queue 5 migration is documented: rename named arguments and Platform job keys from maxCoroutines/batch to coroutines/prefetch; replace Server::batch() with Server::prefetch(). Existing deployment environment names need not change. Map their values to prefetch: max($batch, $coroutines) to preserve the previous effective bound, or omit prefetch to use coroutines. Platform now requires queue 5. Appwrite/Cloud call-site and lock updates remain part of the package-release rollout; no deployment configuration changed here.

Validation: 187 queue unit tests / 459 assertions; 72 Redis, Redis Cluster, NATS and prefetch integration tests / 340 assertions; 16 Dragonfly reservation tests / 54 assertions; 2 Platform worker tests / 8 assertions against the local queue 5 package. New regressions cover unconfirmed messages counting against prefetch, handlers progressing before confirmations, successful confirmations surviving a later transport failure, and rejecting undersized prefetch. Pint, PHPStan, Rector, package validation, documentation lint, and benchmark shell syntax pass.

@loks0n

loks0n commented Sep 21, 2026

Copy link
Copy Markdown
Contributor Author

Re-reviewed head 3addb37842220b3da46691d4d04291762dc5d837. No further blocking findings in this pass.

The three earlier findings are addressed: active Redis payloads survive retention expiry, settlement checks durable ownership independently of heartbeat expiry, and settlement batches are scoped by namespace. Atomic recovery also rechecks ownership and heartbeat before changing state.

The model is substantially cleaner: coroutines controls handler concurrency, prefetch bounds waiting + running + unconfirmed messages, and commit/reject/release are explicit operations. Keeping receipts opaque and using one incremental result contract in the internal buffer avoids adding unnecessary public abstractions. I would keep this structure rather than introduce more domain types.

Independent validation against this head:

  • Unit suite: 187 tests, 459 assertions, passed.
  • RedisReservationTest + PrefetchTest: 20 tests, 66 assertions, passed against local services, covering retention, heartbeat expiry, recovery races, namespace isolation, and prefetch/shutdown behavior.

Platform linked tests and validation are green in CI. Queue tests, benchmark, and Greptile are still pending at the time of this review. Board status remains In review.

@loks0n

loks0n commented Sep 21, 2026

Copy link
Copy Markdown
Contributor Author

Checked outstanding Greptile threads against 3addb37: one unresolved finding, Test Mirrors Lua Internals. It is valid and still present.

InMemoryConnection::execute() branches on Lua source prefixes and copies argument offsets. It already differs from the real script: release with a missing payload removes processing/ownership in the fake, while settle.lua rejects before mutation. PushRecordingConnection::execute() also assumes key/argument positions and returns success without checking ownership.

Recommended fix: use a Consumer fake for adapter lifecycle/concurrency unit tests; test Redis state transitions against real Redis through observable results. Delete the PHP copy of the Lua protocol rather than extending it. This is a test-design issue, not a newly identified runtime defect. Greptile's latest summary reviewed this exact commit and lists no additional findings. No code changed during this check.

Comment thread packages/queue/tests/Queue/E2E/Adapter/RedisCodecTest.php Outdated
Comment on lines +61 to +63
foreach ($this->onWorkerStart as $callback) {
$callback('0');
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Test Mirrors Lifecycle Internals

This test directly iterates Swoole's protected $onWorkerStart storage and reproduces its internal callback argument to enter Server::start(). A behavior-preserving lifecycle refactor could therefore break the test even when prefetch still works correctly. RedisExpiryTest.php:48-50 has the same problem by identifying a Lua operation from its source prefix. This violates the repository directive to test observable behavior rather than mirror source code or configuration, so the requirement must be satisfied before merging.

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: packages/queue/tests/Queue/Unit/PrefetchTest.php
Line: 61-63

Comment:
**Test Mirrors Lifecycle Internals**

This test directly iterates Swoole's protected `$onWorkerStart` storage and reproduces its internal callback argument to enter `Server::start()`. A behavior-preserving lifecycle refactor could therefore break the test even when prefetch still works correctly. `RedisExpiryTest.php:48-50` has the same problem by identifying a Lua operation from its source prefix. This violates the repository directive to test observable behavior rather than mirror source code or configuration, so the requirement must be satisfied before merging.

**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!

Fix in Claude Code Fix in Codex

@loks0n
loks0n merged commit 5f8b175 into main Sep 21, 2026
7 checks passed
@loks0n

loks0n commented Sep 21, 2026

Copy link
Copy Markdown
Contributor Author

Merged #316 with the requested admin override after every check on 92eb36edaec952aa5578b2fb57ac8471ab3577f6 passed. Merge commit: 5f8b17543e520f6e33bf9325c8b40145778409b6.

Published from that merge commit and verified on Packagist:

  • queue 5.0.0, mirror commit 2b7edcbf344c02c8bff72a0b7a573cf8d7af94d4.
  • Platform 1.0.0-rc23, mirror commit faa60fd42bc62d98f7433c6a8f03dd6991a48d8b, requiring queue ^5.0.0.

CI tests: queue unit 145 tests / 329 assertions; queue integration 151 tests / 743 assertions; linked Platform 11 tests / 92 assertions. Validation, static checks, benchmarks, and Greptile checks all passed. Both tag-split release workflows succeeded.

One advisory Greptile test-harness coupling note remains visible; it did not fail the review check and was not resolved by this release.

Next rollout steps remain Appwrite/Cloud configuration-key and package-lock updates, staging validation on both brokers, then the stats-usage canary. No application deployments or deployment defaults changed here. Migration and rollback details are included in the release notes.

@loks0n

loks0n commented Sep 21, 2026

Copy link
Copy Markdown
Contributor Author

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant