mirror of https://github.com/rails/rails
Merge pull request #51010 from Shopify/handle-transactional-fixtures-leak
Gracefully handle transactional fixtures leaks
This commit is contained in:
commit
6daddd99eb
|
@ -130,25 +130,53 @@ module ActiveRecord
|
|||
end
|
||||
|
||||
@fixture_cache = {}
|
||||
@fixture_connections = []
|
||||
@fixture_connections = {}
|
||||
@fixture_cache_key = [self.class.fixture_table_names.dup, self.class.fixture_paths.dup, self.class.fixture_class_names.dup]
|
||||
@@already_loaded_fixtures ||= {}
|
||||
@connection_subscriber = nil
|
||||
@saved_pool_configs = Hash.new { |hash, key| hash[key] = {} }
|
||||
|
||||
# Load fixtures once and begin transaction.
|
||||
if run_in_transaction?
|
||||
# Load fixtures once and begin transaction.
|
||||
@loaded_fixtures = @@already_loaded_fixtures[@fixture_cache_key]
|
||||
unless @loaded_fixtures
|
||||
@@already_loaded_fixtures.clear
|
||||
@loaded_fixtures = @@already_loaded_fixtures[@fixture_cache_key] = load_fixtures(config)
|
||||
end
|
||||
|
||||
setup_transactional_fixtures
|
||||
else
|
||||
# Load fixtures for every test.
|
||||
ActiveRecord::FixtureSet.reset_cache
|
||||
invalidate_already_loaded_fixtures
|
||||
@loaded_fixtures = load_fixtures(config)
|
||||
end
|
||||
|
||||
# Instantiate fixtures for every test if requested.
|
||||
instantiate_fixtures if use_instantiated_fixtures
|
||||
end
|
||||
|
||||
def teardown_fixtures
|
||||
# Rollback changes if a transaction is active.
|
||||
if run_in_transaction?
|
||||
teardown_transactional_fixtures
|
||||
else
|
||||
ActiveRecord::FixtureSet.reset_cache
|
||||
end
|
||||
|
||||
ActiveRecord::Base.connection_handler.clear_active_connections!(:all)
|
||||
end
|
||||
|
||||
private
|
||||
def setup_transactional_fixtures
|
||||
setup_shared_connection_pool
|
||||
|
||||
# Begin transactions for connections already established
|
||||
@fixture_connections = enlist_fixture_connections
|
||||
@fixture_connections.each do |connection|
|
||||
connection.begin_transaction joinable: false, _lazy: false
|
||||
connection.pool.lock_thread = true if lock_threads
|
||||
@fixture_connections = ActiveRecord::Base.connection_handler.connection_pool_list(:writing).to_h do |pool|
|
||||
pool.lock_thread = true if lock_threads
|
||||
connection = pool.connection
|
||||
transaction = connection.begin_transaction(joinable: false, _lazy: false)
|
||||
[connection, transaction]
|
||||
end
|
||||
|
||||
# When connections are established in the future, begin a transaction too
|
||||
|
@ -167,50 +195,38 @@ module ActiveRecord
|
|||
if connection
|
||||
setup_shared_connection_pool
|
||||
|
||||
if !@fixture_connections.include?(connection)
|
||||
connection.begin_transaction joinable: false, _lazy: false
|
||||
if !@fixture_connections.key?(connection)
|
||||
connection.pool.lock_thread = true if lock_threads
|
||||
@fixture_connections << connection
|
||||
@fixture_connections[connection] = connection.begin_transaction(joinable: false, _lazy: false)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Load fixtures for every test.
|
||||
else
|
||||
ActiveRecord::FixtureSet.reset_cache
|
||||
@@already_loaded_fixtures.clear
|
||||
@loaded_fixtures = load_fixtures(config)
|
||||
end
|
||||
|
||||
# Instantiate fixtures for every test if requested.
|
||||
instantiate_fixtures if use_instantiated_fixtures
|
||||
end
|
||||
|
||||
def teardown_fixtures
|
||||
# Rollback changes if a transaction is active.
|
||||
if run_in_transaction?
|
||||
def teardown_transactional_fixtures
|
||||
ActiveSupport::Notifications.unsubscribe(@connection_subscriber) if @connection_subscriber
|
||||
@fixture_connections.each do |connection|
|
||||
connection.rollback_transaction if connection.transaction_open?
|
||||
@fixture_connections.each do |connection, transaction|
|
||||
begin
|
||||
connection.rollback_transaction(transaction)
|
||||
rescue ActiveRecord::StatementInvalid
|
||||
# Something commited or rolled back the transaction.
|
||||
# We can no longer trust the database state is clean.
|
||||
invalidate_already_loaded_fixtures
|
||||
# We also don't know for sure the connection wasn't
|
||||
# mutated in dangerous ways.
|
||||
connection.disconnect!
|
||||
end
|
||||
connection.pool.lock_thread = false
|
||||
end
|
||||
@fixture_connections.clear
|
||||
teardown_shared_connection_pool
|
||||
else
|
||||
ActiveRecord::FixtureSet.reset_cache
|
||||
end
|
||||
|
||||
ActiveRecord::Base.connection_handler.clear_active_connections!(:all)
|
||||
end
|
||||
def invalidate_already_loaded_fixtures
|
||||
@@already_loaded_fixtures.clear
|
||||
end
|
||||
|
||||
def enlist_fixture_connections
|
||||
setup_shared_connection_pool
|
||||
|
||||
ActiveRecord::Base.connection_handler.connection_pool_list(:writing).map(&:connection)
|
||||
end
|
||||
|
||||
private
|
||||
# Shares the writing connection pool with connections on
|
||||
# other handlers.
|
||||
#
|
||||
|
|
|
@ -4,9 +4,11 @@ require "cases/helper"
|
|||
require "rack"
|
||||
|
||||
class ActiveRecordTest < ActiveRecord::TestCase
|
||||
self.use_transactional_tests = false
|
||||
|
||||
unless in_memory_db?
|
||||
test ".disconnect_all! closes all connections" do
|
||||
ActiveRecord::Base.connection.active?
|
||||
ActiveRecord::Base.connection.connect!
|
||||
assert_predicate ActiveRecord::Base, :connected?
|
||||
|
||||
ActiveRecord.disconnect_all!
|
||||
|
|
|
@ -37,7 +37,7 @@ class ConnectionTest < ActiveRecord::AbstractMysqlTestCase
|
|||
assert_not_predicate @connection, :active?
|
||||
ensure
|
||||
# Repair all fixture connections so other tests won't break.
|
||||
@fixture_connections.each(&:verify!)
|
||||
@fixture_connections.each_key(&:verify!)
|
||||
end
|
||||
|
||||
def test_successful_reconnection_after_timeout_with_manual_reconnect
|
||||
|
|
|
@ -145,7 +145,7 @@ module ActiveRecord
|
|||
assert_predicate @connection, :active?
|
||||
ensure
|
||||
# Repair all fixture connections so other tests won't break.
|
||||
@fixture_connections.each(&:verify!)
|
||||
@fixture_connections.each_key(&:verify!)
|
||||
end
|
||||
|
||||
def test_set_session_variable_true
|
||||
|
|
|
@ -6,6 +6,8 @@ require "models/person"
|
|||
module ActiveRecord
|
||||
module ConnectionAdapters
|
||||
class ConnectionHandlerTest < ActiveRecord::TestCase
|
||||
self.use_transactional_tests = false
|
||||
|
||||
fixtures :people
|
||||
|
||||
def setup
|
||||
|
@ -93,6 +95,8 @@ module ActiveRecord
|
|||
connection_handler = ActiveRecord::Base.connection_handler
|
||||
ActiveRecord::Base.connection_handler = ActiveRecord::ConnectionAdapters::ConnectionHandler.new
|
||||
|
||||
setup_transactional_fixtures
|
||||
|
||||
assert_nothing_raised do
|
||||
ActiveRecord::Base.connects_to(database: { reading: :arunit, writing: :arunit })
|
||||
end
|
||||
|
@ -101,6 +105,8 @@ module ActiveRecord
|
|||
ro_conn = ActiveRecord::Base.connection_handler.retrieve_connection("ActiveRecord::Base", role: :reading)
|
||||
|
||||
assert_equal rw_conn, ro_conn
|
||||
|
||||
teardown_transactional_fixtures
|
||||
ensure
|
||||
ActiveRecord::Base.connection_handler = connection_handler
|
||||
end
|
||||
|
|
|
@ -5,6 +5,8 @@ require "cases/helper"
|
|||
module ActiveRecord
|
||||
module ConnectionAdapters
|
||||
class SchemaCacheTest < ActiveRecord::TestCase
|
||||
self.use_transactional_tests = false
|
||||
|
||||
def setup
|
||||
@connection = ARUnit2Model.connection
|
||||
@cache = new_bound_reflection
|
||||
|
|
|
@ -3,6 +3,8 @@
|
|||
require "cases/helper"
|
||||
|
||||
class PrimaryClassTest < ActiveRecord::TestCase
|
||||
self.use_transactional_tests = false
|
||||
|
||||
def teardown
|
||||
clean_up_connection_handler
|
||||
end
|
||||
|
|
Loading…
Reference in New Issue