Last active
April 8, 2017 12:26
-
-
Save motchang/92871cce5c36a780db2ed1760c08e6f1 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# | |
# USAGE1: | |
# User.find(1).deepcache.friends.count.fetch | |
# | |
# 上記のような呼び出しをすると以下のキーで Redis に問い合わせてなければキャッシュします。 | |
# get deepcache:User:1:friends.count | |
# | |
# USAGE2: | |
# Category.deepcache.co_cats.where(slug: URI.encode(request_manager.params['slug'] || '')).limit(1).to_a.fetch | |
# | |
# こうなる | |
# get deepcache:Category:co_cats:where:{:slug=>"%E8%A1%A3%E9%A1%9E%E5%8F%A4%E7%9D%80%E3%83%BB%E9%9D%B4"}:limit:1:to_a | |
# | |
# いい感じにつかえる redis namespaced なキャッシュユーティリティです | |
# (個人の感想であり効果・効能を示すものではありません。 ) | |
# | |
# 当システムではカジュアルにキャッシュ全消し運用してるみたいなのでユーザー側のキャッシュが | |
# ガツガツ消えるのは悲しいなと考えて作りました。 | |
# ref: app/controllers/admins/caches_controller.rb | |
module Concerns::DeepCache | |
extend ActiveSupport::Concern | |
def deepcache | |
Essence.new(self) | |
end | |
module ClassMethods | |
def deepcache | |
Essence.new(self) | |
end | |
end | |
def self.extended(obj) | |
obj.class.include self | |
end | |
included do | |
after_update :self_cleanup_caches, :class_cache_cleanup, :relations_cache_cleanup | |
after_destroy :class_cache_cleanup, :relations_cache_cleanup | |
after_create :class_cache_cleanup, :relations_cache_cleanup | |
end | |
def self_cleanup_caches | |
deepcache.cleanup_caches | |
end | |
def relations_cache_cleanup | |
self.class.reflect_on_all_associations.each do |reflection| | |
case reflection | |
when ActiveRecord::Reflection::HasManyReflection | |
# nop, 自分がリレーション上で親の場合は何もしない | |
when ActiveRecord::Reflection::BelongsToReflection | |
# 自分がリレーション上で子の場合は親のクラスに紐づくキャッシュを消し込む | |
DependenciesFinder.new(reflection).dependencies.each do |klass| | |
klass.include(Concerns::DeepCache) unless klass.class.respond_to?(:deepcache) | |
klass.deepcache.cleanup_caches | |
end | |
end | |
end | |
end | |
def class_cache_cleanup | |
self.class.include(Concerns::DeepCache) unless self.class.respond_to?(:deepcache) | |
self.class.deepcache.cleanup_caches | |
end | |
class BlackholeClient | |
def method_missing(method, *args) | |
nil | |
end | |
def respond_to_missing?(symbol, include_private) | |
false | |
end | |
end | |
# cache fetcher / deleter | |
class Essence | |
include ActiveSupport::Configurable | |
config_accessor :redis_client do | |
BlackholeClient.new | |
end | |
PREFIX = "deepcache".freeze | |
def method_missing(method, *args) | |
@chain.push({ method: method, args: args }) | |
self | |
end | |
def respond_to_missing?(symbol, include_private) | |
false | |
end | |
def fetch(expires_in = nil) | |
key = key_name | |
value = client.get(key) | |
return Marshal.load(value) unless value.nil? | |
value = @chain.inject(@root) do |memo, c| | |
memo&.send(c[:method], *c[:args]) | |
end | |
return nil if value.blank? | |
ttl = expires_in || default_ttl | |
client.setex(key, ttl, Marshal.dump(value)) | |
value | |
end | |
def initialize(root) | |
if root.class != Class && (!root.respond_to?(:id) || root.id.nil?) | |
raise ArgumentError | |
end | |
@chain = [] | |
@root = root | |
end | |
def cleanup_caches | |
delete_keys = client.keys(key_prefix + '*') | |
return if delete_keys.blank? | |
client.del(*delete_keys) | |
end | |
private | |
def default_ttl | |
if @root.respond_to?(:id) | |
# when @root is instance | |
(60 * 60 * 24) | |
else | |
# when @root is constant | |
60 | |
end | |
end | |
def key_name | |
@chain.inject(key_prefix) { |memo, c| | |
[memo, c[:method].to_s, *c[:args].flatten].join(':') | |
} | |
end | |
def key_prefix | |
if @root.respond_to?(:id) | |
# when @root is instance | |
"#{@root.class}:#{@root.id}" | |
else | |
# when @root is constant | |
@root.to_s | |
end | |
end | |
def client | |
config.redis_client | |
end | |
end | |
# https://www.viget.com/articles/identifying-foreign-key-dependencies-from-activerecordbase-classes | |
class DependenciesFinder < Struct.new(:association) | |
delegate :foreign_key, to: :association | |
def klass | |
association.klass unless polymorphic? | |
end | |
def name | |
association.options[:class_name] || association.name | |
end | |
def polymorphic? | |
!!association.options[:polymorphic] | |
end | |
def polymorphic_dependencies | |
return [] unless polymorphic? | |
@polymorphic_dependencies ||= ActiveRecord::Base.subclasses.select { |model| polymorphic_match? model } | |
end | |
def polymorphic_match?(model) | |
model.reflect_on_all_associations(:has_many).any? do |has_many_association| | |
has_many_association.options[:as] == association.name | |
end | |
end | |
def dependencies | |
polymorphic? ? polymorphic_dependencies : Array(klass) | |
end | |
def polymorphic_type | |
association.foreign_type if polymorphic? | |
end | |
end | |
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
require 'rails_helper' | |
RSpec.describe Concerns::DeepCache do | |
before :each do | |
Concerns::DeepCache::Essence.configure do |config| | |
config.redis_client = Redis::Namespace.new(Concerns::DeepCache::Essence::PREFIX) | |
end | |
end | |
after :each do | |
Concerns::DeepCache::Essence.configure do |config| | |
config.redis_client = Concerns::DeepCache::BlackholeClient.new | |
end | |
end | |
subject { Item.new } | |
it { respond_to(:deepcache) } | |
describe '#deepcache' do | |
context 'root is normal AR table instance' do | |
context 'persisted' do | |
let(:item) { create(:item, :published) } | |
it "returns instance of Concerns::DeepCache::Essence" do | |
expect(item.deepcache).to be_instance_of Concerns::DeepCache::Essence | |
end | |
end | |
context 'not persisted' do | |
let(:item) { build(:item, :published) } | |
it "raise error" do | |
expect { item.deepcache }.to raise_error(ArgumentError) | |
end | |
end | |
end | |
context 'root is normal AR table class' do | |
let(:item) { create(:item, :published) } | |
it "returns instance of Concerns::DeepCache::Essence" do | |
expect(item.deepcache).to be_instance_of Concerns::DeepCache::Essence | |
end | |
end | |
end | |
describe '#fetch' do | |
context 'instances cache' do | |
let(:item) { create(:item_with_category, :published) } | |
it 'returns query result' do | |
expected = item.categories.first.name | |
expect(item.deepcache.categories.first.name.fetch).to eq expected | |
key = "Item:#{item.id}:categories:first:name" | |
expect(Marshal.load(Redis::Namespace.new(Concerns::DeepCache::Essence::PREFIX).get(key))).to eq expected | |
end | |
end | |
context 'classes cache' do | |
let(:co_cat) { create(:category, code: 1) } | |
it 'returns query result' do | |
Category.include Concerns::DeepCache | |
expected = Category.co_cats.where(slug: co_cat.slug).limit(1).to_a | |
expect(Category.deepcache.co_cats.where(slug: co_cat.slug).limit(1).to_a.fetch).to eq expected | |
key = "Category:co_cats:where:{:slug=>\"#{co_cat.slug}\"}:limit:1:to_a" | |
expect(Marshal.load(Redis::Namespace.new(Concerns::DeepCache::Essence::PREFIX).get(key))).to eq expected | |
end | |
end | |
end | |
describe '#self_cleanup_caches' do | |
context 'instance updated' do | |
let!(:item) { create(:item_with_category, :published) } | |
it 'delete cache child of instance' do | |
expected = item.categories.first.name | |
expect(item.deepcache.categories.first.name.fetch).to eq expected | |
key = "Item:#{item.id}:categories:first:name" | |
expect(Marshal.load(Redis::Namespace.new(Concerns::DeepCache::Essence::PREFIX).get(key))).to eq expected | |
item.highest_price = 100 | |
item.save | |
expect(Redis::Namespace.new(Concerns::DeepCache::Essence::PREFIX).get(key)).to be_nil | |
end | |
it 'delete cache child of class' do | |
expected = Item.all.to_a | |
expect(Item.deepcache.all.to_a.fetch).to eq expected | |
key = "Item:all:to_a" | |
expect(Marshal.load(Redis::Namespace.new(Concerns::DeepCache::Essence::PREFIX).get(key))).to eq expected | |
item.highest_price = 100 | |
item.save | |
expect(Redis::Namespace.new(Concerns::DeepCache::Essence::PREFIX).get(key)).to be_nil | |
end | |
end | |
end | |
describe "#relations_cache_cleanup" do | |
let(:store) { create(:store, :published) } | |
let!(:shop) { create(:shop, store: store) } | |
it 'delete parent class cache' do | |
expected = Store.all.to_a | |
expect(Store.deepcache.all.to_a.fetch).to eq expected | |
key = "Store:all:to_a" | |
expect(Marshal.load(Redis::Namespace.new(Concerns::DeepCache::Essence::PREFIX).get(key))).to eq expected | |
shop.name = 'hoge' | |
shop.save | |
expect(Redis::Namespace.new(Concerns::DeepCache::Essence::PREFIX).get(key)).to be_nil | |
end | |
end | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment