2019-07-07 11:18:12 +05:30
|
|
|
# frozen_string_literal: true
|
|
|
|
|
2018-11-08 19:23:39 +05:30
|
|
|
require 'spec_helper'
|
|
|
|
|
|
|
|
describe CacheableAttributes do
|
|
|
|
let(:minimal_test_class) do
|
|
|
|
Class.new do
|
|
|
|
include ActiveModel::Model
|
|
|
|
extend ActiveModel::Callbacks
|
2019-10-12 21:52:04 +05:30
|
|
|
include ActiveModel::AttributeMethods
|
2018-11-08 19:23:39 +05:30
|
|
|
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
|
|
|
|
|
2019-02-15 15:39:39 +05:30
|
|
|
def self.column_names
|
|
|
|
%w[foo bar baz]
|
|
|
|
end
|
|
|
|
|
2018-11-08 19:23:39 +05:30
|
|
|
attr_accessor :attributes
|
|
|
|
|
|
|
|
def initialize(attrs = {}, *)
|
|
|
|
@attributes = attrs
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2019-10-12 21:52:04 +05:30
|
|
|
before do
|
|
|
|
stub_const("MinimalTestClass", minimal_test_class)
|
|
|
|
end
|
|
|
|
|
2018-11-08 19:23:39 +05:30
|
|
|
shared_context 'with defaults' do
|
|
|
|
before do
|
2019-10-12 21:52:04 +05:30
|
|
|
MinimalTestClass.define_singleton_method(:defaults) do
|
2018-11-08 19:23:39 +05:30
|
|
|
{ foo: 'a', bar: 'b', baz: 'c' }
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2019-10-12 21:52:04 +05:30
|
|
|
describe '.expire', :use_clean_rails_memory_store_caching, :request_store do
|
|
|
|
it 'wipes the cache' do
|
|
|
|
obj = MinimalTestClass.new
|
|
|
|
obj.cache!
|
|
|
|
expect(MinimalTestClass.cached).not_to eq(nil)
|
|
|
|
|
|
|
|
MinimalTestClass.expire
|
|
|
|
|
|
|
|
expect(MinimalTestClass.cached).to eq(nil)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2018-11-08 19:23:39 +05:30
|
|
|
describe '.current_without_cache' do
|
|
|
|
it 'defaults to last' do
|
2019-10-12 21:52:04 +05:30
|
|
|
expect(MinimalTestClass.current_without_cache).to eq(MinimalTestClass.last)
|
2018-11-08 19:23:39 +05:30
|
|
|
end
|
|
|
|
|
2018-12-13 13:39:08 +05:30
|
|
|
it 'can be overridden' do
|
2019-10-12 21:52:04 +05:30
|
|
|
MinimalTestClass.define_singleton_method(:current_without_cache) do
|
2018-11-08 19:23:39 +05:30
|
|
|
first
|
|
|
|
end
|
|
|
|
|
2019-10-12 21:52:04 +05:30
|
|
|
expect(MinimalTestClass.current_without_cache).to eq(MinimalTestClass.first)
|
2018-11-08 19:23:39 +05:30
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
describe '.cache_key' do
|
|
|
|
it 'excludes cache attributes' do
|
2019-10-12 21:52:04 +05:30
|
|
|
expect(MinimalTestClass.cache_key).to eq("TestClass:#{Gitlab::VERSION}:#{Rails.version}")
|
2018-11-08 19:23:39 +05:30
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
describe '.defaults' do
|
|
|
|
it 'defaults to {}' do
|
2019-10-12 21:52:04 +05:30
|
|
|
expect(MinimalTestClass.defaults).to eq({})
|
2018-11-08 19:23:39 +05:30
|
|
|
end
|
|
|
|
|
|
|
|
context 'with defaults defined' do
|
|
|
|
include_context 'with defaults'
|
|
|
|
|
2018-12-13 13:39:08 +05:30
|
|
|
it 'can be overridden' do
|
2019-10-12 21:52:04 +05:30
|
|
|
expect(MinimalTestClass.defaults).to eq({ foo: 'a', bar: 'b', baz: 'c' })
|
2018-11-08 19:23:39 +05:30
|
|
|
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
|
2019-10-12 21:52:04 +05:30
|
|
|
expect(MinimalTestClass.build_from_defaults.attributes).to eq(MinimalTestClass.defaults.stringify_keys)
|
2018-11-08 19:23:39 +05:30
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
context 'with attributes given' do
|
|
|
|
it 'intializes a new object with the given attributes merged into the defaults' do
|
2019-10-12 21:52:04 +05:30
|
|
|
expect(MinimalTestClass.build_from_defaults(foo: 'd').attributes['foo']).to eq('d')
|
2018-11-08 19:23:39 +05:30
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
describe 'edge cases on concrete implementations' do
|
|
|
|
describe '.build_from_defaults' do
|
|
|
|
context 'without any attributes given' do
|
|
|
|
it 'intializes all attributes even if they are nil' do
|
|
|
|
record = ApplicationSetting.build_from_defaults
|
|
|
|
|
|
|
|
expect(record).not_to be_persisted
|
|
|
|
expect(record.sign_in_text).to be_nil
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
describe '.current', :use_clean_rails_memory_store_caching do
|
|
|
|
context 'redis unavailable' do
|
|
|
|
before do
|
2019-10-12 21:52:04 +05:30
|
|
|
allow(MinimalTestClass).to receive(:last).and_return(:last)
|
|
|
|
expect(Rails.cache).to receive(:read).with(MinimalTestClass.cache_key).and_raise(Redis::BaseError)
|
2018-11-08 19:23:39 +05:30
|
|
|
end
|
|
|
|
|
|
|
|
context 'in production environment' do
|
|
|
|
before do
|
2019-12-04 20:38:33 +05:30
|
|
|
stub_rails_env('production')
|
2018-11-08 19:23:39 +05:30
|
|
|
end
|
|
|
|
|
|
|
|
it 'returns an uncached record and logs a warning' do
|
2020-06-23 00:09:42 +05:30
|
|
|
expect(Gitlab::AppLogger).to receive(:warn).with("Cached record for TestClass couldn't be loaded, falling back to uncached record: Redis::BaseError")
|
2018-11-08 19:23:39 +05:30
|
|
|
|
2019-10-12 21:52:04 +05:30
|
|
|
expect(MinimalTestClass.current).to eq(:last)
|
2018-11-08 19:23:39 +05:30
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
context 'in other environments' do
|
|
|
|
before do
|
2019-12-04 20:38:33 +05:30
|
|
|
stub_rails_env('development')
|
2018-11-08 19:23:39 +05:30
|
|
|
end
|
|
|
|
|
|
|
|
it 'returns an uncached record and logs a warning' do
|
2020-06-23 00:09:42 +05:30
|
|
|
expect(Gitlab::AppLogger).not_to receive(:warn)
|
2018-11-08 19:23:39 +05:30
|
|
|
|
2019-10-12 21:52:04 +05:30
|
|
|
expect { MinimalTestClass.current }.to raise_error(Redis::BaseError)
|
2018-11-08 19:23:39 +05:30
|
|
|
end
|
|
|
|
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(ApplicationSetting).to receive(:current_without_cache).twice.and_return(nil)
|
|
|
|
|
|
|
|
expect(ApplicationSetting.current).to be_nil
|
2019-09-30 21:07:59 +05:30
|
|
|
expect(ApplicationSetting.cache_backend.exist?(ApplicationSetting.cache_key)).to be(false)
|
2018-11-08 19:23:39 +05:30
|
|
|
end
|
|
|
|
|
|
|
|
it 'caches non-nil object' do
|
|
|
|
create(:application_setting)
|
|
|
|
|
|
|
|
expect(ApplicationSetting.current).to eq(ApplicationSetting.last)
|
2019-09-30 21:07:59 +05:30
|
|
|
expect(ApplicationSetting.cache_backend.exist?(ApplicationSetting.cache_key)).to be(true)
|
2018-11-08 19:23:39 +05:30
|
|
|
|
|
|
|
# subsequent calls retrieve the record from the cache
|
|
|
|
last_record = ApplicationSetting.last
|
|
|
|
expect(ApplicationSetting).not_to receive(:current_without_cache)
|
|
|
|
expect(ApplicationSetting.current.attributes).to eq(last_record.attributes)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
describe 'edge cases' do
|
|
|
|
describe 'caching behavior', :use_clean_rails_memory_store_caching do
|
2019-03-02 22:35:43 +05:30
|
|
|
before do
|
|
|
|
stub_commonmark_sourcepos_disabled
|
|
|
|
end
|
|
|
|
|
2018-11-08 19:23:39 +05:30
|
|
|
it 'retrieves upload fields properly' do
|
|
|
|
ar_record = create(:appearance, :with_logo)
|
|
|
|
ar_record.cache!
|
|
|
|
|
|
|
|
cache_record = Appearance.current
|
|
|
|
|
|
|
|
expect(cache_record).to be_persisted
|
|
|
|
expect(cache_record.logo).to be_an(AttachmentUploader)
|
|
|
|
expect(cache_record.logo.url).to end_with('/dk.png')
|
|
|
|
end
|
|
|
|
|
|
|
|
it 'retrieves markdown fields properly' do
|
|
|
|
ar_record = create(:appearance, description: '**Hello**')
|
|
|
|
ar_record.cache!
|
|
|
|
|
|
|
|
cache_record = Appearance.current
|
|
|
|
|
|
|
|
expect(cache_record.description).to eq('**Hello**')
|
|
|
|
expect(cache_record.description_html).to eq('<p dir="auto"><strong>Hello</strong></p>')
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2020-05-24 23:13:21 +05:30
|
|
|
it 'uses RequestStore in addition to process memory cache', :request_store do
|
2018-11-08 19:23:39 +05:30
|
|
|
# Warm up the cache
|
|
|
|
create(:application_setting).cache!
|
|
|
|
|
2020-05-24 23:13:21 +05:30
|
|
|
expect(ApplicationSetting.cache_backend).to eq(Gitlab::ProcessMemoryCache.cache_backend)
|
2019-09-30 21:07:59 +05:30
|
|
|
expect(ApplicationSetting.cache_backend).to receive(:read).with(ApplicationSetting.cache_key).once.and_call_original
|
2018-11-08 19:23:39 +05:30
|
|
|
|
|
|
|
2.times { ApplicationSetting.current }
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
describe '.cached', :use_clean_rails_memory_store_caching do
|
|
|
|
context 'when cache is cold' do
|
|
|
|
it 'returns nil' do
|
2019-10-12 21:52:04 +05:30
|
|
|
expect(MinimalTestClass.cached).to be_nil
|
2018-11-08 19:23:39 +05:30
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
context 'when cached is warm' do
|
|
|
|
before do
|
|
|
|
# Warm up the cache
|
|
|
|
create(:appearance).cache!
|
|
|
|
end
|
|
|
|
|
|
|
|
it 'retrieves the record from cache' do
|
|
|
|
expect(ActiveRecord::QueryRecorder.new { Appearance.cached }.count).to eq(0)
|
|
|
|
expect(Appearance.cached).to eq(Appearance.current_without_cache)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
describe '#cache!', :use_clean_rails_memory_store_caching do
|
|
|
|
let(:record) { create(:appearance) }
|
|
|
|
|
|
|
|
it 'caches the attributes' do
|
|
|
|
record.cache!
|
|
|
|
|
|
|
|
expect(Rails.cache.read(Appearance.cache_key)).to eq(record)
|
|
|
|
end
|
|
|
|
|
|
|
|
describe 'edge cases' do
|
|
|
|
let(:record) { create(:appearance) }
|
|
|
|
|
|
|
|
it 'caches the attributes' do
|
|
|
|
record.cache!
|
|
|
|
|
|
|
|
expect(Rails.cache.read(Appearance.cache_key)).to eq(record)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|