2017-07-09 13:41:28 -04:00
|
|
|
# frozen_string_literal: true
|
|
|
|
|
2011-06-06 14:17:44 -04:00
|
|
|
require "cases/helper"
|
2016-08-06 12:26:20 -04:00
|
|
|
require "concurrent/atomic/count_down_latch"
|
2010-07-16 17:39:40 -04:00
|
|
|
|
|
|
|
module ActiveRecord
|
|
|
|
module ConnectionAdapters
|
|
|
|
class ConnectionPoolTest < ActiveRecord::TestCase
|
2012-03-08 18:40:23 -05:00
|
|
|
attr_reader :pool
|
|
|
|
|
2011-02-04 13:19:02 -05:00
|
|
|
def setup
|
2011-12-30 17:09:39 -05:00
|
|
|
super
|
|
|
|
|
2011-02-04 13:19:02 -05:00
|
|
|
# Keep a duplicate pool so we do not bother others
|
2019-10-16 13:26:43 -04:00
|
|
|
@db_config = ActiveRecord::Base.connection_pool.db_config
|
2019-11-05 17:05:54 -05:00
|
|
|
@pool_config = ActiveRecord::ConnectionAdapters::PoolConfig.new("primary", @db_config)
|
|
|
|
@pool = ConnectionPool.new(@pool_config)
|
2011-02-11 17:32:21 -05:00
|
|
|
|
|
|
|
if in_memory_db?
|
|
|
|
# Separate connections to an in-memory database create an entirely new database,
|
|
|
|
# with an empty schema etc, so we just stub out this schema on the fly.
|
|
|
|
@pool.with_connection do |connection|
|
|
|
|
connection.create_table :posts do |t|
|
|
|
|
t.integer :cololumn
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
2011-02-04 13:19:02 -05:00
|
|
|
end
|
|
|
|
|
2014-03-14 00:35:58 -04:00
|
|
|
teardown do
|
2012-01-03 14:06:27 -05:00
|
|
|
@pool.disconnect!
|
2011-12-30 17:09:39 -05:00
|
|
|
end
|
|
|
|
|
2012-03-08 18:40:23 -05:00
|
|
|
def active_connections(pool)
|
|
|
|
pool.connections.find_all(&:in_use?)
|
|
|
|
end
|
|
|
|
|
2012-03-12 14:51:28 -04:00
|
|
|
def test_checkout_after_close
|
|
|
|
connection = pool.connection
|
2018-01-25 18:14:09 -05:00
|
|
|
assert_predicate connection, :in_use?
|
2012-03-12 14:51:28 -04:00
|
|
|
|
|
|
|
connection.close
|
2018-01-25 18:14:09 -05:00
|
|
|
assert_not_predicate connection, :in_use?
|
2012-03-12 14:51:28 -04:00
|
|
|
|
2018-01-25 18:14:09 -05:00
|
|
|
assert_predicate pool.connection, :in_use?
|
2012-03-12 14:51:28 -04:00
|
|
|
end
|
|
|
|
|
2012-03-08 19:12:49 -05:00
|
|
|
def test_released_connection_moves_between_threads
|
|
|
|
thread_conn = nil
|
|
|
|
|
|
|
|
Thread.new {
|
|
|
|
pool.with_connection do |conn|
|
|
|
|
thread_conn = conn
|
|
|
|
end
|
|
|
|
}.join
|
|
|
|
|
|
|
|
assert thread_conn
|
|
|
|
|
|
|
|
Thread.new {
|
|
|
|
pool.with_connection do |conn|
|
|
|
|
assert_equal thread_conn, conn
|
|
|
|
end
|
|
|
|
}.join
|
|
|
|
end
|
|
|
|
|
2012-03-08 18:40:23 -05:00
|
|
|
def test_with_connection
|
|
|
|
assert_equal 0, active_connections(pool).size
|
|
|
|
|
|
|
|
main_thread = pool.connection
|
|
|
|
assert_equal 1, active_connections(pool).size
|
|
|
|
|
|
|
|
Thread.new {
|
|
|
|
pool.with_connection do |conn|
|
|
|
|
assert conn
|
|
|
|
assert_equal 2, active_connections(pool).size
|
|
|
|
end
|
|
|
|
assert_equal 1, active_connections(pool).size
|
|
|
|
}.join
|
|
|
|
|
|
|
|
main_thread.close
|
|
|
|
assert_equal 0, active_connections(pool).size
|
|
|
|
end
|
|
|
|
|
|
|
|
def test_active_connection_in_use
|
2018-01-25 18:14:09 -05:00
|
|
|
assert_not_predicate pool, :active_connection?
|
2012-03-08 18:40:23 -05:00
|
|
|
main_thread = pool.connection
|
|
|
|
|
2018-01-25 18:14:09 -05:00
|
|
|
assert_predicate pool, :active_connection?
|
2012-03-08 18:40:23 -05:00
|
|
|
|
|
|
|
main_thread.close
|
|
|
|
|
2018-01-25 18:14:09 -05:00
|
|
|
assert_not_predicate pool, :active_connection?
|
2012-03-08 18:40:23 -05:00
|
|
|
end
|
|
|
|
|
2011-12-30 17:56:26 -05:00
|
|
|
def test_full_pool_exception
|
2018-08-19 11:09:32 -04:00
|
|
|
@pool.checkout_timeout = 0.001 # no need to delay test suite by waiting the whole full default timeout
|
2018-01-02 20:47:04 -05:00
|
|
|
@pool.size.times { assert @pool.checkout }
|
2018-08-19 11:09:32 -04:00
|
|
|
|
ConnectionPool, unify exceptions, ConnectionTimeoutError
As a result of different commits, ConnectionPool had become
of two minds about exceptions, sometimes using PoolFullError
and sometimes using ConnectionTimeoutError. In fact, it was
using ConnectionTimeoutError internally, but then recueing
and re-raising as a PoolFullError.
There's no reason for this bifurcation, standardize on
ConnectionTimeoutError, which is the rails2 name and still
accurately describes semantics at this point.
History
In Rails2, ConnectionPool raises a ConnectionTimeoutError if
it can't get a connection within timeout.
Originally in master/rails3, @tenderlove had planned on removing
wait/blocking in connectionpool entirely, at that point he changed
exception to PoolFullError.
But then later wait/blocking came back, but exception remained
PoolFullError.
Then in 02b233556377 pmahoney introduced fair waiting logic, and
brought back ConnectionTimeoutError, introducing the weird bifurcation.
ConnectionTimeoutError accurately describes semantics as of this
point, and is backwards compat with rails2, there's no reason
for PoolFullError to be introduced, and no reason for two
different exception types to be used internally, no reason
to rescue one and re-raise as another. Unify!
2012-09-11 10:38:27 -04:00
|
|
|
assert_raises(ConnectionTimeoutError) do
|
2014-03-17 20:37:05 -04:00
|
|
|
@pool.checkout
|
2011-12-30 17:56:26 -05:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2012-04-15 21:13:15 -04:00
|
|
|
def test_full_pool_blocks
|
|
|
|
cs = @pool.size.times.map { @pool.checkout }
|
|
|
|
t = Thread.new { @pool.checkout }
|
|
|
|
|
|
|
|
# make sure our thread is in the timeout section
|
2015-05-13 21:14:53 -04:00
|
|
|
Thread.pass until @pool.num_waiting_in_queue == 1
|
2012-04-15 21:13:15 -04:00
|
|
|
|
|
|
|
connection = cs.first
|
|
|
|
connection.close
|
|
|
|
assert_equal connection, t.join.value
|
|
|
|
end
|
|
|
|
|
2018-01-12 15:58:04 -05:00
|
|
|
def test_full_pool_blocking_shares_load_interlock
|
|
|
|
@pool.instance_variable_set(:@size, 1)
|
|
|
|
|
|
|
|
load_interlock_latch = Concurrent::CountDownLatch.new
|
|
|
|
connection_latch = Concurrent::CountDownLatch.new
|
|
|
|
|
|
|
|
able_to_get_connection = false
|
|
|
|
able_to_load = false
|
|
|
|
|
|
|
|
thread_with_load_interlock = Thread.new do
|
|
|
|
ActiveSupport::Dependencies.interlock.running do
|
|
|
|
load_interlock_latch.count_down
|
|
|
|
connection_latch.wait
|
|
|
|
|
|
|
|
@pool.with_connection do
|
|
|
|
able_to_get_connection = true
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
thread_with_last_connection = Thread.new do
|
|
|
|
@pool.with_connection do
|
|
|
|
connection_latch.count_down
|
|
|
|
load_interlock_latch.wait
|
|
|
|
|
|
|
|
ActiveSupport::Dependencies.interlock.loading do
|
|
|
|
able_to_load = true
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
thread_with_load_interlock.join
|
|
|
|
thread_with_last_connection.join
|
|
|
|
|
|
|
|
assert able_to_get_connection
|
|
|
|
assert able_to_load
|
|
|
|
end
|
|
|
|
|
2012-04-15 21:13:15 -04:00
|
|
|
def test_removing_releases_latch
|
|
|
|
cs = @pool.size.times.map { @pool.checkout }
|
|
|
|
t = Thread.new { @pool.checkout }
|
|
|
|
|
|
|
|
# make sure our thread is in the timeout section
|
2015-05-13 21:14:53 -04:00
|
|
|
Thread.pass until @pool.num_waiting_in_queue == 1
|
2012-04-15 21:13:15 -04:00
|
|
|
|
|
|
|
connection = cs.first
|
|
|
|
@pool.remove connection
|
|
|
|
assert_respond_to t.join.value, :execute
|
2013-07-08 19:21:47 -04:00
|
|
|
connection.close
|
2012-04-15 21:13:15 -04:00
|
|
|
end
|
|
|
|
|
2011-12-30 17:31:30 -05:00
|
|
|
def test_reap_and_active
|
2011-12-30 17:26:29 -05:00
|
|
|
@pool.checkout
|
|
|
|
@pool.checkout
|
|
|
|
@pool.checkout
|
|
|
|
|
|
|
|
connections = @pool.connections.dup
|
|
|
|
|
|
|
|
@pool.reap
|
|
|
|
|
2011-12-30 17:31:30 -05:00
|
|
|
assert_equal connections.length, @pool.connections.length
|
|
|
|
end
|
|
|
|
|
|
|
|
def test_reap_inactive
|
2015-07-13 14:22:54 -04:00
|
|
|
ready = Concurrent::CountDownLatch.new
|
2011-12-30 17:31:30 -05:00
|
|
|
@pool.checkout
|
2014-03-07 08:36:09 -05:00
|
|
|
child = Thread.new do
|
|
|
|
@pool.checkout
|
|
|
|
@pool.checkout
|
2015-07-13 14:22:54 -04:00
|
|
|
ready.count_down
|
2014-03-07 08:36:09 -05:00
|
|
|
Thread.stop
|
2011-12-30 17:31:30 -05:00
|
|
|
end
|
2015-07-13 14:22:54 -04:00
|
|
|
ready.wait
|
2014-03-07 08:36:09 -05:00
|
|
|
|
|
|
|
assert_equal 3, active_connections(@pool).size
|
2011-12-30 17:31:30 -05:00
|
|
|
|
2014-03-07 08:36:09 -05:00
|
|
|
child.terminate
|
|
|
|
child.join
|
2011-12-30 17:31:30 -05:00
|
|
|
@pool.reap
|
|
|
|
|
2014-03-07 08:36:09 -05:00
|
|
|
assert_equal 1, active_connections(@pool).size
|
2011-12-30 17:26:29 -05:00
|
|
|
ensure
|
2016-07-06 10:20:37 -04:00
|
|
|
@pool.connections.each { |conn| conn.close if conn.in_use? }
|
2011-12-30 17:26:29 -05:00
|
|
|
end
|
|
|
|
|
2018-08-18 22:29:02 -04:00
|
|
|
def test_idle_timeout_configuration
|
|
|
|
@pool.disconnect!
|
2019-12-17 15:42:48 -05:00
|
|
|
|
|
|
|
config = @db_config.configuration_hash.merge(idle_timeout: "0.02")
|
2020-02-20 14:06:17 -05:00
|
|
|
db_config = ActiveRecord::DatabaseConfigurations::HashConfig.new(@db_config.env_name, @db_config.name, config)
|
2019-12-17 15:42:48 -05:00
|
|
|
|
|
|
|
pool_config = ActiveRecord::ConnectionAdapters::PoolConfig.new("primary", db_config)
|
2019-11-05 17:05:54 -05:00
|
|
|
@pool = ConnectionPool.new(pool_config)
|
2018-08-18 22:29:02 -04:00
|
|
|
idle_conn = @pool.checkout
|
|
|
|
@pool.checkin(idle_conn)
|
|
|
|
|
2018-08-19 11:09:32 -04:00
|
|
|
idle_conn.instance_variable_set(
|
|
|
|
:@idle_since,
|
|
|
|
Concurrent.monotonic_time - 0.01
|
|
|
|
)
|
2018-08-18 22:29:02 -04:00
|
|
|
|
|
|
|
@pool.flush
|
|
|
|
assert_equal 1, @pool.connections.length
|
|
|
|
|
2018-08-19 11:09:32 -04:00
|
|
|
idle_conn.instance_variable_set(
|
|
|
|
:@idle_since,
|
|
|
|
Concurrent.monotonic_time - 0.02
|
|
|
|
)
|
|
|
|
|
2018-08-18 22:29:02 -04:00
|
|
|
@pool.flush
|
|
|
|
assert_equal 0, @pool.connections.length
|
|
|
|
end
|
|
|
|
|
|
|
|
def test_disable_flush
|
|
|
|
@pool.disconnect!
|
2019-12-17 15:42:48 -05:00
|
|
|
|
|
|
|
config = @db_config.configuration_hash.merge(idle_timeout: -5)
|
2020-02-20 14:06:17 -05:00
|
|
|
db_config = ActiveRecord::DatabaseConfigurations::HashConfig.new(@db_config.env_name, @db_config.name, config)
|
2019-12-17 15:42:48 -05:00
|
|
|
pool_config = ActiveRecord::ConnectionAdapters::PoolConfig.new("primary", db_config)
|
2019-11-05 17:05:54 -05:00
|
|
|
@pool = ConnectionPool.new(pool_config)
|
2018-08-18 22:29:02 -04:00
|
|
|
idle_conn = @pool.checkout
|
|
|
|
@pool.checkin(idle_conn)
|
|
|
|
|
2018-08-19 11:09:32 -04:00
|
|
|
idle_conn.instance_variable_set(
|
|
|
|
:@idle_since,
|
|
|
|
Concurrent.monotonic_time - 1
|
|
|
|
)
|
2018-08-18 22:29:02 -04:00
|
|
|
|
|
|
|
@pool.flush
|
|
|
|
assert_equal 1, @pool.connections.length
|
|
|
|
end
|
|
|
|
|
2017-11-25 00:35:13 -05:00
|
|
|
def test_flush
|
|
|
|
idle_conn = @pool.checkout
|
|
|
|
recent_conn = @pool.checkout
|
|
|
|
active_conn = @pool.checkout
|
|
|
|
|
|
|
|
@pool.checkin idle_conn
|
|
|
|
@pool.checkin recent_conn
|
|
|
|
|
|
|
|
assert_equal 3, @pool.connections.length
|
|
|
|
|
2018-08-19 11:09:32 -04:00
|
|
|
idle_conn.instance_variable_set(
|
|
|
|
:@idle_since,
|
|
|
|
Concurrent.monotonic_time - 1000
|
|
|
|
)
|
2017-11-25 00:35:13 -05:00
|
|
|
|
|
|
|
@pool.flush(30)
|
|
|
|
|
|
|
|
assert_equal 2, @pool.connections.length
|
|
|
|
|
|
|
|
assert_equal [recent_conn, active_conn].sort_by(&:__id__), @pool.connections.sort_by(&:__id__)
|
|
|
|
ensure
|
|
|
|
@pool.checkin active_conn
|
|
|
|
end
|
|
|
|
|
|
|
|
def test_flush_bang
|
|
|
|
idle_conn = @pool.checkout
|
|
|
|
recent_conn = @pool.checkout
|
|
|
|
active_conn = @pool.checkout
|
|
|
|
_dead_conn = Thread.new { @pool.checkout }.join
|
|
|
|
|
|
|
|
@pool.checkin idle_conn
|
|
|
|
@pool.checkin recent_conn
|
|
|
|
|
|
|
|
assert_equal 4, @pool.connections.length
|
|
|
|
|
|
|
|
def idle_conn.seconds_idle
|
|
|
|
1000
|
|
|
|
end
|
|
|
|
|
|
|
|
@pool.flush!
|
|
|
|
|
|
|
|
assert_equal 1, @pool.connections.length
|
|
|
|
|
|
|
|
assert_equal [active_conn].sort_by(&:__id__), @pool.connections.sort_by(&:__id__)
|
|
|
|
ensure
|
|
|
|
@pool.checkin active_conn
|
|
|
|
end
|
|
|
|
|
2011-12-30 17:09:39 -05:00
|
|
|
def test_remove_connection
|
|
|
|
conn = @pool.checkout
|
2018-01-25 18:14:09 -05:00
|
|
|
assert_predicate conn, :in_use?
|
2011-12-30 17:09:39 -05:00
|
|
|
|
|
|
|
length = @pool.connections.length
|
|
|
|
@pool.remove conn
|
2018-01-25 18:14:09 -05:00
|
|
|
assert_predicate conn, :in_use?
|
2011-12-30 17:09:39 -05:00
|
|
|
assert_equal(length - 1, @pool.connections.length)
|
|
|
|
ensure
|
|
|
|
conn.close
|
|
|
|
end
|
|
|
|
|
2011-12-30 17:14:13 -05:00
|
|
|
def test_remove_connection_for_thread
|
|
|
|
conn = @pool.connection
|
|
|
|
@pool.remove conn
|
|
|
|
assert_not_equal(conn, @pool.connection)
|
|
|
|
ensure
|
2012-01-03 14:06:27 -05:00
|
|
|
conn.close if conn
|
2011-12-30 17:14:13 -05:00
|
|
|
end
|
|
|
|
|
2011-03-28 19:43:34 -04:00
|
|
|
def test_active_connection?
|
2018-01-25 18:14:09 -05:00
|
|
|
assert_not_predicate @pool, :active_connection?
|
2011-03-28 19:43:34 -04:00
|
|
|
assert @pool.connection
|
2018-01-25 18:14:09 -05:00
|
|
|
assert_predicate @pool, :active_connection?
|
2011-03-28 19:43:34 -04:00
|
|
|
@pool.release_connection
|
2018-01-25 18:14:09 -05:00
|
|
|
assert_not_predicate @pool, :active_connection?
|
2011-03-28 19:43:34 -04:00
|
|
|
end
|
|
|
|
|
2010-09-30 02:42:23 -04:00
|
|
|
def test_checkout_behaviour
|
2019-11-05 17:05:54 -05:00
|
|
|
pool = ConnectionPool.new(@pool_config)
|
2016-11-04 15:00:02 -04:00
|
|
|
main_connection = pool.connection
|
|
|
|
assert_not_nil main_connection
|
2010-09-30 02:42:23 -04:00
|
|
|
threads = []
|
|
|
|
4.times do |i|
|
2013-03-14 01:17:13 -04:00
|
|
|
threads << Thread.new(i) do
|
2016-11-04 15:00:02 -04:00
|
|
|
thread_connection = pool.connection
|
|
|
|
assert_not_nil thread_connection
|
|
|
|
thread_connection.close
|
2010-09-30 02:42:23 -04:00
|
|
|
end
|
|
|
|
end
|
2010-11-30 16:38:48 -05:00
|
|
|
|
2011-12-30 14:37:21 -05:00
|
|
|
threads.each(&:join)
|
2010-11-30 16:38:48 -05:00
|
|
|
|
2010-09-30 02:42:23 -04:00
|
|
|
Thread.new do
|
2011-12-30 14:37:21 -05:00
|
|
|
assert pool.connection
|
2011-11-29 18:04:41 -05:00
|
|
|
pool.connection.close
|
|
|
|
end.join
|
2010-09-30 02:42:23 -04:00
|
|
|
end
|
2011-02-04 18:54:32 -05:00
|
|
|
|
2017-04-13 16:05:39 -04:00
|
|
|
def test_checkout_order_is_lifo
|
2017-04-13 15:56:42 -04:00
|
|
|
conn1 = @pool.checkout
|
|
|
|
conn2 = @pool.checkout
|
|
|
|
@pool.checkin conn1
|
|
|
|
@pool.checkin conn2
|
|
|
|
assert_equal [conn2, conn1], 2.times.map { @pool.checkout }
|
|
|
|
end
|
|
|
|
|
2012-05-25 14:19:19 -04:00
|
|
|
# The connection pool is "fair" if threads waiting for
|
2015-04-15 02:13:14 -04:00
|
|
|
# connections receive them in the order in which they began
|
2012-05-25 14:19:19 -04:00
|
|
|
# waiting. This ensures that we don't timeout one HTTP request
|
|
|
|
# even while well under capacity in a multi-threaded environment
|
|
|
|
# such as a Java servlet container.
|
|
|
|
#
|
|
|
|
# We don't need strict fairness: if two connections become
|
2015-04-15 02:13:14 -04:00
|
|
|
# available at the same time, it's fine if two threads that were
|
2012-05-25 14:19:19 -04:00
|
|
|
# waiting acquire the connections out of order.
|
|
|
|
#
|
|
|
|
# Thus this test prepares waiting threads and then trickles in
|
|
|
|
# available connections slowly, ensuring the wakeup order is
|
|
|
|
# correct in this case.
|
|
|
|
def test_checkout_fairness
|
|
|
|
@pool.instance_variable_set(:@size, 10)
|
|
|
|
expected = (1..@pool.size).to_a.freeze
|
|
|
|
# check out all connections so our threads start out waiting
|
|
|
|
conns = expected.map { @pool.checkout }
|
|
|
|
mutex = Mutex.new
|
|
|
|
order = []
|
|
|
|
errors = []
|
|
|
|
|
|
|
|
threads = expected.map do |i|
|
|
|
|
t = Thread.new {
|
|
|
|
begin
|
2012-06-11 20:54:12 -04:00
|
|
|
@pool.checkout # never checked back in
|
2012-05-25 14:19:19 -04:00
|
|
|
mutex.synchronize { order << i }
|
|
|
|
rescue => e
|
|
|
|
mutex.synchronize { errors << e }
|
|
|
|
end
|
|
|
|
}
|
2015-05-13 21:14:53 -04:00
|
|
|
Thread.pass until @pool.num_waiting_in_queue == i
|
2012-05-25 14:19:19 -04:00
|
|
|
t
|
|
|
|
end
|
|
|
|
|
|
|
|
# this should wake up the waiting threads one by one in order
|
|
|
|
conns.each { |conn| @pool.checkin(conn); sleep 0.1 }
|
|
|
|
|
|
|
|
threads.each(&:join)
|
|
|
|
|
|
|
|
raise errors.first if errors.any?
|
|
|
|
|
|
|
|
assert_equal(expected, order)
|
|
|
|
end
|
|
|
|
|
|
|
|
# As mentioned in #test_checkout_fairness, we don't care about
|
|
|
|
# strict fairness. This test creates two groups of threads:
|
|
|
|
# group1 whose members all start waiting before any thread in
|
|
|
|
# group2. Enough connections are checked in to wakeup all
|
|
|
|
# group1 threads, and the fact that only group1 and no group2
|
|
|
|
# threads acquired a connection is enforced.
|
|
|
|
def test_checkout_fairness_by_group
|
|
|
|
@pool.instance_variable_set(:@size, 10)
|
|
|
|
# take all the connections
|
|
|
|
conns = (1..10).map { @pool.checkout }
|
|
|
|
mutex = Mutex.new
|
|
|
|
successes = [] # threads that successfully got a connection
|
|
|
|
errors = []
|
|
|
|
|
|
|
|
make_thread = proc do |i|
|
|
|
|
t = Thread.new {
|
|
|
|
begin
|
2012-06-11 20:54:12 -04:00
|
|
|
@pool.checkout # never checked back in
|
2012-05-25 14:19:19 -04:00
|
|
|
mutex.synchronize { successes << i }
|
|
|
|
rescue => e
|
|
|
|
mutex.synchronize { errors << e }
|
|
|
|
end
|
|
|
|
}
|
2015-05-13 21:14:53 -04:00
|
|
|
Thread.pass until @pool.num_waiting_in_queue == i
|
2012-05-25 14:19:19 -04:00
|
|
|
t
|
|
|
|
end
|
|
|
|
|
|
|
|
# all group1 threads start waiting before any in group2
|
|
|
|
group1 = (1..5).map(&make_thread)
|
|
|
|
group2 = (6..10).map(&make_thread)
|
|
|
|
|
|
|
|
# checkin n connections back to the pool
|
|
|
|
checkin = proc do |n|
|
|
|
|
n.times do
|
|
|
|
c = conns.pop
|
|
|
|
@pool.checkin(c)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
checkin.call(group1.size) # should wake up all group1
|
|
|
|
|
|
|
|
loop do
|
|
|
|
sleep 0.1
|
|
|
|
break if mutex.synchronize { (successes.size + errors.size) == group1.size }
|
|
|
|
end
|
|
|
|
|
|
|
|
winners = mutex.synchronize { successes.dup }
|
|
|
|
checkin.call(group2.size) # should wake up everyone remaining
|
|
|
|
|
|
|
|
group1.each(&:join)
|
|
|
|
group2.each(&:join)
|
|
|
|
|
|
|
|
assert_equal((1..group1.size).to_a, winners.sort)
|
|
|
|
|
|
|
|
if errors.any?
|
|
|
|
raise errors.first
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2017-02-26 11:00:22 -05:00
|
|
|
def test_automatic_reconnect_restores_after_disconnect
|
2019-11-05 17:05:54 -05:00
|
|
|
pool = ConnectionPool.new(@pool_config)
|
2011-02-04 18:54:32 -05:00
|
|
|
assert pool.automatic_reconnect
|
|
|
|
assert pool.connection
|
|
|
|
|
|
|
|
pool.disconnect!
|
|
|
|
assert pool.connection
|
2017-02-26 11:00:22 -05:00
|
|
|
end
|
2011-02-04 18:54:32 -05:00
|
|
|
|
2017-02-26 11:00:22 -05:00
|
|
|
def test_automatic_reconnect_can_be_disabled
|
2019-11-05 17:05:54 -05:00
|
|
|
pool = ConnectionPool.new(@pool_config)
|
2011-02-04 18:54:32 -05:00
|
|
|
pool.disconnect!
|
|
|
|
pool.automatic_reconnect = false
|
|
|
|
|
|
|
|
assert_raises(ConnectionNotEstablished) do
|
|
|
|
pool.connection
|
|
|
|
end
|
|
|
|
|
|
|
|
assert_raises(ConnectionNotEstablished) do
|
|
|
|
pool.with_connection
|
|
|
|
end
|
|
|
|
end
|
2011-08-08 18:27:54 -04:00
|
|
|
|
|
|
|
def test_pool_sets_connection_visitor
|
|
|
|
assert @pool.connection.visitor.is_a?(Arel::Visitors::ToSql)
|
|
|
|
end
|
2013-01-19 18:17:50 -05:00
|
|
|
|
2013-01-21 21:30:45 -05:00
|
|
|
# make sure exceptions are thrown when establish_connection
|
2013-06-15 03:14:28 -04:00
|
|
|
# is called with an anonymous class
|
2016-05-04 15:20:39 -04:00
|
|
|
def test_anonymous_class_exception
|
|
|
|
anonymous = Class.new(ActiveRecord::Base)
|
|
|
|
|
|
|
|
assert_raises(RuntimeError) do
|
|
|
|
anonymous.establish_connection
|
|
|
|
end
|
|
|
|
end
|
2015-04-29 06:02:47 -04:00
|
|
|
|
2017-01-09 11:09:36 -05:00
|
|
|
class ConnectionTestModel < ActiveRecord::Base
|
|
|
|
end
|
|
|
|
|
2015-06-28 18:11:34 -04:00
|
|
|
def test_connection_notification_is_called
|
|
|
|
payloads = []
|
2016-08-06 12:26:20 -04:00
|
|
|
subscription = ActiveSupport::Notifications.subscribe("!connection.active_record") do |name, started, finished, unique_id, payload|
|
2015-06-28 18:11:34 -04:00
|
|
|
payloads << payload
|
|
|
|
end
|
2017-01-09 11:09:36 -05:00
|
|
|
ConnectionTestModel.establish_connection :arunit
|
|
|
|
|
2019-12-18 02:21:04 -05:00
|
|
|
assert_equal [:config, :spec_name], payloads[0].keys.sort
|
2017-01-09 11:09:36 -05:00
|
|
|
assert_equal "ActiveRecord::ConnectionAdapters::ConnectionPoolTest::ConnectionTestModel", payloads[0][:spec_name]
|
2015-06-28 18:11:34 -04:00
|
|
|
ensure
|
|
|
|
ActiveSupport::Notifications.unsubscribe(subscription) if subscription
|
|
|
|
end
|
|
|
|
|
2015-04-29 06:02:47 -04:00
|
|
|
def test_pool_sets_connection_schema_cache
|
|
|
|
connection = pool.checkout
|
|
|
|
schema_cache = SchemaCache.new connection
|
|
|
|
schema_cache.add(:posts)
|
|
|
|
pool.schema_cache = schema_cache
|
|
|
|
|
|
|
|
pool.with_connection do |conn|
|
|
|
|
assert_equal pool.schema_cache.size, conn.schema_cache.size
|
|
|
|
assert_same pool.schema_cache.columns(:posts), conn.schema_cache.columns(:posts)
|
|
|
|
end
|
|
|
|
|
|
|
|
pool.checkin connection
|
|
|
|
end
|
2015-05-13 20:29:59 -04:00
|
|
|
|
|
|
|
def test_concurrent_connection_establishment
|
2015-05-18 08:00:42 -04:00
|
|
|
assert_operator @pool.connections.size, :<=, 1
|
|
|
|
|
2015-07-13 14:22:54 -04:00
|
|
|
all_threads_in_new_connection = Concurrent::CountDownLatch.new(@pool.size - @pool.connections.size)
|
|
|
|
all_go = Concurrent::CountDownLatch.new
|
2015-05-13 20:29:59 -04:00
|
|
|
|
|
|
|
@pool.singleton_class.class_eval do
|
|
|
|
define_method(:new_connection) do
|
2015-07-13 14:22:54 -04:00
|
|
|
all_threads_in_new_connection.count_down
|
|
|
|
all_go.wait
|
2015-05-13 20:29:59 -04:00
|
|
|
super()
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
connecting_threads = []
|
|
|
|
@pool.size.times do
|
|
|
|
connecting_threads << Thread.new { @pool.checkout }
|
|
|
|
end
|
|
|
|
|
|
|
|
begin
|
|
|
|
Timeout.timeout(5) do
|
|
|
|
# the kernel of the whole test is here, everything else is just scaffolding,
|
|
|
|
# this latch will not be released unless conn. pool allows for concurrent
|
|
|
|
# connection creation
|
2015-07-13 14:22:54 -04:00
|
|
|
all_threads_in_new_connection.wait
|
2015-05-13 20:29:59 -04:00
|
|
|
end
|
|
|
|
rescue Timeout::Error
|
2017-01-12 03:39:16 -05:00
|
|
|
flunk "pool unable to establish connections concurrently or implementation has " \
|
2016-08-06 12:26:20 -04:00
|
|
|
"changed, this test then needs to patch a different :new_connection method"
|
2015-05-13 20:29:59 -04:00
|
|
|
ensure
|
|
|
|
# clean up the threads
|
2015-07-13 14:22:54 -04:00
|
|
|
all_go.count_down
|
2015-05-13 20:29:59 -04:00
|
|
|
connecting_threads.map(&:join)
|
|
|
|
end
|
|
|
|
end
|
2015-05-13 21:14:53 -04:00
|
|
|
|
|
|
|
def test_non_bang_disconnect_and_clear_reloadable_connections_throw_exception_if_threads_dont_return_their_conns
|
2018-03-02 07:26:53 -05:00
|
|
|
Thread.report_on_exception, original_report_on_exception = false, Thread.report_on_exception
|
2015-05-13 21:14:53 -04:00
|
|
|
@pool.checkout_timeout = 0.001 # no need to delay test suite by waiting the whole full default timeout
|
|
|
|
[:disconnect, :clear_reloadable_connections].each do |group_action_method|
|
|
|
|
@pool.with_connection do |connection|
|
|
|
|
assert_raises(ExclusiveConnectionTimeoutError) do
|
|
|
|
Thread.new { @pool.send(group_action_method) }.join
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
2017-12-10 18:32:00 -05:00
|
|
|
ensure
|
2018-03-02 07:26:53 -05:00
|
|
|
Thread.report_on_exception = original_report_on_exception
|
2015-05-13 21:14:53 -04:00
|
|
|
end
|
|
|
|
|
|
|
|
def test_disconnect_and_clear_reloadable_connections_attempt_to_wait_for_threads_to_return_their_conns
|
|
|
|
[:disconnect, :disconnect!, :clear_reloadable_connections, :clear_reloadable_connections!].each do |group_action_method|
|
2018-12-20 12:44:01 -05:00
|
|
|
thread = timed_join_result = nil
|
|
|
|
@pool.with_connection do |connection|
|
|
|
|
thread = Thread.new { @pool.send(group_action_method) }
|
|
|
|
|
|
|
|
# give the other `thread` some time to get stuck in `group_action_method`
|
|
|
|
timed_join_result = thread.join(0.3)
|
|
|
|
# thread.join # => `nil` means the other thread hasn't finished running and is still waiting for us to
|
|
|
|
# release our connection
|
|
|
|
assert_nil timed_join_result
|
|
|
|
|
|
|
|
# assert that since this is within default timeout our connection hasn't been forcefully taken away from us
|
|
|
|
assert_predicate @pool, :active_connection?
|
2015-05-13 21:14:53 -04:00
|
|
|
end
|
2018-12-20 12:44:01 -05:00
|
|
|
ensure
|
|
|
|
thread.join if thread && !timed_join_result # clean up the other thread
|
2015-05-13 21:14:53 -04:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2017-02-05 20:00:18 -05:00
|
|
|
def test_bang_versions_of_disconnect_and_clear_reloadable_connections_if_unable_to_acquire_all_connections_proceed_anyway
|
2015-05-13 21:14:53 -04:00
|
|
|
@pool.checkout_timeout = 0.001 # no need to delay test suite by waiting the whole full default timeout
|
|
|
|
[:disconnect!, :clear_reloadable_connections!].each do |group_action_method|
|
|
|
|
@pool.with_connection do |connection|
|
|
|
|
Thread.new { @pool.send(group_action_method) }.join
|
|
|
|
# assert connection has been forcefully taken away from us
|
2018-01-25 18:14:09 -05:00
|
|
|
assert_not_predicate @pool, :active_connection?
|
2016-07-06 10:20:37 -04:00
|
|
|
|
|
|
|
# make a new connection for with_connection to clean up
|
|
|
|
@pool.connection
|
2015-05-13 21:14:53 -04:00
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
def test_disconnect_and_clear_reloadable_connections_are_able_to_preempt_other_waiting_threads
|
|
|
|
with_single_connection_pool do |pool|
|
|
|
|
[:disconnect, :disconnect!, :clear_reloadable_connections, :clear_reloadable_connections!].each do |group_action_method|
|
|
|
|
conn = pool.connection # drain the only available connection
|
2016-11-26 00:05:23 -05:00
|
|
|
second_thread_done = Concurrent::Event.new
|
2015-05-13 21:14:53 -04:00
|
|
|
|
2016-11-26 00:05:23 -05:00
|
|
|
begin
|
|
|
|
# create a first_thread and let it get into the FIFO queue first
|
|
|
|
first_thread = Thread.new do
|
|
|
|
pool.with_connection { second_thread_done.wait }
|
|
|
|
end
|
|
|
|
|
|
|
|
# wait for first_thread to get in queue
|
|
|
|
Thread.pass until pool.num_waiting_in_queue == 1
|
|
|
|
|
|
|
|
# create a different, later thread, that will attempt to do a "group action",
|
|
|
|
# but because of the group action semantics it should be able to preempt the
|
|
|
|
# first_thread when a connection is made available
|
|
|
|
second_thread = Thread.new do
|
|
|
|
pool.send(group_action_method)
|
|
|
|
second_thread_done.set
|
|
|
|
end
|
|
|
|
|
|
|
|
# wait for second_thread to get in queue
|
|
|
|
Thread.pass until pool.num_waiting_in_queue == 2
|
|
|
|
|
|
|
|
# return the only available connection
|
|
|
|
pool.checkin(conn)
|
|
|
|
|
|
|
|
# if the second_thread is not able to preempt the first_thread,
|
|
|
|
# they will temporarily (until either of them timeouts with ConnectionTimeoutError)
|
|
|
|
# deadlock and a join(2) timeout will be reached
|
|
|
|
assert second_thread.join(2), "#{group_action_method} is not able to preempt other waiting threads"
|
|
|
|
|
|
|
|
ensure
|
|
|
|
# post test clean up
|
|
|
|
failed = !second_thread_done.set?
|
|
|
|
|
|
|
|
if failed
|
|
|
|
second_thread_done.set
|
|
|
|
|
|
|
|
first_thread.join(2)
|
|
|
|
second_thread.join(2)
|
|
|
|
end
|
|
|
|
|
|
|
|
first_thread.join(10) || raise("first_thread got stuck")
|
|
|
|
second_thread.join(10) || raise("second_thread got stuck")
|
2015-05-13 21:14:53 -04:00
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
def test_clear_reloadable_connections_creates_new_connections_for_waiting_threads_if_necessary
|
|
|
|
with_single_connection_pool do |pool|
|
|
|
|
conn = pool.connection # drain the only available connection
|
|
|
|
def conn.requires_reloading? # make sure it gets removed from the pool by clear_reloadable_connections
|
|
|
|
true
|
|
|
|
end
|
|
|
|
|
|
|
|
stuck_thread = Thread.new do
|
2018-09-25 13:18:20 -04:00
|
|
|
pool.with_connection { }
|
2015-05-13 21:14:53 -04:00
|
|
|
end
|
|
|
|
|
|
|
|
# wait for stuck_thread to get in queue
|
|
|
|
Thread.pass until pool.num_waiting_in_queue == 1
|
|
|
|
|
|
|
|
pool.clear_reloadable_connections
|
|
|
|
|
|
|
|
unless stuck_thread.join(2)
|
2016-08-06 12:26:20 -04:00
|
|
|
flunk "clear_reloadable_connections must not let other connection waiting threads get stuck in queue"
|
2015-05-13 21:14:53 -04:00
|
|
|
end
|
|
|
|
|
|
|
|
assert_equal 0, pool.num_waiting_in_queue
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2016-11-08 12:10:16 -05:00
|
|
|
def test_connection_pool_stat
|
|
|
|
with_single_connection_pool do |pool|
|
|
|
|
pool.with_connection do |connection|
|
|
|
|
stats = pool.stat
|
|
|
|
assert_equal({ size: 1, connections: 1, busy: 1, dead: 0, idle: 0, waiting: 0, checkout_timeout: 5 }, stats)
|
|
|
|
end
|
|
|
|
|
|
|
|
stats = pool.stat
|
|
|
|
assert_equal({ size: 1, connections: 1, busy: 0, dead: 0, idle: 1, waiting: 0, checkout_timeout: 5 }, stats)
|
|
|
|
|
|
|
|
Thread.new do
|
|
|
|
pool.checkout
|
|
|
|
Thread.current.kill
|
|
|
|
end.join
|
|
|
|
|
|
|
|
stats = pool.stat
|
|
|
|
assert_equal({ size: 1, connections: 1, busy: 0, dead: 1, idle: 0, waiting: 0, checkout_timeout: 5 }, stats)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2019-06-13 14:23:13 -04:00
|
|
|
def test_public_connections_access_threadsafe
|
|
|
|
_conn1 = @pool.checkout
|
|
|
|
conn2 = @pool.checkout
|
|
|
|
|
|
|
|
connections = @pool.connections
|
|
|
|
found_conn = nil
|
|
|
|
|
|
|
|
# Without assuming too much about implementation
|
|
|
|
# details make sure that a concurrent change to
|
|
|
|
# the pool is thread-safe.
|
|
|
|
connections.each_index do |idx|
|
|
|
|
if connections[idx] == conn2
|
|
|
|
Thread.new do
|
|
|
|
@pool.remove(conn2)
|
|
|
|
end.join
|
|
|
|
end
|
|
|
|
found_conn = connections[idx]
|
|
|
|
end
|
|
|
|
|
|
|
|
assert_not_nil found_conn
|
|
|
|
end
|
|
|
|
|
2015-05-13 21:14:53 -04:00
|
|
|
private
|
2016-08-06 13:55:02 -04:00
|
|
|
def with_single_connection_pool
|
2019-12-17 15:42:48 -05:00
|
|
|
config = @db_config.configuration_hash.merge(pool: 1)
|
|
|
|
db_config = ActiveRecord::DatabaseConfigurations::HashConfig.new("arunit", "primary", config)
|
2019-11-05 17:05:54 -05:00
|
|
|
pool_config = ActiveRecord::ConnectionAdapters::PoolConfig.new("primary", db_config)
|
2019-12-17 15:42:48 -05:00
|
|
|
|
2019-11-05 17:05:54 -05:00
|
|
|
yield(pool = ConnectionPool.new(pool_config))
|
2016-08-06 13:55:02 -04:00
|
|
|
ensure
|
|
|
|
pool.disconnect! if pool
|
|
|
|
end
|
2010-07-16 17:39:40 -04:00
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|