Rails.cache と Redis::Store と Rack::Attack
自分用メモ。
遭遇した問題
- Rack::Attack で redis に保存したデータに有効期限が設定されない
- namespace を定義した時だけ問題が発生する
結論
調査方法
コードを読んだ。
Rails.cache
Rack::Attack はデフォルトでは Rails.cache を使うことになってるのでここから始める。
Rails で config.cache_store = :redis_store
すると Rails.cache として Redis::Store を使うことができる。コードは以下を見るとわかる。
- Rails::Application::Bootstrap の
initializer :initialize_cache
あたり - そこから呼ばれる ActiveSupport::Cache.lookup_store
- そして require される ActiveSupport::Cache::RedisStore。これは redis-activesupport にコードがある
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 に辿り着くのだった
感想
コードがいろんなリポジトリに分散してるので追うのに苦労した。
あと、コードを読む時は使っているバージョンと同じバージョンのコードを読みましょう。