mirror of https://github.com/rails/rails
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.
This commit is contained in:
parent
bab4aa7cb2
commit
6f3b46b02d
|
@ -1,3 +1,13 @@
|
|||
* 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.
|
||||
|
||||
*Jean Boussier*
|
||||
|
||||
* Support `:source_location` tag option for query log tags
|
||||
|
||||
```ruby
|
||||
|
|
|
@ -718,13 +718,14 @@ module ActiveRecord
|
|||
end
|
||||
end
|
||||
|
||||
|
||||
# Disconnects from the database if already connected. Otherwise, this
|
||||
# method does nothing.
|
||||
def disconnect!
|
||||
clear_cache!(new_connection: true)
|
||||
reset_transaction
|
||||
@raw_connection_dirty = false
|
||||
@lock.synchronize do
|
||||
clear_cache!(new_connection: true)
|
||||
reset_transaction
|
||||
@raw_connection_dirty = false
|
||||
end
|
||||
end
|
||||
|
||||
# Immediately forget this connection ever existed. Unlike disconnect!,
|
||||
|
@ -780,19 +781,17 @@ module ActiveRecord
|
|||
# is no longer active, then this method will reconnect to the database.
|
||||
def verify!
|
||||
unless active?
|
||||
if @unconfigured_connection
|
||||
@lock.synchronize do
|
||||
if @unconfigured_connection
|
||||
@raw_connection = @unconfigured_connection
|
||||
@unconfigured_connection = nil
|
||||
configure_connection
|
||||
@verified = true
|
||||
return
|
||||
end
|
||||
@lock.synchronize do
|
||||
if @unconfigured_connection
|
||||
@raw_connection = @unconfigured_connection
|
||||
@unconfigured_connection = nil
|
||||
configure_connection
|
||||
@verified = true
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
reconnect!(restore_transactions: true)
|
||||
reconnect!(restore_transactions: true)
|
||||
end
|
||||
end
|
||||
|
||||
@verified = true
|
||||
|
|
|
@ -113,7 +113,7 @@ module ActiveRecord
|
|||
end
|
||||
|
||||
def active?
|
||||
!!@raw_connection&.ping
|
||||
connected? && @lock.synchronize { @raw_connection&.ping } || false
|
||||
end
|
||||
|
||||
alias :reset! :reconnect!
|
||||
|
@ -121,15 +121,19 @@ module ActiveRecord
|
|||
# Disconnects from the database if already connected.
|
||||
# Otherwise, this method does nothing.
|
||||
def disconnect!
|
||||
super
|
||||
@raw_connection&.close
|
||||
@raw_connection = nil
|
||||
@lock.synchronize do
|
||||
super
|
||||
@raw_connection&.close
|
||||
@raw_connection = nil
|
||||
end
|
||||
end
|
||||
|
||||
def discard! # :nodoc:
|
||||
super
|
||||
@raw_connection&.automatic_close = false
|
||||
@raw_connection = nil
|
||||
@lock.synchronize do
|
||||
super
|
||||
@raw_connection&.automatic_close = false
|
||||
@raw_connection = nil
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
@ -144,9 +148,11 @@ module ActiveRecord
|
|||
end
|
||||
|
||||
def reconnect
|
||||
@raw_connection&.close
|
||||
@raw_connection = nil
|
||||
connect
|
||||
@lock.synchronize do
|
||||
@raw_connection&.close
|
||||
@raw_connection = nil
|
||||
connect
|
||||
end
|
||||
end
|
||||
|
||||
def configure_connection
|
||||
|
|
|
@ -117,7 +117,7 @@ module ActiveRecord
|
|||
end
|
||||
|
||||
def active?
|
||||
connection&.ping || false
|
||||
connected? && @lock.synchronize { @raw_connection&.ping } || false
|
||||
rescue ::Trilogy::Error
|
||||
false
|
||||
end
|
||||
|
@ -125,18 +125,18 @@ module ActiveRecord
|
|||
alias reset! reconnect!
|
||||
|
||||
def disconnect!
|
||||
super
|
||||
unless connection.nil?
|
||||
connection.close
|
||||
self.connection = nil
|
||||
@lock.synchronize do
|
||||
super
|
||||
@raw_connection&.close
|
||||
@raw_connection = nil
|
||||
end
|
||||
end
|
||||
|
||||
def discard!
|
||||
super
|
||||
unless connection.nil?
|
||||
connection.discard!
|
||||
self.connection = nil
|
||||
@lock.synchronize do
|
||||
super
|
||||
@raw_connection&.discard!
|
||||
@raw_connection = nil
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -166,23 +166,15 @@ module ActiveRecord
|
|||
exception.error_code if exception.respond_to?(:error_code)
|
||||
end
|
||||
|
||||
def connection
|
||||
@raw_connection
|
||||
end
|
||||
|
||||
def connection=(conn)
|
||||
@raw_connection = conn
|
||||
end
|
||||
|
||||
def connect
|
||||
self.connection = self.class.new_client(@config)
|
||||
@raw_connection = self.class.new_client(@config)
|
||||
rescue ConnectionNotEstablished => ex
|
||||
raise ex.set_pool(@pool)
|
||||
end
|
||||
|
||||
def reconnect
|
||||
connection&.close
|
||||
self.connection = nil
|
||||
@raw_connection&.close
|
||||
@raw_connection = nil
|
||||
connect
|
||||
end
|
||||
|
||||
|
|
|
@ -809,6 +809,48 @@ module ActiveRecord
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
class AdapterThreadSafetyTest < ActiveRecord::TestCase
|
||||
setup do
|
||||
@threads = []
|
||||
@connection = ActiveRecord::Base.connection_pool.checkout
|
||||
end
|
||||
|
||||
teardown do
|
||||
@threads.each(&:kill)
|
||||
end
|
||||
|
||||
test "#active? is synchronized" do
|
||||
threads(2, 25) { @connection.select_all("SELECT 1") }
|
||||
threads(2, 25) { @connection.verify! }
|
||||
threads(2, 25) { @connection.disconnect! }
|
||||
|
||||
join
|
||||
end
|
||||
|
||||
test "#verify! is synchronized" do
|
||||
threads(2, 25) { @connection.verify! }
|
||||
threads(2, 25) { @connection.disconnect! }
|
||||
|
||||
join
|
||||
end
|
||||
|
||||
private
|
||||
def join
|
||||
@threads.shuffle.each(&:join)
|
||||
end
|
||||
|
||||
def threads(count, times)
|
||||
@threads += count.times.map do
|
||||
Thread.new do
|
||||
times.times do
|
||||
yield
|
||||
Thread.pass
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if ActiveRecord::Base.connection.supports_advisory_locks?
|
||||
|
|
|
@ -95,24 +95,24 @@ class TrilogyAdapterTest < ActiveRecord::TrilogyTestCase
|
|||
end
|
||||
|
||||
test "#active? answers false with connection and exception" do
|
||||
@conn.send(:connection).stub(:ping, -> { raise ::Trilogy::BaseError.new }) do
|
||||
@conn.instance_variable_get(:@raw_connection).stub(:ping, -> { raise ::Trilogy::BaseError.new }) do
|
||||
assert_equal false, @conn.active?
|
||||
end
|
||||
end
|
||||
|
||||
test "#reconnect answers new connection with existing connection" do
|
||||
old_connection = @conn.send(:connection)
|
||||
old_connection = @conn.instance_variable_get(:@raw_connection)
|
||||
@conn.reconnect!
|
||||
connection = @conn.send(:connection)
|
||||
connection = @conn.instance_variable_get(:@raw_connection)
|
||||
|
||||
assert_instance_of Trilogy, connection
|
||||
assert_not_equal old_connection, connection
|
||||
end
|
||||
|
||||
test "#reset answers new connection with existing connection" do
|
||||
old_connection = @conn.send(:connection)
|
||||
old_connection = @conn.instance_variable_get(:@raw_connection)
|
||||
@conn.reset!
|
||||
connection = @conn.send(:connection)
|
||||
connection = @conn.instance_variable_get(:@raw_connection)
|
||||
|
||||
assert_instance_of Trilogy, connection
|
||||
assert_not_equal old_connection, connection
|
||||
|
|
Loading…
Reference in New Issue