Skip to content

Instantly share code, notes, and snippets.

@josevalim
Created July 10, 2010 16:10
Show Gist options
  • Save josevalim/470808 to your computer and use it in GitHub Desktop.
Save josevalim/470808 to your computer and use it in GitHub Desktop.
# In your test_helper.rb
class ActiveRecord::Base
mattr_accessor :shared_connection
@@shared_connection = nil
def self.connection
@@shared_connection || retrieve_connection
end
end
# Forces all threads to share the same connection. This works on
# Capybara because it starts the web server in a thread.
ActiveRecord::Base.shared_connection = ActiveRecord::Base.connection
@divineforest
Copy link

@ka8725 @gustavowt @MrJaba @mikecmpbll @rochers @cavneb @killthekitten

Here is robust and still fast solution for these problems

PG::UnableToSend: another command is already in progress
undefined method `fields' for nil:NilClass
PG::UnableToSend: socket not open

It uses shared connection, connection pool and waits for the ajax requests to finish at the end of each js: true spec.

# spec/support/capybara.rb
def wait_for_ajax
  return unless respond_to?(:evaluate_script)
  wait_until { finished_all_ajax_requests? }
end

def finished_all_ajax_requests?
  evaluate_script("!window.jQuery") || evaluate_script("jQuery.active").zero?
end

def wait_until(max_execution_time_in_seconds = Capybara.default_wait_time)
  Timeout.timeout(max_execution_time_in_seconds) do
    loop do
      if yield
        return true
      else
        sleep(0.05)
        next
      end
    end
  end
end

class ActiveRecord::Base
  mattr_accessor :shared_connection
  @@shared_connection = nil

  def self.connection
    @@shared_connection || ConnectionPool::Wrapper.new(size: 1) { retrieve_connection }
  end
end

RSpec.configure do |config|
  config.before :all do
    # Forces all threads to share the same connection. This works on
    # Capybara because it starts the web server in a thread.
    ActiveRecord::Base.shared_connection = ActiveRecord::Base.connection
  end

  config.before :each, js: true do
    # Need to wait for active connections because of shared_connection hack
    # Fixes errors like
    # PG::UnableToSend: another command is already in progress
    # undefined method `fields' for nil:NilClass
    # PG::UnableToSend: socket not open
    wait_for_ajax
  end
end

thanks @abgoldstein for the idea

I suggest also to add this to your config/environments/test.rb:

config.eager_load = true

as it helps to avoid wrong exceptions about circular dependency.

@aprescott
Copy link

The approach given by @mikecmpbll of using Mutex in https://gist.github.com/josevalim/470808#comment-1280898 worked very well.

Without the change, 40 test runs saw 12 failures. With the change, there were 0 failures.

class ActiveRecord::Base
  mattr_accessor :shared_connection
  @@shared_connection = nil

  def self.connection
    @@shared_connection || ConnectionPool::Wrapper.new(:size => 1) { retrieve_connection }
  end
end
ActiveRecord::Base.shared_connection = ActiveRecord::Base.connection

raise "adapter was expected to be mysql2" unless ActiveRecord::Base.connection.adapter_name.downcase == "mysql2"

module MutexLockedQuerying
  @@semaphore = Mutex.new

  def query(*)
    @@semaphore.synchronize { super }
  end
end

Mysql2::Client.prepend(MutexLockedQuerying)

I tweaked it to use prepend instead of aliasing methods, so I can simply call super.

@ardavis
Copy link

ardavis commented Mar 12, 2015

@divineforest I think that fix worked for the root cause mentioned, but introduced another error for me.

We are using Rails 4, Cucumber with a transactional strategy, we can't change this right now unfortunately. (The really strange part is that on Rails 3.2 this shared connection thing works perfectly.. upgraded to Rails 4.0.11 (soon to 4.1, then 4.2...) and am having issues.

My team has a single model that stores it's data in a separate database. How can I use this shared_connection approach for multiple databases? The error I'm getting now is:

PG::UndefinedTable: ERROR:  relation "user_groups" does not exist
LINE 1: SELECT  "user_groups".* FROM "user_groups"  WHERE "u...
                                     ^
: SELECT  "user_groups".* FROM "user_groups"  WHERE "user_groups"."name" = 'All'  ORDER BY "user_groups"."id" ASC LIMIT 1 (ActiveRecord::StatementInvalid)

Here's my UserGroup and AdminService models:

class UserGroup < AdminService
  self.table_name = 'user_groups'
  ..
end

class AdminService <  ActiveRecord::Base
  self.abstract_class = true
  establish_connection SERVICES_CONFIG['admin']
end

Here's the services.yml file that SERVICES_CONFIG gets it's data from:

admin: &admin_defaults
  adapter: postgresql
  encoding: unicode
  pool: 5
  host: localhost
  port: 5432
  username: my_username
  password: <%= begin IO.read("$HOME/.db") rescue "" end %>

test_defaults: &test_defaults
  admin:
    <<: *admin_defaults
    database: admin_service_test
    domain: test_domain

test: &test
  <<: *test_defaults

Someone mentioned this issue I'm having in a Railscast comment a long time ago found here:

http://railscasts.com/episodes/257-request-specs-and-capybara?view=comments#comment_157994

The answer in the reply was to do:

class ActiveRecord::Base
  mattr_accessor :shared_connection
  @@shared_connection = {}

  def self.connection
    @@shared_connection[self.connection_config[:database]] ||= retrieve_connection
  end
end

But it only ended up in the following error:

undefined method `[]' for #<ActiveRecord::ConnectionAdapters::PostgreSQLAdapter:0x0000001f827700> (NoMethodError)

Thanks for your time everyone!

@mrsimo
Copy link

mrsimo commented Mar 18, 2015

We had some problems with the newest versions of connection_pool (from version 2.1.1) and using the semaphore without the connection pool seemed to work. We also connect to more than one database, here's our snippet:

class ActiveRecord::Base
  mattr_accessor :shared_connections
  self.shared_connections = {}

  def self.connection
    shared_connections[connection_config[:database]] ||= begin
      retrieve_connection
    end
  end
end

module MutexLockedQuerying
  @@semaphore = Mutex.new

  def query(*)
    @@semaphore.synchronize { super }
  end
end

Mysql2::Client.prepend(MutexLockedQuerying)

@blueimpb
Copy link

We had the exact same error with a very plain-vanilla setup using Rails and the "Devise" gem for user session management. Without going to the database in app code at all, we saw lots of:

ActiveRecord::StatementInvalid (Mysql2::Error: This connection is in use by: #<Thread:0x00000004f64740 sleep>: SELECT users.* FROM users WHERE users.id = 13 ORDER BY users.id ASC LIMIT 1): app/controllers/home_controller.rb:12:in `index'

We implemented the fix suggested by mikecmpbll because it seemed very reasonable, essentially forcing Mysql2 to do what the infrastructure above it should already be doing. Problem disappeared. Thanks mikecmpbll!!

@mark-ellul
Copy link

thank you, muito obrigado, gracias! This has helped me resolve an error that was bugging me!

@Fjan
Copy link

Fjan commented Jul 28, 2015

We also had some flakiness caused by AJAX requests that arrived after the test had ended. This can actually also lead to heisenbugs without a shared DB connection so instead of waiting for the calls to complete, as in @divineforest's solution, we instead failed the tests and fixed them. This is all that is needed in the test helper to detect tests that need fixing:

class ActionDispatch::IntegrationTest
  def teardown
    # detects both Prototype and jQuery AJAX requests
    active=evaluate_script('window.Ajax ? Ajax.activeRequestCount : (window.jQuery ? jQuery.active : 0)')
    assert_equal 0,active,'Active AJAX request after test end'
  end
end

@mariusandra
Copy link

I'd been getting PG::UnableToSend another command is already in progress errors when running a few concurrent AJAX requests upon loading a page. One of them would pass, the others would get a 500 with the aforementioned error.

The simple solution for me was to add config.allow_concurrency = false to config/environments/test.rb.

@oneamtu
Copy link

oneamtu commented Sep 10, 2015

@mikecmpbll's solution with @aprescott's improvement, implemented for postgres. Thanks! Solves
PG::UnableToSend: another command is already in progress
and
undefined method 'fields'
and other flaky race errors.

class ActiveRecord::Base
  mattr_accessor :shared_connection
  @@shared_connection = nil

  def self.connection
    @@shared_connection || ConnectionPool::Wrapper.new(:size => 1) { retrieve_connection }
  end
end

ActiveRecord::Base.shared_connection = ActiveRecord::Base.connection

# hack a mutex in the query execution so that we don't
# get competing queries that can timeout and not get cleaned up
module MutexLockedQuerying
  @@semaphore = Mutex.new

  def async_exec(*)
    @@semaphore.synchronize { super }
  end
end

PG::Connection.prepend(MutexLockedQuerying)

@snoblenet
Copy link

Hi there,

Currently the following code, in Sidekiq worker, works in development:

url = "http://localhost:3000/downloads/#{download.unique_id}" if Rails.env.development?

However, the following code in the same Sidekiq worker does not work in test:

url = "http://localhost:4000/downloads/#{download.unique_id}" if Rails.env.test?

I get a connection refused error.

Capybara is configured as default_port = 4000 and run_server = true.

Do you think I could fix this with a variant of the code you've posted here?

@grillermo
Copy link

I gave up, and i'm using truncation in all my tests and the problem es gone.

@kuroda
Copy link

kuroda commented Mar 4, 2016

The solution using Mutex (thanks to @mikecmpbll) and wait_for_ajax (thanks to @divineforest) works well for me.

Here is another tip from me.

In some occation, the undefined method 'fields' error can occur even if you call wait_for_ajax after triggering an Ajax call.

In fact, we have to wait for all database accesses are completed, not just for ajax.

Suppose that you have following scenario among others:

scenario 'Update account' do
  visit edit_account_path
  fill_in 'account_name', with: 'foo'
  find('#update-account').click # Triggers an Ajax call
  wait_for_ajax
  user.reload
  expect(user.name).to eq('foo')
end

If any database access occurs after this scenario ends, we will get the undefined method 'fields' error on the next scenario.

To prevent this problem, we have to write another expectation that is fulfilled only after all database accesses are completed.

For example, if we know that a text appears on the browser screen at the end of scenario, we can write like this:

scenario 'Update account' do
  visit edit_account_path
  fill_in 'account_name', with: 'foo'
  find('#update-account').click # Triggers an Ajax call
  expect(page).to have_text('The account is updated.')
  user.reload
  expect(user.name).to eq('foo')
end

@jwg2s
Copy link

jwg2s commented Jul 18, 2016

@kuroda - have you found a more systematic way to fix this issue? We're experiencing the same thing, and rather than run around fixing a bunch of tests, ideally there's a lower level way to address the problem.

@lakim
Copy link

lakim commented Sep 9, 2016

We should work on a reliable solution for everyone.

The best take I've seen so far is @iangreenleaf's article and gem:
http://technotes.iangreenleaf.com/posts/the-one-true-guide-to-database-transactions-with-capybara.html
https://github.com/iangreenleaf/transactional_capybara

It bundles the shared connection monkey patch and the wait_for_ajax helper.
Version 0.1.0 has just been released with support for Capybara >= 2.6.0.

It doesn't use connection_pool nor a Mutex.
I haven't had the need for it so far, but it's just a PR away.

@chrise86
Copy link

Sorry to dig up an old thread (no pun intended), but I've been facing this issue on a Rails 5 app recently, and @mperham's solution worked great for me!

@gogocurtis
Copy link

lol yeah this thread is filled with golden lessons in concurrency.

@gnclmorais
Copy link

It’s 2019, do we still need this in Rails 5.2?

@nflorentin
Copy link

@gnclmorais, it seems that this is not useful anymore: https://github.com/teamcapybara/capybara#transactions-and-database-setup if you are running Rails 5.1 +.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment