app/models/concerns/caching.rb
require 'colored'
# To understand our caching conventions, have a look at our wiki page:
# https://github.com/fiedl/wingolfsplattform/wiki/Caching
#
concern :Caching do
included do
private :cached_method
private :cached_block
private :process_result_for_caching
private :rescue_from_too_big_to_marshal
end
def use_caching?
self.class.use_caching?
end
# Case 1: Use it to call a cached method result.
#
# user.cached(:name)
#
# Case 2: Use it to call a cached method with arguments.
# Use this with care, since there is a cache for each argument!
#
# user.cached(:membership_in, group)
#
# Case 3: Use it with a block within a method definition.
# This moves the responsibility to cache into the model itself.
#
# class User
# def name
# cached { "#{first_name} #{last_name}" }
# end
# end
#
# user.name # already uses the cache!
#
def cached(method_name = nil, arguments = nil, &block)
if method_name
cached_method(method_name, arguments)
else
cached_block(&block)
end
end
def cached_method(method_name, arguments = nil)
cached_block(method_name: method_name, arguments: arguments) do
arguments ? send(method_name, *arguments) : send(method_name)
end
end
# options:
# method_name
# arguments
#
def cached_block(options = {}, &block)
# This gives the method name that called the #cached method.
# See: http://www.ruby-doc.org/core-2.1.2/Kernel.html
#
if options[:method_name] && options[:arguments] && options[:arguments].any?
key = [options[:method_name], options[:arguments]]
elsif options[:method_name]
key = options[:method_name]
else
caller_method_name = caller_locations(2,1)[0].label
key = caller_method_name
end
if self.id
rescue_from_too_big_to_marshal(block) do
Rails.cache.fetch([self.cache_key, key], expires_in: new_caches_expire_in) do
process_result_for_caching(yield)
end
end
else
yield
end
end
# For inspections, it might be useful to list all caches of a record.
#
def cache
Rails.cache.find_entries_by_regex(/#{cache_key}\/*/)
end
def read_cached(method)
Rails.cache.read([self.cache_key, method])
end
def new_caches_expire_in
1.year
end
def process_result_for_caching(result)
# Not all ActiveRecord::Relation objects can be stored in cache.
# Convert them to Arrays. Otherwise, this might raise an 'cannot dump' error.
result = result.to_a if result.kind_of? ActiveRecord::Relation
# This circumvents a bug: https://github.com/mperham/dalli/issues/250
Marshal.dump(result)
# Store the result in cache.
return result
end
def rescue_from_too_big_to_marshal(block_without_caching, &block_with_caching)
begin
yield
rescue ArgumentError, NameError => exc
if exc.message.include? 'year too big to marshal'
block_without_caching.call
else
raise exc
end
end
end
def invalidate_cache
# Be careful in specs. This takes one second to count as invalid.
self.touch
end
def delete_cached(method_name)
if self.class.use_caching?
# p "DEBUG DELETE CACHED #{self} #{method_name}"
Rails.cache.delete [self, method_name]
Rails.cache.delete_matched "#{self.cache_key}/#{method_name}/*"
end
end
def bulk_delete_cached(method_name, objects)
ids = objects.map &:id
regex = /.*\/(#{ids.join('|')})(-.*|)\/#{method_name}.*/
# p "DEBUG BULK DELETE CACHE #{regex}"
Rails.cache.delete_regex regex
end
def delete_cache
# print "DEBUG DELETE CACHE #{self}\n".red.bold
Rails.cache.delete_matched "#{self.cache_key}/*"
end
def renew_cache(time = Time.zone.now)
print "~" if ENV['CI'] == 'travis' # in order to keep tests alive
Rails.cache.renew(time) do
fill_cache
end
end
def renew_cache_later(options = {})
options[:time] ||= Rails.cache.renew_at || Time.zone.now
RenewCacheJob.perform_later(self, time: options[:time], method: options[:method])
end
# The default way to fill the cache is to call all methods
# that are registered as methods to cache. But each class may
# override or extend this `fill_cache` method.
#
# The class method `cached_methods` is automatically populated
# when declaring a method as cached like this:
#
# class User
# cache :title
# end
#
def fill_cache
Sidekiq::Logging.logger.info "#{title} # fill_cache" if Sidekiq::Logging.logger && (! Rails.env.test?)
self.class.cached_methods.try(:each) do |method_name|
self.fill_cached_method method_name
end
end
def fill_cached_method(method)
Sidekiq::Logging.logger.info "#{title} # fill_cached_method #{method}" if Sidekiq::Logging.logger && (! Rails.env.test?)
if Rails.cache.running_from_background_job && Rails.cache.renew_at
# When running from a background job, split it into sub-tasks.
self.renew_cache_later method: method
else
self.send method
end
end
def cache_created_at(method_name, arguments = nil)
::CacheAdditions
Rails.cache.created_at [self, method_name, arguments]
end
# This method ensures that no app cache is used to produce the result.
# If you call
#
# user.uncached :title
#
# this calls `user.title` but makes sure, no app cache is used at all.
# Note: This does not prevent the sql cache to be used.
#
# You could use this in specs:
#
# user.cached(:title).should == user.uncached(:title)
#
# ## What about user.title?
#
# Usually, user.title returns the uncached version; but if cached methods
# are used in the implementation of `User#title` then `user.title` does use these
# caches. If you call `user.uncached(:title)`, all app caches are ignored.
#
def uncached(method_name, args = nil)
Rails.cache.uncached do
if args
self.send method_name, *args
else
self.send method_name
end
end
end
class_methods do
# This class method provides a new way to cache methods.
#
# Example:
#
# class User
# def title
# self.foo
# end
# cache :title
# end
#
# This is a shortcut for:
#
# class User
# def title
# cached { self.foo }
# end
# def fill_cache
# title
# end
# end
#
def cache(method_name)
cache_method method_name
end
# This is really cool! This method re-defines the given method
# and wraps the original method in a cached block.
#
# class Foo
# def bar
# "bar"
# end
# cache :bar
# end
#
# ## With subclassing
#
# class MegaFoo < Foo
# def bar
# "mega #{super}"
# end
# cache :bar
# end
#
# ## How does this work?
#
# Previously, we've used `alias_method` to reference the
# original method. But we ran into issues with that when using
# class inheritance.
#
# Ruby's `prepend` and its cool meta programming came to the
# rescue!
#
# Inhale this:
#
# class Cachable
#
# def self.cache(method_name)
# caching_module = Module.new
# caching_module.module_eval do
# define_method method_name do |*args|
# "cached " + super(*args)
# end
# end
#
# self.prepend caching_module
# end
#
# end
#
# class Foo < Cachable
#
# def foo
# "foo"
# end
#
# cache :foo
# end
#
# p Foo.new.foo # => "cached foo"
#
def cache_method(method_name)
if use_caching?
# If a setter method exists as well, make the setter method
# also renew the cache.
#
setter_method_name = "#{method_name.to_s.gsub('?', '')}="
need_to_patch_setter_method = method_defined?(setter_method_name)
caching_module = Module.new
caching_module.module_eval do
define_method(method_name) { |*args|
cached_block(method_name: method_name, arguments: args) { super(*args) }
}
if need_to_patch_setter_method
define_method(setter_method_name) { |new_value|
result = super(new_value)
Rails.cache.renew { self.send method_name } if self.id
}
end
end
self.prepend caching_module
self.cached_methods = (self.cached_methods + [method_name]).uniq
end
end
def use_caching?
not ENV['NO_CACHING']
end
def cached_methods=(methods)
@cached_methods = methods
end
def cached_methods
@cached_methods ||= self.ancestors.collect { |ancestor_class|
ancestor_class.cached_methods if (ancestor_class.name != self.name) && ancestor_class.respond_to?(:cached_methods)
}.flatten.uniq - [nil]
end
end
end