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.
Rails main no longer begins a transaction in pin_connection! or rolls it back in unpin_connection!; ActiveRecord::TestFixtures now does both itself. Begin and roll back the transaction when pinning didn't, so the strategy works on 7.2-8.1 and on edge. Check the new specs' records by id rather than by table count, so rows left behind on sqlite3 by other examples don't fail them.
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.
Rails edge: Rails
mainno longer opens a transaction inpin_connection!or rolls it back inunpin_connection!;TestFixturesnow does both itself. The strategy opens and rolls back its own transaction whenever pinning didn't, so it works either way.Tested locally on Rails 7.1, 7.2, 8.0 and 8.1 against MySQL, Trilogy, Postgres and SQLite: full suite green on each. On Rails edge, the same 8 SQLite deletion, truncation and transaction examples fail with and without this change (rows from earlier examples survive into later ones); that's pre-existing on
main.