aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJeremy Wadsack <jeremy.wadsack@gmail.com>2015-06-28 15:11:34 -0700
committerJeremy Wadsack <jeremy.wadsack@gmail.com>2016-07-12 23:35:29 -0700
commit31a8588a01cf5aadb864f609efad6ee24b5d4055 (patch)
tree6d5929b7d83782b51650c0a548362656b2c11f84
parentfc50f1fd50b05dc7e0f259dbe840e6d292e85aef (diff)
downloadrails-31a8588a01cf5aadb864f609efad6ee24b5d4055.tar.gz
rails-31a8588a01cf5aadb864f609efad6ee24b5d4055.tar.bz2
rails-31a8588a01cf5aadb864f609efad6ee24b5d4055.zip
Create connection.active_record notification and use that to ensure that lazy-
loaded model classes have their connections wrapped in transactions. See #17776 In Rails 4 config.eager_load was changed to false in the test environment. This means that model classes that connect to alternate databases with establish_connection are not loaded at start up. If use_transactional_fixtures is enabled, transactions are wrapped around the connections that have been established only at the start of the test suite. So model classes loaded later don't have transactions causing data created in the alternate database not to be removed. This change resolves that by creating a new connection.active_record notification that gets fired whenever a connection is established. I then added a subscriber after we set up transactions in the test environment to listen for additional connections and wrap those in transactions as well.
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb15
-rw-r--r--activerecord/lib/active_record/fixtures.rb28
-rw-r--r--activerecord/test/cases/connection_pool_test.rb12
-rw-r--r--activerecord/test/cases/fixtures_test.rb40
4 files changed, 95 insertions, 0 deletions
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb
index 9b74c3a10f..1d9579f1a8 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb
@@ -849,6 +849,21 @@ module ActiveRecord
remove_connection(spec.name)
owner_to_pool[spec.name] = ConnectionAdapters::ConnectionPool.new(spec)
+
+ message_bus = ActiveSupport::Notifications.instrumenter
+ payload = {
+ connection_id: object_id
+ }
+ if spec
+ payload[:class_name] = spec.name
+ payload[:config] = spec.config
+ end
+
+ message_bus.instrument('!connection.active_record', payload) do
+ owner_to_pool[spec.name] = ConnectionAdapters::ConnectionPool.new(spec)
+ end
+
+ owner_to_pool[spec.name]
end
# Returns true if there are any active connections among the connection
diff --git a/activerecord/lib/active_record/fixtures.rb b/activerecord/lib/active_record/fixtures.rb
index 51bf12d0bf..6d142285cd 100644
--- a/activerecord/lib/active_record/fixtures.rb
+++ b/activerecord/lib/active_record/fixtures.rb
@@ -968,6 +968,7 @@ module ActiveRecord
@fixture_cache = {}
@fixture_connections = []
@@already_loaded_fixtures ||= {}
+ @connection_subscriber = nil
# Load fixtures once and begin transaction.
if run_in_transaction?
@@ -977,10 +978,36 @@ module ActiveRecord
@loaded_fixtures = load_fixtures(config)
@@already_loaded_fixtures[self.class] = @loaded_fixtures
end
+
+ # Begin transactions for connections already established
@fixture_connections = enlist_fixture_connections
@fixture_connections.each do |connection|
connection.begin_transaction joinable: false
end
+
+ # When connections are established in the future, begin a transaction too
+ @connection_subscriber = ActiveSupport::Notifications.subscribe('!connection.active_record') do |_, _, _, _, payload|
+ model_class = nil
+ begin
+ model_class = payload[:class_name].constantize if payload[:class_name]
+ rescue NameError
+ model_class = nil
+ end
+
+ if model_class
+ begin
+ connection = ActiveRecord::Base.connection_handler.retrieve_connection(model_class)
+ rescue ConnectionNotEstablished
+ connection = nil
+ end
+
+ if connection && !@fixture_connections.include?(connection)
+ connection.begin_transaction joinable: false
+ @fixture_connections << connection
+ end
+ end
+ end
+
# Load fixtures for every test.
else
ActiveRecord::FixtureSet.reset_cache
@@ -995,6 +1022,7 @@ module ActiveRecord
def teardown_fixtures
# Rollback changes if a transaction is active.
if run_in_transaction?
+ ActiveSupport::Notifications.unsubscribe(@connection_subscriber) if @connection_subscriber
@fixture_connections.each do |connection|
connection.rollback_transaction if connection.transaction_open?
end
diff --git a/activerecord/test/cases/connection_pool_test.rb b/activerecord/test/cases/connection_pool_test.rb
index 09e7848bda..2bdabaee62 100644
--- a/activerecord/test/cases/connection_pool_test.rb
+++ b/activerecord/test/cases/connection_pool_test.rb
@@ -341,6 +341,18 @@ module ActiveRecord
end
end
+ def test_connection_notification_is_called
+ payloads = []
+ subscription = ActiveSupport::Notifications.subscribe('!connection.active_record') do |name, started, finished, unique_id, payload|
+ payloads << payload
+ end
+ ActiveRecord::Base.establish_connection :arunit
+ assert_equal [:class_name, :config, :connection_id], payloads[0].keys.sort
+ assert_equal 'primary', payloads[0][:class_name]
+ ensure
+ ActiveSupport::Notifications.unsubscribe(subscription) if subscription
+ end
+
def test_pool_sets_connection_schema_cache
connection = pool.checkout
schema_cache = SchemaCache.new connection
diff --git a/activerecord/test/cases/fixtures_test.rb b/activerecord/test/cases/fixtures_test.rb
index 9455d4886c..a06aa4b247 100644
--- a/activerecord/test/cases/fixtures_test.rb
+++ b/activerecord/test/cases/fixtures_test.rb
@@ -622,6 +622,46 @@ class TransactionalFixturesOnCustomConnectionTest < ActiveRecord::TestCase
end
end
+class TransactionalFixturesOnConnectionNotification < ActiveRecord::TestCase
+ self.use_transactional_tests = true
+ self.use_instantiated_fixtures = false
+
+ def test_transaction_created_on_connection_notification
+ connection = stub(:transaction_open? => false)
+ connection.expects(:begin_transaction).with(joinable: false)
+ fire_connection_notification(connection)
+ end
+
+ def test_notification_established_transactions_are_rolled_back
+ # Mocha is not thread-safe so define our own stub to test
+ connection = Class.new do
+ attr_accessor :rollback_transaction_called
+ def transaction_open?; true; end
+ def begin_transaction(*args); end
+ def rollback_transaction(*args)
+ @rollback_transaction_called = true
+ end
+ end.new
+ fire_connection_notification(connection)
+ teardown_fixtures
+ assert(connection.rollback_transaction_called, "Expected <mock connection>#rollback_transaction to be called but was not")
+ end
+
+ private
+
+ def fire_connection_notification(connection)
+ ActiveRecord::Base.connection_handler.stubs(:retrieve_connection).with(Book).returns(connection)
+ message_bus = ActiveSupport::Notifications.instrumenter
+ payload = {
+ class_name: 'Book',
+ config: nil,
+ connection_id: connection.object_id
+ }
+
+ message_bus.instrument('!connection.active_record', payload) {}
+ end
+end
+
class InvalidTableNameFixturesTest < ActiveRecord::TestCase
fixtures :funny_jokes
# Set to false to blow away fixtures cache and ensure our fixtures are loaded