summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>2017-01-30 14:46:10 +0200
committerDmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>2017-01-30 20:15:50 +0200
commiteac0097113c067552d1556d660cb76fe0b20794a (patch)
treebe56423e2db7cb542c20ebd79f5aeb5b3e587183
parent6aec85ac4403e2e687473a5d14a72d7d1aa6df4a (diff)
downloadgitlab-ce-eac0097113c067552d1556d660cb76fe0b20794a.tar.gz
Add CacheMethod concern
Based on caching code from Repository. Since repository has some specific cases and in future will be changed even more I decided to create separate class specifically for AR model needs * cache return value to the instance variable and cache store * method to clear cached value * allow access to uncached version of method Signed-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>
-rw-r--r--app/models/concerns/cache_method.rb65
-rw-r--r--spec/models/concerns/cache_method_spec.rb44
2 files changed, 109 insertions, 0 deletions
diff --git a/app/models/concerns/cache_method.rb b/app/models/concerns/cache_method.rb
new file mode 100644
index 00000000000..dca411bb644
--- /dev/null
+++ b/app/models/concerns/cache_method.rb
@@ -0,0 +1,65 @@
+# Concern for caching method into instance variable and redis cache
+module CacheMethod
+ extend ActiveSupport::Concern
+
+ module ClassMethods
+ # Wraps around the given method and caches its output in Redis and an instance
+ # variable.
+ #
+ # This only works for methods that do not take any arguments.
+ def cache_method(name)
+ original = :"_uncached_#{name}"
+
+ alias_method(original, name)
+
+ define_method(name) do
+ cache_method_output(name) { __send__(original) }
+ end
+ end
+ end
+
+ # Expires the caches of a specific set of methods
+ def expire_method_caches(methods)
+ methods.each do |key|
+ cache.delete(cache_key(key))
+
+ ivar = cache_instance_variable_name(key)
+
+ remove_instance_variable(ivar) if instance_variable_defined?(ivar)
+ end
+ end
+
+ private
+
+ # Caches the supplied block both in a cache and in an instance variable.
+ #
+ # The cache key and instance variable are named the same way as the value of
+ # the `key` argument.
+ #
+ # This method will return `nil` if the corresponding instance variable is also
+ # set to `nil`. This ensures we don't keep yielding the block when it returns
+ # `nil`.
+ #
+ # name - The name of the key to cache the data in.
+ def cache_method_output(name, &block)
+ ivar = cache_instance_variable_name(name)
+
+ if instance_variable_defined?(ivar)
+ instance_variable_get(ivar)
+ else
+ instance_variable_set(ivar, cache.fetch(cache_key(name), &block))
+ end
+ end
+
+ def cache_instance_variable_name(key)
+ :"@#{key.to_s.tr('?!', '')}"
+ end
+
+ def cache
+ @cache ||= Rails.cache
+ end
+
+ def cache_key(key)
+ "#{self.class.name.tableize}:#{id}:#{key}"
+ end
+end
diff --git a/spec/models/concerns/cache_method_spec.rb b/spec/models/concerns/cache_method_spec.rb
new file mode 100644
index 00000000000..e44bb146fff
--- /dev/null
+++ b/spec/models/concerns/cache_method_spec.rb
@@ -0,0 +1,44 @@
+require 'spec_helper'
+
+describe CacheMethod, caching: true do
+ class TestClassForCacheMethod
+ include CacheMethod
+
+ def id
+ 73
+ end
+
+ def time
+ Time.now
+ end
+
+ cache_method :time
+ end
+
+ subject { TestClassForCacheMethod.new }
+
+ let!(:time) { subject.time }
+
+ describe 'cache_method' do
+ it 'returns cached value' do
+ expect(subject.time).to eq(time)
+ end
+
+ it 'sets instance variable' do
+ expect(subject.send(:instance_variable_get, '@time')).to eq(time)
+ end
+
+ it 'sets value in the cache store' do
+ another_instance = TestClassForCacheMethod.new
+
+ expect(another_instance.time).to eq(time)
+ end
+ end
+
+ describe 'expire_method_caches' do
+ it 'expires cache and returns new value' do
+ subject.expire_method_caches(%w(time))
+ expect(subject.time).not_to eq(time)
+ end
+ end
+end