canvas-lms/lib/model_cache.rb

152 lines
4.5 KiB
Ruby

# frozen_string_literal: true
#
# Copyright (C) 2012 - present Instructure, Inc.
#
# This file is part of Canvas.
#
# Canvas is free software: you can redistribute it and/or modify it under
# the terms of the GNU Affero General Public License as published by the Free
# Software Foundation, version 3 of the License.
#
# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along
# with this program. If not, see <http://www.gnu.org/licenses/>.
#
# on-demand in-memory model caching for those times where the rails query
# cache can't help you, like in this contrived example:
#
# # given:
#
# class Foo < ActiveRecord::Base
# has_many :bars
# end
#
# class Bar < ActiveRecord::Base
# belongs_to :foo
# def something!
# update_attribute :stuff, "my foo is: #{foo.name}"
# end
# end
#
# # if you do:
# foo.bars.each(&:something!)
#
# # then by default, AR loads each bar's foo separately and won't use the
# # query cache, since each update blows it away.
#
# granted, the example is a bit contrived, as preloading would be one possible
# solution. ModelCache is useful for those times when preloading is just not
# feasible. See Conversation and ConversatonParticipant for real-world usage.
module ModelCache
module ClassMethods
# e.g. use this to cache calls to ConversationParticipant#conversation,
# no matter how those conversation_participants were loaded
def cacheable_method(method, options = {})
options[:cache_name] ||= method.to_s.pluralize.to_sym
options[:key_method] ||= "#{method}_id"
options[:key_lookup] ||= :id
options[:type] ||= :instance
# ensure the target class is ModelCache-aware, and set up the :id lookup
target_klass = reflections[method.to_s].klass
raise "`#{target_klass}` needs to `include ModelCache` before you can make `#{self}##{method}` cacheable" unless target_klass.included_modules.include?(ModelCache)
unless ModelCache.keys[target_klass.name].include?(options[:key_lookup])
ModelCache.keys[target_klass.name] << options[:key_lookup]
end
ModelCache.make_cacheable self, method, options
end
end
module InstanceMethods
def add_to_caches
return unless (cache = ModelCache[self.class.name.underscore.pluralize.to_sym])
cache.each_key do |key|
cache[key][send(key)] = self
end
end
def update_in_caches
return unless (cache = ModelCache[self.class.name.underscore.pluralize.to_sym])
cache.each_key do |key|
if saved_change_to_attribute?(key)
cache[key][send(key)] = self
cache[key].delete(attribute_before_last_save(key))
end
end
end
end
def self.with_cache(lookups)
@cache = lookups.transform_values do |v|
prepare_lookups(v)
end
yield
ensure
@cache = nil
end
def self.[](cache_name)
return nil unless @cache
@cache[cache_name] || {}
end
def self.keys
@keys ||= Hash.new { |h, k| h[k] = [] }
end
def self.prepare_lookups(records)
return records if records.is_a?(Hash)
return {} if records.empty?
keys[records.first.class.name].index_with do |k|
records.index_by(&k)
end
end
def self.make_cacheable(klass, method, options = {})
options[:type] ||= :class
options[:cache_name] ||= klass.name.underscore.pluralize.to_sym
options[:key_lookup] ||= method
orig_method = "super"
alias_method = nil
key_value = options[:key_method] || "args.first"
# if extra args are provided, we should clear out the current value
# (e.g. if you call c.user(:lock => true) )
expected_args = options[:key_method] ? 0 : 1
maybe_reset = "cache[#{key_value}] = #{orig_method} if args.size > #{expected_args}"
klass.send((options[:type] == :instance) ? :class_eval : :instance_eval, <<~RUBY, __FILE__, __LINE__ + 1)
def #{method}(*args)
if cache = ModelCache[#{options[:cache_name].inspect}] and cache = cache[#{options[:key_lookup].inspect}]
#{maybe_reset}
cache[#{key_value}] ||= #{orig_method}
else
#{orig_method}
end
end
#{alias_method}
RUBY
end
def self.included(klass)
klass.include InstanceMethods
klass.extend ClassMethods
klass.after_create :add_to_caches
klass.after_update :update_in_caches
end
end