Introduce a new CacheableAttributes concern
Signed-off-by: Rémy Coutable <remy@rymai.me>
This commit is contained in:
parent
a2dbca4a27
commit
02f17a0988
3 changed files with 211 additions and 0 deletions
54
app/models/concerns/cacheable_attributes.rb
Normal file
54
app/models/concerns/cacheable_attributes.rb
Normal file
|
@ -0,0 +1,54 @@
|
|||
module CacheableAttributes
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
after_commit { self.class.expire }
|
||||
end
|
||||
|
||||
class_methods do
|
||||
# Can be overriden
|
||||
def current_without_cache
|
||||
last
|
||||
end
|
||||
|
||||
def cache_key
|
||||
"#{name}:#{Gitlab::VERSION}:#{Gitlab.migrations_hash}:json".freeze
|
||||
end
|
||||
|
||||
def defaults
|
||||
{}
|
||||
end
|
||||
|
||||
def build_from_defaults(attributes = {})
|
||||
new(defaults.merge(attributes))
|
||||
end
|
||||
|
||||
def cached
|
||||
json_attributes = Rails.cache.read(cache_key)
|
||||
return nil unless json_attributes.present?
|
||||
|
||||
build_from_defaults(JSON.parse(json_attributes))
|
||||
end
|
||||
|
||||
def current
|
||||
cached_record = cached
|
||||
return cached_record if cached_record.present?
|
||||
|
||||
current_without_cache.tap { |current_record| current_record&.cache! }
|
||||
rescue
|
||||
# Fall back to an uncached value if there are any problems (e.g. Redis down)
|
||||
current_without_cache
|
||||
end
|
||||
|
||||
def expire
|
||||
Rails.cache.delete(cache_key)
|
||||
rescue
|
||||
# Gracefully handle when Redis is not available. For example,
|
||||
# omnibus may fail here during gitlab:assets:compile.
|
||||
end
|
||||
end
|
||||
|
||||
def cache!
|
||||
Rails.cache.write(self.class.cache_key, attributes.to_json)
|
||||
end
|
||||
end
|
|
@ -9,6 +9,10 @@ module Gitlab
|
|||
Settings
|
||||
end
|
||||
|
||||
def self.migrations_hash
|
||||
@_migrations_hash ||= Digest::MD5.hexdigest(ActiveRecord::Migrator.get_all_versions.to_s)
|
||||
end
|
||||
|
||||
COM_URL = 'https://gitlab.com'.freeze
|
||||
APP_DIRS_PATTERN = %r{^/?(app|config|ee|lib|spec|\(\w*\))}
|
||||
SUBDOMAIN_REGEX = %r{\Ahttps://[a-z0-9]+\.gitlab\.com\z}
|
||||
|
|
153
spec/models/concerns/cacheable_attributes_spec.rb
Normal file
153
spec/models/concerns/cacheable_attributes_spec.rb
Normal file
|
@ -0,0 +1,153 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe CacheableAttributes do
|
||||
let(:minimal_test_class) do
|
||||
Class.new do
|
||||
include ActiveModel::Model
|
||||
extend ActiveModel::Callbacks
|
||||
define_model_callbacks :commit
|
||||
include CacheableAttributes
|
||||
|
||||
def self.name
|
||||
'TestClass'
|
||||
end
|
||||
|
||||
def self.first
|
||||
@_first ||= new('foo' => 'a')
|
||||
end
|
||||
|
||||
def self.last
|
||||
@_last ||= new('foo' => 'a', 'bar' => 'b')
|
||||
end
|
||||
|
||||
attr_accessor :attributes
|
||||
|
||||
def initialize(attrs = {})
|
||||
@attributes = attrs
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
shared_context 'with defaults' do
|
||||
before do
|
||||
minimal_test_class.define_singleton_method(:defaults) do
|
||||
{ foo: 'a', bar: 'b', baz: 'c' }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '.current_without_cache' do
|
||||
it 'defaults to last' do
|
||||
expect(minimal_test_class.current_without_cache).to eq(minimal_test_class.last)
|
||||
end
|
||||
|
||||
it 'can be overriden' do
|
||||
minimal_test_class.define_singleton_method(:current_without_cache) do
|
||||
first
|
||||
end
|
||||
|
||||
expect(minimal_test_class.current_without_cache).to eq(minimal_test_class.first)
|
||||
end
|
||||
end
|
||||
|
||||
describe '.cache_key' do
|
||||
it 'excludes cache attributes' do
|
||||
expect(minimal_test_class.cache_key).to eq("TestClass:#{Gitlab::VERSION}:#{Gitlab.migrations_hash}:json")
|
||||
end
|
||||
end
|
||||
|
||||
describe '.defaults' do
|
||||
it 'defaults to {}' do
|
||||
expect(minimal_test_class.defaults).to eq({})
|
||||
end
|
||||
|
||||
context 'with defaults defined' do
|
||||
include_context 'with defaults'
|
||||
|
||||
it 'can be overriden' do
|
||||
expect(minimal_test_class.defaults).to eq({ foo: 'a', bar: 'b', baz: 'c' })
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '.build_from_defaults' do
|
||||
include_context 'with defaults'
|
||||
|
||||
context 'without any attributes given' do
|
||||
it 'intializes a new object with the defaults' do
|
||||
expect(minimal_test_class.build_from_defaults).not_to be_persisted
|
||||
end
|
||||
end
|
||||
|
||||
context 'without attributes given' do
|
||||
it 'intializes a new object with the given attributes merged into the defaults' do
|
||||
expect(minimal_test_class.build_from_defaults(foo: 'd').attributes[:foo]).to eq('d')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '.current', :use_clean_rails_memory_store_caching do
|
||||
context 'redis unavailable' do
|
||||
it 'returns an uncached record' do
|
||||
allow(minimal_test_class).to receive(:last).and_return(:last)
|
||||
expect(Rails.cache).to receive(:read).and_raise(Redis::BaseError)
|
||||
|
||||
expect(minimal_test_class.current).to eq(:last)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when a record is not yet present' do
|
||||
it 'does not cache nil object' do
|
||||
# when missing settings a nil object is returned, but not cached
|
||||
allow(minimal_test_class).to receive(:last).twice.and_return(nil)
|
||||
|
||||
expect(minimal_test_class.current).to be_nil
|
||||
expect(Rails.cache.exist?(minimal_test_class.cache_key)).to be(false)
|
||||
end
|
||||
|
||||
it 'cache non-nil object' do
|
||||
# when the settings are set the method returns a valid object
|
||||
allow(minimal_test_class).to receive(:last).and_call_original
|
||||
|
||||
expect(minimal_test_class.current).to eq(minimal_test_class.last)
|
||||
expect(Rails.cache.exist?(minimal_test_class.cache_key)).to be(true)
|
||||
|
||||
# subsequent calls retrieve the record from the cache
|
||||
last_record = minimal_test_class.last
|
||||
expect(minimal_test_class).not_to receive(:last)
|
||||
expect(minimal_test_class.current.attributes).to eq(last_record.attributes)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '.cached', :use_clean_rails_memory_store_caching do
|
||||
context 'when cache is cold' do
|
||||
it 'returns nil' do
|
||||
expect(minimal_test_class.cached).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
context 'when cached settings do not include the latest defaults' do
|
||||
before do
|
||||
Rails.cache.write(minimal_test_class.cache_key, { bar: 'b', baz: 'c' }.to_json)
|
||||
minimal_test_class.define_singleton_method(:defaults) do
|
||||
{ foo: 'a', bar: 'b', baz: 'c' }
|
||||
end
|
||||
end
|
||||
|
||||
it 'includes attributes from defaults' do
|
||||
expect(minimal_test_class.cached.attributes[:foo]).to eq(minimal_test_class.defaults[:foo])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#cache!', :use_clean_rails_memory_store_caching do
|
||||
let(:appearance_record) { create(:appearance) }
|
||||
|
||||
it 'caches the attributes' do
|
||||
appearance_record.cache!
|
||||
|
||||
expect(Rails.cache.read(Appearance.cache_key)).to eq(appearance_record.attributes.to_json)
|
||||
end
|
||||
end
|
||||
end
|
Loading…
Reference in a new issue