今の ActiveRecord は connection_pool ありきの実装になっているが、「永続接続が許されるのは中規模サイトまでだよねー」と誰かが言ったとか言わないとかで、永続接続を止める。対象は activerecord 4.1。
TL; DR
https://github.com/sonots/activerecord-refresh_connection を利用できる。
事前知識
Rails が起動すると、ActiveRecord::Base#establish_connection が呼ばれるが、実は establish_connection では connection は貼られず、設定情報が渡されるだけである。(正確には、すでにあった接続を破棄する処理も行われるが)
lib/active_record/connection_handling.rb#L37-L47
def establish_connection(spec = ENV["DATABASE_URL"])
resolver = ConnectionAdapters::ConnectionSpecification::Resolver.new spec, configurations
spec = resolver.spec
unless respond_to?(spec.adapter_method)
raise AdapterNotFound, "database configuration specifies nonexistent #{spec.config[:adapter]} adapter"
end
remove_connection # disconnect connections in a poll
connection_handler.establish_connection self, spec
end
lib/active_record/connection_adapters/abstract/connection_pool.rb#L537-L541
ConnectionPool のインスタンスがセットされているけれど、connection が作られてはいないことがわかる。
def establish_connection(owner, spec)
@class_to_pool.clear
raise RuntimeError, "Anonymous class is not allowed." unless owner.name
owner_to_pool[owner.name] = ConnectionAdapters::ConnectionPool.new(spec)
end
lib/active_record/connection_adapters/abstract/connection_pool.rb#L261-L267
では、いつどこで connection が貼られるのかというと、ActiveRecord::Base.connection が呼ばれたときである。 ここで connection pool にキャッシュされ、すでにキャッシュされていればソレを利用するようになっている。
# Retrieve the connection associated with the current thread, or call
# #checkout to obtain one if necessary.
#
# #connection can be called any number of times; the connection is
# held in a hash keyed by the thread id.
def connection
# this is correctly done double-checked locking
# (ThreadSafe::Cache's lookups have volatile semantics)
@reserved_connections[current_connection_id] || synchronize do
@reserved_connections[current_connection_id] ||= checkout
end
end
lib/active_record/connection_adapters/abstract/connection_pool.rb#L349-L355
ちなみに #checkout は、connection pool に connection があれば取り出し、なければ新しい connection を作るメソッド。 新しい connection を作る処理をたどっていくと最終的に mysql2_connection などの adapter の connection メソッドにいきつく。
def checkout
synchronize do
conn = acquire_connection
conn.lease
checkout_and_verify(conn)
end
end
で、その connection
メソッドがいつ呼ばれるかというと、クエリを投げるときに呼ばれる。
lib/active_record/querying.rb#L37-L48
def find_by_sql(sql, binds = [])
result_set = connection.select_all(sanitize_sql(sql), "#{name} Load", binds)
....
end
永続接続をやめる方法
- アプリコードで with_connection を利用する
- クエリ毎に connection を貼る
- リクエスト毎に connection を貼る
(1) アプリコードで with_connection を利用する
#with_connection というメソッドがあるので、それを使って接続を open しては close するようにアプリケーションコードを書くことができる。
ActiveRecord::Base.connection_pool.with_connection do
Post.create(body: 'foo')
end
(2) クエリ毎に connection を貼る
#connection
を呼ぶたびに新しく接続を open しては close すれば良い。が、close するタイミングがないな。。。
用途も思いつかないので、今回は考えないものとする。
(3) リクエスト毎に connection を貼る
Rails のリクエスト処理の最後に、プロセスが持っている connection pool の接続をすべて閉じてしまえば良い。 rails の Controller の after_action などでやっても良いが、Rack middleware にしてしまうのが、一番よさそう。 Sinatra でも使えるし。
module ActiveRecord
module ConnectionAdapters
class RefreshConnectionManagement
def initialize(app)
@app = app
end
def call(env)
testing = env.key?('rack.test')
response = @app.call(env)
response[2] = ::Rack::BodyProxy.new(response[2]) do
ActiveRecord::Base.clear_all_connections! unless testing
end
response
rescue Exception
ActiveRecord::Base.clear_all_connections! unless testing
raise
end
end
end
end
元々 rails がやっている ActiveRecord::ConnectionAdapters::ConnectionManagement を挿げ替える。 それのActiveRecord::Base.clear_active_connections!
を ActiveRecord::Base.clear_all_connections!
に置き換えただけである。
# config/application.rb
class Application < Rails::Application
config.autoload_paths += %W(#{config.root}/lib)
config.middleware.swap ActiveRecord::ConnectionAdapters::ConnectionManagement,
"ActiveRecord::ConnectionAdapters::RefreshConnectionManagement"
end
サンプルの rails アプリを作って、mysql に接続、show processlist; で connection が残っていないことを確認した。
おわりに
「(3) リクエスト毎に connection を貼る」の方針でやる予定。=> gem 作った。https://github.com/sonots/activerecord-refresh_connection