Rails.cache と Redis::Store と Rack::Attack

自分用メモ。

遭遇した問題

  • Rack::Attack で redis に保存したデータに有効期限が設定されない
  • namespace を定義した時だけ問題が発生する

結論

  • Redis::Store が古かった。1.1.4 を使っていたが、有効期限系のメソッド(expire, setex, ttlなど)が namespace に対応したのは 1.1.5 からだった

調査方法

コードを読んだ。

Rails.cache

Rack::Attack はデフォルトでは Rails.cache を使うことになってるのでここから始める。

Railsconfig.cache_store = :redis_store すると Rails.cache として Redis::Store を使うことができる。コードは以下を見るとわかる。

Rack::Attack

Rack::Attack はルールを定義してアクセス制限することができる Rack ミドルウェア。Throttle というのを使うとアクセス数をカウントしてしきい値を超えたら制限するということができる。そのバックエンドとして redis を使える。アクセス数をカウントするコードは以下を見る。

  • Rack::Attack.throttle はルールを定義するときに呼ばれるメソッドなのでざっと見ておく
  • アクセスが来たときは Rack::Attack.throttled? が呼ばれて、そこから Rack::Attack::Throttle#[] が呼ばれる。このメソッドはアクセスをカウントしてしきい値を超えたら true を返すという役割
  • アクセスのカウントは cache.count を見る

cache とは何か?

  • cache は Rack::Attack.cache である
  • Rack::Attack.cache は Rack::Attack::Cache.new
  • Rack::Attack::Cache.new は Rails.cache が定義されていたらそれを self.store に代入する
  • そして self.store= が呼ばれる (1回目にコードを読んだときにこれを見逃しててだいぶさまよってしまった)
  • self.store=Rails.cache を引数に StoreProxy#build を呼ぶ
  • StoreProxy#build は何をするかというと、Rails.cache が自分が知ってるキャッシュストアだったらそれをクラスでラップする
  • redis の場合は Rails.cache の実体は ActiveSupport::Cache::RedisStore だが、実際にラップするのはこのクラスではなく、それが持っている Redis::Store オブジェクトである。それは @data というインスタンス変数が保持しているので、StoreProxy は instance_variable_get(:@data) をつかって取り出している。
  • で、その Redis::Store オブジェクトを Rack::Attack::StoreProxy::RedisStoreProxy クラスでラップするのだが、そのために SimpleDelegator というクラスを継承している。これは Rubyist Magazine - 標準添付ライブラリ紹介 【第 6 回】 委譲 を見るとわかりやすいが、new に渡したオブジェクトに全てのメソッドを移譲することができる。なるほど。

つまり、cache というのは Rack::Attack::Cache で、データストアとしてRedis::Store オブジェクトをラップしたプロキシオブジェクトを持っているのだ。

cache.count を読もう

  • count は do_count メソッドに key と expires_in を渡す
  • do_count は store.increment したあと、それが nil を返したら store.write する
  • increment は RedisStoreProxy が実装している。何をしているかというと、self.incrby して self.expire している
  • その実装を見るために移譲先の Redis::Store を見てみるとそんなメソッドは定義されていない。よく見ると Redis::Store は Redis を継承しているので Redis を見ればよいかと思いきや、実はコンストラクタで _extend_namespace というメソッドを呼んでおり、ここで extend Namespace している。Redis::Store::Namespace を見ると namespace が定義されていたら key の先頭にそれをつけてから、元のメソッド(incrbyなど)を呼ぶようになっている
  • で、ようやく Redis に辿り着くのだった

感想

コードがいろんなリポジトリに分散してるので追うのに苦労した。

あと、コードを読む時は使っているバージョンと同じバージョンのコードを読みましょう。