※現在、ブログ記事を移行中のため一部表示が崩れる場合がございます。
順次修正対応にあたっておりますので何卒ご了承いただけますよう、お願い致します。

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 なインターフェースを用意した