Discard batches whose enclosing transaction rolls back - #7
Open
jpcamara wants to merge 1 commit into
Open
Conversation
A batch created inside an application transaction is written on Solid Queue's own connection. When that's a different connection—the default, and what the install generator configures—the batch commits as soon as it's enqueued, and an application rollback never reaches it. The batch row survives, and so does every job in it that wasn't deferred until commit. Those leftovers aren't inert. Maintenance can't tell such a batch from one whose creating process crashed after enqueueing jobs, so once it's older than the stalled window it starts it, finds nothing pending, completes it, and fires its callbacks—reporting success for work that was rolled back. The jobs that survived run too, against data that no longer exists. Register cleanup on the open transactions the batch's own writes aren't part of, so the batch and its jobs are removed if any of them rolls back. Transactions are compared by connection pool rather than by database: a queue database pointed at the same database as the app still gets its own connection, and so still commits independently. When Solid Queue shares the application's connection there's nothing to register—the batch is already inside that transaction and rolls back with it—so this is a no-op there and single-database setups keep behaving exactly as they did. Jobs a worker already claimed are left alone, since a running job can't be recalled; they're reported in the instrumentation payload instead. Rails 7.1 has no transaction rollback hooks, so batches there keep the old behaviour. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Draft for review — opened against my own fork, not upstream.
The problem
SolidQueue::Batch#enqueuepersists the batch on Solid Queue's connection:When Solid Queue has its own connection — the default, and what the install generator configures — that's a genuinely separate transaction. It commits while the surrounding application transaction is still open, so a rollback never reaches it.
What's left behind, measured on PostgreSQL:
:pendingThe leftovers aren't inert. Maintenance can't distinguish such a batch from one whose creating process crashed after enqueueing jobs, so once it's older than the stalled window it starts it, finds nothing pending, completes it, and fires
on_success— for a transaction that rolled back:enqueue_after_transaction_commitdoesn't help — it defers jobs, but Solid Queue writes the batch row itself, before any of that applies.The fix
Register cleanup on the open transactions the batch's own writes aren't part of:
Mirrors how
ActiveRecord.after_all_transactions_commitenumerates transactions, usingTransaction#after_rollbackfor the other side.Transactions are compared by connection pool, not by database. A
queue:entry pointed at the same database as the app still gets its own pool, so it still commits independently and still needs this. Conversely, when Solid Queue shares the app's connection the list is empty and this is a no-op — single-database setups behave exactly as before.Verification
Four scenarios × both connection layouts × Rails 7.2 / 8.0 / 8.1 / 8.2, on PostgreSQL:
Identical output for shared-pool and separate-pool configurations on every version. Full suite green on 7.2/8.0/8.1/main; rubocop clean. Five regression tests added, all of which fail without the change.
Known gaps
Transaction#after_rollbackarrived in 7.2. The olderadd_transaction_recordcould work but needs a sentinel implementingrolledback!/trigger_transactional_callbacks?, which is version-sensitive. Tests skip there. Worth noting 7.1 is where this is worst:after_commit :start, on: :createfires on the queue-side commit, so a rolled-back empty batch completes and enqueueson_successimmediately, with no sweeper involved.claimed_jobsin thediscard_rolled_back_batchinstrumentation payload.This is independent of the
total_jobs != 0sweeper guard onbatch-deferred-enqueue-sweep; that one covers a batch in a transaction that commits but outlives the stalled window.🤖 Generated with Claude Code