Rails 7で低レベルキャッシュを導入しました。Patternモデルのレコード数か最新レコードのタイムスタンプが変わらなければ、Pattern#fetch_regex
メソッドでキャッシュが効くようにしました。
class Pattern
# クエリ結果をキャッシュする
def fetch_regex
latest_regex = Pattern.order(:updated_at).last.updated_at.to_formatted_s(:number)
Rails.cache.fetch("/model/pattern/check/#{Pattern.count}_#{latest_regex}", expires_in: 1.day, compress: true) do
Pattern.pluck(*KEYS).map do |line|
line[1] = Regexp.new("#{line[1]}#{DELIMITER_TAIL}")
KEYS.zip(line).to_h
end
end
end
end
以下のテストを書いたのですが、2回に1回ぐらいの割合でテストが失敗しました。
class PatternTest < ActiveSupport::TestCase
test "Patternモデル: レコード更新でキャッシュキーが更新される" do
cache01 = cache_key(Pattern)
patterns = Pattern.all.order(:updated_at)
assert_equal cache01[:latest_regex], patterns.last.updated_at.to_formatted_s(:number)
first = Pattern.first
first.regex = '42'
first.save
Pattern.fetch_regex
cache02 = cache_key(Pattern)
# カウンタは更新されないが最終更新日は更新される
assert_equal cache02[:count], cache01[:count]
assert_not_equal cache02[:latest_regex], cache01[:latest_regex]
end
private
def cache_key(pattern)
return {
latest_regex: pattern.order(:updated_at).last.regex,
count: pattern.count
}
end
end
結局、原因は更新のタイムスタンプが1秒単位だったことでした(msec単位の更新は取れません)。
試しにsleep(1)
を追加してタイムスタンプの更新を検出可能にしたところ、落ちなくなりました。しかしテストの実行時間が長くなるうえに、タイミング依存の問題が生じる可能性があるので、これは避けたい。
test "Patternモデル: レコード更新でキャッシュキーが更新される" do
cache01 = cache_key(Pattern)
patterns = Pattern.all.order(:updated_at)
assert_equal cache01[:latest_regex], patterns.last.updated_at.to_formatted_s(:number)
first = Pattern.first
first.regex = '42'
first.save
sleep(1) # 1秒待つ(更新タイムスタンプが秒未満を取れないため)
Pattern.fetch_regex
cache02 = cache_key(Pattern)
# カウンタは更新されないが最終更新日は更新される
assert_equal cache02[:count], cache01[:count]
assert_not_equal cache02[:latest_regex], cache01[:latest_regex]
考えてみれば、更新のタイムスタンプを当てにするより、最新レコードの更新内容(uniqueなregex
)そのものを使う方がよさそうです。
最終的に以下のようにコードとテストを両方修正したことで、sleep(1)
なしでもテストが安定するようになりました。
class Pattern
# クエリ結果をキャッシュする
def fetch_regex
Rails.cache.fetch("/model/pattern/check/#{Pattern.count}_#{Pattern.order(:updated_at).last.regex}", expires_in: 1.day, compress: true) do
Pattern.pluck(*KEYS).map do |line|
line[1] = Regexp.new("#{line[1]}#{DELIMITER_TAIL}")
KEYS.zip(line).to_h
end
end
end
end
test 'Patternモデル: レコード更新でキャッシュキーが更新される' do
cache01 = cache_key(Pattern)
patterns = Pattern.all.order(:updated_at)
assert_equal cache01[:latest_regex], patterns.last.regex
first = Pattern.first
first.regex = '42'
first.save
Pattern.new.fetch_regex
cache02 = cache_key(Pattern)
# レコード数は更新されないがregexは更新される
assert_equal cache02[:count], cache01[:count]
assert_not_equal cache02[:latest_regex], cache01[:latest_regex]
end
private
def cache_key(pattern)
return {
latest_regex: pattern.order(:updated_at).last.regex,
count: pattern.count
}
end