Commit 3640292b authored by Michael Kozono's avatar Michael Kozono

Cache `Repository#exists?` false in RequestStore

* Only truthy values are cached in Redis.
* All values are cached in RequestStore and in an instance variable.
parent d9c4ebc5
...@@ -510,7 +510,7 @@ class Repository ...@@ -510,7 +510,7 @@ class Repository
raw_repository.exists? raw_repository.exists?
end end
cache_method :exists? cache_method_asymmetrically :exists?
# We don't need to cache the output of this method because both exists? and # We don't need to cache the output of this method because both exists? and
# has_visible_content? are already memoized and cached. There's no guarantee # has_visible_content? are already memoized and cached. There's no guarantee
...@@ -1029,6 +1029,10 @@ class Repository ...@@ -1029,6 +1029,10 @@ class Repository
@cache ||= Gitlab::RepositoryCache.new(self) @cache ||= Gitlab::RepositoryCache.new(self)
end end
def request_store_cache
@request_store_cache ||= Gitlab::RepositoryCache.new(self, backend: Gitlab::SafeRequestStore)
end
def tags_sorted_by_committed_date def tags_sorted_by_committed_date
tags.sort_by do |tag| tags.sort_by do |tag|
# Annotated tags can point to any object (e.g. a blob), but generally # Annotated tags can point to any object (e.g. a blob), but generally
......
...@@ -29,5 +29,21 @@ module Gitlab ...@@ -29,5 +29,21 @@ module Gitlab
def read(key) def read(key)
backend.read(cache_key(key)) backend.read(cache_key(key))
end end
def write(key, value)
backend.write(cache_key(key), value)
end
def fetch_without_caching_false(key, &block)
value = read(key)
return value if value
value = yield
# Don't cache false values
write(key, value) if value
value
end
end end
end end
...@@ -15,6 +15,20 @@ module Gitlab ...@@ -15,6 +15,20 @@ module Gitlab
wrap_method(name, :cache_method_output, fallback: fallback) wrap_method(name, :cache_method_output, fallback: fallback)
end end
# Caches truthy values from the method. All values are strongly memoized,
# and cached in RequestStore.
#
# Currently only used to cache `exists?` since stale false values are
# particularly troublesome. This can occur, for example, when an NFS mount
# is temporarily down.
#
# This only works for methods that do not take any arguments.
#
# name - The name of the method to be cached.
def cache_method_asymmetrically(name)
wrap_method(name, :cache_method_output_asymmetrically)
end
# Strongly memoizes the method. # Strongly memoizes the method.
# #
# This only works for methods that do not take any arguments. # This only works for methods that do not take any arguments.
...@@ -42,6 +56,12 @@ module Gitlab ...@@ -42,6 +56,12 @@ module Gitlab
end end
end end
# RequestStore-backed RepositoryCache to be used. Should be overridden by
# the including class
def request_store_cache
raise NotImplementedError
end
# RepositoryCache to be used. Should be overridden by the including class # RepositoryCache to be used. Should be overridden by the including class
def cache def cache
raise NotImplementedError raise NotImplementedError
...@@ -63,6 +83,22 @@ module Gitlab ...@@ -63,6 +83,22 @@ module Gitlab
end end
end end
# Caches truthy values from the supplied block. All values are strongly
# memoized, and cached in RequestStore.
#
# Currently only used to cache `exists?` since stale false values are
# particularly troublesome. This can occur, for example, when an NFS mount
# is temporarily down.
#
# name - The name of the method to be cached.
def cache_method_output_asymmetrically(name, &block)
memoize_method_output(name) do
request_store_cache.fetch(name) do
cache.fetch_without_caching_false(name, &block)
end
end
end
# Strongly memoizes the supplied block. # Strongly memoizes the supplied block.
# #
# name - The name of the method to be memoized. # name - The name of the method to be memoized.
......
...@@ -90,6 +90,11 @@ describe Projects::PipelinesController do ...@@ -90,6 +90,11 @@ describe Projects::PipelinesController do
context 'when performing gitaly calls', :request_store do context 'when performing gitaly calls', :request_store do
it 'limits the Gitaly requests' do it 'limits the Gitaly requests' do
# Isolate from test preparation (Repository#exists? is also cached in RequestStore)
RequestStore.end!
RequestStore.clear!
RequestStore.begin!
expect { get_pipelines_index_json } expect { get_pipelines_index_json }
.to change { Gitlab::GitalyClient.get_request_count }.by(2) .to change { Gitlab::GitalyClient.get_request_count }.by(2)
end end
......
...@@ -65,6 +65,86 @@ describe Gitlab::RepositoryCacheAdapter do ...@@ -65,6 +65,86 @@ describe Gitlab::RepositoryCacheAdapter do
end end
end end
describe '#cache_method_output_asymmetrically', :use_clean_rails_memory_store_caching, :request_store do
let(:request_store_cache) { repository.send(:request_store_cache) }
context 'with a non-existing repository' do
let(:project) { create(:project) } # No repository
let(:object) { double }
subject do
repository.cache_method_output_asymmetrically(:cats) do
object.cats_call_stub
end
end
it 'returns the output of the original method' do
expect(object).to receive(:cats_call_stub).and_return('output')
expect(subject).to eq('output')
end
end
context 'with a method throwing a non-existing-repository error' do
subject do
repository.cache_method_output_asymmetrically(:cats) do
raise Gitlab::Git::Repository::NoRepository
end
end
it 'returns nil' do
expect(subject).to eq(nil)
end
it 'does not cache the data' do
subject
expect(repository.instance_variable_defined?(:@cats)).to eq(false)
expect(cache.exist?(:cats)).to eq(false)
end
end
context 'with an existing repository' do
let(:object) { double }
context 'when it returns truthy' do
before do
expect(object).to receive(:cats).once.and_return('truthy output')
end
it 'caches the output in RequestStore' do
expect do
repository.cache_method_output_asymmetrically(:cats) { object.cats }
end.to change { request_store_cache.read(:cats) }.from(nil).to('truthy output')
end
it 'caches the output in RepositoryCache' do
expect do
repository.cache_method_output_asymmetrically(:cats) { object.cats }
end.to change { cache.read(:cats) }.from(nil).to('truthy output')
end
end
context 'when it returns false' do
before do
expect(object).to receive(:cats).once.and_return(false)
end
it 'caches the output in RequestStore' do
expect do
repository.cache_method_output_asymmetrically(:cats) { object.cats }
end.to change { request_store_cache.read(:cats) }.from(nil).to(false)
end
it 'does NOT cache the output in RepositoryCache' do
expect do
repository.cache_method_output_asymmetrically(:cats) { object.cats }
end.not_to change { cache.read(:cats) }.from(nil)
end
end
end
end
describe '#memoize_method_output' do describe '#memoize_method_output' do
let(:fallback) { 10 } let(:fallback) { 10 }
......
...@@ -47,4 +47,89 @@ describe Gitlab::RepositoryCache do ...@@ -47,4 +47,89 @@ describe Gitlab::RepositoryCache do
expect(backend).to have_received(:fetch).with("baz:#{namespace}", &p) expect(backend).to have_received(:fetch).with("baz:#{namespace}", &p)
end end
end end
describe '#fetch_without_caching_false', :use_clean_rails_memory_store_caching do
let(:key) { :foo }
let(:backend) { Rails.cache }
it 'requires a block' do
expect do
cache.fetch_without_caching_false(key)
end.to raise_error(LocalJumpError)
end
context 'when the key does not exist in the cache' do
context 'when the result of the block is truthy' do
it 'returns the result of the block' do
result = cache.fetch_without_caching_false(key) { true }
expect(result).to be true
end
it 'caches the value' do
expect(backend).to receive(:write).with("#{key}:#{namespace}", true)
cache.fetch_without_caching_false(key) { true }
end
end
context 'when the result of the block is falsey' do
let(:p) { -> { false } }
it 'returns the result of the block' do
result = cache.fetch_without_caching_false(key, &p)
expect(result).to be false
end
it 'does not cache the value' do
expect(backend).not_to receive(:write).with("#{key}:#{namespace}", true)
cache.fetch_without_caching_false(key, &p)
end
end
end
context 'when the cached value is truthy' do
before do
backend.write("#{key}:#{namespace}", true)
end
it 'returns the cached value' do
result = cache.fetch_without_caching_false(key) { 'block result' }
expect(result).to be true
end
it 'does not execute the block' do
expect do |b|
cache.fetch_without_caching_false(key, &b)
end.not_to yield_control
end
it 'does not write to the cache' do
expect(backend).not_to receive(:write)
cache.fetch_without_caching_false(key) { 'block result' }
end
end
context 'when the cached value is falsey' do
before do
backend.write("#{key}:#{namespace}", false)
end
it 'returns the result of the block' do
result = cache.fetch_without_caching_false(key) { 'block result' }
expect(result).to eq 'block result'
end
it 'writes the truthy value to the cache' do
expect(backend).to receive(:write).with("#{key}:#{namespace}", 'block result')
cache.fetch_without_caching_false(key) { 'block result' }
end
end
end
end end
...@@ -1044,6 +1044,47 @@ describe Repository do ...@@ -1044,6 +1044,47 @@ describe Repository do
expect_to_raise_storage_error { broken_repository.exists? } expect_to_raise_storage_error { broken_repository.exists? }
end end
end end
context 'asymmetric caching', :use_clean_rails_memory_store_caching, :request_store do
let(:cache) { repository.send(:cache) }
let(:request_store_cache) { repository.send(:request_store_cache) }
context 'when it returns true' do
before do
expect(repository.raw_repository).to receive(:exists?).once.and_return(true)
end
it 'caches the output in RequestStore' do
expect do
repository.exists?
end.to change { request_store_cache.read(:exists?) }.from(nil).to(true)
end
it 'caches the output in RepositoryCache' do
expect do
repository.exists?
end.to change { cache.read(:exists?) }.from(nil).to(true)
end
end
context 'when it returns false' do
before do
expect(repository.raw_repository).to receive(:exists?).once.and_return(false)
end
it 'caches the output in RequestStore' do
expect do
repository.exists?
end.to change { request_store_cache.read(:exists?) }.from(nil).to(false)
end
it 'does NOT cache the output in RepositoryCache' do
expect do
repository.exists?
end.not_to change { cache.read(:exists?) }.from(nil)
end
end
end
end end
describe '#has_visible_content?' do describe '#has_visible_content?' do
...@@ -1892,7 +1933,7 @@ describe Repository do ...@@ -1892,7 +1933,7 @@ describe Repository do
match[1].to_sym if match match[1].to_sym if match
end.compact end.compact
expect(methods).to match_array(Repository::CACHED_METHODS + Repository::MEMOIZED_CACHED_METHODS) expect(Repository::CACHED_METHODS + Repository::MEMOIZED_CACHED_METHODS).to include(*methods)
end end
end end
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment