1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346
|
require "cases/helper"
require 'active_support/concurrency/latch'
module ActiveRecord
module ConnectionAdapters
class ConnectionPoolTest < ActiveRecord::TestCase
attr_reader :pool
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
teardown do
@pool.disconnect!
end
def active_connections(pool)
pool.connections.find_all(&:in_use?)
end
def test_checkout_after_close
connection = pool.connection
assert connection.in_use?
connection.close
assert !connection.in_use?
assert pool.connection.in_use?
end
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
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
assert !pool.active_connection?
main_thread = pool.connection
assert pool.active_connection?
main_thread.close
assert !pool.active_connection?
end
def test_full_pool_exception
@pool.size.times { @pool.checkout }
assert_raises(ConnectionTimeoutError) do
@pool.checkout
end
end
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 t.status == "sleep"
connection = cs.first
connection.close
assert_equal connection, t.join.value
end
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 t.status == "sleep"
connection = cs.first
@pool.remove connection
assert_respond_to t.join.value, :execute
connection.close
end
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
def test_reap_inactive
ready = ActiveSupport::Concurrency::Latch.new
@pool.checkout
child = Thread.new do
@pool.checkout
@pool.checkout
ready.release
Thread.stop
end
ready.await
assert_equal 3, active_connections(@pool).size
child.terminate
child.join
@pool.reap
assert_equal 1, active_connections(@pool).size
ensure
@pool.connections.each(&:close)
end
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
def test_remove_connection_for_thread
conn = @pool.connection
@pool.remove conn
assert_not_equal(conn, @pool.connection)
ensure
conn.close if conn
end
def test_active_connection?
assert !@pool.active_connection?
assert @pool.connection
assert @pool.active_connection?
@pool.release_connection
assert !@pool.active_connection?
end
def test_checkout_behaviour
pool = ConnectionPool.new ActiveRecord::Base.connection_pool.spec
connection = pool.connection
assert_not_nil connection
threads = []
4.times do |i|
threads << Thread.new(i) do
connection = pool.connection
assert_not_nil connection
connection.close
end
end
threads.each(&:join)
Thread.new do
assert pool.connection
pool.connection.close
end.join
end
# The connection pool is "fair" if threads waiting for
# connections receive them 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
# 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
@pool.checkout # never checked back in
mutex.synchronize { order << i }
rescue => e
mutex.synchronize { errors << e }
end
}
Thread.pass until t.status == "sleep"
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
@pool.checkout # never checked back in
mutex.synchronize { successes << i }
rescue => e
mutex.synchronize { errors << e }
end
}
Thread.pass until t.status == "sleep"
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
def test_automatic_reconnect=
pool = ConnectionPool.new ActiveRecord::Base.connection_pool.spec
assert pool.automatic_reconnect
assert pool.connection
pool.disconnect!
assert pool.connection
pool.disconnect!
pool.automatic_reconnect = false
assert_raises(ConnectionNotEstablished) do
pool.connection
end
assert_raises(ConnectionNotEstablished) do
pool.with_connection
end
end
def test_pool_sets_connection_visitor
assert @pool.connection.visitor.is_a?(Arel::Visitors::ToSql)
end
# make sure exceptions are thrown when establish_connection
# is called with an anonymous class
def test_anonymous_class_exception
anonymous = Class.new(ActiveRecord::Base)
handler = ActiveRecord::Base.connection_handler
assert_raises(RuntimeError) {
handler.establish_connection anonymous, nil
}
end
end
end
end
|