Each instance of the DatabaseCleaner::ActiveRecord::Truncation strategy initializes its @connection field only once, using the ActiveRecord default connection. When the connection is first requested, ActiveRecord checks it out of the connection pool. Various hooks (e.g. ActiveRecord::TestFixtures.teardown_fixtures) can return the connection to the pool, but Truncation is unaware of this and holds onto the connection object.
If another thread checks the same connection out of the pool and calls disconnect!, then when Truncation tries to use the connection to clean the database, it will find that the connection is closed and raise an error.
Steps to reproduce:
In a Rails/PostgreSQL project using RSpec, configure DatabaseCleaner as follows:
RSpec.configure do |config|
config.before(:suite) do
DatabaseCleaner.strategy = :truncation
end
config.around do |example|
DatabaseCleaner.cleaning do
example.run
end
end
end
Write a test that, in a background thread, checks a connection out of the pool, removes it from the pool, and disconnects it, e.g.
describe 'connection pooling' do
def do_disconnect
Thread.new do
connection_pool = ActiveRecord::Base.connection_pool
connection = connection_pool.checkout.tap do |conn|
connection_pool.remove(conn)
end
begin
connection.execute('SELECT 1')
ensure
connection.disconnect!
end
end
end
5.times do |i|
it "test #{i}" do
ActiveRecord::Base.connection.execute('SELECT 1')
sleep(0.5)
do_disconnect if i % 2 == 0
end
end
end
@dmolesUC Thanks a lot! Your workaround saved my life! We use DatabaseCleaner in test API for end-to-end testing. Same issue - connection is closed after ~5 mins.
Summary
Each instance of the
DatabaseCleaner::ActiveRecord::Truncation
strategy initializes its@connection
field only once, using the ActiveRecord default connection. When the connection is first requested, ActiveRecord checks it out of the connection pool. Various hooks (e.g.ActiveRecord::TestFixtures.teardown_fixtures
) can return the connection to the pool, butTruncation
is unaware of this and holds onto the connection object.If another thread checks the same connection out of the pool and calls
disconnect!
, then whenTruncation
tries to use the connection to clean the database, it will find that the connection is closed and raise an error.Steps to reproduce:
In a Rails/PostgreSQL project using RSpec, configure
DatabaseCleaner
as follows:Write a test that, in a background thread, checks a connection out of the pool, removes it from the pool, and disconnects it, e.g.
(This example is obviously quite contrived; I ran into the problem in a more realistic situation. See discussion in https://github.com/bensheldon/good_job/issues/849.)
Expected
Actual
ActiveRecord::ConnectionNotEstablished: connection is closed
raised fromDatabaseCleaner.cleaning
viaTruncation#clean
Workaround
Instead of using
DatabaseCleaner.cleaning
in anaround
block, explicitly callDatabaseCleaner.clean_with(:truncation)
in anafter(:each)
block:Proposed fix
Get a fresh connection in each call to
Truncation.clean
-- I simulated this with a prepended module and it seems to work: