Repository navigation
Conversation
Rails' executor releases a thread's connection when its transaction is not joinable, so an inline job or a request in a request spec hands the test's connection back to the pool. If another thread (the pool reaper's keepalive, for one) checks it out before the test queries again, the test leases a different connection that can't see its uncommitted records. Pin the connection the way ActiveRecord::TestFixtures does, and add a :lock_threads option matching Rails' lock_threads.
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.
On Rails 7.2+, a test using the transaction strategy can lose sight of its own uncommitted records partway through.
Transaction#startopens a transaction withjoinable: false. Rails'ConnectionPool::ExecutorHooks.completereleases a thread's connection back to the pool whenever its current transaction isn't joinable, and every executor wrap runs that hook: an inline job (perform_enqueued_jobs,deliver_later), or each request in a request spec. So after the first job or request, the test's connection, with its open transaction, goes back to the pool. Usually the next lease gets the same connection back. But if another thread has it checked out at that moment (on Rails 8.1 the pool reaper's keepalive briefly checks out idle connections), the test leases a different connection. That connection can't see the test's records, which fails with things likeActiveJob::DeserializationError: Couldn't find Model with 'id'=.... We hit this as an intermittent failure on a Rails 8.1 suite.ActiveRecord::TestFixturesavoids this by pinning the connection (pool.pin_connection!(lock_threads)in setup,unpin_connection!in teardown). This PR does the same in the transaction strategy:startpins the connection andcleanunpins it, rolling back the transaction. A pinned connection is never checked back in, so executor wraps can't release it, and every thread gets it, so other threads see the test's data the way they do under Rails' transactional tests.:lock_threadsoption, defaulttrue, matching Rails'lock_threads. It makes threads take turns on the shared connection.The new specs reproduce the race deterministically. After
ExecutorHooks.complete, another thread checks out the released connection and holds it while the test queries. They fail onmainand pass with this change.Behavior change to note: a test that needs a genuinely separate database session from a second thread (for example, to check that a table lock blocks another session) now shares the pinned connection, as it would under Rails' transactional tests. Such tests need the deletion or truncation strategy.
Tested locally on Rails 7.1, 7.2, 8.0 and 8.1 against MySQL, Trilogy, Postgres and SQLite: full suite green on each.