271 lines
9.3 KiB
Ruby
271 lines
9.3 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
module Gitlab
|
|
module RepositoryCacheAdapter
|
|
extend ActiveSupport::Concern
|
|
include Gitlab::Utils::StrongMemoize
|
|
|
|
class_methods do
|
|
# Caches and strongly memoizes the method.
|
|
#
|
|
# This only works for methods that do not take any arguments.
|
|
#
|
|
# name - The name of the method to be cached.
|
|
# fallback - A value to fall back to if the repository does not exist, or
|
|
# in case of a Git error. Defaults to nil.
|
|
def cache_method(name, fallback: nil)
|
|
uncached_name = alias_uncached_method(name)
|
|
|
|
define_method(name) do
|
|
cache_method_output(name, fallback: fallback) do
|
|
__send__(uncached_name) # rubocop:disable GitlabSecurity/PublicSend
|
|
end
|
|
end
|
|
end
|
|
|
|
# Caches and strongly memoizes the method as a Redis Set.
|
|
#
|
|
# This only works for methods that do not take any arguments. The method
|
|
# should return an Array of Strings to be cached.
|
|
#
|
|
# In addition to overriding the named method, a "name_include?" method is
|
|
# defined. This uses the "SISMEMBER" query to efficiently check membership
|
|
# without needing to load the entire set into memory.
|
|
#
|
|
# name - The name of the method to be cached.
|
|
# fallback - A value to fall back to if the repository does not exist, or
|
|
# in case of a Git error. Defaults to nil.
|
|
#
|
|
# It is not safe to use this method prior to the release of 12.3, since
|
|
# 12.2 does not correctly invalidate the redis set cache value. A mixed
|
|
# code environment containing both 12.2 and 12.3 nodes breaks, while a
|
|
# mixed code environment containing both 12.3 and 12.4 nodes will work.
|
|
def cache_method_as_redis_set(name, fallback: nil)
|
|
uncached_name = alias_uncached_method(name)
|
|
|
|
define_method(name) do
|
|
cache_method_output_as_redis_set(name, fallback: fallback) do
|
|
__send__(uncached_name) # rubocop:disable GitlabSecurity/PublicSend
|
|
end
|
|
end
|
|
|
|
# Attempt to determine whether a value is in the set of values being
|
|
# cached, by performing a redis SISMEMBERS query if appropriate.
|
|
#
|
|
# If the full list is already in-memory, we're better using it directly.
|
|
#
|
|
# If the cache is not yet populated, querying it directly will give the
|
|
# wrong answer. We handle that by querying the full list - which fills
|
|
# the cache - and using it directly to answer the question.
|
|
define_method("#{name}_include?") do |value|
|
|
ivar = "@#{name}_include"
|
|
memoized = instance_variable_get(ivar) || {}
|
|
lookup = proc { __send__(name).include?(value) } # rubocop:disable GitlabSecurity/PublicSend
|
|
|
|
next memoized[value] if memoized.key?(value)
|
|
|
|
memoized[value] =
|
|
if strong_memoized?(name)
|
|
lookup.call
|
|
else
|
|
result, exists = redis_set_cache.try_include?(name, value)
|
|
|
|
exists ? result : lookup.call
|
|
end
|
|
|
|
instance_variable_set(ivar, memoized)[value]
|
|
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)
|
|
uncached_name = alias_uncached_method(name)
|
|
|
|
define_method(name) do
|
|
cache_method_output_asymmetrically(name) do
|
|
__send__(uncached_name) # rubocop:disable GitlabSecurity/PublicSend
|
|
end
|
|
end
|
|
end
|
|
|
|
# Strongly memoizes the method.
|
|
#
|
|
# This only works for methods that do not take any arguments.
|
|
#
|
|
# name - The name of the method to be memoized.
|
|
# fallback - A value to fall back to if the repository does not exist, or
|
|
# in case of a Git error. Defaults to nil. The fallback value
|
|
# is not memoized.
|
|
def memoize_method(name, fallback: nil)
|
|
uncached_name = alias_uncached_method(name)
|
|
|
|
define_method(name) do
|
|
memoize_method_output(name, fallback: fallback) do
|
|
__send__(uncached_name) # rubocop:disable GitlabSecurity/PublicSend
|
|
end
|
|
end
|
|
end
|
|
|
|
# Prepends "_uncached_" to the target method name
|
|
#
|
|
# Returns the uncached method name
|
|
def alias_uncached_method(name)
|
|
uncached_name = :"_uncached_#{name}"
|
|
|
|
alias_method(uncached_name, name)
|
|
|
|
uncached_name
|
|
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
|
|
def cache
|
|
raise NotImplementedError
|
|
end
|
|
|
|
# RepositorySetCache to be used. Should be overridden by the including class
|
|
def redis_set_cache
|
|
raise NotImplementedError
|
|
end
|
|
|
|
# RepositoryHashCache to be used. Should be overridden by the including class
|
|
def redis_hash_cache
|
|
raise NotImplementedError
|
|
end
|
|
|
|
# List of cached methods. Should be overridden by the including class
|
|
def cached_methods
|
|
raise NotImplementedError
|
|
end
|
|
|
|
# Caches and strongly memoizes the supplied block.
|
|
#
|
|
# name - The name of the method to be cached.
|
|
# fallback - A value to fall back to if the repository does not exist, or
|
|
# in case of a Git error. Defaults to nil.
|
|
def cache_method_output(name, fallback: nil, &block)
|
|
memoize_method_output(name, fallback: fallback) do
|
|
cache.fetch(name, &block)
|
|
end
|
|
end
|
|
|
|
# Caches and strongly memoizes the supplied block as a Redis Set. The result
|
|
# will be provided as a sorted array.
|
|
#
|
|
# name - The name of the method to be cached.
|
|
# fallback - A value to fall back to if the repository does not exist, or
|
|
# in case of a Git error. Defaults to nil.
|
|
def cache_method_output_as_redis_set(name, fallback: nil, &block)
|
|
memoize_method_output(name, fallback: fallback) do
|
|
redis_set_cache.fetch(name, &block).sort
|
|
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.
|
|
#
|
|
# name - The name of the method to be memoized.
|
|
# fallback - A value to fall back to if the repository does not exist, or
|
|
# in case of a Git error. Defaults to nil. The fallback value is
|
|
# not memoized.
|
|
def memoize_method_output(name, fallback: nil, &block)
|
|
no_repository_fallback(name, fallback: fallback) do
|
|
strong_memoize(memoizable_name(name), &block)
|
|
end
|
|
end
|
|
|
|
# Returns the fallback value if the repository does not exist
|
|
def no_repository_fallback(name, fallback: nil, &block)
|
|
# Avoid unnecessary gRPC invocations
|
|
return fallback if fallback && fallback_early?(name)
|
|
|
|
yield
|
|
rescue Gitlab::Git::Repository::NoRepository
|
|
# Even if the `#exists?` check in `fallback_early?` passes, these errors
|
|
# might still occur (for example because of a non-existing HEAD). We
|
|
# want to gracefully handle this and not memoize anything.
|
|
fallback
|
|
end
|
|
|
|
def memoize_method_cache_value(method, value)
|
|
strong_memoize(memoizable_name(method)) { value }
|
|
end
|
|
|
|
# Expires the caches of a specific set of methods
|
|
def expire_method_caches(methods)
|
|
methods.each do |name|
|
|
unless cached_methods.include?(name.to_sym)
|
|
Gitlab::AppLogger.error "Requested to expire non-existent method '#{name}' for Repository"
|
|
next
|
|
end
|
|
|
|
cache.expire(name)
|
|
|
|
clear_memoization(memoizable_name(name))
|
|
end
|
|
|
|
expire_redis_set_method_caches(methods)
|
|
expire_redis_hash_method_caches(methods)
|
|
expire_request_store_method_caches(methods)
|
|
end
|
|
|
|
private
|
|
|
|
def memoizable_name(name)
|
|
"#{name.to_s.tr('?!', '')}"
|
|
end
|
|
|
|
def expire_request_store_method_caches(methods)
|
|
methods.each do |name|
|
|
request_store_cache.expire(name)
|
|
end
|
|
end
|
|
|
|
def expire_redis_set_method_caches(methods)
|
|
redis_set_cache.expire(*methods)
|
|
end
|
|
|
|
def expire_redis_hash_method_caches(methods)
|
|
redis_hash_cache.delete(*methods)
|
|
end
|
|
|
|
# All cached repository methods depend on the existence of a Git repository,
|
|
# so if the repository doesn't exist, we already know not to call it.
|
|
def fallback_early?(method_name)
|
|
# Avoid infinite loop
|
|
return false if method_name == :exists?
|
|
|
|
!exists?
|
|
end
|
|
end
|
|
end
|