先日 (2/2)、redis gem のバージョン 4.6.0 がリリースされました。 このバージョン 4.6.0 では、いくつかの API が deprecated 扱いになりました。
https://github.com/redis/redis-rb/blob/v4.6.0/CHANGELOG.md#460
我々の Rails アプリケーションのうち、いくつかは Redis.current を使うようになっていたため、この廃止の影響を受けました。この記事では、Redis.current の廃止の経緯と我々の対応策についてご紹介します。
これまで、我々のチームでは Rails アプリで利用する redis のコネクションをconfig/initializers/redis.rb
で以下のように設定していました。
# config/initializers/redis.rb
Redis.current = Redis.new(...)
この設定をしておくと、Rails アプリ内では `Redis.current` でコネクションを取り出すことができます。
その答えは Redis.current 廃止のコミットでやり取り されていました。
Because multi-threaded environments are very much the default these days,
and sharing the same Redis instance between threads leads to tons of locking.
…
It’s not the role of a database client to manage the lifecycle to the connection,
it’s up to the application to do that.
要約すると
ということですね。
上記のやり取りにあるように、コネクション管理の責務を切り離すことになったため、Redis gem には代替となる API は提供されません。
別のイシュー では (スレッドの問題を無視するのであれば) グローバル変数においてはどうか、という話も出ていましたが、この方式は workaround でしかなく、大本の問題の解消には至りません。
そこで、我々は以下のツイートを参考に、かんたんなコネクションプールを実装することにしました (というかほぼそのまま採用しました)。
Using Redis in your Rails app?
Use a connection pool instead of https://t.co/AhKF2H19ar or Redis.current. The former will create a new connection every time, and the latter will have every thread fighting for the same connection. pic.twitter.com/RN1D9qFMhj
— Adam McCrea (@adamlogic) September 9, 2021
connection_pool gem を使って、スレッド数分のコネクションを用意するコネクションプーラーです。
class RedisPool
# 既存の Pool オブジェクトを受け取るタイプの ConnectionPool::Wrapper
# (connection_pool gem の Wrapper は新たな Pool を作成してしまう)
class Wrapper < ConnectionPool::Wrapper
def initialize(pool)
@pool = pool
end
end
class << self
# Redis へのコネクションを取得する
def with(&block)
pool.with(&block)
end
# Redis へのコネクションを取得する (redis.gem との互換性維持用)
def get
Wrapper.new(pool)
end
private
def pool
# 引数部分は設定ファイルから読み込んでいるため、この記事では割愛します。
# size パラメータでスレッド数分のコネクションを用意します。
@pool ||= ConnectionPool.new(...) do
Redis.new(...)
end
end
end
end
このファイルを lib 以下に保存して利用しています。
(一瞬だけ gem 化も検討しましたが、設定ファイルの読み込みなどの固有のコードを含むので断念しました)
使い方は以下の 2通りです。
RedisPool.with do |redis|
redis.set ...
end
redis = RedisPool.get
redis.set ...
adamlogic さんのツイートの実装では前者だけ提供されていたのですが、既存のコードを書き換えやすくするために
ConnectionPool::Wrapper を使って Redis ライクなオブジェクトを取り出す方法も追加しました。