diff options
Diffstat (limited to 'activerecord/test/cases/connection_pool_test.rb')
-rw-r--r-- | activerecord/test/cases/connection_pool_test.rb | 199 |
1 files changed, 189 insertions, 10 deletions
diff --git a/activerecord/test/cases/connection_pool_test.rb b/activerecord/test/cases/connection_pool_test.rb index 8d15a76735..efa3e0455e 100644 --- a/activerecord/test/cases/connection_pool_test.rb +++ b/activerecord/test/cases/connection_pool_test.rb @@ -1,5 +1,5 @@ require "cases/helper" -require 'active_support/concurrency/latch' +require 'concurrent/atomic/count_down_latch' module ActiveRecord module ConnectionAdapters @@ -100,7 +100,7 @@ module ActiveRecord t = Thread.new { @pool.checkout } # make sure our thread is in the timeout section - Thread.pass until t.status == "sleep" + Thread.pass until @pool.num_waiting_in_queue == 1 connection = cs.first connection.close @@ -112,7 +112,7 @@ module ActiveRecord t = Thread.new { @pool.checkout } # make sure our thread is in the timeout section - Thread.pass until t.status == "sleep" + Thread.pass until @pool.num_waiting_in_queue == 1 connection = cs.first @pool.remove connection @@ -133,15 +133,15 @@ module ActiveRecord end def test_reap_inactive - ready = ActiveSupport::Concurrency::Latch.new + ready = Concurrent::CountDownLatch.new @pool.checkout child = Thread.new do @pool.checkout @pool.checkout - ready.release + ready.count_down Thread.stop end - ready.await + ready.wait assert_equal 3, active_connections(@pool).size @@ -204,13 +204,13 @@ module ActiveRecord end # The connection pool is "fair" if threads waiting for - # connections receive them the order in which they began + # connections receive them in the order in which they began # 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 - # available at the same time, it's fine of two threads that were + # available at the same time, it's fine if two threads that were # waiting acquire the connections out of order. # # Thus this test prepares waiting threads and then trickles in @@ -234,7 +234,7 @@ module ActiveRecord mutex.synchronize { errors << e } end } - Thread.pass until t.status == "sleep" + Thread.pass until @pool.num_waiting_in_queue == i t end @@ -271,7 +271,7 @@ module ActiveRecord mutex.synchronize { errors << e } end } - Thread.pass until t.status == "sleep" + Thread.pass until @pool.num_waiting_in_queue == i t end @@ -341,6 +341,185 @@ module ActiveRecord handler.establish_connection anonymous, nil } end + + 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_not_same pool.schema_cache, conn.schema_cache + 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 + + def test_concurrent_connection_establishment + assert_operator @pool.connections.size, :<=, 1 + + all_threads_in_new_connection = Concurrent::CountDownLatch.new(@pool.size - @pool.connections.size) + all_go = Concurrent::CountDownLatch.new + + @pool.singleton_class.class_eval do + define_method(:new_connection) do + all_threads_in_new_connection.count_down + all_go.wait + 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 + all_threads_in_new_connection.wait + end + rescue Timeout::Error + flunk 'pool unable to establish connections concurrently or implementation has ' << + 'changed, this test then needs to patch a different :new_connection method' + ensure + # clean up the threads + all_go.count_down + connecting_threads.map(&:join) + end + end + + def test_non_bang_disconnect_and_clear_reloadable_connections_throw_exception_if_threads_dont_return_their_conns + @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 + 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| + begin + 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 @pool.active_connection? + end + ensure + thread.join if thread && !timed_join_result # clean up the other thread + end + end + end + + def test_bang_versions_of_disconnect_and_clear_reloadable_connections_if_unable_to_aquire_all_connections_proceed_anyway + @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 + assert_not @pool.active_connection? + 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 + second_thread_done = Concurrent::CountDownLatch.new + + # 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.count_down + 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 + failed = true unless second_thread.join(2) + + #--- post test clean up start + second_thread_done.count_down if failed + + # after `pool.disconnect()` the first thread will be left stuck in queue, no need to wait for + # it to timeout with ConnectionTimeoutError + if (group_action_method == :disconnect || group_action_method == :disconnect!) && pool.num_waiting_in_queue > 0 + pool.with_connection {} # create a new connection in case there are threads still stuck in a queue + end + + first_thread.join + second_thread.join + #--- post test clean up end + + flunk "#{group_action_method} is not able to preempt other waiting threads" if failed + 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 + pool.with_connection {} + 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) + flunk 'clear_reloadable_connections must not let other connection waiting threads get stuck in queue' + end + + assert_equal 0, pool.num_waiting_in_queue + end + end + + private + def with_single_connection_pool + one_conn_spec = ActiveRecord::Base.connection_pool.spec.dup + one_conn_spec.config[:pool] = 1 # this is safe to do, because .dupped ConnectionSpecification also auto-dups its config + yield(pool = ConnectionPool.new(one_conn_spec)) + ensure + pool.disconnect! if pool + end end end end |