Skip to content

Commit 6f3b46b

Browse files
committed
Properly synchronize Mysql2Adapter#active? and TrilogyAdapter#active?
As well as `disconnect!` and `verify!`. This generally isn't a big problem as connections must not be shared between threads, but is required when running transactional tests or system tests and could lead to a SEGV.
1 parent bab4aa7 commit 6f3b46b

File tree

6 files changed

+99
-50
lines changed

6 files changed

+99
-50
lines changed

activerecord/CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,13 @@
1+
* Properly synchronize `Mysql2Adapter#active?` and `TrilogyAdapter#active?`
2+
3+
As well as `disconnect!` and `verify!`.
4+
5+
This generally isn't a big problem as connections must not be shared between
6+
threads, but is required when running transactional tests or system tests
7+
and could lead to a SEGV.
8+
9+
*Jean Boussier*
10+
111
* Support `:source_location` tag option for query log tags
212

313
```ruby

activerecord/lib/active_record/connection_adapters/abstract_adapter.rb

Lines changed: 14 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -718,13 +718,14 @@ def reconnect!(restore_transactions: false)
718718
end
719719
end
720720

721-
722721
# Disconnects from the database if already connected. Otherwise, this
723722
# method does nothing.
724723
def disconnect!
725-
clear_cache!(new_connection: true)
726-
reset_transaction
727-
@raw_connection_dirty = false
724+
@lock.synchronize do
725+
clear_cache!(new_connection: true)
726+
reset_transaction
727+
@raw_connection_dirty = false
728+
end
728729
end
729730

730731
# Immediately forget this connection ever existed. Unlike disconnect!,
@@ -780,19 +781,17 @@ def requires_reloading?
780781
# is no longer active, then this method will reconnect to the database.
781782
def verify!
782783
unless active?
783-
if @unconfigured_connection
784-
@lock.synchronize do
785-
if @unconfigured_connection
786-
@raw_connection = @unconfigured_connection
787-
@unconfigured_connection = nil
788-
configure_connection
789-
@verified = true
790-
return
791-
end
784+
@lock.synchronize do
785+
if @unconfigured_connection
786+
@raw_connection = @unconfigured_connection
787+
@unconfigured_connection = nil
788+
configure_connection
789+
@verified = true
790+
return
792791
end
793-
end
794792

795-
reconnect!(restore_transactions: true)
793+
reconnect!(restore_transactions: true)
794+
end
796795
end
797796

798797
@verified = true

activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -113,23 +113,27 @@ def connected?
113113
end
114114

115115
def active?
116-
!!@raw_connection&.ping
116+
connected? && @lock.synchronize { @raw_connection&.ping } || false
117117
end
118118

119119
alias :reset! :reconnect!
120120

121121
# Disconnects from the database if already connected.
122122
# Otherwise, this method does nothing.
123123
def disconnect!
124-
super
125-
@raw_connection&.close
126-
@raw_connection = nil
124+
@lock.synchronize do
125+
super
126+
@raw_connection&.close
127+
@raw_connection = nil
128+
end
127129
end
128130

129131
def discard! # :nodoc:
130-
super
131-
@raw_connection&.automatic_close = false
132-
@raw_connection = nil
132+
@lock.synchronize do
133+
super
134+
@raw_connection&.automatic_close = false
135+
@raw_connection = nil
136+
end
133137
end
134138

135139
private
@@ -144,9 +148,11 @@ def connect
144148
end
145149

146150
def reconnect
147-
@raw_connection&.close
148-
@raw_connection = nil
149-
connect
151+
@lock.synchronize do
152+
@raw_connection&.close
153+
@raw_connection = nil
154+
connect
155+
end
150156
end
151157

152158
def configure_connection

activerecord/lib/active_record/connection_adapters/trilogy_adapter.rb

Lines changed: 12 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -117,26 +117,26 @@ def connected?
117117
end
118118

119119
def active?
120-
connection&.ping || false
120+
connected? && @lock.synchronize { @raw_connection&.ping } || false
121121
rescue ::Trilogy::Error
122122
false
123123
end
124124

125125
alias reset! reconnect!
126126

127127
def disconnect!
128-
super
129-
unless connection.nil?
130-
connection.close
131-
self.connection = nil
128+
@lock.synchronize do
129+
super
130+
@raw_connection&.close
131+
@raw_connection = nil
132132
end
133133
end
134134

135135
def discard!
136-
super
137-
unless connection.nil?
138-
connection.discard!
139-
self.connection = nil
136+
@lock.synchronize do
137+
super
138+
@raw_connection&.discard!
139+
@raw_connection = nil
140140
end
141141
end
142142

@@ -166,23 +166,15 @@ def error_number(exception)
166166
exception.error_code if exception.respond_to?(:error_code)
167167
end
168168

169-
def connection
170-
@raw_connection
171-
end
172-
173-
def connection=(conn)
174-
@raw_connection = conn
175-
end
176-
177169
def connect
178-
self.connection = self.class.new_client(@config)
170+
@raw_connection = self.class.new_client(@config)
179171
rescue ConnectionNotEstablished => ex
180172
raise ex.set_pool(@pool)
181173
end
182174

183175
def reconnect
184-
connection&.close
185-
self.connection = nil
176+
@raw_connection&.close
177+
@raw_connection = nil
186178
connect
187179
end
188180

activerecord/test/cases/adapter_test.rb

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -809,6 +809,48 @@ def kill_connection_from_server(connection_id)
809809
end
810810
end
811811
end
812+
813+
class AdapterThreadSafetyTest < ActiveRecord::TestCase
814+
setup do
815+
@threads = []
816+
@connection = ActiveRecord::Base.connection_pool.checkout
817+
end
818+
819+
teardown do
820+
@threads.each(&:kill)
821+
end
822+
823+
test "#active? is synchronized" do
824+
threads(2, 25) { @connection.select_all("SELECT 1") }
825+
threads(2, 25) { @connection.verify! }
826+
threads(2, 25) { @connection.disconnect! }
827+
828+
join
829+
end
830+
831+
test "#verify! is synchronized" do
832+
threads(2, 25) { @connection.verify! }
833+
threads(2, 25) { @connection.disconnect! }
834+
835+
join
836+
end
837+
838+
private
839+
def join
840+
@threads.shuffle.each(&:join)
841+
end
842+
843+
def threads(count, times)
844+
@threads += count.times.map do
845+
Thread.new do
846+
times.times do
847+
yield
848+
Thread.pass
849+
end
850+
end
851+
end
852+
end
853+
end
812854
end
813855

814856
if ActiveRecord::Base.connection.supports_advisory_locks?

activerecord/test/cases/adapters/trilogy/trilogy_adapter_test.rb

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -95,24 +95,24 @@ class TrilogyAdapterTest < ActiveRecord::TrilogyTestCase
9595
end
9696

9797
test "#active? answers false with connection and exception" do
98-
@conn.send(:connection).stub(:ping, -> { raise ::Trilogy::BaseError.new }) do
98+
@conn.instance_variable_get(:@raw_connection).stub(:ping, -> { raise ::Trilogy::BaseError.new }) do
9999
assert_equal false, @conn.active?
100100
end
101101
end
102102

103103
test "#reconnect answers new connection with existing connection" do
104-
old_connection = @conn.send(:connection)
104+
old_connection = @conn.instance_variable_get(:@raw_connection)
105105
@conn.reconnect!
106-
connection = @conn.send(:connection)
106+
connection = @conn.instance_variable_get(:@raw_connection)
107107

108108
assert_instance_of Trilogy, connection
109109
assert_not_equal old_connection, connection
110110
end
111111

112112
test "#reset answers new connection with existing connection" do
113-
old_connection = @conn.send(:connection)
113+
old_connection = @conn.instance_variable_get(:@raw_connection)
114114
@conn.reset!
115-
connection = @conn.send(:connection)
115+
connection = @conn.instance_variable_get(:@raw_connection)
116116

117117
assert_instance_of Trilogy, connection
118118
assert_not_equal old_connection, connection

0 commit comments

Comments
 (0)