Skip to content

Commit 6daddd9

Browse files
authored
Merge pull request rails#51010 from Shopify/handle-transactional-fixtures-leak
Gracefully handle transactional fixtures leaks
2 parents bab4aa7 + 26de25d commit 6daddd9

File tree

7 files changed

+66
-38
lines changed

7 files changed

+66
-38
lines changed

activerecord/lib/active_record/test_fixtures.rb

Lines changed: 51 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -130,25 +130,53 @@ def setup_fixtures(config = ActiveRecord::Base)
130130
end
131131

132132
@fixture_cache = {}
133-
@fixture_connections = []
133+
@fixture_connections = {}
134134
@fixture_cache_key = [self.class.fixture_table_names.dup, self.class.fixture_paths.dup, self.class.fixture_class_names.dup]
135135
@@already_loaded_fixtures ||= {}
136136
@connection_subscriber = nil
137137
@saved_pool_configs = Hash.new { |hash, key| hash[key] = {} }
138138

139-
# Load fixtures once and begin transaction.
140139
if run_in_transaction?
140+
# Load fixtures once and begin transaction.
141141
@loaded_fixtures = @@already_loaded_fixtures[@fixture_cache_key]
142142
unless @loaded_fixtures
143143
@@already_loaded_fixtures.clear
144144
@loaded_fixtures = @@already_loaded_fixtures[@fixture_cache_key] = load_fixtures(config)
145145
end
146146

147+
setup_transactional_fixtures
148+
else
149+
# Load fixtures for every test.
150+
ActiveRecord::FixtureSet.reset_cache
151+
invalidate_already_loaded_fixtures
152+
@loaded_fixtures = load_fixtures(config)
153+
end
154+
155+
# Instantiate fixtures for every test if requested.
156+
instantiate_fixtures if use_instantiated_fixtures
157+
end
158+
159+
def teardown_fixtures
160+
# Rollback changes if a transaction is active.
161+
if run_in_transaction?
162+
teardown_transactional_fixtures
163+
else
164+
ActiveRecord::FixtureSet.reset_cache
165+
end
166+
167+
ActiveRecord::Base.connection_handler.clear_active_connections!(:all)
168+
end
169+
170+
private
171+
def setup_transactional_fixtures
172+
setup_shared_connection_pool
173+
147174
# Begin transactions for connections already established
148-
@fixture_connections = enlist_fixture_connections
149-
@fixture_connections.each do |connection|
150-
connection.begin_transaction joinable: false, _lazy: false
151-
connection.pool.lock_thread = true if lock_threads
175+
@fixture_connections = ActiveRecord::Base.connection_handler.connection_pool_list(:writing).to_h do |pool|
176+
pool.lock_thread = true if lock_threads
177+
connection = pool.connection
178+
transaction = connection.begin_transaction(joinable: false, _lazy: false)
179+
[connection, transaction]
152180
end
153181

154182
# When connections are established in the future, begin a transaction too
@@ -167,50 +195,38 @@ def setup_fixtures(config = ActiveRecord::Base)
167195
if connection
168196
setup_shared_connection_pool
169197

170-
if !@fixture_connections.include?(connection)
171-
connection.begin_transaction joinable: false, _lazy: false
198+
if !@fixture_connections.key?(connection)
172199
connection.pool.lock_thread = true if lock_threads
173-
@fixture_connections << connection
200+
@fixture_connections[connection] = connection.begin_transaction(joinable: false, _lazy: false)
174201
end
175202
end
176203
end
177204
end
178-
179-
# Load fixtures for every test.
180-
else
181-
ActiveRecord::FixtureSet.reset_cache
182-
@@already_loaded_fixtures.clear
183-
@loaded_fixtures = load_fixtures(config)
184205
end
185206

186-
# Instantiate fixtures for every test if requested.
187-
instantiate_fixtures if use_instantiated_fixtures
188-
end
189-
190-
def teardown_fixtures
191-
# Rollback changes if a transaction is active.
192-
if run_in_transaction?
207+
def teardown_transactional_fixtures
193208
ActiveSupport::Notifications.unsubscribe(@connection_subscriber) if @connection_subscriber
194-
@fixture_connections.each do |connection|
195-
connection.rollback_transaction if connection.transaction_open?
209+
@fixture_connections.each do |connection, transaction|
210+
begin
211+
connection.rollback_transaction(transaction)
212+
rescue ActiveRecord::StatementInvalid
213+
# Something commited or rolled back the transaction.
214+
# We can no longer trust the database state is clean.
215+
invalidate_already_loaded_fixtures
216+
# We also don't know for sure the connection wasn't
217+
# mutated in dangerous ways.
218+
connection.disconnect!
219+
end
196220
connection.pool.lock_thread = false
197221
end
198222
@fixture_connections.clear
199223
teardown_shared_connection_pool
200-
else
201-
ActiveRecord::FixtureSet.reset_cache
202224
end
203225

204-
ActiveRecord::Base.connection_handler.clear_active_connections!(:all)
205-
end
206-
207-
def enlist_fixture_connections
208-
setup_shared_connection_pool
209-
210-
ActiveRecord::Base.connection_handler.connection_pool_list(:writing).map(&:connection)
211-
end
226+
def invalidate_already_loaded_fixtures
227+
@@already_loaded_fixtures.clear
228+
end
212229

213-
private
214230
# Shares the writing connection pool with connections on
215231
# other handlers.
216232
#

activerecord/test/cases/active_record_test.rb

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,11 @@
44
require "rack"
55

66
class ActiveRecordTest < ActiveRecord::TestCase
7+
self.use_transactional_tests = false
8+
79
unless in_memory_db?
810
test ".disconnect_all! closes all connections" do
9-
ActiveRecord::Base.connection.active?
11+
ActiveRecord::Base.connection.connect!
1012
assert_predicate ActiveRecord::Base, :connected?
1113

1214
ActiveRecord.disconnect_all!

activerecord/test/cases/adapters/abstract_mysql_adapter/connection_test.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ def test_no_automatic_reconnection_after_timeout
3737
assert_not_predicate @connection, :active?
3838
ensure
3939
# Repair all fixture connections so other tests won't break.
40-
@fixture_connections.each(&:verify!)
40+
@fixture_connections.each_key(&:verify!)
4141
end
4242

4343
def test_successful_reconnection_after_timeout_with_manual_reconnect

activerecord/test/cases/adapters/postgresql/connection_test.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,7 @@ def test_reconnection_after_actual_disconnection_with_verify
145145
assert_predicate @connection, :active?
146146
ensure
147147
# Repair all fixture connections so other tests won't break.
148-
@fixture_connections.each(&:verify!)
148+
@fixture_connections.each_key(&:verify!)
149149
end
150150

151151
def test_set_session_variable_true

activerecord/test/cases/connection_adapters/connection_handler_test.rb

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
module ActiveRecord
77
module ConnectionAdapters
88
class ConnectionHandlerTest < ActiveRecord::TestCase
9+
self.use_transactional_tests = false
10+
911
fixtures :people
1012

1113
def setup
@@ -93,6 +95,8 @@ def test_fixtures_dont_raise_if_theres_no_writing_pool_config
9395
connection_handler = ActiveRecord::Base.connection_handler
9496
ActiveRecord::Base.connection_handler = ActiveRecord::ConnectionAdapters::ConnectionHandler.new
9597

98+
setup_transactional_fixtures
99+
96100
assert_nothing_raised do
97101
ActiveRecord::Base.connects_to(database: { reading: :arunit, writing: :arunit })
98102
end
@@ -101,6 +105,8 @@ def test_fixtures_dont_raise_if_theres_no_writing_pool_config
101105
ro_conn = ActiveRecord::Base.connection_handler.retrieve_connection("ActiveRecord::Base", role: :reading)
102106

103107
assert_equal rw_conn, ro_conn
108+
109+
teardown_transactional_fixtures
104110
ensure
105111
ActiveRecord::Base.connection_handler = connection_handler
106112
end

activerecord/test/cases/connection_adapters/schema_cache_test.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
module ActiveRecord
66
module ConnectionAdapters
77
class SchemaCacheTest < ActiveRecord::TestCase
8+
self.use_transactional_tests = false
9+
810
def setup
911
@connection = ARUnit2Model.connection
1012
@cache = new_bound_reflection

activerecord/test/cases/primary_class_test.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
require "cases/helper"
44

55
class PrimaryClassTest < ActiveRecord::TestCase
6+
self.use_transactional_tests = false
7+
68
def teardown
79
clean_up_connection_handler
810
end

0 commit comments

Comments
 (0)