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

Active Record locks connections to threads that checked them out #37092

Closed
janko opened this issue Aug 30, 2019 · 9 comments
Closed

Active Record locks connections to threads that checked them out #37092

janko opened this issue Aug 30, 2019 · 9 comments
Labels

Comments

@janko
Copy link
Contributor

janko commented Aug 30, 2019

Steps to reproduce

require "active_record"

ActiveRecord::Base.establish_connection(
  adapter:          "sqlite3",
  database:         ":memory:",
  pool:             5, # default
  checkout_timeout: 5, # default
)

threads = 10.times.map do
  Thread.new do
    ActiveRecord::Base.connection.execute "SELECT 1"
  end
end

threads.each(&:join)

Expected behavior

I expected the above code to complete without errors.

The connection pool is intentionally smaller than the number of threads, but since we're executing cheap queries, I expected connections to be immediately checked back into the pool, so that other threads can use them.

Actual behavior

I get connection timeout from 6 threads:

ActiveRecord::ConnectionTimeoutError: could not obtain a connection
from the pool within 5.000 seconds (waited 5.001 seconds);
all pooled connections were in use

This is because, once Active Record checks out a connection, it locks it to the thread that checked it out, so only that thread can continue to use it. Once the main thread + 4 other threads checked out their 5 connections, the other 6 threads didn't have any connections available.

Current workarounds

Historically, Active Record users have been recommended to either

  1. clear out active connections with AR::Base.clear_active_connections!
  2. manually check out the connection with AR::Base.connection_pool.with_connection { ... }

In Rails, this has been managed automatically. Originally, there was a ConnectionManagement middleware that called AR::Base.clear_active_connections! after each request, releasing locked connections back into the pool. Today, this is surprisingly done by AR::QueryCache, which makes it appear as if though this functionality is only needed for query caching, when in fact it affects Active Record usage in general.

In other web frameworks such as Sinatra, you need to call manually AR::Base.clear_active_connections! after each request. In background jobs, it's recommended to use AR::Base.connection_pool.with_connection { ... }.

Rationale

I believe Active Record's connection pool should automatically check connections back into the pool after queries are finished, available for any other thread to use it. Basically, what AR::Base.connection_pool.with_connection { ... } does.

This is how Sequel's connection pool works. If we run the same script using Sequel, it will complete without errors:

require "sequel"

DB = Sequel.sqlite(max_connections: 5, pool_timeout: 5)

threads = 10.times.map do
  Thread.new do
    DB.run "SELECT 1"
  end
end

threads.each(&:join)

I believe this change would reduce the chance of people running out of connections. For example, they wouldn't need to remember to use ActiveRecord::Base.connection_pool.with_connection { ... } in background jobs. Yes, users should configure sufficiently large pools, but maybe at some point they start using threads inside background jobs, and don't realize that this requires them to increase the pool size even more.

I also believe this could reduce the load on the database, because it would allow using the smaller number of connections in certain cases. A pool of 50 background workers could theoretically use only 10 connections if the queries are fast.

System configuration

Rails version: master, 6.0.0, and probably any other version before

Ruby version: 2.6.0

@p8
Copy link
Member

p8 commented Sep 4, 2019

I can confirm the test fails on stable-4-2, stable-5-0, stable-5-1 and stable-5-2 branches.

@p8
Copy link
Member

p8 commented Sep 4, 2019

Reading the documentation of ConnectionPool it looks like connections are cached in @thread_cached_conns

      def connection
        @thread_cached_conns[connection_cache_key(current_thread)] ||= checkout
      end

I guess creating connections is expensive and you don't want to delete the query cache:

This variable tracks the cache of threads mapped to reserved connections, with the
sole purpose of speeding up the +connection+ method. It is not the authoritative
registry of which thread owns which connection.

# This variable tracks the cache of threads mapped to reserved connections, with the

@janko
Copy link
Contributor Author

janko commented Sep 5, 2019

Yes, connections should definitely stay cached. But they don't need to stay assigned to any thread, they can be made available "for grabs" by any thread.

@rails-bot
Copy link

rails-bot bot commented Dec 4, 2019

This issue has been automatically marked as stale because it has not been commented on for at least three months.
The resources of the Rails team are limited, and so we are asking for your help.
If you can still reproduce this error on the 6-0-stable branch or on master, please reply with all of the information you have about it in order to keep the issue open.
Thank you for all your contributions.

@rails-bot rails-bot bot added the stale label Dec 4, 2019
@janko
Copy link
Contributor Author

janko commented Dec 4, 2019

The original script still reproduces the failure on 6-0-stable.

@rails-bot rails-bot bot removed the stale label Dec 4, 2019
@abrisse
Copy link

abrisse commented Feb 6, 2020

I encountered the same issue and had to explicitely use ActiveRecord::Base.clear_active_connection! in all the threads I manually create.

Just saw that issue, so to give my 2 cents, I find it really counter-intuitive since that's not how a standard pool works. I guess the current design may be related to some threaded cache in AR.

@rails-bot
Copy link

rails-bot bot commented May 6, 2020

This issue has been automatically marked as stale because it has not been commented on for at least three months.
The resources of the Rails team are limited, and so we are asking for your help.
If you can still reproduce this error on the 6-0-stable branch or on master, please reply with all of the information you have about it in order to keep the issue open.
Thank you for all your contributions.

@Ben-Fenner
Copy link

Ben-Fenner commented Jan 8, 2024

I've run into this exact issue just now and would love to know the current thinking on this matter. Does the Fiber-safe ticket shown above take this issue and run with it? I'll need to give it a better read...

@jjb
Copy link
Contributor

jjb commented Mar 7, 2024

there's various recent work on this 🎉 such as

#50793
#50938

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

No branches or pull requests

5 participants