Ruby: Redis.current の廃止と向き合った件


2022年 02月 14日

先日 (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 の廃止の経緯と我々の対応策についてご紹介します。

Redis.current ってなんだっけ?

これまで、我々のチームでは Rails アプリで利用する redis のコネクションを
config/initializers/redis.rb で以下のように設定していました。

# config/initializers/redis.rb
Redis.current = Redis.new(...)

この設定をしておくと、Rails アプリ内では `Redis.current` でコネクションを取り出すことができます。

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 でしかなく、大本の問題の解消には至りません。

そこで、我々は以下のツイートを参考に、かんたんなコネクションプールを実装することにしました (というかほぼそのまま採用しました)。

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通りです。

  • .with メソッドにブロックを渡す方法
  RedisPool.with do |redis|
    redis.set ...
  end
  • .get メソッドでコネクションを取り出す方法
  redis = RedisPool.get
  redis.set ...  

adamlogic さんのツイートの実装では前者だけ提供されていたのですが、既存のコードを書き換えやすくするために
ConnectionPool::Wrapper を使って Redis ライクなオブジェクトを取り出す方法も追加しました。

まとめ

  • redis-4.6.0 から Redis.current が廃止になった
  • RedisPool クラスを作ってそちらを参照するようにした
  • 既存の実装にも使いやすいよう、adhoc なインターフェースを用意した