1
0
Fork 0
mirror of https://github.com/rails/rails.git synced 2022-11-09 12:12:34 -05:00

Fix connection leak when a thread checks in additional connections.

The code in `ConnectionPool#release` assumed that a single thread only
ever holds a single connection, and thus that releasing a connection
only requires the owning thread_id.

There is a trivial counterexample to this assumption: code that checks
out additional connections from the pool in the same thread. For
instance:

    connection_1 = ActiveRecord::Base.connection
    connection_2 = ActiveRecord::Base.connection_pool.checkout
    ActiveRecord::Base.connection_pool.checkin(connection_2)
    connection_3 = ActiveRecord::Base.connection

At this point, connection_1 has been removed from the
`@reserved_connections` hash, causing a NEW connection to be returned as
connection_3 and the loss of any tracking info on connection_1. As long
as the thread in this example lives, connection_1 will be inaccessible
and un-reapable. If this block of code runs more times than the size of
the connection pool in a single thread, every subsequent connection
attempt will timeout, as all of the available connections have been
leaked.

Reverts parts of 9e457a8654 and
essentially all of 4367d2f05c
This commit is contained in:
Matt Jones 2014-12-23 09:54:52 -05:00
parent d9d865aa40
commit 5e024070ab
2 changed files with 36 additions and 4 deletions

View file

@ -363,7 +363,7 @@ module ActiveRecord
conn.expire
end
release owner
release conn, owner
@available.add conn
end
@ -376,7 +376,7 @@ module ActiveRecord
@connections.delete conn
@available.delete conn
release conn.owner
release conn, conn.owner
@available.add checkout_new_connection if @available.any_waiting?
end
@ -424,11 +424,13 @@ module ActiveRecord
end
end
def release(owner)
def release(conn, owner)
thread_id = owner.object_id
if @reserved_connections[thread_id] == conn
@reserved_connections.delete thread_id
end
end
def new_connection
Base.send(spec.adapter_method, spec.config)

View file

@ -35,6 +35,22 @@ class PooledConnectionsTest < ActiveRecord::TestCase
end
end
def checkout_checkin_connections_loop(pool_size, loops)
ActiveRecord::Base.establish_connection(@connection.merge({:pool => pool_size, :checkout_timeout => 0.5}))
@connection_count = 0
@timed_out = 0
loops.times do
begin
conn = ActiveRecord::Base.connection_pool.checkout
ActiveRecord::Base.connection_pool.checkin conn
@connection_count += 1
ActiveRecord::Base.connection.tables
rescue ActiveRecord::ConnectionTimeoutError
@timed_out += 1
end
end
end
def test_pooled_connection_checkin_one
checkout_checkin_connections 1, 2
assert_equal 2, @connection_count
@ -42,6 +58,20 @@ class PooledConnectionsTest < ActiveRecord::TestCase
assert_equal 1, ActiveRecord::Base.connection_pool.connections.size
end
def test_pooled_connection_checkin_two
checkout_checkin_connections_loop 2, 3
assert_equal 3, @connection_count
assert_equal 0, @timed_out
assert_equal 2, ActiveRecord::Base.connection_pool.connections.size
end
def test_pooled_connection_remove
ActiveRecord::Base.establish_connection(@connection.merge({:pool => 2, :checkout_timeout => 0.5}))
old_connection = ActiveRecord::Base.connection
extra_connection = ActiveRecord::Base.connection_pool.checkout
ActiveRecord::Base.connection_pool.remove(extra_connection)
assert_equal ActiveRecord::Base.connection, old_connection
end
private