class ActiveRecord::ConnectionAdapters::ConnectionPoolTest

Attributes

pool[R]

Public Instance Methods

active_connections(pool) click to toggle source
# File activerecord/test/cases/connection_pool_test.rb, line 32
def active_connections(pool)
  pool.connections.find_all(&:in_use?)
end
setup() click to toggle source
Calls superclass method
# File activerecord/test/cases/connection_pool_test.rb, line 11
def setup
  super

  # Keep a duplicate pool so we do not bother others
  @pool = ConnectionPool.new ActiveRecord::Base.connection_pool.spec

  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
end
test_active_connection?() click to toggle source
# File activerecord/test/cases/connection_pool_test.rb, line 179
def test_active_connection?
  assert !@pool.active_connection?
  assert @pool.connection
  assert @pool.active_connection?
  @pool.release_connection
  assert !@pool.active_connection?
end
test_active_connection_in_use() click to toggle source
# File activerecord/test/cases/connection_pool_test.rb, line 82
def test_active_connection_in_use
  assert !pool.active_connection?
  main_thread = pool.connection

  assert pool.active_connection?

  main_thread.close

  assert !pool.active_connection?
end
test_anonymous_class_exception() click to toggle source

make sure exceptions are thrown when establish_connection is called with an anonymous class

# File activerecord/test/cases/connection_pool_test.rb, line 341
def test_anonymous_class_exception
  anonymous = Class.new(ActiveRecord::Base)

  assert_raises(RuntimeError) do
    anonymous.establish_connection
  end
end
test_automatic_reconnect_can_be_disabled() click to toggle source
# File activerecord/test/cases/connection_pool_test.rb, line 321
def test_automatic_reconnect_can_be_disabled
  pool = ConnectionPool.new ActiveRecord::Base.connection_pool.spec
  pool.disconnect!
  pool.automatic_reconnect = false

  assert_raises(ConnectionNotEstablished) do
    pool.connection
  end

  assert_raises(ConnectionNotEstablished) do
    pool.with_connection
  end
end
test_automatic_reconnect_restores_after_disconnect() click to toggle source
# File activerecord/test/cases/connection_pool_test.rb, line 312
def test_automatic_reconnect_restores_after_disconnect
  pool = ConnectionPool.new ActiveRecord::Base.connection_pool.spec
  assert pool.automatic_reconnect
  assert pool.connection

  pool.disconnect!
  assert pool.connection
end
test_bang_versions_of_disconnect_and_clear_reloadable_connections_if_unable_to_acquire_all_connections_proceed_anyway() click to toggle source
# File activerecord/test/cases/connection_pool_test.rb, line 449
def test_bang_versions_of_disconnect_and_clear_reloadable_connections_if_unable_to_acquire_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?

      # make a new connection for with_connection to clean up
      @pool.connection
    end
  end
end
test_checkout_after_close() click to toggle source
# File activerecord/test/cases/connection_pool_test.rb, line 36
def test_checkout_after_close
  connection = pool.connection
  assert connection.in_use?

  connection.close
  assert !connection.in_use?

  assert pool.connection.in_use?
end
test_checkout_behaviour() click to toggle source
# File activerecord/test/cases/connection_pool_test.rb, line 187
def test_checkout_behaviour
  pool = ConnectionPool.new ActiveRecord::Base.connection_pool.spec
  main_connection = pool.connection
  assert_not_nil main_connection
  threads = []
  4.times do |i|
    threads << Thread.new(i) do
      thread_connection = pool.connection
      assert_not_nil thread_connection
      thread_connection.close
    end
  end

  threads.each(&:join)

  Thread.new do
    assert pool.connection
    pool.connection.close
  end.join
end
test_checkout_fairness() click to toggle source

The connection pool is “fair” if threads waiting for 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 if two threads that were 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.

# File activerecord/test/cases/connection_pool_test.rb, line 221
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
        @pool.checkout # never checked back in
        mutex.synchronize { order << i }
      rescue => e
        mutex.synchronize { errors << e }
      end
    }
    Thread.pass until @pool.num_waiting_in_queue == i
    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
test_checkout_fairness_by_group() click to toggle source

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.

# File activerecord/test/cases/connection_pool_test.rb, line 259
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
        @pool.checkout # never checked back in
        mutex.synchronize { successes << i }
      rescue => e
        mutex.synchronize { errors << e }
      end
    }
    Thread.pass until @pool.num_waiting_in_queue == i
    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
test_clear_reloadable_connections_creates_new_connections_for_waiting_threads_if_necessary() click to toggle source
# File activerecord/test/cases/connection_pool_test.rb, line 515
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
test_concurrent_connection_establishment() click to toggle source
Calls superclass method
# File activerecord/test/cases/connection_pool_test.rb, line 380
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
test_connection_notification_is_called() click to toggle source
# File activerecord/test/cases/connection_pool_test.rb, line 352
def test_connection_notification_is_called
  payloads = []
  subscription = ActiveSupport::Notifications.subscribe("!connection.active_record") do |name, started, finished, unique_id, payload|
    payloads << payload
  end
  ConnectionTestModel.establish_connection :arunit

  assert_equal [:config, :connection_id, :spec_name], payloads[0].keys.sort
  assert_equal "ActiveRecord::ConnectionAdapters::ConnectionPoolTest::ConnectionTestModel", payloads[0][:spec_name]
ensure
  ActiveSupport::Notifications.unsubscribe(subscription) if subscription
end
test_connection_pool_stat() click to toggle source
# File activerecord/test/cases/connection_pool_test.rb, line 539
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
test_disconnect_and_clear_reloadable_connections_are_able_to_preempt_other_waiting_threads() click to toggle source
# File activerecord/test/cases/connection_pool_test.rb, line 463
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::Event.new

      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")
      end
    end
  end
end
test_disconnect_and_clear_reloadable_connections_attempt_to_wait_for_threads_to_return_their_conns() click to toggle source
# File activerecord/test/cases/connection_pool_test.rb, line 427
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
test_full_pool_blocks() click to toggle source
# File activerecord/test/cases/connection_pool_test.rb, line 100
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
  Thread.pass until @pool.num_waiting_in_queue == 1

  connection = cs.first
  connection.close
  assert_equal connection, t.join.value
end
test_full_pool_exception() click to toggle source
# File activerecord/test/cases/connection_pool_test.rb, line 93
def test_full_pool_exception
  @pool.size.times { @pool.checkout }
  assert_raises(ConnectionTimeoutError) do
    @pool.checkout
  end
end
test_non_bang_disconnect_and_clear_reloadable_connections_throw_exception_if_threads_dont_return_their_conns() click to toggle source
# File activerecord/test/cases/connection_pool_test.rb, line 416
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
test_pool_sets_connection_schema_cache() click to toggle source
# File activerecord/test/cases/connection_pool_test.rb, line 365
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
test_pool_sets_connection_visitor() click to toggle source
# File activerecord/test/cases/connection_pool_test.rb, line 335
def test_pool_sets_connection_visitor
  assert @pool.connection.visitor.is_a?(Arel::Visitors::ToSql)
end
test_reap_and_active() click to toggle source
# File activerecord/test/cases/connection_pool_test.rb, line 125
def test_reap_and_active
  @pool.checkout
  @pool.checkout
  @pool.checkout

  connections = @pool.connections.dup

  @pool.reap

  assert_equal connections.length, @pool.connections.length
end
test_reap_inactive() click to toggle source
# File activerecord/test/cases/connection_pool_test.rb, line 137
def test_reap_inactive
  ready = Concurrent::CountDownLatch.new
  @pool.checkout
  child = Thread.new do
    @pool.checkout
    @pool.checkout
    ready.count_down
    Thread.stop
  end
  ready.wait

  assert_equal 3, active_connections(@pool).size

  child.terminate
  child.join
  @pool.reap

  assert_equal 1, active_connections(@pool).size
ensure
  @pool.connections.each { |conn| conn.close if conn.in_use? }
end
test_released_connection_moves_between_threads() click to toggle source
# File activerecord/test/cases/connection_pool_test.rb, line 46
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
test_remove_connection() click to toggle source
# File activerecord/test/cases/connection_pool_test.rb, line 159
def test_remove_connection
  conn = @pool.checkout
  assert conn.in_use?

  length = @pool.connections.length
  @pool.remove conn
  assert conn.in_use?
  assert_equal(length - 1, @pool.connections.length)
ensure
  conn.close
end
test_remove_connection_for_thread() click to toggle source
# File activerecord/test/cases/connection_pool_test.rb, line 171
def test_remove_connection_for_thread
  conn = @pool.connection
  @pool.remove conn
  assert_not_equal(conn, @pool.connection)
ensure
  conn.close if conn
end
test_removing_releases_latch() click to toggle source
# File activerecord/test/cases/connection_pool_test.rb, line 112
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
  Thread.pass until @pool.num_waiting_in_queue == 1

  connection = cs.first
  @pool.remove connection
  assert_respond_to t.join.value, :execute
  connection.close
end
test_with_connection() click to toggle source
# File activerecord/test/cases/connection_pool_test.rb, line 64
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

Private Instance Methods

with_single_connection_pool() { |pool = connection_pool| ... } click to toggle source
# File activerecord/test/cases/connection_pool_test.rb, line 560
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