Repository navigation
Add an opt-in transactional queue outbox - #70
Conversation
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.
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configuration
You can disable this status message by setting the Use the checkbox below for a quick retry:
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. Comment |
PR Summary by QodoAdd an opt-in transactional queue outbox and relay
AI Description
Diagram
High-Level Assessment
Files changed (132)
|
Code Review by Qodo
1. Later jobs in batched chains skip the outbox
|
[Medium impact] The PR appears safe to merge; both previous findings are addressed and no new actionable issues remain. SummaryAdds an opt-in transactional queue outbox that stores jobs with database writes and relays them through existing queue drivers.
Both previous findings are addressed. No new actionable issues were found. Reviews (2) · Last reviewed commit: "docs(queue): align outbox guidance with ..." · Reviewed by Greptile |
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.
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.Usage
Applications can select the outbox per dispatch with
viaOutbox(), per job class withShouldQueueViaOutbox, or per queue connection withoutbox => 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:
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
SKIP LOCKED; SQLite has an atomic-update fallback.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:
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:
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 publicpushRaw()override. DirectpushRaw()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.
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 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.
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.
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.
Summary by cubic
Adds an opt-in transactional outbox so queued jobs can be stored in the same database transaction as the business write and published by a separate relay. This closes the crash window between commit and publish without replacing existing queue drivers, dispatch APIs, or workers.
New Features
viaOutbox(), theShouldQueueViaOutboxmarker, or per-connectionoutbox => true;withoutOutbox()overrides any default, andonOutboxDatabase()picks the owning connection.queue:relayalongside existing workers; Horizon remains optional and continues managing jobs after publication.Migration
php artisan make:queue-outbox-tableandphp artisan migrate, then startqueue:relayalongsidequeue:work.DatabaseTransactionsManagermust use the resolved connection'sgetNameWithReadWriteType()instead of basegetName().Written for commit 2f11e2e. Summary will update on new commits.
Note
Add opt-in transactional queue outbox with relay worker and console commands
viaOutbox(),onOutboxDatabase(), theShouldQueueViaOutboxmarker, or a queue connection default are staged in a job outbox table inside the dispatching transaction, then published by a relay worker after commit. See StagesOutboxJobs.php, OutboxDispatcher.php, and OutboxWorker.php.queue:relay,queue:outbox(listing),queue:outbox-monitor,queue:outbox-retry,queue:outbox-forget, andmake:queue-outbox-table, plus newOutboxBusy,OutboxBatchDelivered, andOutboxBatchDeliveryFailedevents and OpenTelemetry delivery metrics.PendingBatch::viaOutboxand friends), pending dispatches, and queued mailables, notifications, broadcasts, and listeners; unused outbox properties are not serialized.ManagesTransactions,DatabaseTransactionsManager,RequestContextSynchronizer). Also reworks HorizonRedisQueuepayload preparation aroundprepareForQueueing/ push-time hooks and removes the last-pushed coroutine context.getName()must switch togetNameWithReadWriteType().RedisTagRepositoryreturns early for empty tag lists. SyncQueue jobs selected for outbox are deferred until commit instead of stored.Macroscope summarized 2f11e2e.