Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Stale connection in DatabaseCleaner::ActiveRecord::Truncation#connection #86

Open
dmolesUC opened this issue Apr 14, 2023 · 1 comment

Comments

@dmolesUC
Copy link

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, 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:

  1. 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
  2. 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

    (This example is obviously quite contrived; I ran into the problem in a more realistic situation. See discussion in Fix flakey async test thread error bensheldon/good_job#849.)

Expected

  • Tests pass.

Actual

  • First couple of tests pass
  • Subsequent tests fail with ActiveRecord::ConnectionNotEstablished: connection is closed raised from DatabaseCleaner.cleaning via Truncation#clean

Workaround

Instead of using DatabaseCleaner.cleaning in an around block, explicitly call DatabaseCleaner.clean_with(:truncation) in an after(:each) block:

RSpec.configure do |config|
  config.after(:each) do
    DatabaseCleaner.clean_with(:truncation)
  end
end

Proposed fix

Get a fresh connection in each call to Truncation.clean -- I simulated this with a prepended module and it seems to work:

module Cleaninator
  def clean
    @connection = nil
    super
  end
end

class DatabaseCleaner::ActiveRecord::Truncation
  prepend Cleaninator
end
@olehvavryniv
Copy link

@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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants