先日、Rails.cache.fetch
を使っていた時、有効期限を短くしてるにもかかわらず古い値が残り続けるという現象が起きました。
結論から書くと、
間違えたコード
Rails.cache.fetch
のexpired_in
は5秒と設定し、0をキャッシュさせます。
有効期限を5秒と設定しているので、5秒後にはこのキャッシュは消えているはずです。
number = Rails.cache.fetch('key', expired_in: 5.seconds) do
0
end
=> 0
しかし、5秒後に中の値を変更して実行してみるとキャッシュが残り続けていました。
number = Rails.cache.fetch('key', expired_in: 5.seconds) do
1
end
=> 0
Railsのソースコード調査
初めてこの現象に遭遇したのでRailsのコードを読んでみることにしました。
Rails.cacheに関するコードはactivesupport/lib/active_support/cache.rb
にありました。
fetchメソッドは347行目にあります。
def fetch(name, options = nil, &block)
if block_given?
options = merged_options(options)
key = normalize_key(name, options)
entry = nil
instrument(:read, name, options) do |payload|
cached_entry = read_entry(key, **options, event: payload) unless options[:force]
entry = handle_expired_entry(cached_entry, key, options)
entry = nil if entry && entry.mismatched?(normalize_version(name, options))
payload[:super_operation] = :fetch if payload
payload[:hit] = !!entry if payload
end
if entry
get_entry_value(entry, name, options)
else
save_block_result_to_cache(name, options, &block)
end
elsif options && options[:force]
raise ArgumentError, "Missing block: Calling `Cache#fetch` with `force: true` requires a block."
else
read(name, options)
end
end
get_entry_value(entry, name, options)
でentryからキャッシュされた値を取り出しているみたいですね。
Entryの実装を見に行きましょう。
906行目あたりにあります。
expired?
メソッドの実装を見ると当然ではありますが、きちんと計算されていそうです。
もしかすると保存先をRedisにしてるからRedisの内部でキャッシュが解放されないからかもと、一瞬頭をよぎったけどそんなわけはなかった。
def expired?
@expires_in && @created_at + @expires_in <= Time.now.to_f
end
余談ですが、Time
をfloat
へ変換してから比較してるの、便利そうですね。
ここでinitialize
メソッドをみてこの現象が起きていた理由を理解しました。
解決方法
Entry
クラスのinitialize
メソッドを見ると引数に指定できるのはexpires_in
またはexpires_at
となっています。
そう、expired_in
という引数は存在せず指定できなかったのです。僕が参考にしたサイトが間違っていたのが原因というオチでした。
class Entry # :nodoc:
class << self
def unpack(members)
new(members[0], expires_at: members[1], version: members[2])
end
end
attr_reader :version
# Creates a new cache entry for the specified value. Options supported are
# +:compressed+, +:version+, +:expires_at+ and +:expires_in+.
def initialize(value, compressed: false, version: nil, expires_in: nil, expires_at: nil, **)
@value = value
@version = version
@created_at = 0.0
@expires_in = expires_at&.to_f || expires_in && (expires_in.to_f + Time.now.to_f)
@compressed = true if compressed
end
expires_in
やexpires_at
はnil
でもエラーにならないので、まあそうなるなという挙動ですね。
まとめ
しょうもないミスにがっかりでしたが、Rails自体のコードを読んだこと自体はいい勉強になってよかったです。