Skip to content

Add an opt-in transactional queue outbox - #685

Merged
binaryfire merged 33 commits into
0.4from
feature/queue-outbox
Oct 23, 2026
Merged

binaryfire merged 33 commits into
0.4from
feature/queue-outbox

Conversation

@binaryfire

@binaryfire binaryfire commented Oct 23, 2026 •

Copy link
Copy Markdown
Member

Summary

This adds an opt-in transactional outbox to the queue package. Jobs are stored alongside the business write in the same database transaction, then published to their existing queue by a separate relay process.

afterCommit() prevents a worker from seeing uncommitted data, but it cannot close the gap between committing the database transaction and publishing the job. If the process stops in that gap, the business write survives without its job. The outbox closes that gap without replacing the existing queue drivers, dispatch APIs, or workers.

DB::transaction(function () use ($attributes) {
    $order = Order::create($attributes);

    SendOrderReceipt::dispatch($order)->viaOutbox();
});

Usage

Applications can select the outbox per dispatch with viaOutbox(), per job class with ShouldQueueViaOutbox, or per queue connection with outbox => true. withoutOutbox() overrides either default. onOutboxDatabase() selects the owning connection when more than one transaction is active.

The existing bulk and batch APIs use set-based inserts. Batch defaults also apply to jobs added later. Queued listeners, mailables, notifications, and broadcasts pass their selection to their existing wrapper jobs. Jobs that do not select the feature keep their normal dispatch path, without outbox database access or outbox service resolution.

Setup consists of an outbox table and a supervised relay alongside the existing workers:

php artisan make:queue-outbox-table
php artisan migrate
php artisan queue:relay --batch=100 --concurrency=4
php artisan queue:work redis

Horizon remains optional and continues managing Redis jobs after publication. The detailed guide is src/docs/queue-outbox.md; the queue and producer guides link to it.

Delivery and recovery

  • Staging uses the exact database session that owns the transaction. Rollback removes the rows and releases dispatch-owned locks. With several active connections, the caller must choose an owner rather than relying on a guess.
  • The relay claims bounded batches, publishes through the existing drivers, and deletes only confirmed deliveries. PostgreSQL uses a single claim statement; MySQL and MariaDB use locking reads with SKIP LOCKED; SQLite has an atomic-update fallback.
  • Database connections and locks are released before broker calls. Publication and settlement have separate, short resource lifetimes. Multiple relay processes and bounded concurrent batches are supported.
  • Redis and database publication are batched. SQS uses its request limits and partial-success responses. Other drivers use their existing raw-payload API, with one pooled connection per publication batch.
  • Failed and uncertain deliveries remain retryable with jittered backoff. Failed sources and destinations are throttled without blocking healthy sources. Ownership tokens prevent a stale relay from deleting another relay's claim.
  • Payload UUIDs, intended delays, SQS FIFO options, and Horizon classification survive staging. Separate SQS overflow publication attempts have separate cleanup ownership; existing overflow retention settings are unchanged.

The guarantee is at least once, not exactly once. A crash after broker acceptance but before settlement can publish the same job again. Handlers must be idempotent where repeated effects would be harmful. Publication order is best effort, and the outbox cannot make writes to separate databases or external services atomic.

Driver compatibility and operations

Selecting the feature does not require changing job code when switching drivers. A database queue already participating in the owning transaction inserts directly. Sync, deferred, background, and failover connections preserve their execution behavior through an after-commit fallback, without claiming crash durability. Null and fake queues do not create outbox rows. Direct pushRaw() remains a bypass.

The relay supports restart signals, graceful shutdown, bounded runtime and job limits, and one-shot execution. Operator commands provide pending-row inspection, monitoring, immediate retry, and deliberate removal with owner-matched unique-lock cleanup:

php artisan queue:outbox
php artisan queue:outbox-monitor --max=10000 --age=60
php artisan queue:outbox-retry JOB_UUID
php artisan queue:outbox-forget JOB_UUID

Delivery events and OpenTelemetry metrics are recorded at batch boundaries. There is no backlog polling on the application dispatch path and no duplicate producer span at relay time.

Related correctness changes

The integration also fixes behavior shared with ordinary dispatch:

  • Transaction records and callbacks identify the resolved base/read/write session, preventing separate sessions from consuming each other's callbacks.
  • Dispatch-lock cleanup captures the context in which the lock was acquired. Transactional storage keeps an independent rollback snapshot after acceptance.
  • Batch notifications preserve confirmed acceptance even when an observer fails. Horizon's post-publication work cannot turn an accepted job into a failed dispatch or release its lock.
  • Generic lifecycle events retain the finalized payload used by existing tracing integrations, separate from transport decoration.
  • Uninitialized public broadcast properties are skipped, and custom or memoized cache repositories retain their store identity for unique-lock cleanup.

Public dispatch method signatures remain unchanged. Two extension behaviors are documented: role-qualified transaction records now use the exact session name, and Horizon's normal push() uses the shared Redis storage helper instead of routing through its public pushRaw() override. Direct pushRaw() remains supported.

Performance

Measurements compare the pre-feature baseline with this branch, using independent installs with identical dependencies. The environment was a Linux VM exposing six CPUs, PHP 8.4.26, Swoole 6.3.0RC1, CLI OPcache enabled, JIT disabled, and local PostgreSQL 17.11, MySQL 9.5.0, MariaDB 10.11.19, and Redis 8.2.1. PostgreSQL durability settings stayed enabled. Job contents were 256 bytes; stored outbox envelopes were 1,246 bytes.

These measurements include the branch's related correctness changes. They measure publication, not execution of application jobs. Tables show median jobs/second with the observed range.

Ordinary dispatch, without the outbox

Ten alternating baseline/current pairs per case. Single runs dispatched 20,000 jobs; bulk runs dispatched 100,000 jobs in batches of 1,000. Warmup was 1,000 jobs.

Dispatch Baseline jobs/s This branch jobs/s
Single 5,074 (4,992–5,210) 5,073 (4,981–5,180)
Bulk, 1,000 per call 40,074 (39,107–41,351) 43,491 (40,829–44,366)

Single dispatch was effectively unchanged. Bulk improved by 8.5% using the ratio of medians; every matched pair improved. Median PHP CPU for bulk fell from 1.990 to 1.809 seconds. Ending heap growth was identical between revisions in both cases; the peak heap difference was about 24 KB.

Transactional staging

Three samples per engine and batch size. Single runs staged 2,000 jobs; bulk runs staged 10,000. Each call had its own transaction, so bulk amortizes commit cost.

Database Single jobs/s 1,000 per call jobs/s 10,000 per call jobs/s
PostgreSQL 266 (260–266) 10,964 (9,425–11,241) 11,312 (11,007–12,124)
MySQL 86 (82–89) 1,046 (378–4,581) 7,504 (7,426–7,508)
MariaDB 283 (276–284) 11,281 (8,455–11,428) 11,237 (9,021–11,657)

Database wall times varied substantially while PHP CPU stayed steadier. The ranges retain those outliers; these results are not an engine ranking.

Relay

PostgreSQL source, 30,000 jobs per sample, 1,000 warmup jobs, and three samples per configuration. The table below uses four concurrent tasks and four pool slots. Claim, publication, settlement, and worker lifecycle are timed; seeding is not. Restart checks use an in-memory cache.

Target Claim batch Jobs/s
Redis 100 8,211 (7,770–8,421)
Redis 1,000 22,387 (14,396–22,700)
Database 100 10,963 (9,450–11,153)
Database 1,000 11,754 (6,867–14,457)

The full report also includes concurrency-one and concurrency-eight runs. Those retained four pool slots, so the concurrency-eight results include pool contention rather than isolating scaling with matched capacity.

Claim design

A PostgreSQL comparison tested the current indexed eligibility deadline against a separate, unindexed reservation deadline. Five alternating pairs per workload used eight clients, eight pool slots, batches of 100, and a mixed backlog under continuous claim/settle/refill activity.

Backlog / live leases Current claim statement, median ms Reservation alternative, median ms Eligibility-index buffer hits, current / alternative
10,000 / 800 2.086 2.505 158 / 657
13,800 / 8,000 2.066 4.948 166 / 2,219
100,000 / 800 1.857 2.309 158 / 642
100,000 / 8,000 1.732 4.581 158 / 2,109

The alternative scanned live leases and showed no consistent statement-WAL saving. The production design is unchanged, and the temporary comparison code is removed. An earlier oversubscribed run hit checkout timeouts; the report retains that failure and the follow-up investigation rather than hiding it or changing timeout settings. No connection-lifetime defect was confirmed.

The retained harness and full results, including commands, ranges, runtime settings, resource checks, and diagnostic limitations, are in tests/Benchmarks/QueueOutbox/.

Verification

Local verification covered formatting, source and type-fixture static analysis, the framework tests, Testbench package contracts, and focused checks after corrections. Live database tests cover PostgreSQL, MySQL, MariaDB, and SQLite; Redis integration tests exercise actual publication and worker delivery.

Coverage includes transaction ownership and rollback, batch atomicity, producer propagation, partial and uncertain publication, lease expiry and stale settlement, cancellation, observer failures, operator commands, and tiny-pool resource lifetimes. Benchmark runs check completed counts and released connections, and clean up their own tables and queue keys. No timing thresholds were added to CI.

View guided diff Turn on auto-fix

Note

Add an opt-in transactional queue outbox with relay and queue-driver integration

  • Adds a transactional outbox for queued jobs. Jobs opt in via ShouldQueueViaOutbox, viaOutbox()/withoutOutbox()/onOutboxDatabase(), or a per-connection outbox flag; staged rows commit and roll back with the owning database transaction. Covers jobs, batches, listeners, mailables, notifications, and broadcasts.
  • Adds a queue:relay worker with bounded concurrency, pause/shutdown signals, and batch/time/memory limits. The relay claims leased rows via DatabaseOutboxRepository (PostgreSQL FOR UPDATE SKIP LOCKED path included), publishes through each queue driver, and settles per group.
  • Adds queue:outbox, queue:outbox-monitor, queue:outbox-forget, queue:outbox-retry, and queue:outbox-table commands, plus a migration stub and queue/OpenTelemetry configuration for outbox databases, table name, retry interval, and delivery metrics.
  • Integrates staging and publication into the base Queue class and the database, Redis, SQS, Beanstalkd, sync, failover, null, and pool-proxy drivers. Unique and debounce dispatch locks roll back with the transaction; release-context wrappers restore the acquisition context.
  • Changes transaction-manager keys in ManagesTransactions, Connection::resetTransactionState, DatabaseTransactionsManager, RequestContextSynchronizer, and Pennant tracking to the read/write-qualified connection name, so base, read, and write sessions keep independent transaction records.
  • Risk: OutboxDispatcher::databaseFor throws LogicException for ambiguous active transactions, read-role owners, or databases not configured for outbox draining; check ownership resolution in OutboxDispatcher.php. RedisQueue::pushRaw no longer recovers a job from coroutine context (removed setLastPushed/LAST_PUSHED_CONTEXT_KEY); custom Horizon RedisQueue subclasses relying on push() invoking a pushRaw() override must switch to payload-preparation hooks per porting-from-laravel.md. Extensions comparing transaction records by base connection name must use the read/write-qualified name.

Macroscope summarized 2f11e2e.

Reconcile the outbox design with the current queue, database, coroutine, Horizon and telemetry implementations while preserving opt-in queue APIs and the unchanged ordinary publication path.

Specify exact transaction-session ownership separately from relay source coverage, writer-owned staging, and post-insert rollback snapshots. Add framework prerequisites for context-safe dispatch-lock release, SQS bulk lifecycle cleanup and single-pass Horizon preparation. Require owner approval before changing role-qualified transaction-record compatibility.

Use bigint epoch deadlines and available_at lease expiry, bounded retry-bucket updates, existing Redis bulk Lua publication and normal queue forwarding. Keep concurrent relay resource scopes and operational commands while removing the configuration holder, redundant validators and speculative diagnostics.

Refresh schema, integration boundaries, documentation, test coverage and benchmark requirements. Keep private integrations outside the public plan and avoid duplicate tests or fixed optimizer-plan assertions.

Validation: source-backed design review and full plan self-review completed; staged whitespace checks passed. Documentation only; no implementation tests or benchmarks run.
Choose rerun scope from the correction's impact rather than repeating the original broad command. Finish outstanding verification and reserve another full test-suite run for fixes with framework-wide impact.
Use the resolved base/read/write connection identity for transaction lifecycle records and connection-bound callbacks. Independent role sessions must not consume each other’s commit or rollback work, while shared in-memory SQLite aliases retain their single owner.

Align Foundation wrapping transactions, request context synchronization and Pennant invalidation with exact ownership. Add role-session and rollback regressions and document the extension-facing identity convention.
Capture release context before unique or debounce lock acquisition and retain the composed wrapper with live ownership records and independent rollback snapshots. Release through the acquisition context while restoring the caller’s context afterwards.

Keep registration boot-scoped, avoid closure allocation when no wrapper is installed, and clear registrations with existing framework test cleanup. Cover exact-owner release, composition, capture failures and delayed coroutine cleanup without introducing a separate lock registry.
Add the ShouldQueueViaOutbox marker and fluent viaOutbox, withoutOutbox and onOutboxDatabase options to the existing job and pending-dispatch surfaces. Keep nullable state uninitialized until selected so ordinary serialized jobs gain no fields.

Apply batch defaults without overwriting initialized job choices, including jobs added later and first jobs in batched chains. Propagate effective selection through queued listeners, mail, notifications and broadcasts without introducing producer-specific APIs.

Preserve explicit false over markers and null fallthrough to markers. Skip uninitialized broadcast properties while retaining initialized nulls and existing payload formatting. Cover public APIs, batch precedence and wrapper propagation.
Introduce immutable stored-message and batch-result values plus the optional PublishesOutboxMessages capability. Keep accepted, deferred and failed outcomes separate so confirmed publications are not retried when later work fails.

Provide one sequential fallback for non-batching transports and pooled adapters. Defer future messages without publishing, retain accepted prefixes on ordinary failure, and propagate coroutine cancellation unchanged. Preserve the existing Queue contract and pushRaw API.
Integrate opt-in staging into the existing single and bulk queue lifecycle. Resolve exact transaction owners before writing, reject ambiguous or read-role ownership, preserve finalized payload bytes and UUIDs, and store owner groups with binding-safe atomic inserts. Reuse direct database and local-driver fast paths without outbox storage access.

Add the outbox schema, migration generator, lazy configuration and repository operations. Use atomic PostgreSQL claims, short locking-select transactions where required, conditional claims elsewhere, lease-token settlement and bounded retry operations. Read monitoring counts and oldest eligibility in one aggregate query.

Publish Redis batches with confirmed Lua counts, database batches through shared binding-aware insertion, and SQS batches with protocol size limits and explicit partial results. Preserve queue-default versus requested delays, FIFO options and claim-specific overflow ownership. Pooled publication borrows once per batch; generic lifecycle events retain their finalized payload correlation.

Accept confirmed dispatch locks before terminal observers and retain independent rollback snapshots for transactional storage. Keep failover forwarding job-scoped, satisfy explicit-owner callbacks without reentering unrelated transactions, and preserve independent dispatches from sync handlers. Inline after-commit work retains caller ownership and avoids a second pooled borrow.

Cover staging precedence, owner selection, atomic bulk rollback, driver fast paths, partial acceptance, cancellation, payload identity, failover regressions and schema generation. This checkpoint supplies staging and transport foundations; relay orchestration, operator commands and end-to-end runtime coverage remain in the active implementation plan.
Classify staged jobs while the original object is available, then add publication timing only when the stored payload reaches Redis. Preserve Horizon identifiers, stored tags, silencing and retry metadata without redispatching the command.

Use the shared preparation and post-acceptance hooks for normal pushes and batched outbox publication. Keep generic lifecycle payloads stable for tracing, accept confirmed jobs before observers, and preserve confirmed batches if a later notification fails.

Encode combined classification and timing once, remove the obsolete last-pushed context handoff, and document the approved protected-hook routing for Horizon subclasses. Extend direct, delayed, raw-retry and batch lifecycle coverage.
Return immediately when the requested tag list is empty instead of loading the monitored tag set from Redis. Keep normal tag intersection behavior unchanged and cover both empty and populated requests.
Document exact resolved transaction identities for integrations and Horizon’s shared protected publication hooks. Keep additive outbox APIs out of the Laravel compatibility catalogue.

Update the active implementation plan to the verified staging and transport design, including failover forwarding scope, inline lock ownership, finalized lifecycle payloads, atomic claim boundaries and single-query monitoring. Mark the implemented foundations clearly while retaining relay, operations, telemetry, live-engine coverage, canonical documentation and performance verification as unfinished work.
…o stores

Retain the configured store name when a custom creator returns an unnamed repository, without overriding an explicit identity or naming ad hoc repositories. Carry the underlying name through memoization while preserving event suppression.

This lets unique-job cleanup resolve the owning cache store when queued model restoration fails. Cover custom, memoized and memo-tagged repositories, explicit-name preservation, duplicate event suppression and the missing-model cleanup path.
Separate claims, connection-group publication, settlement and observers into fresh child coroutines. Join cleanup before advancing or propagating cancellation so blocked broker operations never retain a source database connection.

Validate accepted and deferred row IDs without imposing array-key shape, settle only confirmed outcomes, and report publication and settlement failures independently. Observer failures cannot republish settled rows or throttle healthy delivery; reporter failures propagate.

Add guarded batch delivery events and focused result, partial-failure, cancellation and observer tests. Exercise real single-slot database and queue pools to prove connection availability during blocked publication and cleanup after cancellation.
…down

Admit serial round-robin claims into bounded concurrent publication tasks, cap each claim against the remaining job allowance, and cool down failed sources without delaying healthy databases.

Support once, stop-when-empty, source pinning, restart signals, pause and resume, memory and time limits. Keep invocation state local, isolate restart-cache reads and exception reporting, and join owned tasks before cancellation leaves the worker.

Hold a failed delivery task through its retry sleep to protect unavailable transports from a fresh-row backlog. Test exact admission limits, concurrency, source fairness, lifecycle controls, resource cleanup and error propagation.
Register queue:relay and bounded list, monitor, retry and forget commands. Expose readable UTC dates and compact errors in tables while preserving raw diagnostic values in JSON, and report mutually exclusive backlog states with optional threshold alerts.

Resolve ambiguous job UUIDs explicitly, retain atomic live-lease guards and preserve retry history. Confirm destructive operations before querying; force skips only confirmation, never an active publication lease.

Forget deletes the row before exact-owner unique-lock cleanup, reads only serialized scalar ownership metadata and reports post-delete cleanup failures without restoring discarded work. Cover command options, scripting, listing bounds, monitoring, source ambiguity and replacement-lock safety.
Record confirmed delivered messages, batch publication duration and oldest eligible message age from guarded outbox events. Register listeners only when their metrics are enabled, without querying backlog, resolving queues, decoding payloads or starting another producer lifecycle.

Use error-tagged duration observations for failed-batch counts instead of a redundant counter. Keep latency attributes independent of exception type and align its buckets with whole-second values to limit series and recording overhead.

Expose matching published and normalized defaults, with tests for partial acceptance, settlement failure, disabled listeners and bounded metric series.
Specify coordinator-owned admission, resource-isolated delivery phases, cancellation cleanup, observer reporting and value-based result validation. Clarify cache identity and exact unique-lock cleanup, scriptable confirmation and disjoint monitoring states.

Document the reduced batch telemetry surface and remaining OpenTelemetry reference updates. Record completed runtime and operator scope, with live-engine coverage, remaining API integration, documentation, approved performance checks and final verification still required.
Cover inherited Beanstalkd and custom-driver bulk staging while preserving direct partitions. Require multiple settlement rows per outcome to complete in three statements, and verify bounded retry-all updates exclude later arrivals.

Exercise custom non-PDO connections through the driver-neutral repository contract, retaining real SQL behavior without requiring PDO capability methods.
Share live-engine coverage for bounded claim statements, original eligibility, independent write-role transaction ownership, competing claims and expired-lease settlement protection.

Verify skip-locked progress past an uncommitted row and force competing fallback candidates on file-backed SQLite. Keep engine entry points in the existing CI discovery layout and migrate only the shipped outbox schema.
Exercise queued listeners, mailables, notifications and broadcasts through their existing public APIs and wrapper jobs, without broker access during staging.

Run committed outbox payloads through the real Redis relay and worker, covering encrypted model restoration, stable payloads, unique-lock ownership, debounce, delays, closures, batches and nested chains. Reuse existing event-deferral coverage instead of duplicating it.
Add one canonical guide for selection, transactional ownership, driver behavior, delays, relay deployment, concurrency, recovery, operator commands and custom driver publication.

Link the guide from queue and producer documentation. Explain whole-event after-commit timing, local-driver limitations, at-least-once processing and idempotency without duplicating the operating reference across pages.
Document queue-confirmed delivery counts, publication duration and eligible-age observations at connection-batch boundaries. Explain failure counting through duration attributes and the absence of backlog polling or a second producer lifecycle.

Remove stale guidance that ordinary later batch-finalizer failures leave earlier producer states unfinished now that guarded queue lifecycles terminate outstanding work.
Provide developer-only direct dispatch, transactional staging, real relay and concurrent claim-churn cases using isolated tables and queue names. Verify completed counts, released pool connections and resource cleanup on every run.

Capture executing engine plans outside timing and compare PostgreSQL availability-index claims with a narrowly scoped reservation fixture. Make seeded lease counts explicit and label workload WAL, in-memory restart checks, CPU and heap measurements accurately.

Keep baseline dispatch independent of feature-only fixtures, document fresh-process comparison methodology, and retain raw reports outside Git. Functional smoke checks are complete; performance measurements require an idle-machine window.
…cope

Mark implementation, coverage, documentation and the functionally verified benchmark harness complete. Retain the explicitly deferred idle-machine measurements without claiming performance results.

Specify focused benchmark cases and realistic lease populations, preserve automated query and resource-lifetime guarantees, and align verification instructions with scoped reruns after localized corrections.
Warn that redeclaring outbox properties with defaults conflicts with the declarations supplied by Queueable and Batchable. Direct users to the marker for class defaults and fluent methods for individual objects.

Limit public property-default guidance to classes without those trait-provided properties, including queued listeners whose defaults are inspected without construction.
Require the marker for queued listeners using Queueable or Batchable. Both traits provide uninitialized outbox properties, so redeclaring those properties with defaults is a PHP conflict rather than an alternative convention.

Align the active implementation plan with the canonical guide. No runtime behavior changes.
Remove the temporary PostgreSQL reservation-column fixture and its CLI selector after comparing it with the production claim path. Keep the retained harness focused on production dispatch, staging, relay and claim workloads.

The comparison increased live-lease scan work without a consistent statement-WAL saving, so no production schema or claim changes are needed. Remove the unused schema import, option validation and alternate backlog setup.

Verified targeted formatting and PHP syntax, all four tiny harness cases, both relay targets, PostgreSQL executing plans, exact counts and released resources. Rechecked SQLite claims after the final fixture cleanup.
Record paired ordinary-dispatch measurements, transactional staging, real relay throughput and concurrent claim churn across PostgreSQL, MySQL and MariaDB. Include runtime settings, sample sizes, medians and ranges, CPU and heap observations, and the PostgreSQL claim-plan and WAL comparison.

Keep the failed oversubscribed sample and its investigation visible without claiming an unproven cause. Explain that claim latency includes checkout and that the relay concurrency-eight samples used four pool slots, rather than presenting them as matched-capacity scaling results.

Update reproducible examples to match pool capacity to concurrency, link the measured report, and remove obsolete reservation-selector instructions. Preserve raw evidence outside Git; temporary benchmark resources have been removed. Production behavior remains unchanged.
Mark implementation and performance measurements complete and link the retained benchmark report. Replace deferred-measurement and temporary-comparison instructions with the measured rationale for retaining indexed eligibility and the production claim path.

Clarify heap versus allocation measurements and the checkout component of claim latency. Record the requirement to include performance tables with workload, concurrency and pool settings when opening the pull request.
@coderabbitai

coderabbitai Bot commented Oct 23, 2026 •

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@binaryfire

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Oct 23, 2026 •

Copy link
Copy Markdown
⚠️ Action not completed

Review skipped: 132 files exceed the limit of 100.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@binaryfire

Copy link
Copy Markdown
Member Author

@cubic-dev-ai review

@cubic-dev-ai

cubic-dev-ai Bot commented Oct 23, 2026

Copy link
Copy Markdown

@cubic-dev-ai review

@binaryfire cubic can't start this review because your workspace has reached its free monthly review limit. cubic has reviewed 123,074 of the 120,000 allowed lines of code this month. Reviews resume on 10 November 2026 (in 19 days). Paid plans include much higher monthly review limits. Upgrade now to resume reviews.

To help optimise your usage, you can tune cubic to get the most out of your usage limits:

Learn more →

@qodo-free-for-open-source-projects

Copy link
Copy Markdown

PR Summary by Qodo

Add an opt-in transactional queue outbox and relay

✨ Enhancement 🐞 Bug fix 🧪 Tests 📝 Documentation ⚙️ Configuration changes 🕐 40+ Minutes

Grey Divider

AI Description

• Stage selected jobs atomically with business writes, then relay them through existing queue
 drivers.
• Preserve ordinary dispatch behavior while adding bounded claims, retries, operator commands, and
 delivery metrics.
• Fix transaction ownership and dispatch-lock cleanup; document guarantees and cover cross-driver
 behavior.
Diagram

graph TD
  P["Job producers"] --> Q["Queue dispatch"] --> S["Outbox staging"] --> D[("Outbox tables")] --> R["Relay worker"] --> T["Queue drivers"] --> W["Existing workers"]
  Q -->|"Not selected"| T
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Database change-data-capture relay
  • ➕ Could avoid periodic eligibility polling and integrate with an existing CDC platform.
  • ➖ Requires database-specific replication infrastructure and more complex handling of delays, retries, and operational recovery.
2. Use after-commit publication only
  • ➕ Requires no outbox table or separately supervised relay.
  • ➖ Does not close the crash window between commit and broker acceptance.

Recommendation: Keep the opt-in SQL outbox and reuse the existing queue drivers. It closes the stated durability gap without imposing CDC infrastructure or changing normal dispatch; document and review its at-least-once semantics, lease sizing, and multi-database ownership carefully.

Files changed (132) +10445 / -327

Enhancement (42) +3022 / -256
BroadcastEvent.phpPropagate broadcast outbox selection +16/-0

Propagate broadcast outbox selection

• Copies outbox options or the marker interface to the queued broadcast wrapper and skips uninitialized public event properties.

src/broadcasting/src/BroadcastEvent.php

Batch.phpApply outbox defaults when adding batch jobs +15/-3

Apply outbox defaults when adding batch jobs

• Applies stored batch defaults to new jobs without replacing explicit selections, including first jobs in chains.

src/bus/src/Batch.php

Batchable.phpExpose outbox options to batchable jobs +2/-0

Expose outbox options to batchable jobs

• Includes the shared outbox-selection trait for jobs using Batchable without Queueable.

src/bus/src/Batchable.php

InteractsWithQueueOutbox.phpAdd fluent outbox selection methods +63/-0

Add fluent outbox selection methods

• Introduces viaOutbox, withoutOutbox, and onOutboxDatabase while leaving unused properties uninitialized.

src/bus/src/InteractsWithQueueOutbox.php

PendingBatch.phpStore outbox defaults on pending batches +40/-0

Store outbox defaults on pending batches

• Adds fluent batch-level selection and database-owner defaults for current and subsequently added jobs.

src/bus/src/PendingBatch.php

Queueable.phpExpose outbox selection on queueable jobs +2/-0

Expose outbox selection on queueable jobs

• Composes the shared fluent outbox trait into Queueable.

src/bus/src/Queueable.php

PublishesOutboxMessages.phpDefine batched outbox publication contract +19/-0

Define batched outbox publication contract

• Introduces the driver contract for returning confirmed, deferred, and failed publication outcomes.

src/contracts/src/Queue/PublishesOutboxMessages.php

ShouldQueueViaOutbox.phpAdd job-level outbox marker +9/-0

Add job-level outbox marker

• Defines a ShouldQueue-derived interface for class-level outbox selection.

src/contracts/src/Queue/ShouldQueueViaOutbox.php

Dispatcher.phpForward listener outbox options +11/-0

Forward listener outbox options

• Copies listener marker and explicit outbox options onto the existing queued-listener job.

src/events/src/Dispatcher.php

PendingDispatch.phpExpose outbox choices on pending dispatches +30/-0

Expose outbox choices on pending dispatches

• Forwards fluent outbox and owning-database choices to the underlying job.

src/foundation/src/Bus/PendingDispatch.php

RedisQueue.phpShare Redis storage with Horizon publication +35/-58

Share Redis storage with Horizon publication

• Separates prepared-payload storage from public pushRaw, preserving lifecycle correlation and outbox publication metadata.

src/horizon/src/RedisQueue.php

SendQueuedMailable.phpCarry mailable outbox options into its job +11/-0

Carry mailable outbox options into its job

• Propagates a mailable's marker or explicit selection and database owner to the queued wrapper.

src/mail/src/SendQueuedMailable.php

SendQueuedNotifications.phpCarry notification outbox options into its job +11/-0

Carry notification outbox options into its job

• Propagates outbox selection and owner to the queued notification wrapper.

src/notifications/src/SendQueuedNotifications.php

QueueInstrumentation.phpMeasure outbox delivery at batch boundaries +77/-0

Measure outbox delivery at batch boundaries

• Observes delivered and failed batch events without polling storage or creating another producer span.

src/opentelemetry/src/Instrumentation/QueueInstrumentation.php

BeanstalkdQueue.phpSupport Beanstalkd outbox destinations +16/-0

Support Beanstalkd outbox destinations

• Captures the logical queue destination and preserves delayed publication through the existing driver.

src/queue/src/BeanstalkdQueue.php

StagesOutboxJobs.phpStage selected single and bulk jobs +271/-0

Stage selected single and bulk jobs

• Selects outbox delivery, finalizes payloads, groups inserts by transaction owner, and preserves per-job lifecycle and lock acceptance.

src/queue/src/Concerns/StagesOutboxJobs.php

FindsOutboxMessages.phpResolve outbox UUIDs without guessing +34/-0

Resolve outbox UUIDs without guessing

• Provides shared lookup for operator commands and rejects UUIDs found on multiple sources without an explicit database.

src/queue/src/Console/Concerns/FindsOutboxMessages.php

ForgetOutboxCommand.phpAdd deliberate outbox removal command +117/-0

Add deliberate outbox removal command

• Requires confirmation, refuses live leases, and releases only the recorded unique-lock owner after deletion.

src/queue/src/Console/ForgetOutboxCommand.php

ListOutboxCommand.phpAdd bounded pending-message inspection +90/-0

Add bounded pending-message inspection

• Lists outbox rows without exposing payload bodies or ownership tokens.

src/queue/src/Console/ListOutboxCommand.php

MonitorOutboxCommand.phpAdd backlog monitoring command +68/-0

Add backlog monitoring command

• Reports outbox states and eligible age, with optional thresholds and alert events.

src/queue/src/Console/MonitorOutboxCommand.php

OutboxTableCommand.phpGenerate an application outbox migration +45/-0

Generate an application outbox migration

• Adds a migration generator using the configured table name and an alias command.

src/queue/src/Console/OutboxTableCommand.php

RelayCommand.phpExpose the supervised relay CLI +61/-0

Expose the supervised relay CLI

• Accepts source, batch, concurrency, runtime, shutdown, and one-shot options for the outbox worker.

src/queue/src/Console/RelayCommand.php

RetryOutboxCommand.phpAdd controlled outbox retry command +74/-0

Add controlled outbox retry command

• Allows immediate retry of eligible attempted rows, including bounded retry across a selected source.

src/queue/src/Console/RetryOutboxCommand.php

DatabaseQueue.phpIntegrate atomic database dispatch and publication +147/-30

Integrate atomic database dispatch and publication

• Avoids redundant staging on an already-owning database queue, snapshots rollback cleanup, and publishes relayed rows in set-based inserts.

src/queue/src/DatabaseQueue.php

OutboxBatchDelivered.phpDescribe successful delivery batches +20/-0

Describe successful delivery batches

• Adds a batch event carrying source, destination, accepted count, duration, and eligibility age.

src/queue/src/Events/OutboxBatchDelivered.php

OutboxBatchDeliveryFailed.phpDescribe failed delivery batches +24/-0

Describe failed delivery batches

• Adds a batch event with confirmed and failed counts, timings, and the delivery error.

src/queue/src/Events/OutboxBatchDeliveryFailed.php

OutboxBusy.phpExpose outbox backlog alerts +19/-0

Expose outbox backlog alerts

• Adds the monitoring event emitted when configured backlog thresholds are reached.

src/queue/src/Events/OutboxBusy.php

FailoverQueue.phpPreserve failover semantics for selected jobs +47/-3

Preserve failover semantics for selected jobs

• Defers the complete failover attempt when needed and prevents child connections from independently restaging the forwarded job.

src/queue/src/FailoverQueue.php

NullQueue.phpKeep null queues out of outbox storage +8/-0

Keep null queues out of outbox storage

• Ensures selected jobs remain discarded without creating durable rows.

src/queue/src/NullQueue.php

PublishesOutboxMessagesSequentially.phpProvide raw-payload publication fallback +51/-0

Provide raw-payload publication fallback

• Uses existing pushRaw methods for due messages while reporting confirmed prefixes and deferring future messages.

src/queue/src/Outbox/Concerns/PublishesOutboxMessagesSequentially.php

DatabaseOutboxRepository.phpImplement outbox claims and settlement +403/-0

Implement outbox claims and settlement

• Adds engine-aware bounded claims, token-guarded settlement, jittered retries, and operator queries against configured writer connections.

src/queue/src/Outbox/DatabaseOutboxRepository.php

OutboxBatchResult.phpRepresent publication outcomes +23/-0

Represent publication outcomes

• Carries accepted IDs, deferred IDs, and an optional error for safe settlement.

src/queue/src/Outbox/OutboxBatchResult.php

OutboxDispatcher.phpResolve the transaction owner for staging +87/-0

Resolve the transaction owner for staging

• Rejects ambiguous or uncovered owners, inserts on the owning session, and registers lock cleanup on rollback.

src/queue/src/Outbox/OutboxDispatcher.php

OutboxMessage.phpRepresent claimed outbox messages +26/-0

Represent claimed outbox messages

• Provides an immutable publication value containing payload, destination, timing, and claim ownership.

src/queue/src/Outbox/OutboxMessage.php

OutboxRelay.phpSeparate claim, publish, and settle lifetimes +228/-0

Separate claim, publish, and settle lifetimes

• Publishes grouped messages through existing drivers, validates partial results, and reports outcomes after releasing borrowed resources.

src/queue/src/Outbox/OutboxRelay.php

OutboxWorker.phpCoordinate bounded concurrent relay work +218/-0

Coordinate bounded concurrent relay work

• Visits sources, admits limited batches, throttles failures, and handles restart signals, cancellation, and graceful shutdown.

src/queue/src/Outbox/OutboxWorker.php

OutboxWorkerOptions.phpValidate relay execution limits +36/-0

Validate relay execution limits

• Defines typed batch, concurrency, sleep, memory, runtime, and one-shot worker options.

src/queue/src/Outbox/OutboxWorkerOptions.php

Queue.phpWire opt-in staging into queue dispatch +128/-11

Wire opt-in staging into queue dispatch

• Integrates selection and bulk staging while separating finalized lifecycle payloads from stored transport payloads and protecting accepted locks from observer failures.

src/queue/src/Queue.php

QueuePoolProxy.phpPublish a batch with one pooled lease +21/-3

Publish a batch with one pooled lease

• Delegates native batched publication or a sequential fallback while borrowing the transport only once.

src/queue/src/QueuePoolProxy.php

RedisQueue.phpBatch Redis outbox publication +107/-36

Batch Redis outbox publication

• Groups stored payloads by destination, reuses the existing bulk Lua script, and retains confirmed acceptance across later failures.

src/queue/src/RedisQueue.php

SqsQueue.phpBatch SQS publication with partial recovery +273/-111

Batch SQS publication with partial recovery

• Captures FIFO options, observes SQS batch limits and delays, and separates confirmed, rejected, and uncertain outcomes with attempt-owned overflow cleanup.

src/queue/src/SqsQueue.php

SyncQueue.phpKeep local queues on an after-commit fallback +39/-1

Keep local queues on an after-commit fallback

• Preserves local execution instead of creating outbox rows and isolates forwarding context from jobs dispatched by handlers.

src/queue/src/SyncQueue.php

Bug fix (13) +180 / -28
DebounceLock.phpCapture debounce-lock release context +2/-1

Capture debounce-lock release context

• Records the acquisition context so deferred lock cleanup runs under the correct context.

src/bus/src/DebounceLock.php

DispatchLockContext.phpMake dispatch-lock cleanup context-aware +117/-5

Make dispatch-lock cleanup context-aware

• Adds registration, snapshots, and restoration of the context needed to release dispatch-owned locks after acceptance or rollback.

src/bus/src/DispatchLockContext.php

UniqueLock.phpCapture unique-lock release context +2/-1

Capture unique-lock release context

• Associates acquired unique locks with context-aware deferred cleanup.

src/bus/src/UniqueLock.php

CacheManager.phpPreserve cache repository store identity +9/-2

Preserve cache repository store identity

• Names memoized and custom repositories so unique-lock cleanup can resolve their original store.

src/cache/src/CacheManager.php

Repository.phpAllow repository store names to be retained +15/-0

Allow repository store names to be retained

• Adds store-name assignment for cache repositories created through custom drivers.

src/cache/src/Repository.php

ManagesTransactions.phpIdentify transactions by resolved session +7/-7

Identify transactions by resolved session

• Uses read/write-qualified connection names for transaction records and commit or rollback callbacks.

src/database/src/Concerns/ManagesTransactions.php

Connection.phpAlign connection cleanup with session identity +1/-1

Align connection cleanup with session identity

• Updates connection transaction bookkeeping to use the resolved session name.

src/database/src/Connection.php

DatabaseTransactionsManager.phpScope transaction records to exact owners +2/-2

Scope transaction records to exact owners

• Adjusts transaction-manager ownership matching for separate connection sessions.

src/database/src/DatabaseTransactionsManager.php

DatabaseTransactionsManager.phpMatch test transactions to resolved sessions +1/-1

Match test transactions to resolved sessions

• Updates test transaction tracking to distinguish role-qualified owners.

src/foundation/src/Testing/DatabaseTransactionsManager.php

RequestContextSynchronizer.phpSynchronize exact transaction owners +1/-1

Synchronize exact transaction owners

• Keeps request-context transaction records aligned with resolved database sessions.

src/foundation/src/Testing/RequestContextSynchronizer.php

JobPayload.phpSeparate classification from publication stamping +17/-4

Separate classification from publication stamping

• Prepares Horizon classification once while allowing pushed-at metadata to be added when the payload reaches Redis.

src/horizon/src/JobPayload.php

RedisTagRepository.phpAvoid unnecessary Horizon tag queries +4/-0

Avoid unnecessary Horizon tag queries

• Skips monitored-tag lookup when a job has no tags.

src/horizon/src/Repositories/RedisTagRepository.php

Decorator.phpTrack Pennant rollbacks by exact session +2/-3

Track Pennant rollbacks by exact session

• Matches state rollback tracking to the resolved database connection rather than its base name.

src/pennant/src/Drivers/Decorator.php

Refactor (1) +12 / -2
InsertsDatabaseRows.phpShare binding-aware database insertion +12/-2

Share binding-aware database insertion

• Derives insertion chunk sizes from the connection, with a fallback for non-PDO drivers.

src/queue/src/Concerns/InsertsDatabaseRows.php

Documentation (15) +1170 / -2
AGENTS.mdClarify repository guidance +1/-1

Clarify repository guidance

• Adjusts the contributor instructions accompanying the outbox work.

AGENTS.md

2026-10-22-1522-hypervel-transactional-queue-outbox-codex.mdRecord the transactional outbox design +595/-0

Record the transactional outbox design

• Adds the implementation plan covering transaction ownership, staging, relay recovery, integrations, tests, and benchmarks.

docs/plans/2026-10-22-1522-hypervel-transactional-queue-outbox-codex.md

README.mdLink database users to outbox guidance +1/-0

Link database users to outbox guidance

• Adds a reference to transactional queue outbox documentation.

src/database/README.md

broadcasting.mdDocument queued broadcast outbox selection +14/-0

Document queued broadcast outbox selection

• Explains how broadcast events select transactional staging.

src/docs/broadcasting.md

documentation.mdAdd outbox to documentation navigation +1/-0

Add outbox to documentation navigation

• Links the transactional outbox guide from the documentation index.

src/docs/documentation.md

events.mdDocument queued listener outbox behavior +13/-0

Document queued listener outbox behavior

• Explains selection on listeners and the implications of deferring an entire event until commit.

src/docs/events.md

mail.mdDocument queued mailable staging +10/-0

Document queued mailable staging

• Shows how mailables select outbox delivery through their queued wrapper.

src/docs/mail.md

notifications.mdDocument queued notification staging +8/-0

Document queued notification staging

• Describes selecting the outbox for queued notifications.

src/docs/notifications.md

opentelemetry.mdDocument relay delivery metrics +10/-1

Document relay delivery metrics

• Adds the outbox delivery instruments and distinguishes relay telemetry from producer spans.

src/docs/opentelemetry.md

porting-from-laravel.mdExplain outbox portability considerations +4/-0

Explain outbox portability considerations

• Adds guidance about the outbox APIs when porting queue usage.

src/docs/porting-from-laravel.md

queue-outbox.mdAdd operational transactional outbox guide +319/-0

Add operational transactional outbox guide

• Documents setup, selection, transaction ownership, driver compatibility, relay operations, recovery, and at-least-once guarantees.

src/docs/queue-outbox.md

queues.mdLink queue dispatch guidance to the outbox +18/-0

Link queue dispatch guidance to the outbox

• Introduces transactional outbox usage alongside ordinary queue and after-commit dispatch guidance.

src/docs/queues.md

README.mdLink Horizon users to outbox guidance +1/-0

Link Horizon users to outbox guidance

• Points Horizon users to the optional queue outbox integration.

src/horizon/README.md

README.mdDocument benchmark execution +67/-0

Document benchmark execution

• Explains workload setup, measurement commands, and interpretation limits.

tests/Benchmarks/QueueOutbox/README.md

results.mdRecord measured performance and diagnostics +108/-0

Record measured performance and diagnostics

• Reports paired dispatch results, database staging, relay throughput, claim-design comparison, and pool-contention limitations.

tests/Benchmarks/QueueOutbox/results.md

Other (61) +6061 / -39
queue.phpPublish disabled-by-default outbox settings +32/-0

Publish disabled-by-default outbox settings

• Adds per-connection selection flags and configured outbox sources, table, and lease timeout.

src/foundation/config/queue.php

opentelemetry.phpExpose outbox metric configuration +3/-0

Expose outbox metric configuration

• Adds configuration entries for relay delivery count, duration, and latency metrics.

src/opentelemetry/config/opentelemetry.php

ConfigurationNormalizer.phpNormalize outbox instrument settings +3/-0

Normalize outbox instrument settings

• Includes the new outbox metrics in telemetry configuration normalization.

src/opentelemetry/src/Support/ConfigurationNormalizer.php

SqsConnector.phpPass SQS outbox configuration through connector +1/-0

Pass SQS outbox configuration through connector

• Makes the connection's outbox selection available to the constructed SQS queue.

src/queue/src/Connectors/SqsConnector.php

outbox.stubDefine the generated outbox schema +41/-0

Define the generated outbox schema

• Provides a configurable migration with payload, destination, attempts, deadlines, lease token, and eligibility index.

src/queue/src/Console/stubs/outbox.stub

QueueServiceProvider.phpRegister outbox services and commands +31/-0

Register outbox services and commands

• Registers the repository lazily and exposes relay, migration, inspection, monitoring, retry, and forget commands.

src/queue/src/QueueServiceProvider.php

0001_01_01_000009_testbench_create_job_outbox_table.phpInstall the Testbench outbox table +40/-0

Install the Testbench outbox table

• Adds the outbox schema and eligibility index to new Testbench applications.

src/testbench/hypervel/migrations/0001_01_01_000009_testbench_create_job_outbox_table.php

OutboxBenchmarkJob.phpProvide a benchmark job fixture +27/-0

Provide a benchmark job fixture

• Defines the representative job used to measure ordinary dispatch and outbox publication.

tests/Benchmarks/QueueOutbox/Fixtures/OutboxBenchmarkJob.php

OutboxWorkload.phpBuild repeatable outbox workloads +369/-0

Build repeatable outbox workloads

• Seeds and validates staging, relay, and claim workloads while checking completed counts and resource cleanup.

tests/Benchmarks/QueueOutbox/Fixtures/OutboxWorkload.php

benchmark.phpAdd an executable outbox benchmark harness +276/-0

Add an executable outbox benchmark harness

• Runs ordinary dispatch, transactional staging, relay, and claim measurements without CI timing thresholds.

tests/Benchmarks/QueueOutbox/benchmark.php

BroadcastEventTest.phpTest broadcast payload property initialization +22/-0

Test broadcast payload property initialization

• Verifies that uninitialized public properties are omitted safely.

tests/Broadcasting/BroadcastEventTest.php

BusBatchTest.phpTest persisted batch outbox defaults +46/-0

Test persisted batch outbox defaults

• Checks defaults on newly added jobs and confirms later chain jobs do not inherit selection.

tests/Bus/BusBatchTest.php

DispatchLockContextTest.phpExtend dispatch-lock metadata coverage +1/-0

Extend dispatch-lock metadata coverage

• Checks lock metadata behavior alongside the revised context tracking.

tests/Bus/DispatchLockContextTest.php

DispatchLockReleaseContextTest.phpTest context-safe lock release +248/-0

Test context-safe lock release

• Covers snapshots, timer cleanup, capture failures, restoration, and releases outside the acquiring coroutine.

tests/Bus/DispatchLockReleaseContextTest.php

OutboxOptionsTest.phpTest fluent outbox option precedence +107/-0

Test fluent outbox option precedence

• Checks pending dispatch and batch forwarding, empty-owner rejection, and unchanged serialization for unused properties.

tests/Bus/OutboxOptionsTest.php

CacheManagerTest.phpTest custom and memoized store identity +43/-0

Test custom and memoized store identity

• Verifies repository naming without overwriting explicit names or duplicating events.

tests/Cache/CacheManagerTest.php

DatabaseTransactionIdentityTest.phpTest role-qualified transaction isolation +113/-0

Test role-qualified transaction isolation

• Exercises independent callbacks, savepoint rollback, and disconnection across separate sessions.

tests/Database/DatabaseTransactionIdentityTest.php

DatabaseTransactionsManagerTest.phpTest resolved owners in test transactions +32/-0

Test resolved owners in test transactions

• Ensures role wrappers exclude only their own underlying session's transaction.

tests/Foundation/Testing/DatabaseTransactionsManagerTest.php

RefreshDatabaseTest.phpAlign refresh-database transaction assertions +2/-2

Align refresh-database transaction assertions

• Adjusts tests for exact transaction-session identity.

tests/Foundation/Testing/RefreshDatabaseTest.php

RequestContextSynchronizerTest.phpTest role-specific context synchronization +34/-0

Test role-specific context synchronization

• Ensures synchronization replaces only retained transaction records belonging to the same session.

tests/Foundation/Testing/RequestContextSynchronizerTest.php

RedisQueueTest.phpTest Horizon payload and acceptance lifecycle +152/-0

Test Horizon payload and acceptance lifecycle

• Covers payload correlation, post-push observer failures, and cancellation after acceptance.

tests/Horizon/Unit/RedisQueueTest.php

RedisTagRepositoryTest.phpTest untagged Horizon jobs +21/-0

Test untagged Horizon jobs

• Ensures untagged jobs do not query monitored tags.

tests/Horizon/Unit/RedisTagRepositoryTest.php

QueueOutboxTableCommandTest.phpTest outbox migration generation +29/-0

Test outbox migration generation

• Verifies that the new generator creates its migration.

tests/Integration/Generators/QueueOutboxTableCommandTest.php

QueueProcessingTest.phpAlign Horizon processing tests with preparation lifecycle +0/-34

Align Horizon processing tests with preparation lifecycle

• Removes an obsolete assertion tied to the former payload-preparation path.

tests/Integration/Horizon/Feature/QueueProcessingTest.php

RedisPayloadTest.phpTest single-pass Horizon metadata preparation +53/-0

Test single-pass Horizon metadata preparation

• Verifies customized metadata encoding when classification and publication are prepared separately.

tests/Integration/Horizon/Feature/RedisPayloadTest.php

OutboxClaimTest.phpRun claim contracts on MariaDB +13/-0

Run claim contracts on MariaDB

• Applies shared live-database claim and ownership tests to MariaDB.

tests/Integration/Queue/Database/MariaDb/OutboxClaimTest.php

OutboxClaimTest.phpRun claim contracts on MySQL +13/-0

Run claim contracts on MySQL

• Applies shared live-database claim and ownership tests to MySQL.

tests/Integration/Queue/Database/MySql/OutboxClaimTest.php

OutboxClaimTest.phpRun claim contracts on PostgreSQL +13/-0

Run claim contracts on PostgreSQL

• Applies shared live-database claim and ownership tests to PostgreSQL.

tests/Integration/Queue/Database/Postgres/OutboxClaimTest.php

OutboxClaimTest.phpTest SQLite claim fallback +56/-0

Test SQLite claim fallback

• Exercises atomic conditional claims and ownership behavior on SQLite.

tests/Integration/Queue/Database/Sqlite/OutboxClaimTest.php

WorkerResourceLifetimeTest.phpTest relay resource release with tiny pools +117/-0

Test relay resource release with tiny pools

• Checks publication, blocking, and cancellation without retaining one-slot database or transport leases.

tests/Integration/Queue/Database/Sqlite/WorkerResourceLifetimeTest.php

OutboxClaimTestCase.phpShare live-engine claim verification +242/-0

Share live-engine claim verification

• Tests bounded claims, exact writer ownership, competing sessions, lease expiry, and skip-locked progress.

tests/Integration/Queue/Fixtures/OutboxClaimTestCase.php

QueueConnectionTest.phpIsolate queue connection transaction tests +4/-0

Isolate queue connection transaction tests

• Adjusts existing transaction fixtures for the new outbox-aware environment.

tests/Integration/Queue/QueueConnectionTest.php

OutboxDeliveryTest.phpTest live Redis relay and worker delivery +199/-0

Test live Redis relay and worker delivery

• Exercises committed encrypted jobs, unique ownership, batches, chains, closures, debounce, and delays.

tests/Integration/Queue/Redis/OutboxDeliveryTest.php

UniqueJobTest.phpTest unique-lock cleanup across cache stores +43/-3

Test unique-lock cleanup across cache stores

• Extends missing-model cleanup coverage to alternate repository configurations.

tests/Integration/Queue/UniqueJobTest.php

QueueInstrumentationTest.phpTest outbox metrics without duplicate spans +39/-0

Test outbox metrics without duplicate spans

• Checks batch acceptance and settlement-failure recording independently of producer lifecycle instrumentation.

tests/OpenTelemetry/Instrumentation/QueueInstrumentationTest.php

PublishedConfigurationTest.phpTest published outbox metric defaults +3/-0

Test published outbox metric defaults

• Asserts that configuration exposes the new delivery instruments.

tests/OpenTelemetry/PublishedConfigurationTest.php

TransactionTest.phpTest Pennant role-specific rollback +39/-0

Test Pennant role-specific rollback

• Confirms rollback forgets values tracked by the affected session only.

tests/Pennant/TransactionTest.php

DatabaseQueueRollbackTest.phpTest accepted database-job lock cleanup +176/-0

Test accepted database-job lock cleanup

• Covers storage-owner rollback, chunked bulk, savepoints, and reacquisition after deadlock retry.

tests/Queue/DatabaseQueueRollbackTest.php

FailoverQueueTest.phpTest failover lock ownership after inline commit +53/-0

Test failover lock ownership after inline commit

• Verifies child fallback retains dispatch-lock ownership when commit callbacks trigger attempts.

tests/Queue/FailoverQueueTest.php

DatabaseOutboxPublicationTest.phpTest set-based database publication +101/-0

Test set-based database publication

• Checks destination grouping, absolute availability, and rollback when a later insert chunk fails.

tests/Queue/Outbox/DatabaseOutboxPublicationTest.php

DatabaseOutboxRepositoryTest.phpTest claims, settlement, and operator storage +287/-0

Test claims, settlement, and operator storage

• Covers expiry, stale tokens, retry buckets, bounded retry-all updates, and pending-state statistics.

tests/Queue/Outbox/DatabaseOutboxRepositoryTest.php

OutboxCommandsTest.phpTest outbox operator safeguards +254/-0

Test outbox operator safeguards

• Exercises bounded inspection, monitoring, cross-source ambiguity, lease protection, confirmation, and exact unique-lock cleanup.

tests/Queue/Outbox/OutboxCommandsTest.php

OutboxDispatchTest.phpTest opt-in staging and dispatch lifecycle +382/-0

Test opt-in staging and dispatch lifecycle

• Checks selection precedence, bulk inserts, finalized payloads, SQS timing, fast paths, and observer failures.

tests/Queue/Outbox/OutboxDispatchTest.php

OutboxDispatcherTest.phpTest transaction-owner resolution +95/-0

Test transaction-owner resolution

• Covers explicit, implicit, ambiguous, and uncovered database owners.

tests/Queue/Outbox/OutboxDispatcherTest.php

OutboxFastPathTest.phpTest non-staging driver behavior +357/-0

Test non-staging driver behavior

• Verifies sync, null, database, fake, and failover paths avoid inappropriate outbox access while retaining commit semantics.

tests/Queue/Outbox/OutboxFastPathTest.php

OutboxProducerTest.phpTest public producer API propagation +150/-0

Test public producer API propagation

• Confirms producer dispatch APIs stage their existing queued wrapper jobs.

tests/Queue/Outbox/OutboxProducerTest.php

OutboxRelayTest.phpTest relay phase isolation and outcomes +290/-0

Test relay phase isolation and outcomes

• Covers partial success, invalid results, deferred rows, settlement failures, observers, and cancellation cleanup.

tests/Queue/Outbox/OutboxRelayTest.php

OutboxTransactionTest.phpTest atomic outbox inserts and rollback +180/-0

Test atomic outbox inserts and rollback

• Verifies failed insert chunks leave no partial group and nested rollback releases only relevant accepted locks.

tests/Queue/Outbox/OutboxTransactionTest.php

OutboxWorkerTest.phpTest relay coordination and shutdown +277/-0

Test relay coordination and shutdown

• Covers round-robin admission, concurrency, source cooldown, limits, restart signals, pause/resume, and cancellation joins.

tests/Queue/Outbox/OutboxWorkerTest.php

OutboxWrapperTest.phpTest queued wrapper option propagation +167/-0

Test queued wrapper option propagation

• Checks marker and explicit options on constructed producer and queued-listener wrappers.

tests/Queue/Outbox/OutboxWrapperTest.php

PooledOutboxPublicationTest.phpTest pooled publication lifetimes +128/-0

Test pooled publication lifetimes

• Checks native and sequential publication with one lease and confirms transport release after cancellation.

tests/Queue/Outbox/PooledOutboxPublicationTest.php

RedisOutboxPublicationTest.phpTest batched Redis and Horizon delivery +158/-0

Test batched Redis and Horizon delivery

• Checks destination grouping, unchanged stored payloads, partial confirmation, and Horizon publication notifications.

tests/Queue/Outbox/RedisOutboxPublicationTest.php

SqsOutboxPublicationTest.phpTest SQS limits and partial acceptance +187/-0

Test SQS limits and partial acceptance

• Covers FIFO and standard delay rules, request limits, SDK uncertainty, and attempt-owned overflow cleanup.

tests/Queue/Outbox/SqsOutboxPublicationTest.php

QueueBackgroundQueueTest.phpAlign background queue callback expectations +11/-0

Align background queue callback expectations

• Updates ordinary after-commit and lock-cleanup tests for session-aware transaction callbacks.

tests/Queue/QueueBackgroundQueueTest.php

QueueDatabaseQueueUnitTest.phpExtend database queue acceptance assertions +7/-0

Extend database queue acceptance assertions

• Checks bulk notification and rollback ownership under the revised accepted-dispatch lifecycle.

tests/Queue/QueueDatabaseQueueUnitTest.php

QueueDeferredQueueTest.phpAlign deferred queue callback expectations +10/-0

Align deferred queue callback expectations

• Updates existing transaction and rollback assertions for exact callback ownership.

tests/Queue/QueueDeferredQueueTest.php

QueueRedisQueueTest.phpExtend Redis bulk acceptance assertions +5/-0

Extend Redis bulk acceptance assertions

• Checks accepted-job ownership when observers fail and retains locks if Redis does not confirm the batch.

tests/Queue/QueueRedisQueueTest.php

QueueSyncQueueTest.phpAlign sync queue callback expectations +6/-0

Align sync queue callback expectations

• Updates ordinary after-commit and rollback assertions for session-aware callbacks.

tests/Queue/QueueSyncQueueTest.php

RelayCommandTest.phpTest relay command option handling +80/-0

Test relay command option handling

• Checks s...

@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Oct 23, 2026 •

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (2) 📘 Rule violations (0) 📎 Requirement gaps (0) 🎨 UX issues (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)

Grey Divider


Remediation recommended

1. Broadcasts expose outbox settings 🐞 Bug ≡ Correctness
Description
InteractsWithQueueOutbox adds public dispatch settings that
BroadcastEvent::getPayloadFromEvent() includes when it reflects an event's initialized public
properties. If a broadcast event uses Queueable and selects the outbox without defining
broadcastWith(), subscribers receive viaOutbox and, when selected, the internal outboxDatabase
name.
Code

src/bus/src/InteractsWithQueueOutbox.php[19]

+    public ?bool $viaOutbox;
Evidence
Queueable imports the new trait, whose public setting is initialized by viaOutbox(); the default
broadcast payload reflects initialized public properties and removes only broadcastQueue.

src/bus/src/Queueable.php[21-24]
src/bus/src/InteractsWithQueueOutbox.php[19-32]
src/broadcasting/src/BroadcastEvent.php[123-145]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Outbox dispatch settings on a broadcast event become fields in its default broadcast payload.
## Fix Focus Areas
- src/broadcasting/src/BroadcastEvent.php[127-145]
- src/bus/src/InteractsWithQueueOutbox.php[19-24]
## Recommended Fix
Exclude the outbox control properties from the reflected broadcast payload, including when they have been explicitly initialized, while preserving application-defined payload fields.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Chained batch jobs skip outbox defaults 🐞 Bug ≡ Correctness
Description
Batch::add() applies its outbox options only to the first job after chain() has already
serialized the remaining jobs. When an outbox-enabled batch contains a chain whose later jobs have
no explicit outbox selection, those jobs are dispatched from their saved state without the batch
default and take the ordinary queue path.
Code

src/bus/src/Batch.php[R86-88]

+            if (array_key_exists('viaOutbox', $this->options) || array_key_exists('outboxDatabase', $this->options)) {
+                // Preserve initialized nulls as well as explicit values on each job.
+                $properties = get_object_vars($job);
Evidence
Batch options are copied onto $job only after chain() has serialized the other members;
dispatchNextJobInChain restores those serialized jobs without copying outbox settings, and outbox
selection reads each dispatched job's own setting.

src/bus/src/PendingBatch.php[287-323]
src/bus/src/Batch.php[62-98]
src/bus/src/Queueable.php[210-219]
src/bus/src/Queueable.php[295-312]
src/queue/src/Concerns/StagesOutboxJobs.php[30-38]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Outbox defaults on a batch do not reach later jobs in a chain because those jobs are serialized before the defaults are applied.
## Fix Focus Areas
- src/bus/src/Batch.php[62-98]
- src/bus/src/Batch.php[119-129]
## Recommended Fix
Apply batch outbox defaults to every chain member before calling `chain()`, preserving any explicitly initialized per-job overrides, and cover dispatch of a later chained job.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Tip of the day
💡 Did you know, you can reply 'qodo' on any finding to push back, ask questions, or dig deeper

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment thread src/bus/src/InteractsWithQueueOutbox.php
Comment thread src/bus/src/Batch.php
@macroscopeapp

macroscopeapp Bot commented Oct 23, 2026 •

Copy link
Copy Markdown

Manual reviews triggered for commit 529cea1:

All prior checks · these links stay valid even if you push more commits.

@macroscopeapp

macroscopeapp Bot commented Oct 23, 2026

Copy link
Copy Markdown

Review started; results will be posted as check runs.

@binaryfire

Copy link
Copy Markdown
Member Author

@macroscope-app review

@macroscopeapp

macroscopeapp Bot commented Oct 23, 2026

Copy link
Copy Markdown

Review started; results will be posted as check runs.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔵 Needs a closer look

It introduces a large new durability subsystem and changes core database transaction-identity semantics frameworkwide, so the blast radius and correctness/coroutine-safety implications require human review.

No open issues 🎉

What changed in this PR

This PR adds an opt-in transactional queue outbox to the queue package. Jobs selected via viaOutbox(), the ShouldQueueViaOutbox marker, a batch default, or a per-connection outbox => true flag are stored in a job_outbox table inside the business database transaction, then published to their normal queue by a separate lease-based queue:relay worker. The guarantee is at-least-once, closing the crash gap that afterCommit() cannot. To make outbox ownership correct, the PR also changes database transaction identity across the framework from the base connection name (getName()) to the role-qualified resolved name (getNameWithReadWriteType()), so base/::read/::write sessions own independent callbacks.

Changes:

  • New outbox subsystem: staging trait, OutboxDispatcher/DatabaseOutboxRepository/OutboxRelay/OutboxWorker, queue:relay plus queue:outbox* operator commands, migration/stub, events, and OpenTelemetry delivery metrics (enabled by default).
  • Cross-cutting opt-in plumbing: InteractsWithQueueOutbox on Queueable/Batchable, propagation through PendingDispatch, PendingBatch, Batch::add, queued mailables/notifications/listeners/broadcasts, and pooled/sequential publication in QueuePoolProxy.
  • Correctness changes shared with ordinary dispatch: role-qualified transaction identity (database, foundation, pennant), Horizon RedisQueue/JobPayload payload-lifecycle refactor, cache repository store-identity preservation, and uninitialized-broadcast-property handling.
File Description
src/​bus/​src/​InteractsWithQueueOutbox.php, Queueable.php, Batchable.php New trait providing viaOutbox()/withoutOutbox()/onOutboxDatabase() with unserialized default state.
src/​bus/​src/​Batch.php, PendingBatch.php Batch-level outbox defaults applied to new jobs without overriding explicit choices.
src/​bus/​src/​UniqueLock.php, DebounceLock.php Capture dispatch-lock release context for rollback cleanup.
src/​foundation/​src/​Bus/​PendingDispatch.php Forward outbox selection to the job.
src/​queue/​src/​Concerns/​StagesOutboxJobs.php, Queue.php (context) Core staging lifecycle and owner-group insertion.
src/​queue/​src/​Outbox/​* Dispatcher, repository, relay, worker, message/result/options value objects, sequential publication.
src/​queue/​src/​Console/​* queue:relay and queue:outbox* commands, migration stub, FindsOutboxMessages.
src/​queue/​src/​{SyncQueue,NullQueue,FailoverQueue,BeanstalkdQueue}.php Driver-specific outbox fallbacks and hooks.
src/​queue/​src/​QueuePoolProxy.php, Concerns/​InsertsDatabaseRows.php, Connectors/​SqsConnector.php Pooled publication, binding-limit helper, SQS config passthrough.
src/​database/​src/​{Connection,Concerns/​ManagesTransactions,DatabaseTransactionsManager}.php Transaction identity switched to getNameWithReadWriteType().
src/​foundation/​src/​Testing/​*, src/​pennant/​src/​Drivers/​Decorator.php Role-qualified owner propagation in testing + pennant.
src/​horizon/​src/​{RedisQueue,JobPayload}.php, Repositories/​RedisTagRepository.php, README.md Payload lifecycle refactor; push() no longer routes through pushRaw().
src/​cache/​src/​{CacheManager,Repository}.php Preserve/assign store name for custom/memoized repositories.
src/​opentelemetry/​*, src/​events/​src/​Dispatcher.php, src/​mail/​*, src/​notifications/​*, src/​broadcasting/​* Outbox metrics + producer propagation.
src/​foundation/​config/​queue.php, src/​testbench/​.../​job_outbox migration Outbox config section and testbench schema.
src/​docs/​*, src/​database/​README.md, AGENTS.md Documentation for the outbox and transaction-identity change.
tests/​** Extensive unit/integration coverage across engines, producers, relay lifecycle, and transaction identity.

Review effort: Balanced

Give ordinary relay tests an explicit memory budget so their behavior does not depend on how much of the framework PHPUnit has already loaded. Apply the same budget to Redis delivery tests that invoke the relay command.

Keep the dedicated low-memory exit test and production worker defaults unchanged. This fixes premature memory-limit exits in the framework CI jobs without weakening relay lifecycle assertions.
Reject declared outbox owners that resolve to a different session from the database batch repository before changing counters or publishing jobs. This prevents an independently rolled-back business transaction from removing jobs while leaving committed batch counters behind.

Resolve ownership lazily, compare resolved session names rather than requested aliases, and preserve explicit outbox opt-outs and custom batch repositories. Keep validation at Batch::add rather than introducing queue-side batch context.

Cover conflicting owners across staged, database and local delivery, same-owner rollback, shared SQLite aliases, and explicit opt-outs without resolving their unused owners.
Use the local after-commit fallback for outbox selection only when an applicable transaction exists. Without a transaction, synchronous dispatch retains its immediate result and delayed coroutine dispatch retains its timer identifier.

Preserve ordinary explicit and configured after-commit behavior, including explicit-owner validation when transactions are active. Regression coverage checks both immediate results and the shared coroutine timer path without resolving outbox storage.
Clarify that batch outbox defaults apply to each chain head, while successors retain their own selection and connection defaults.

Align the active implementation plan with batch-owner preflight and local dispatch return-value preservation. Replace the completed PR-opening instruction with the remaining local-commit and owner-direction step.
Explain outbox setup and delivery through application-facing language and examples rather than implementation terminology. Give delayed jobs, configuration changes, monitoring, retries and deletion their own sections while preserving the delivery and custom-driver contracts.

Clarify required database registration, batch storage, local-driver transaction behavior, retry eligibility and telemetry interpretation. Match the surrounding prose in the queue, event, mail, notification, broadcast and porting guides, and keep their detailed instructions linked to the canonical outbox guide.

Validate documentation anchors, cross-links and code fences. Update the active plan's remaining step to PR review and CI verification. No runtime behavior changes.
@binaryfire
binaryfire merged commit 97be03f into 0.4 Oct 23, 2026
50 checks passed
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.

2 participants